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
|
description: Feature request proposals in the RFC format
|
||||||
color: '#D621C3'
|
color: '#D621C3'
|
||||||
aliases: ['area/RFC']
|
aliases: ['area/RFC']
|
||||||
- name: backport:release/v2.6.x
|
- name: backport:release/v2.3.x
|
||||||
description: To be backported to release/v2.6.x
|
description: To be backported to release/v2.3.x
|
||||||
color: '#ffd700'
|
color: '#ffd700'
|
||||||
- name: backport:release/v2.7.x
|
- name: backport:release/v2.4.x
|
||||||
description: To be backported to release/v2.7.x
|
description: To be backported to release/v2.4.x
|
||||||
color: '#ffd700'
|
color: '#ffd700'
|
||||||
- name: backport:release/v2.8.x
|
- name: backport:release/v2.5.x
|
||||||
description: To be backported to release/v2.8.x
|
description: To be backported to release/v2.5.x
|
||||||
color: '#ffd700'
|
color: '#ffd700'
|
||||||
|
|||||||
2
.github/workflows/README.md
vendored
2
.github/workflows/README.md
vendored
@@ -23,7 +23,7 @@ amd when it finds a new controller version, the workflow performs the following
|
|||||||
- Updates the controller API package version in `go.mod`.
|
- Updates the controller API package version in `go.mod`.
|
||||||
- Patches the controller CRDs version in the `manifests/crds` overlay.
|
- Patches the controller CRDs version in the `manifests/crds` overlay.
|
||||||
- Patches the controller Deployment version in `manifests/bases` overlay.
|
- Patches the controller Deployment version in `manifests/bases` overlay.
|
||||||
- Opens a Pull Request against the checked out branch.
|
- Opens a Pull Request against the `main` branch.
|
||||||
- Triggers the e2e test suite to run for the opened PR.
|
- Triggers the e2e test suite to run for the opened PR.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/action.yaml
vendored
2
.github/workflows/action.yaml
vendored
@@ -24,6 +24,6 @@ jobs:
|
|||||||
name: action on ${{ matrix.version }}
|
name: action on ${{ matrix.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup flux
|
- name: Setup flux
|
||||||
uses: ./action
|
uses: ./action
|
||||||
|
|||||||
35
.github/workflows/backport.yaml
vendored
35
.github/workflows/backport.yaml
vendored
@@ -1,13 +1,34 @@
|
|||||||
name: backport
|
name: backport
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [closed, labeled]
|
types: [closed, labeled]
|
||||||
permissions: read-all
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backport:
|
pull-request:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # for reading and creating branches.
|
contents: write
|
||||||
pull-requests: write # for creating pull requests against release branches.
|
pull-requests: write
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/backport.yaml@v0.9.0
|
if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
|
||||||
secrets:
|
steps:
|
||||||
github-token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
- 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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ 'main', 'update-components-**', 'release/**', 'conform*' ]
|
branches: [ 'main', 'update-components', 'release/**', 'conform*' ]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.26.x
|
GO_VERSION: 1.24.x
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
conform-kubernetes:
|
conform-kubernetes:
|
||||||
@@ -19,13 +19,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||||
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
|
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
|
||||||
KUBERNETES_VERSION: [1.33.0, 1.34.1, 1.35.0]
|
KUBERNETES_VERSION: [1.31.5, 1.32.1, 1.33.0]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
@@ -40,9 +40,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
make build
|
make build
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.27.0
|
||||||
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
|
node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
@@ -76,13 +76,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
# Keep this list up-to-date with https://endoflife.date/kubernetes
|
||||||
# Available versions can be found with "replicated cluster versions"
|
# Available versions can be found with "replicated cluster versions"
|
||||||
K3S_VERSION: [ 1.33.7, 1.34.3, 1.35.0 ]
|
K3S_VERSION: [ 1.31.8, 1.32.4, 1.33.0 ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
@@ -107,7 +107,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||||
- name: Create cluster
|
- name: Create cluster
|
||||||
id: create-cluster
|
id: create-cluster
|
||||||
uses: replicatedhq/replicated-actions/create-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "k3s"
|
kubernetes-distribution: "k3s"
|
||||||
@@ -120,7 +120,8 @@ jobs:
|
|||||||
run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e
|
run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e
|
||||||
- name: Run flux bootstrap
|
- name: Run flux bootstrap
|
||||||
run: |
|
run: |
|
||||||
./bin/flux bootstrap git --manifests ./manifests/test/ \
|
./bin/flux bootstrap git --manifests ./manifests/install/ \
|
||||||
|
--components-extra=image-reflector-controller,image-automation-controller \
|
||||||
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
||||||
--branch=main \
|
--branch=main \
|
||||||
--path=clusters/k3s \
|
--path=clusters/k3s \
|
||||||
@@ -150,7 +151,7 @@ jobs:
|
|||||||
kubectl delete ns flux-system --wait
|
kubectl delete ns flux-system --wait
|
||||||
- name: Delete cluster
|
- name: Delete cluster
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: replicatedhq/replicated-actions/remove-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
@@ -168,13 +169,13 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
|
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
|
||||||
OPENSHIFT_VERSION: [ 4.20.0-okd ]
|
OPENSHIFT_VERSION: [ 4.18.0-okd ]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
@@ -189,7 +190,7 @@ jobs:
|
|||||||
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
|
||||||
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Create repository
|
- name: Create repository
|
||||||
@@ -199,7 +200,7 @@ jobs:
|
|||||||
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
|
||||||
- name: Create cluster
|
- name: Create cluster
|
||||||
id: create-cluster
|
id: create-cluster
|
||||||
uses: replicatedhq/replicated-actions/create-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
uses: replicatedhq/replicated-actions/create-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
kubernetes-distribution: "openshift"
|
kubernetes-distribution: "openshift"
|
||||||
@@ -211,6 +212,7 @@ jobs:
|
|||||||
- name: Run flux bootstrap
|
- name: Run flux bootstrap
|
||||||
run: |
|
run: |
|
||||||
./bin/flux bootstrap git --manifests ./manifests/openshift/ \
|
./bin/flux bootstrap git --manifests ./manifests/openshift/ \
|
||||||
|
--components-extra=image-reflector-controller,image-automation-controller \
|
||||||
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
|
||||||
--branch=main \
|
--branch=main \
|
||||||
--path=clusters/openshift \
|
--path=clusters/openshift \
|
||||||
@@ -240,7 +242,7 @@ jobs:
|
|||||||
kubectl delete ns flux-system --wait
|
kubectl delete ns flux-system --wait
|
||||||
- name: Delete cluster
|
- name: Delete cluster
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
uses: replicatedhq/replicated-actions/remove-cluster@7e1b21f10a961592f292e7dadda93466d886427f # v1.24.0
|
uses: replicatedhq/replicated-actions/remove-cluster@49b440dabd7e0e868cbbabda5cfc0d8332a279fa # v1.19.0
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
|
||||||
|
|||||||
51
.github/workflows/e2e-azure.yaml
vendored
51
.github/workflows/e2e-azure.yaml
vendored
@@ -22,21 +22,22 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e-aks:
|
e2e-aks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./tests/integration
|
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:
|
steps:
|
||||||
- name: CheckoutD
|
- name: CheckoutD
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.24.x
|
||||||
cache-dependency-path: tests/integration/go.sum
|
cache-dependency-path: tests/integration/go.sum
|
||||||
- name: Setup Terraform
|
- name: Setup Terraform
|
||||||
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
run: make build
|
run: make build
|
||||||
working-directory: ./
|
working-directory: ./
|
||||||
@@ -48,9 +49,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SOPS_VER: 3.7.1
|
SOPS_VER: 3.7.1
|
||||||
- name: Authenticate to Azure
|
- name: Authenticate to Azure
|
||||||
uses: Azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v1.4.6
|
uses: Azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v1.4.6
|
||||||
with:
|
with:
|
||||||
creds: '{"clientId":"${{ secrets.ARM_CLIENT_ID }}","clientSecret":"${{ secrets.ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.ARM_TENANT_ID }}"}'
|
creds: '{"clientId":"${{ secrets.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
|
- name: Set dynamic variables in .env
|
||||||
run: |
|
run: |
|
||||||
cat > .env <<EOF
|
cat > .env <<EOF
|
||||||
@@ -60,35 +61,33 @@ jobs:
|
|||||||
run: cat .env
|
run: cat .env
|
||||||
- name: Run Azure e2e tests
|
- name: Run Azure e2e tests
|
||||||
env:
|
env:
|
||||||
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
|
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
|
||||||
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
|
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
|
||||||
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
|
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
|
||||||
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
|
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
|
||||||
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
|
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
|
||||||
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
|
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 }}
|
||||||
GITREPO_SSH_CONTENTS: ${{ secrets.GIT_SSH_IDENTITY }}
|
GITREPO_SSH_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_CONTENTS }}
|
||||||
GITREPO_SSH_PUB_CONTENTS: ${{ secrets.GIT_SSH_IDENTITY_PUB }}
|
GITREPO_SSH_PUB_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_PUB_CONTENTS }}
|
||||||
run: |
|
run: |
|
||||||
source .env
|
source .env
|
||||||
mkdir -p ./build/ssh
|
mkdir -p ./build/ssh
|
||||||
cat <<EOF > build/ssh/key
|
touch ./build/ssh/key
|
||||||
$GITREPO_SSH_CONTENTS
|
echo $GITREPO_SSH_CONTENTS | base64 -d > build/ssh/key
|
||||||
EOF
|
|
||||||
export GITREPO_SSH_PATH=build/ssh/key
|
export GITREPO_SSH_PATH=build/ssh/key
|
||||||
cat <<EOF > build/ssh/key.pub
|
touch ./build/ssh/key.pub
|
||||||
$GITREPO_SSH_PUB_CONTENTS
|
echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub
|
||||||
EOF
|
|
||||||
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
|
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
|
||||||
make test-azure
|
make test-azure
|
||||||
- name: Ensure resource cleanup
|
- name: Ensure resource cleanup
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
env:
|
env:
|
||||||
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
|
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
|
||||||
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
|
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
|
||||||
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
|
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
|
||||||
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
|
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
|
||||||
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
|
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
|
||||||
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
|
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
|
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]'
|
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.24.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.24.0
|
||||||
cluster_name: kind
|
cluster_name: kind
|
||||||
# The versions below should target the newest Kubernetes version
|
# The versions below should target the newest Kubernetes version
|
||||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
||||||
kubectl_version: v1.33.0
|
kubectl_version: v1.32.0
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||||
- name: Setup yq
|
- name: Setup yq
|
||||||
uses: fluxcd/pkg/actions/yq@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
uses: fluxcd/pkg/actions/yq@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make build-dev
|
run: make build-dev
|
||||||
- name: Set outputs
|
- name: Set outputs
|
||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
|
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
|
||||||
- name: bootstrap init
|
- name: bootstrap init
|
||||||
run: |
|
run: |
|
||||||
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
||||||
--owner=fluxcd-testing \
|
--owner=fluxcd-testing \
|
||||||
--image-pull-secret=ghcr-auth \
|
--image-pull-secret=ghcr-auth \
|
||||||
--registry-creds=fluxcd:$GITHUB_TOKEN \
|
--registry-creds=fluxcd:$GITHUB_TOKEN \
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson
|
kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson
|
||||||
- name: bootstrap no-op
|
- name: bootstrap no-op
|
||||||
run: |
|
run: |
|
||||||
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
||||||
--owner=fluxcd-testing \
|
--owner=fluxcd-testing \
|
||||||
--image-pull-secret=ghcr-auth \
|
--image-pull-secret=ghcr-auth \
|
||||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
- name: bootstrap customize
|
- name: bootstrap customize
|
||||||
run: |
|
run: |
|
||||||
make setup-bootstrap-patch
|
make setup-bootstrap-patch
|
||||||
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
||||||
--owner=fluxcd-testing \
|
--owner=fluxcd-testing \
|
||||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||||
--branch=main \
|
--branch=main \
|
||||||
@@ -98,14 +98,13 @@ jobs:
|
|||||||
- name: test image automation
|
- name: test image automation
|
||||||
run: |
|
run: |
|
||||||
make setup-image-automation
|
make setup-image-automation
|
||||||
./bin/flux bootstrap github --manifests ./manifests/test/ \
|
./bin/flux bootstrap github --manifests ./manifests/install/ \
|
||||||
--owner=fluxcd-testing \
|
--owner=fluxcd-testing \
|
||||||
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
--repository=${{ steps.vars.outputs.test_repo_name }} \
|
||||||
--branch=main \
|
--branch=main \
|
||||||
--path=test-cluster \
|
--path=test-cluster \
|
||||||
--read-write-key
|
--read-write-key
|
||||||
./bin/flux reconcile image repository podinfo
|
./bin/flux reconcile image repository podinfo
|
||||||
./bin/flux reconcile image policy podinfo
|
|
||||||
./bin/flux reconcile image update flux-system
|
./bin/flux reconcile image update flux-system
|
||||||
./bin/flux get images all
|
./bin/flux get images all
|
||||||
./bin/flux -n flux-system events --for ImageUpdateAutomation/flux-system
|
./bin/flux -n flux-system events --for ImageUpdateAutomation/flux-system
|
||||||
|
|||||||
20
.github/workflows/e2e-gcp.yaml
vendored
20
.github/workflows/e2e-gcp.yaml
vendored
@@ -22,21 +22,21 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
e2e-gcp:
|
e2e-gcp:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./tests/integration
|
working-directory: ./tests/integration
|
||||||
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.24.x
|
||||||
cache-dependency-path: tests/integration/go.sum
|
cache-dependency-path: tests/integration/go.sum
|
||||||
- name: Setup Terraform
|
- name: Setup Terraform
|
||||||
uses: hashicorp/setup-terraform@5e8dbf3c6d9deaf4193ca7a8fb23f2ac83bb6c85 # v4.0.0
|
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
run: make build
|
run: make build
|
||||||
working-directory: ./
|
working-directory: ./
|
||||||
@@ -48,19 +48,19 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SOPS_VER: 3.7.1
|
SOPS_VER: 3.7.1
|
||||||
- name: Authenticate to Google Cloud
|
- 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'
|
id: 'auth'
|
||||||
with:
|
with:
|
||||||
credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}'
|
credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}'
|
||||||
token_format: 'access_token'
|
token_format: 'access_token'
|
||||||
- name: Setup gcloud
|
- 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
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
- name: Log into us-central1-docker.pkg.dev
|
- name: Log into us-central1-docker.pkg.dev
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: us-central1-docker.pkg.dev
|
registry: us-central1-docker.pkg.dev
|
||||||
username: oauth2accesstoken
|
username: oauth2accesstoken
|
||||||
|
|||||||
21
.github/workflows/e2e.yaml
vendored
21
.github/workflows/e2e.yaml
vendored
@@ -23,30 +23,30 @@ jobs:
|
|||||||
- 5000:5000
|
- 5000:5000
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.24.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
- name: Setup Kubernetes
|
- name: Setup Kubernetes
|
||||||
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # v1.14.0
|
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
|
||||||
with:
|
with:
|
||||||
version: v0.30.0
|
version: v0.24.0
|
||||||
cluster_name: kind
|
cluster_name: kind
|
||||||
wait: 5s
|
wait: 5s
|
||||||
config: .github/kind/config.yaml # disable KIND-net
|
config: .github/kind/config.yaml # disable KIND-net
|
||||||
# The versions below should target the oldest supported Kubernetes version
|
# The versions below should target the oldest supported Kubernetes version
|
||||||
# Keep this up-to-date with https://endoflife.date/kubernetes
|
# Keep this up-to-date with https://endoflife.date/kubernetes
|
||||||
node_image: ghcr.io/fluxcd/kindest/node:v1.33.0-amd64
|
node_image: ghcr.io/fluxcd/kindest/node:v1.31.5-amd64
|
||||||
kubectl_version: v1.33.0
|
kubectl_version: v1.32.0
|
||||||
- name: Setup Calico for network policy
|
- name: Setup Calico for network policy
|
||||||
run: |
|
run: |
|
||||||
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
|
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: make test
|
run: make test
|
||||||
- name: Run e2e tests
|
- name: Run e2e tests
|
||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
./bin/flux check --pre
|
./bin/flux check --pre
|
||||||
- name: flux install --manifests
|
- name: flux install --manifests
|
||||||
run: |
|
run: |
|
||||||
./bin/flux install --manifests ./manifests/test/
|
./bin/flux install --manifests ./manifests/install/
|
||||||
- name: flux create secret
|
- name: flux create secret
|
||||||
run: |
|
run: |
|
||||||
./bin/flux create secret git git-ssh-test \
|
./bin/flux create secret git git-ssh-test \
|
||||||
@@ -238,9 +238,6 @@ jobs:
|
|||||||
- name: flux check
|
- name: flux check
|
||||||
run: |
|
run: |
|
||||||
./bin/flux check
|
./bin/flux check
|
||||||
- name: flux migrate
|
|
||||||
run: |
|
|
||||||
./bin/flux migrate
|
|
||||||
- name: flux version
|
- name: flux version
|
||||||
run: |
|
run: |
|
||||||
./bin/flux version
|
./bin/flux version
|
||||||
|
|||||||
8
.github/workflows/ossf.yaml
vendored
8
.github/workflows/ossf.yaml
vendored
@@ -19,21 +19,21 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Run analysis
|
- name: Run analysis
|
||||||
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
|
uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1
|
||||||
with:
|
with:
|
||||||
results_file: results.sarif
|
results_file: results.sarif
|
||||||
results_format: sarif
|
results_format: sarif
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_results: true
|
publish_results: true
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: SARIF file
|
name: SARIF file
|
||||||
path: results.sarif
|
path: results.sarif
|
||||||
retention-days: 5
|
retention-days: 5
|
||||||
- name: Upload SARIF results
|
- name: Upload SARIF results
|
||||||
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
uses: github/codeql-action/upload-sarif@60168efe1c415ce0f5521ea06d5c2062adbeed1b # v3.28.17
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
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 }}
|
hashes: ${{ steps.slsa.outputs.hashes }}
|
||||||
image_url: ${{ steps.slsa.outputs.image_url }}
|
image_url: ${{ steps.slsa.outputs.image_url }}
|
||||||
image_digest: ${{ steps.slsa.outputs.image_digest }}
|
image_digest: ${{ steps.slsa.outputs.image_digest }}
|
||||||
runs-on:
|
runs-on: ubuntu-latest
|
||||||
group: "Default Larger Runners"
|
|
||||||
labels: ubuntu-latest-16-cores
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # needed to write releases
|
contents: write # needed to write releases
|
||||||
id-token: write # needed for keyless signing
|
id-token: write # needed for keyless signing
|
||||||
packages: write # needed for ghcr access
|
packages: write # needed for ghcr access
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Unshallow
|
- name: Unshallow
|
||||||
run: git fetch --prune --unshallow
|
run: git fetch --prune --unshallow
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.24.x
|
||||||
cache: false
|
cache: false
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
- name: Setup Syft
|
- name: Setup Syft
|
||||||
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
uses: anchore/sbom-action/download-syft@9f7302141466aa6482940f15371237e9d9f4c34a # v0.19.0
|
||||||
- name: Setup Cosign
|
- name: Setup Cosign
|
||||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||||
with:
|
|
||||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||||
@@ -63,7 +59,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
kustomize build manifests/crds > all-crds.yaml
|
kustomize build manifests/crds > all-crds.yaml
|
||||||
- name: Generate OpenAPI JSON schemas from CRDs
|
- name: Generate OpenAPI JSON schemas from CRDs
|
||||||
uses: fluxcd/pkg/actions/crdjsonschema@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
uses: fluxcd/pkg/actions/crdjsonschema@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||||
with:
|
with:
|
||||||
crd: all-crds.yaml
|
crd: all-crds.yaml
|
||||||
output: schemas
|
output: schemas
|
||||||
@@ -72,7 +68,7 @@ jobs:
|
|||||||
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
|
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
id: run-goreleaser
|
id: run-goreleaser
|
||||||
uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7.2.1
|
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --skip=validate
|
args: release --skip=validate
|
||||||
@@ -103,26 +99,24 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Kustomize
|
- name: Setup Kustomize
|
||||||
uses: fluxcd/pkg/actions/kustomize@1bfcca47168cb6d2d7dfdc5b35d8b379a773976d # main
|
uses: fluxcd/pkg/actions/kustomize@7e9c75bbb6a47b08c194edefa11d1c436e5bdd9e # main
|
||||||
- name: Setup Flux CLI
|
- name: Setup Flux CLI
|
||||||
uses: ./action/
|
uses: ./action/
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
id: prep
|
id: prep
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(flux version --client | awk '{ print $NF }')
|
VERSION=$(flux version --client | awk '{ print $NF }')
|
||||||
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
username: fluxcdbot
|
username: fluxcdbot
|
||||||
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
|
||||||
@@ -150,9 +144,7 @@ jobs:
|
|||||||
--path="./flux-system" \
|
--path="./flux-system" \
|
||||||
--source=${{ github.repositoryUrl }} \
|
--source=${{ github.repositoryUrl }} \
|
||||||
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
|
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
|
||||||
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
- uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
|
||||||
with:
|
|
||||||
cosign-release: v2.6.1 # TODO: remove after Flux 2.8 with support for cosign v3
|
|
||||||
- name: Sign manifests
|
- name: Sign manifests
|
||||||
env:
|
env:
|
||||||
COSIGN_EXPERIMENTAL: 1
|
COSIGN_EXPERIMENTAL: 1
|
||||||
|
|||||||
50
.github/workflows/scan.yaml
vendored
50
.github/workflows/scan.yaml
vendored
@@ -1,4 +1,5 @@
|
|||||||
name: scan
|
name: scan
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
@@ -7,13 +8,46 @@ on:
|
|||||||
branches: [ 'main', 'release/**' ]
|
branches: [ 'main', 'release/**' ]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '18 10 * * 3'
|
- cron: '18 10 * * 3'
|
||||||
permissions: read-all
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
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:
|
permissions:
|
||||||
contents: read # for reading the repository code.
|
security-events: write
|
||||||
security-events: write # for uploading the CodeQL analysis results.
|
if: github.actor != 'dependabot[bot]'
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/code-scan.yaml@v0.9.0
|
steps:
|
||||||
secrets:
|
- name: Checkout repository
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
fossa-token: ${{ secrets.FOSSA_TOKEN }}
|
- 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
|
- main
|
||||||
paths:
|
paths:
|
||||||
- .github/labels.yaml
|
- .github/labels.yaml
|
||||||
permissions: read-all
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-labels:
|
labels:
|
||||||
|
name: Run sync
|
||||||
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # for reading the labels file.
|
issues: write
|
||||||
issues: write # for creating and updating labels.
|
steps:
|
||||||
uses: fluxcd/gha-workflows/.github/workflows/labels-sync.yaml@v0.9.0
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
secrets:
|
- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 * * * *"
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
@@ -16,37 +18,24 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.x
|
go-version: 1.24.x
|
||||||
cache-dependency-path: |
|
cache-dependency-path: |
|
||||||
**/go.sum
|
**/go.sum
|
||||||
**/go.mod
|
**/go.mod
|
||||||
- name: Update component versions
|
- name: Update component versions
|
||||||
id: update
|
id: update
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: |
|
run: |
|
||||||
PR_BODY=$(mktemp)
|
PR_BODY=$(mktemp)
|
||||||
|
|
||||||
bump_version() {
|
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')
|
local LATEST_VERSION=$(curl -s 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 CTRL_VERSION=$(sed -n "s/.*$1\/releases\/download\/\(.*\)\/.*/\1/p;n" manifests/bases/$1/kustomization.yaml)
|
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 CRD_VERSION=$(sed -n "s/.*$1\/releases\/download\/\(.*\)\/.*/\1/p" manifests/crds/kustomization.yaml)
|
||||||
|
local MOD_VERSION=$(go list -m -f '{{ .Version }}' "github.com/fluxcd/$1/api")
|
||||||
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 changed=false
|
local changed=false
|
||||||
|
|
||||||
@@ -61,7 +50,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${MOD_VERSION}" != "${LATEST_VERSION}" ]]; then
|
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
|
make tidy
|
||||||
changed=true
|
changed=true
|
||||||
fi
|
fi
|
||||||
@@ -80,7 +69,6 @@ jobs:
|
|||||||
bump_version notification-controller
|
bump_version notification-controller
|
||||||
bump_version image-reflector-controller
|
bump_version image-reflector-controller
|
||||||
bump_version image-automation-controller
|
bump_version image-automation-controller
|
||||||
bump_version source-watcher
|
|
||||||
|
|
||||||
# diff change
|
# diff change
|
||||||
git diff
|
git diff
|
||||||
@@ -96,7 +84,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
id: cpr
|
id: cpr
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
token: ${{ secrets.BOT_GITHUB_TOKEN }}
|
||||||
commit-message: |
|
commit-message: |
|
||||||
@@ -106,7 +94,7 @@ jobs:
|
|||||||
committer: GitHub <noreply@github.com>
|
committer: GitHub <noreply@github.com>
|
||||||
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
|
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
|
||||||
signoff: true
|
signoff: true
|
||||||
branch: update-components-${{ github.ref_name }}
|
branch: update-components
|
||||||
title: Update toolkit components
|
title: Update toolkit components
|
||||||
body: |
|
body: |
|
||||||
${{ steps.update.outputs.pr_body }}
|
${{ steps.update.outputs.pr_body }}
|
||||||
|
|||||||
13
.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")
|
generate_completions_from_executable(bin/"flux", "completion")
|
||||||
test: |
|
test: |
|
||||||
system "#{bin}/flux --version"
|
system "#{bin}/flux --version"
|
||||||
|
publishers:
|
||||||
|
- name: aur-pkg-bin
|
||||||
|
env:
|
||||||
|
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
||||||
|
cmd: |
|
||||||
|
.github/aur/flux-bin/publish.sh {{ .Version }}
|
||||||
|
- name: aur-pkg-scm
|
||||||
|
env:
|
||||||
|
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
||||||
|
cmd: |
|
||||||
|
.github/aur/flux-scm/publish.sh {{ .Version }}
|
||||||
|
- name: aur-pkg-go
|
||||||
|
env:
|
||||||
|
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
|
||||||
|
cmd: |
|
||||||
|
.github/aur/flux-go/publish.sh {{ .Version }}
|
||||||
dockers:
|
dockers:
|
||||||
- image_templates:
|
- image_templates:
|
||||||
- 'fluxcd/flux-cli:{{ .Tag }}-amd64'
|
- 'fluxcd/flux-cli:{{ .Tag }}-amd64'
|
||||||
|
|||||||
151
AGENTS.md
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
|
# Contributing
|
||||||
|
|
||||||
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and accepts contributions via GitHub pull requests.
|
Flux is [Apache 2.0 licensed](https://github.com/fluxcd/flux2/blob/main/LICENSE) and
|
||||||
This document outlines the conventions to get your contribution accepted.
|
accepts contributions via GitHub pull requests. This document outlines
|
||||||
We gratefully welcome improvements to documentation as well as code contributions.
|
some of the conventions on to make it easier to get your contribution
|
||||||
|
accepted.
|
||||||
|
|
||||||
If you are new to the project, we recommend starting with documentation improvements or
|
We gratefully welcome improvements to issues and documentation as well as to
|
||||||
small bug fixes to get familiar with the codebase and the contribution process.
|
code.
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
The Flux project consists of a set of Kubernetes controllers and tools that implement the GitOps pattern.
|
|
||||||
The main repositories in the Flux project are:
|
|
||||||
|
|
||||||
- [fluxcd/flux2](https://github.com/fluxcd/flux2): The Flux distribution and command-line interface (CLI)
|
|
||||||
- [fluxcd/pkg](https://github.com/fluxcd/pkg): The GitOps Toolkit Go SDK for building Flux controllers and CLI plugins
|
|
||||||
- [fluxcd/source-controller](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git, OCI and Helm repositories, S3-compatible Buckets)
|
|
||||||
- [fluxcd/source-watcher](https://github.com/fluxcd/source-watcher): Kubernetes operator for advanced source composition and decomposition patterns
|
|
||||||
- [fluxcd/kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
|
|
||||||
- [fluxcd/helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for lifecycle management of Helm releases
|
|
||||||
- [fluxcd/notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events (alerts and webhook receivers)
|
|
||||||
- [fluxcd/image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries for new image tags and digests
|
|
||||||
- [fluxcd/image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patching container image tags and digests in Git repositories
|
|
||||||
- [fluxcd/website](https://github.com/fluxcd/website): The Flux documentation website accessible at <https://fluxcd.io/>
|
|
||||||
|
|
||||||
## AI Coding Assistants Guidance
|
|
||||||
|
|
||||||
Using AI Agents to help write your PR is acceptable, but as the author, you are responsible
|
|
||||||
for understanding the code and the documentation you submit. Please review all the AI-generated
|
|
||||||
content and make sure it follows the guidelines in this document before submitting your PR.
|
|
||||||
|
|
||||||
All Flux repositories contain an `AGENTS.md` file. You must point your AI Agent to
|
|
||||||
`AGENTS.md` and ask it to follow the guidelines and conventions described there.
|
|
||||||
|
|
||||||
Trim down the verbiage in the PR description, commit messages and code comments.
|
|
||||||
When engaging with Flux maintainers please refrain from using AI Agents to
|
|
||||||
generate responses, we want to talk to you, not to your AI Agent.
|
|
||||||
|
|
||||||
AI Agents **must not** add `Signed-off-by` or `Co-authored-by` tags to the commit message.
|
|
||||||
Only humans can legally certify the Developer Certificate of Origin ([DCO](https://developercertificate.org/)).
|
|
||||||
|
|
||||||
You should disclose the use of AI Agents in the description of your PR and
|
|
||||||
in the commit message using the `Assisted-by: AGENT_NAME/LLM_VERSION` tag.
|
|
||||||
|
|
||||||
Adding the `Assisted-by` tag to the commit message can be done with:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git commit -s -m "Your commit message" --trailer "Assisted-by: <agent>/<model>"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Note** that the `Signed-off-by` tag is set via the `-s` flag using your real name and email
|
|
||||||
(`user.name` and `user.email` must be set in Git config).
|
|
||||||
|
|
||||||
Example of a commit message disclosing the use of AI assistance:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Add version info to plugin listing
|
|
||||||
|
|
||||||
Add a version column to the `flux plugin list` table output and populate
|
|
||||||
it with the semantic version info extracted from the plugin's recipe file.
|
|
||||||
For plugins installed via symlinks, the version is set to `unknown`.
|
|
||||||
|
|
||||||
Signed-off-by: Jane Doe <jane.doe@example.com>
|
|
||||||
Assisted-by: copilot/gpt-5.4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Certificate of Origin
|
## Certificate of Origin
|
||||||
|
|
||||||
By contributing to this project you agree to the Developer Certificate of Origin (DCO).
|
By contributing to this project you agree to the Developer Certificate of
|
||||||
This document was created by the Linux Kernel community and is a simple statement that you,
|
Origin (DCO). This document was created by the Linux Kernel community and is a
|
||||||
as a contributor, have the legal right to make the contribution.
|
simple statement that you, as a contributor, have the legal right to make the
|
||||||
|
contribution.
|
||||||
|
|
||||||
We require all commits to be signed. By signing off with your signature, you certify that you wrote
|
We require all commits to be signed. By signing off with your signature, you
|
||||||
the patch or otherwise have the right to contribute the material by the rules of the [DCO](https://raw.githubusercontent.com/fluxcd/flux2/refs/heads/main/DCO):
|
certify that you wrote the patch or otherwise have the right to contribute the
|
||||||
|
material by the rules of the [DCO](DCO):
|
||||||
|
|
||||||
`Signed-off-by: Jane Doe <jane.doe@example.com>`
|
`Signed-off-by: Jane Doe <jane.doe@example.com>`
|
||||||
|
|
||||||
The signature must contain your real name (sorry, no pseudonyms or anonymous contributions).
|
The signature must contain your real name
|
||||||
If your `user.name` and `user.email` are set in your Git config,
|
(sorry, no pseudonyms or anonymous contributions)
|
||||||
|
If your `user.name` and `user.email` are configured in your Git config,
|
||||||
you can sign your commit automatically with `git commit -s`.
|
you can sign your commit automatically with `git commit -s`.
|
||||||
|
|
||||||
|
## Communications
|
||||||
|
|
||||||
|
For realtime communications we use Slack: To join the conversation, simply
|
||||||
|
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the
|
||||||
|
[#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel.
|
||||||
|
|
||||||
|
To discuss ideas and specifications we use [Github
|
||||||
|
Discussions](https://github.com/fluxcd/flux2/discussions).
|
||||||
|
|
||||||
|
For announcements we use a mailing list as well. Simply subscribe to
|
||||||
|
[flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev)
|
||||||
|
to join the conversation (there you can also add calendar invites
|
||||||
|
to your Google calendar for our [Flux
|
||||||
|
meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)).
|
||||||
|
|
||||||
|
## Understanding Flux and the GitOps Toolkit
|
||||||
|
|
||||||
|
If you are entirely new to Flux and the GitOps Toolkit,
|
||||||
|
you might want to take a look at the [introductory talk and demo](https://www.youtube.com/watch?v=qQBtSkgl7tI).
|
||||||
|
|
||||||
|
This project is composed of:
|
||||||
|
|
||||||
|
- [flux2](https://github.com/fluxcd/flux2): The Flux CLI
|
||||||
|
- [source-manager](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git and Helm repositories, S3-compatible Buckets)
|
||||||
|
- [kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
|
||||||
|
- [helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm
|
||||||
|
- [notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events
|
||||||
|
- [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries
|
||||||
|
- [image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patches container image tags in Git
|
||||||
|
|
||||||
|
### Understanding the code
|
||||||
|
|
||||||
|
To get started with developing controllers, you might want to review
|
||||||
|
[our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which
|
||||||
|
walks you through writing a short and concise controller that watches out
|
||||||
|
for source changes.
|
||||||
|
|
||||||
|
## How to run the test suite
|
||||||
|
|
||||||
|
Prerequisites:
|
||||||
|
|
||||||
|
* go >= 1.24
|
||||||
|
* kubectl >= 1.30
|
||||||
|
* kustomize >= 5.0
|
||||||
|
* coreutils (on Mac OS)
|
||||||
|
|
||||||
|
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make install-envtest
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can run the unit tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
After [installing Kubernetes kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) on your machine,
|
||||||
|
create a cluster for testing with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make setup-kind
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can run the end-to-end tests with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
When the output of the Flux CLI changes, to automatically update the golden
|
||||||
|
files used in the test, pass `-update` flag to the test as:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make e2e TEST_ARGS="-update"
|
||||||
|
```
|
||||||
|
|
||||||
|
Since not all packages use golden files for testing, `-update` argument must be
|
||||||
|
passed only for the packages that use golden files. Use the variables
|
||||||
|
`TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the
|
||||||
|
path of the target test package:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Unit test
|
||||||
|
make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
||||||
|
# e2e test
|
||||||
|
make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
|
||||||
|
```
|
||||||
|
|
||||||
|
Teardown the e2e environment with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make cleanup-kind
|
||||||
|
```
|
||||||
|
|
||||||
## Acceptance policy
|
## Acceptance policy
|
||||||
|
|
||||||
These things will make a PR more likely to be accepted:
|
These things will make a PR more likely to be accepted:
|
||||||
|
|
||||||
- Addressing an open issue, if one doesn't exist, please open an issue to discuss the problem and the proposed solution before submitting a PR.
|
- a well-described requirement
|
||||||
- Flux is GA software and we are committed to maintaining backward compatibility. If your contribution introduces a breaking change, expect for your PR to be rejected.
|
- tests for new code
|
||||||
- New code and tests must follow the conventions in the existing code and tests. All new code must have good test coverage and be well documented.
|
- tests for old code!
|
||||||
- All top-level Go code and exported names should have doc comments, as should non-trivial unexported type or function declarations.
|
- new code and tests follow the conventions in old code and tests
|
||||||
- Before submitting a PR, make sure that your code is properly formatted by running `make tidy fmt vet` and that all tests are passing by running `make test`.
|
- a good commit message (see below)
|
||||||
|
- all code must abide [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
|
||||||
|
- names should abide [What's in a name](https://talks.golang.org/2014/names.slide#1)
|
||||||
|
- code must build on both Linux and Darwin, via plain `go build`
|
||||||
|
- code should have appropriate test coverage and tests should be written
|
||||||
|
to work with `go test`
|
||||||
|
|
||||||
In general, we will merge a PR once one maintainer has endorsed it.
|
In general, we will merge a PR once one maintainer has endorsed it.
|
||||||
For substantial changes, more people may become involved, and you might
|
For substantial changes, more people may become involved, and you might
|
||||||
get asked to resubmit the PR or divide the changes into more than one PR.
|
get asked to resubmit the PR or divide the changes into more than one PR.
|
||||||
|
|
||||||
## Format of the Commit Message
|
### Format of the Commit Message
|
||||||
|
|
||||||
For the Flux project we prefer the following rules:
|
For the GitOps Toolkit controllers we prefer the following rules for good commit messages:
|
||||||
|
|
||||||
- Limit the subject to 50 characters, start with a capital letter and do not end with a period.
|
- Limit the subject to 50 characters and write as the continuation
|
||||||
- Explain what and why in the body, if more than a trivial change; wrap it at 72 characters.
|
of the sentence "If applied, this commit will ..."
|
||||||
- Use the imperative mood in the subject line (e.g., "Add support for X" instead of "Added support for X" or "Adds support for X").
|
- Explain what and why in the body, if more than a trivial change;
|
||||||
- Do not include GitHub mentions to issues in the commit message, use the PR description instead (e.g., "Fixes #123" or "Closes #123").
|
wrap it at 72 characters.
|
||||||
- Do not include GitHub mentions to accounts (e.g., `@username` or `@team`) within the commit message.
|
|
||||||
|
|
||||||
## Pull Request Process
|
The [following article](https://chris.beams.io/posts/git-commit/#seven-rules)
|
||||||
|
has some more helpful advice on documenting your work.
|
||||||
Fork the repository and create a new branch for your changes, do not commit directly to the `main` branch.
|
|
||||||
Once you have made your changes and committed them, push your branch to your fork and open a pull request
|
|
||||||
against the `main` branch of the Flux repository.
|
|
||||||
|
|
||||||
During the review process, you may be asked to make changes to your PR. Add commits to address the feedback
|
|
||||||
without force pushing, as this will make it easier for reviewers to see the changes.
|
|
||||||
Before committing, make sure to run `make test` to ensure that your code will pass the CI checks.
|
|
||||||
|
|
||||||
When the review process is complete, you will be asked to **squash** the commits and **rebase** your branch.
|
|
||||||
**Do not merge** the `main` branch into your branch, instead, rebase your branch on top of the latest `main`
|
|
||||||
branch after **syncing your fork** with the latest changes from the Flux repository. After rebasing,
|
|
||||||
you can push your branch with the `--force-with-lease` option to update the PR.
|
|
||||||
|
|
||||||
## Communications
|
|
||||||
|
|
||||||
For realtime communications we use Slack. To reach out to the Flux maintainers and contributors,
|
|
||||||
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the [#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel.
|
|
||||||
To discuss ideas and specifications we use [GitHub Discussions](https://github.com/fluxcd/flux2/discussions).
|
|
||||||
|
|
||||||
For announcements, we use a mailing list as well. Subscribe to
|
|
||||||
[flux-dev on cncf.io](https://lists.cncf.io/g/cncf-flux-dev), there you can also add calendar invites
|
|
||||||
to your Google calendar for our [Flux dev meeting](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view).
|
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
FROM alpine:3.23 AS builder
|
FROM alpine:3.21 AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates curl
|
RUN apk add --no-cache ca-certificates curl
|
||||||
|
|
||||||
ARG ARCH=linux/amd64
|
ARG ARCH=linux/amd64
|
||||||
ARG KUBECTL_VER=1.35.0
|
ARG KUBECTL_VER=1.33.0
|
||||||
|
|
||||||
RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \
|
RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \
|
||||||
-o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl
|
-o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl
|
||||||
|
|
||||||
RUN kubectl version --client=true
|
RUN kubectl version --client=true
|
||||||
|
|
||||||
FROM alpine:3.23 AS flux-cli
|
FROM alpine:3.21 AS flux-cli
|
||||||
|
|
||||||
RUN apk add --no-cache ca-certificates
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -17,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2
|
|||||||
all: test build
|
all: test build
|
||||||
|
|
||||||
tidy:
|
tidy:
|
||||||
go mod tidy -compat=1.26
|
go mod tidy -compat=1.24
|
||||||
cd tests/integration && go mod tidy -compat=1.26
|
cd tests/integration && go mod tidy -compat=1.24
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
|||||||
@@ -52,14 +52,12 @@ guides](https://fluxcd.io/flux/gitops-toolkit/source-watcher/).
|
|||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
- [Source Controllers](https://fluxcd.io/flux/components/source/)
|
- [Source Controller](https://fluxcd.io/flux/components/source/)
|
||||||
- [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/)
|
- [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/)
|
||||||
- [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/)
|
- [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/)
|
||||||
- [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/)
|
- [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/)
|
||||||
- [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/)
|
- [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/)
|
||||||
- [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/)
|
- [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/)
|
||||||
- [ExternalArtifact CRD](https://fluxcd.io/flux/components/source/externalartifacts/)
|
|
||||||
- [ArtifactGenerator CRD](https://fluxcd.io/flux/components/source/artifactgenerators/)
|
|
||||||
- [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/)
|
- [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/)
|
||||||
- [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/)
|
- [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/)
|
||||||
- [Helm Controller](https://fluxcd.io/flux/components/helm/)
|
- [Helm Controller](https://fluxcd.io/flux/components/helm/)
|
||||||
|
|||||||
@@ -16,24 +16,23 @@ inputs:
|
|||||||
description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE."
|
description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE."
|
||||||
required: false
|
required: false
|
||||||
token:
|
token:
|
||||||
description: "Token used to authenticate against the GitHub.com API."
|
description: "Token used to authentication against the GitHub.com API. Defaults to the token from the GitHub context of the workflow."
|
||||||
required: false
|
required: false
|
||||||
runs:
|
runs:
|
||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- name: "Download the binary to the runner's cache dir"
|
- name: "Download the binary to the runner's cache dir"
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
|
||||||
VERSION: "${{ inputs.version }}"
|
|
||||||
FLUX_TOOL_DIR: "${{ inputs.bindir }}"
|
|
||||||
TOKEN: "${{ inputs.token }}"
|
|
||||||
run: |
|
run: |
|
||||||
|
VERSION=${{ inputs.version }}
|
||||||
|
|
||||||
|
TOKEN=${{ inputs.token }}
|
||||||
|
if [[ -z "$TOKEN" ]]; then
|
||||||
|
TOKEN=${{ github.token }}
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -z "$VERSION" ]] || [[ "$VERSION" = "latest" ]]; then
|
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)
|
||||||
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
|
|
||||||
fi
|
fi
|
||||||
if [[ -z "$VERSION" ]]; then
|
if [[ -z "$VERSION" ]]; then
|
||||||
echo "Unable to determine Flux CLI version"
|
echo "Unable to determine Flux CLI version"
|
||||||
@@ -60,6 +59,7 @@ runs:
|
|||||||
FLUX_EXEC_FILE="${FLUX_EXEC_FILE}.exe"
|
FLUX_EXEC_FILE="${FLUX_EXEC_FILE}.exe"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
FLUX_TOOL_DIR=${{ inputs.bindir }}
|
||||||
if [[ -z "$FLUX_TOOL_DIR" ]]; then
|
if [[ -z "$FLUX_TOOL_DIR" ]]; then
|
||||||
FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}"
|
FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}"
|
||||||
fi
|
fi
|
||||||
@@ -77,36 +77,8 @@ runs:
|
|||||||
|
|
||||||
FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/"
|
FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/"
|
||||||
|
|
||||||
MAX_RETRIES=5
|
curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"
|
||||||
RETRY_DELAY=5
|
curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"
|
||||||
|
|
||||||
for i in $(seq 1 $MAX_RETRIES); do
|
|
||||||
echo "Downloading flux binary (attempt $i/$MAX_RETRIES)"
|
|
||||||
if curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ $i -lt $MAX_RETRIES ]; then
|
|
||||||
echo "Download failed, retrying in ${RETRY_DELAY} seconds..."
|
|
||||||
sleep $RETRY_DELAY
|
|
||||||
else
|
|
||||||
echo "Failed to download flux binary after $MAX_RETRIES attempts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
for i in $(seq 1 $MAX_RETRIES); do
|
|
||||||
echo "Downloading checksums file (attempt $i/$MAX_RETRIES)"
|
|
||||||
if curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
if [ $i -lt $MAX_RETRIES ]; then
|
|
||||||
echo "Download failed, retrying in ${RETRY_DELAY} seconds..."
|
|
||||||
sleep $RETRY_DELAY
|
|
||||||
else
|
|
||||||
echo "Failed to download checksums file after $MAX_RETRIES attempts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "Verifying checksum"
|
echo "Verifying checksum"
|
||||||
sum=""
|
sum=""
|
||||||
|
|||||||
@@ -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,
|
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.defaultComponents, "components", rootArgs.defaults.Components,
|
||||||
"list of components, accepts comma-separated values")
|
"list of components, accepts comma-separated values")
|
||||||
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.extraComponents, "components-extra", nil,
|
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",
|
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd",
|
||||||
"container registry where the Flux controller images are published")
|
"container registry where the Flux controller images are published")
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import (
|
|||||||
var bootstrapGitLabCmd = &cobra.Command{
|
var bootstrapGitLabCmd = &cobra.Command{
|
||||||
Use: "gitlab",
|
Use: "gitlab",
|
||||||
Short: "Deploy Flux on a cluster connected to a GitLab repository",
|
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.
|
commits the Flux manifests to the specified branch.
|
||||||
Then it configures the target cluster to synchronize with that repository.
|
Then it configures the target cluster to synchronize with that repository.
|
||||||
If the Flux components are present on the cluster,
|
If the Flux components are present on the cluster,
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -49,10 +48,9 @@ from the given directory or a single manifest file.`,
|
|||||||
}
|
}
|
||||||
|
|
||||||
type buildArtifactFlags struct {
|
type buildArtifactFlags struct {
|
||||||
output string
|
output string
|
||||||
path string
|
path string
|
||||||
ignorePaths []string
|
ignorePaths []string
|
||||||
resolveSymlinks bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
|
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
|
||||||
@@ -63,7 +61,6 @@ func init() {
|
|||||||
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.")
|
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.")
|
||||||
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.")
|
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.")
|
||||||
buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
|
buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
|
||||||
buildArtifactCmd.Flags().BoolVar(&buildArtifactArgs.resolveSymlinks, "resolve-symlinks", false, "resolve symlinks by copying their targets into the artifact")
|
|
||||||
|
|
||||||
buildCmd.AddCommand(buildArtifactCmd)
|
buildCmd.AddCommand(buildArtifactCmd)
|
||||||
}
|
}
|
||||||
@@ -88,15 +85,6 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path)
|
return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
if buildArtifactArgs.resolveSymlinks {
|
|
||||||
resolved, cleanupDir, err := resolveSymlinks(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("resolving symlinks failed: %w", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(cleanupDir)
|
|
||||||
path = resolved
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Actionf("building artifact from %s", path)
|
logger.Actionf("building artifact from %s", path)
|
||||||
|
|
||||||
ociClient := oci.NewClient(oci.DefaultOptions())
|
ociClient := oci.NewClient(oci.DefaultOptions())
|
||||||
@@ -108,141 +96,6 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveSymlinks creates a temporary directory with symlinks resolved to their
|
|
||||||
// real file contents. This allows building artifacts from symlink trees (e.g.,
|
|
||||||
// those created by Nix) where the actual files live outside the source directory.
|
|
||||||
// It returns the resolved path and the temporary directory path for cleanup.
|
|
||||||
func resolveSymlinks(srcPath string) (string, string, error) {
|
|
||||||
absPath, err := filepath.Abs(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
info, err := os.Stat(absPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// For a single file, resolve the symlink and return the path to the
|
|
||||||
// copied file within the temp dir, preserving file semantics for callers.
|
|
||||||
if !info.IsDir() {
|
|
||||||
resolved, err := filepath.EvalSymlinks(absPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("resolving symlink for %s: %w", absPath, err)
|
|
||||||
}
|
|
||||||
tmpDir, err := os.MkdirTemp("", "flux-artifact-*")
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
dst := filepath.Join(tmpDir, filepath.Base(absPath))
|
|
||||||
if err := copyFile(resolved, dst); err != nil {
|
|
||||||
os.RemoveAll(tmpDir)
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
return dst, tmpDir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpDir, err := os.MkdirTemp("", "flux-artifact-*")
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
visited := make(map[string]bool)
|
|
||||||
if err := copyDir(absPath, tmpDir, visited); err != nil {
|
|
||||||
os.RemoveAll(tmpDir)
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmpDir, tmpDir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyDir recursively copies the contents of srcDir to dstDir, resolving any
|
|
||||||
// symlinks encountered along the way. The visited map tracks resolved real
|
|
||||||
// directory paths to detect and break symlink cycles.
|
|
||||||
func copyDir(srcDir, dstDir string, visited map[string]bool) error {
|
|
||||||
real, err := filepath.EvalSymlinks(srcDir)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("resolving symlink %s: %w", srcDir, err)
|
|
||||||
}
|
|
||||||
abs, err := filepath.Abs(real)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting absolute path for %s: %w", real, err)
|
|
||||||
}
|
|
||||||
if visited[abs] {
|
|
||||||
return nil // break the cycle
|
|
||||||
}
|
|
||||||
visited[abs] = true
|
|
||||||
defer delete(visited, abs)
|
|
||||||
entries, err := os.ReadDir(srcDir)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
srcPath := filepath.Join(srcDir, entry.Name())
|
|
||||||
dstPath := filepath.Join(dstDir, entry.Name())
|
|
||||||
|
|
||||||
// Resolve symlinks to get the real path and info.
|
|
||||||
realPath, err := filepath.EvalSymlinks(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("resolving symlink %s: %w", srcPath, err)
|
|
||||||
}
|
|
||||||
realInfo, err := os.Stat(realPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("stat resolved path %s: %w", realPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if realInfo.IsDir() {
|
|
||||||
if err := os.MkdirAll(dstPath, realInfo.Mode()); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Recursively copy the resolved directory contents.
|
|
||||||
if err := copyDir(realPath, dstPath, visited); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !realInfo.Mode().IsRegular() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := copyFile(realPath, dstPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
|
||||||
srcInfo, err := os.Stat(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
in, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(out, in); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return out.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func saveReaderToFile(reader io.Reader) (string, error) {
|
func saveReaderToFile(reader io.Reader) (string, error) {
|
||||||
b, err := io.ReadAll(bufio.NewReader(reader))
|
b, err := io.ReadAll(bufio.NewReader(reader))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -69,149 +68,3 @@ data:
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_resolveSymlinks(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
|
|
||||||
// Create source directory with a real file
|
|
||||||
srcDir := t.TempDir()
|
|
||||||
realFile := filepath.Join(srcDir, "real.yaml")
|
|
||||||
g.Expect(os.WriteFile(realFile, []byte("apiVersion: v1\nkind: Namespace\nmetadata:\n name: test\n"), 0o644)).To(Succeed())
|
|
||||||
|
|
||||||
// Create a directory with symlinks pointing to files outside it
|
|
||||||
symlinkDir := t.TempDir()
|
|
||||||
symlinkFile := filepath.Join(symlinkDir, "linked.yaml")
|
|
||||||
g.Expect(os.Symlink(realFile, symlinkFile)).To(Succeed())
|
|
||||||
|
|
||||||
// Also add a regular file in the symlink dir
|
|
||||||
regularFile := filepath.Join(symlinkDir, "regular.yaml")
|
|
||||||
g.Expect(os.WriteFile(regularFile, []byte("apiVersion: v1\nkind: ConfigMap\n"), 0o644)).To(Succeed())
|
|
||||||
|
|
||||||
// Create a symlinked subdirectory
|
|
||||||
subDir := filepath.Join(srcDir, "subdir")
|
|
||||||
g.Expect(os.MkdirAll(subDir, 0o755)).To(Succeed())
|
|
||||||
g.Expect(os.WriteFile(filepath.Join(subDir, "nested.yaml"), []byte("nested"), 0o644)).To(Succeed())
|
|
||||||
g.Expect(os.Symlink(subDir, filepath.Join(symlinkDir, "linkeddir"))).To(Succeed())
|
|
||||||
|
|
||||||
// Resolve symlinks
|
|
||||||
resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
|
||||||
|
|
||||||
// Verify the regular file was copied
|
|
||||||
content, err := os.ReadFile(filepath.Join(resolved, "regular.yaml"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(string(content)).To(Equal("apiVersion: v1\nkind: ConfigMap\n"))
|
|
||||||
|
|
||||||
// Verify the symlinked file was resolved and copied
|
|
||||||
content, err = os.ReadFile(filepath.Join(resolved, "linked.yaml"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(string(content)).To(ContainSubstring("kind: Namespace"))
|
|
||||||
|
|
||||||
// Verify that the resolved file is a regular file, not a symlink
|
|
||||||
info, err := os.Lstat(filepath.Join(resolved, "linked.yaml"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(info.Mode().IsRegular()).To(BeTrue())
|
|
||||||
|
|
||||||
// Verify that the symlinked directory was resolved and its contents were copied
|
|
||||||
content, err = os.ReadFile(filepath.Join(resolved, "linkeddir", "nested.yaml"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(string(content)).To(Equal("nested"))
|
|
||||||
|
|
||||||
// Verify that the file inside the symlinked directory is a regular file
|
|
||||||
info, err = os.Lstat(filepath.Join(resolved, "linkeddir", "nested.yaml"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(info.Mode().IsRegular()).To(BeTrue())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_resolveSymlinks_singleFile(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
|
|
||||||
// Create a real file
|
|
||||||
srcDir := t.TempDir()
|
|
||||||
realFile := filepath.Join(srcDir, "manifest.yaml")
|
|
||||||
g.Expect(os.WriteFile(realFile, []byte("kind: ConfigMap"), 0o644)).To(Succeed())
|
|
||||||
|
|
||||||
// Create a symlink to the real file
|
|
||||||
linkDir := t.TempDir()
|
|
||||||
linkFile := filepath.Join(linkDir, "link.yaml")
|
|
||||||
g.Expect(os.Symlink(realFile, linkFile)).To(Succeed())
|
|
||||||
|
|
||||||
// Resolve the single symlinked file
|
|
||||||
resolved, cleanupDir, err := resolveSymlinks(linkFile)
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
|
||||||
|
|
||||||
// The returned path should be a file, not a directory
|
|
||||||
info, err := os.Stat(resolved)
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(info.IsDir()).To(BeFalse())
|
|
||||||
|
|
||||||
// Verify contents
|
|
||||||
content, err := os.ReadFile(resolved)
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(string(content)).To(Equal("kind: ConfigMap"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_resolveSymlinks_cycle(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
|
|
||||||
// Create a directory with a symlink cycle: dir/link -> dir
|
|
||||||
dir := t.TempDir()
|
|
||||||
g.Expect(os.WriteFile(filepath.Join(dir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
|
|
||||||
g.Expect(os.Symlink(dir, filepath.Join(dir, "cycle"))).To(Succeed())
|
|
||||||
|
|
||||||
// resolveSymlinks should not infinite-loop
|
|
||||||
resolved, cleanupDir, err := resolveSymlinks(dir)
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
|
||||||
|
|
||||||
// The file should be copied
|
|
||||||
content, err := os.ReadFile(filepath.Join(resolved, "file.yaml"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(string(content)).To(Equal("data"))
|
|
||||||
|
|
||||||
// The cycle directory should exist but not cause infinite nesting
|
|
||||||
_, err = os.Stat(filepath.Join(resolved, "cycle"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
|
|
||||||
// There should NOT be deeply nested cycle/cycle/cycle/... paths
|
|
||||||
_, err = os.Stat(filepath.Join(resolved, "cycle", "cycle", "cycle"))
|
|
||||||
g.Expect(os.IsNotExist(err)).To(BeTrue())
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_resolveSymlinks_multipleLinksSameTarget(t *testing.T) {
|
|
||||||
g := NewWithT(t)
|
|
||||||
|
|
||||||
// Create source directory with a real file inside a dir
|
|
||||||
srcDir := t.TempDir()
|
|
||||||
targetDir := filepath.Join(srcDir, "target")
|
|
||||||
g.Expect(os.MkdirAll(targetDir, 0o755)).To(Succeed())
|
|
||||||
g.Expect(os.WriteFile(filepath.Join(targetDir, "file.yaml"), []byte("data"), 0o644)).To(Succeed())
|
|
||||||
|
|
||||||
// Create a directory with multiple symlinks pointing to targetDir
|
|
||||||
symlinkDir := t.TempDir()
|
|
||||||
|
|
||||||
// Link 1
|
|
||||||
link1 := filepath.Join(symlinkDir, "link1")
|
|
||||||
g.Expect(os.Symlink(targetDir, link1)).To(Succeed())
|
|
||||||
|
|
||||||
// Link 2
|
|
||||||
link2 := filepath.Join(symlinkDir, "link2")
|
|
||||||
g.Expect(os.Symlink(targetDir, link2)).To(Succeed())
|
|
||||||
|
|
||||||
// Resolve symlinks
|
|
||||||
resolved, cleanupDir, err := resolveSymlinks(symlinkDir)
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
t.Cleanup(func() { os.RemoveAll(cleanupDir) })
|
|
||||||
|
|
||||||
// Verify link1 has the file
|
|
||||||
content, err := os.ReadFile(filepath.Join(resolved, "link1", "file.yaml"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(string(content)).To(Equal("data"))
|
|
||||||
|
|
||||||
// Verify link2 ALSO has the file
|
|
||||||
content2, err := os.ReadFile(filepath.Join(resolved, "link2", "file.yaml"))
|
|
||||||
g.Expect(err).To(BeNil())
|
|
||||||
g.Expect(string(content2)).To(Equal("data"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
@@ -72,7 +71,6 @@ type buildKsFlags struct {
|
|||||||
strictSubst bool
|
strictSubst bool
|
||||||
recursive bool
|
recursive bool
|
||||||
localSources map[string]string
|
localSources map[string]string
|
||||||
inMemoryBuild bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var buildKsArgs buildKsFlags
|
var buildKsArgs buildKsFlags
|
||||||
@@ -86,8 +84,6 @@ func init() {
|
|||||||
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
||||||
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
|
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
|
||||||
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
||||||
buildKsCmd.Flags().BoolVar(&buildKsArgs.inMemoryBuild, "in-memory-build", true,
|
|
||||||
"Use in-memory filesystem during build.")
|
|
||||||
buildCmd.AddCommand(buildKsCmd)
|
buildCmd.AddCommand(buildKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,13 +97,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
|||||||
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normalize the path to handle Windows absolute and relative paths correctly
|
|
||||||
buildKsArgs.path, err = filepath.Abs(buildKsArgs.path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to resolve absolute path: %w", err)
|
|
||||||
}
|
|
||||||
buildKsArgs.path = filepath.Clean(buildKsArgs.path)
|
|
||||||
|
|
||||||
if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() {
|
if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() {
|
||||||
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
|
||||||
}
|
}
|
||||||
@@ -133,7 +122,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
|||||||
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
||||||
build.WithRecursive(buildKsArgs.recursive),
|
build.WithRecursive(buildKsArgs.recursive),
|
||||||
build.WithLocalSources(buildKsArgs.localSources),
|
build.WithLocalSources(buildKsArgs.localSources),
|
||||||
build.WithInMemoryBuild(buildKsArgs.inMemoryBuild),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
builder, err = build.NewBuilder(name, buildKsArgs.path,
|
builder, err = build.NewBuilder(name, buildKsArgs.path,
|
||||||
@@ -144,7 +132,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
|
|||||||
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
build.WithStrictSubstitute(buildKsArgs.strictSubst),
|
||||||
build.WithRecursive(buildKsArgs.recursive),
|
build.WithRecursive(buildKsArgs.recursive),
|
||||||
build.WithLocalSources(buildKsArgs.localSources),
|
build.WithLocalSources(buildKsArgs.localSources),
|
||||||
build.WithInMemoryBuild(buildKsArgs.inMemoryBuild),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,12 +52,6 @@ func TestBuildKustomization(t *testing.T) {
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "build podinfo (on-disk)",
|
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "build podinfo without service",
|
name: "build podinfo without service",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service",
|
||||||
@@ -76,24 +70,12 @@ func TestBuildKustomization(t *testing.T) {
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "build ignore (on-disk)",
|
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\" --in-memory-build=false",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "build with recursive",
|
name: "build with recursive",
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
|
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "build with recursive (on-disk)",
|
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
@@ -163,12 +145,6 @@ spec:
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "build podinfo (on-disk)",
|
|
||||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo --in-memory-build=false",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "build podinfo without service",
|
name: "build podinfo without service",
|
||||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
|
||||||
@@ -199,18 +175,6 @@ spec:
|
|||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
assertFunc: "assertGoldenTemplateFile",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "build with recursive (on-disk)",
|
|
||||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "build with recursive in dry-run mode (on-disk)",
|
|
||||||
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --in-memory-build=false --dry-run",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl := map[string]string{
|
tmpl := map[string]string{
|
||||||
@@ -254,71 +218,3 @@ spec:
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBuildKustomizationPathNormalization verifies that absolute and complex
|
|
||||||
// paths are normalized to prevent path concatenation bugs (issue #5673).
|
|
||||||
// Without normalization, paths could be duplicated like: /path/test/path/test/file
|
|
||||||
func TestBuildKustomizationPathNormalization(t *testing.T) {
|
|
||||||
// Get absolute path to testdata to test absolute path handling
|
|
||||||
absTestDataPath, err := filepath.Abs("testdata/build-kustomization/podinfo")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to get absolute path: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args string
|
|
||||||
resultFile string
|
|
||||||
assertFunc string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "build with absolute path",
|
|
||||||
args: "build kustomization podinfo --path " + absTestDataPath,
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "build with absolute path (on-disk)",
|
|
||||||
args: "build kustomization podinfo --path " + absTestDataPath + " --in-memory-build=false",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "build with complex relative path (parent dir)",
|
|
||||||
args: "build kustomization podinfo --path ./testdata/build-kustomization/../build-kustomization/podinfo",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "build with path containing redundant separators",
|
|
||||||
args: "build kustomization podinfo --path ./testdata//build-kustomization//podinfo",
|
|
||||||
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
|
|
||||||
assertFunc: "assertGoldenTemplateFile",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl := map[string]string{
|
|
||||||
"fluxns": allocateNamespace("flux-system"),
|
|
||||||
}
|
|
||||||
setup(t, tmpl)
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
var assert assertFunc
|
|
||||||
|
|
||||||
switch tt.assertFunc {
|
|
||||||
case "assertGoldenTemplateFile":
|
|
||||||
assert = assertGoldenTemplateFile(tt.resultFile, tmpl)
|
|
||||||
case "assertError":
|
|
||||||
assert = assertError(tt.resultFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := cmdTestCase{
|
|
||||||
args: tt.args + " -n " + tmpl["fluxns"],
|
|
||||||
assert: assert,
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.runTestCmd(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ type checkFlags struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var kubernetesConstraints = []string{
|
var kubernetesConstraints = []string{
|
||||||
">=1.33.0-0",
|
">=1.31.0-0",
|
||||||
}
|
}
|
||||||
|
|
||||||
var checkArgs checkFlags
|
var checkArgs checkFlags
|
||||||
|
|||||||
@@ -94,13 +94,6 @@ var createHelmReleaseCmd = &cobra.Command{
|
|||||||
--source=HelmRepository/podinfo \
|
--source=HelmRepository/podinfo \
|
||||||
--chart=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
|
# Create a HelmRelease using a source from a different namespace
|
||||||
flux create hr podinfo \
|
flux create hr podinfo \
|
||||||
--namespace=default \
|
--namespace=default \
|
||||||
@@ -134,7 +127,6 @@ type helmReleaseFlags struct {
|
|||||||
chartVersion string
|
chartVersion string
|
||||||
chartRef string
|
chartRef string
|
||||||
targetNamespace string
|
targetNamespace string
|
||||||
storageNamespace string
|
|
||||||
createNamespace bool
|
createNamespace bool
|
||||||
valuesFiles []string
|
valuesFiles []string
|
||||||
valuesFrom []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().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().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.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().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.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)")
|
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 {
|
func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
name := args[0]
|
name := args[0]
|
||||||
|
|
||||||
if helmReleaseArgs.storageNamespace == "" && helmReleaseArgs.targetNamespace != "" {
|
|
||||||
helmReleaseArgs.storageNamespace = helmReleaseArgs.targetNamespace
|
|
||||||
}
|
|
||||||
|
|
||||||
if helmReleaseArgs.chart == "" && helmReleaseArgs.chartRef == "" {
|
if helmReleaseArgs.chart == "" && helmReleaseArgs.chartRef == "" {
|
||||||
return fmt.Errorf("chart or chart-ref is required")
|
return fmt.Errorf("chart or chart-ref is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
if helmReleaseArgs.chart != "" && helmReleaseArgs.chartRef != "" {
|
|
||||||
return fmt.Errorf("cannot use --chart in combination with --chart-ref")
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceLabels, err := parseLabels()
|
sourceLabels, err := parseLabels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -208,27 +191,15 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
},
|
},
|
||||||
Spec: helmv2.HelmReleaseSpec{
|
Spec: helmv2.HelmReleaseSpec{
|
||||||
ReleaseName: helmReleaseArgs.name,
|
ReleaseName: helmReleaseArgs.name,
|
||||||
|
DependsOn: utils.MakeDependsOn(helmReleaseArgs.dependsOn),
|
||||||
Interval: metav1.Duration{
|
Interval: metav1.Duration{
|
||||||
Duration: createArgs.interval,
|
Duration: createArgs.interval,
|
||||||
},
|
},
|
||||||
TargetNamespace: helmReleaseArgs.targetNamespace,
|
TargetNamespace: helmReleaseArgs.targetNamespace,
|
||||||
StorageNamespace: helmReleaseArgs.storageNamespace,
|
Suspend: false,
|
||||||
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 {
|
switch {
|
||||||
case helmReleaseArgs.chart != "":
|
case helmReleaseArgs.chart != "":
|
||||||
helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{
|
helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{
|
||||||
@@ -263,7 +234,7 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if helmReleaseArgs.kubeConfigSecretRef != "" {
|
if helmReleaseArgs.kubeConfigSecretRef != "" {
|
||||||
helmRelease.Spec.KubeConfig = &meta.KubeConfigReference{
|
helmRelease.Spec.KubeConfig = &meta.KubeConfigReference{
|
||||||
SecretRef: &meta.SecretKeyReference{
|
SecretRef: meta.SecretKeyReference{
|
||||||
Name: helmReleaseArgs.kubeConfigSecretRef,
|
Name: helmReleaseArgs.kubeConfigSecretRef,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,11 +42,6 @@ func TestCreateHelmRelease(t *testing.T) {
|
|||||||
args: "create helmrelease podinfo --export",
|
args: "create helmrelease podinfo --export",
|
||||||
assert: assertError("chart or chart-ref is required"),
|
assert: assertError("chart or chart-ref is required"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "chart and chartRef used in combination",
|
|
||||||
args: "create helmrelease podinfo --chart podinfo --chart-ref foobar/podinfo --export",
|
|
||||||
assert: assertError("cannot use --chart in combination with --chart-ref"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "unknown source kind",
|
name: "unknown source kind",
|
||||||
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
|
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
|
||||||
|
|||||||
@@ -29,18 +29,18 @@ import (
|
|||||||
|
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"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{
|
var createImagePolicyCmd = &cobra.Command{
|
||||||
Use: "policy [name]",
|
Use: "policy [name]",
|
||||||
Short: "Create or update an ImagePolicy object",
|
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
|
An ImagePolicy object calculates a "latest image" given an image
|
||||||
repository and a policy, e.g., semver.
|
repository and a policy, e.g., semver.
|
||||||
|
|
||||||
The image that sorts highest according to the policy is recorded in
|
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
|
Example: ` # Create an ImagePolicy to select the latest stable release
|
||||||
flux create image policy podinfo \
|
flux create image policy podinfo \
|
||||||
--image-ref=podinfo \
|
--image-ref=podinfo \
|
||||||
@@ -81,6 +81,12 @@ func init() {
|
|||||||
createImageCmd.AddCommand(createImagePolicyCmd)
|
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 {
|
func createImagePolicyRun(cmd *cobra.Command, args []string) error {
|
||||||
objectName := args[0]
|
objectName := args[0]
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ import (
|
|||||||
|
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"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{
|
var createImageRepositoryCmd = &cobra.Command{
|
||||||
Use: "repository [name]",
|
Use: "repository [name]",
|
||||||
Short: "Create or update an ImageRepository object",
|
Short: "Create or update an ImageRepository object",
|
||||||
Long: `The create image repository command generates an ImageRepository resource.
|
Long: withPreviewNote(`The create image repository command generates an ImageRepository resource.
|
||||||
An ImageRepository object specifies an image repository to scan.`,
|
An ImageRepository object specifies an image repository to scan.`),
|
||||||
Example: ` # Create an ImageRepository object to scan the alpine image repository:
|
Example: ` # Create an ImageRepository object to scan the alpine image repository:
|
||||||
flux create image repository alpine-repo --image alpine --interval 20m
|
flux create image repository alpine-repo --image alpine --interval 20m
|
||||||
|
|
||||||
|
|||||||
@@ -22,16 +22,16 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
var createImageUpdateCmd = &cobra.Command{
|
var createImageUpdateCmd = &cobra.Command{
|
||||||
Use: "update [name]",
|
Use: "update [name]",
|
||||||
Short: "Create or update an ImageUpdateAutomation object",
|
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
|
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
|
Example: ` # Configure image updates for the main repository created by flux bootstrap
|
||||||
flux create image update flux-system \
|
flux create image update flux-system \
|
||||||
--git-repo-ref=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(), "./") {
|
if !strings.HasPrefix(kustomizationArgs.path.String(), "./") {
|
||||||
return fmt.Errorf("path must begin with ./")
|
return fmt.Errorf("path must begin with ./")
|
||||||
}
|
}
|
||||||
if kustomizationArgs.source.Name == "" {
|
|
||||||
return fmt.Errorf("source is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !createArgs.export {
|
if !createArgs.export {
|
||||||
logger.Generatef("generating Kustomization")
|
logger.Generatef("generating Kustomization")
|
||||||
@@ -156,6 +153,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
Labels: kslabels,
|
Labels: kslabels,
|
||||||
},
|
},
|
||||||
Spec: kustomizev1.KustomizationSpec{
|
Spec: kustomizev1.KustomizationSpec{
|
||||||
|
DependsOn: utils.MakeDependsOn(kustomizationArgs.dependsOn),
|
||||||
Interval: metav1.Duration{
|
Interval: metav1.Duration{
|
||||||
Duration: createArgs.interval,
|
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 != "" {
|
if kustomizationArgs.kubeConfigSecretRef != "" {
|
||||||
kustomization.Spec.KubeConfig = &meta.KubeConfigReference{
|
kustomization.Spec.KubeConfig = &meta.KubeConfigReference{
|
||||||
SecretRef: &meta.SecretKeyReference{
|
SecretRef: meta.SecretKeyReference{
|
||||||
Name: kustomizationArgs.kubeConfigSecretRef,
|
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"
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
"github.com/fluxcd/pkg/apis/meta"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/flags"
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -50,7 +49,7 @@ var createReceiverCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type receiverFlags struct {
|
type receiverFlags struct {
|
||||||
receiverType flags.ReceiverType
|
receiverType string
|
||||||
secretRef string
|
secretRef string
|
||||||
events []string
|
events []string
|
||||||
resources []string
|
resources []string
|
||||||
@@ -59,7 +58,7 @@ type receiverFlags struct {
|
|||||||
var receiverArgs receiverFlags
|
var receiverArgs receiverFlags
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
createReceiverCmd.Flags().Var(&receiverArgs.receiverType, "type", receiverArgs.receiverType.Description())
|
createReceiverCmd.Flags().StringVar(&receiverArgs.receiverType, "type", "", "")
|
||||||
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "")
|
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "")
|
||||||
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values")
|
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values")
|
||||||
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values")
|
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values")
|
||||||
@@ -110,7 +109,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
Labels: sourceLabels,
|
Labels: sourceLabels,
|
||||||
},
|
},
|
||||||
Spec: notificationv1.ReceiverSpec{
|
Spec: notificationv1.ReceiverSpec{
|
||||||
Type: receiverArgs.receiverType.String(),
|
Type: receiverArgs.receiverType,
|
||||||
Events: receiverArgs.events,
|
Events: receiverArgs.events,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
SecretRef: meta.LocalObjectReference{
|
SecretRef: meta.LocalObjectReference{
|
||||||
|
|||||||
@@ -56,22 +56,6 @@ func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.S
|
|||||||
}
|
}
|
||||||
|
|
||||||
existing.StringData = secret.StringData
|
existing.StringData = secret.StringData
|
||||||
if secret.Annotations != nil {
|
|
||||||
if existing.Annotations == nil {
|
|
||||||
existing.Annotations = make(map[string]string)
|
|
||||||
}
|
|
||||||
for k, v := range secret.Annotations {
|
|
||||||
existing.Annotations[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if secret.Labels != nil {
|
|
||||||
if existing.Labels == nil {
|
|
||||||
existing.Labels = make(map[string]string)
|
|
||||||
}
|
|
||||||
for k, v := range secret.Labels {
|
|
||||||
existing.Labels[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := kubeClient.Update(ctx, &existing); err != nil {
|
if err := kubeClient.Update(ctx, &existing); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,18 +46,16 @@ var createSecretGitHubAppCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type secretGitHubAppFlags struct {
|
type secretGitHubAppFlags struct {
|
||||||
appID string
|
appID string
|
||||||
appInstallationOwner string
|
appInstallationID string
|
||||||
appInstallationID string
|
privateKeyFile string
|
||||||
privateKeyFile string
|
baseURL string
|
||||||
baseURL string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var secretGitHubAppArgs = secretGitHubAppFlags{}
|
var secretGitHubAppArgs = secretGitHubAppFlags{}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID")
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID")
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationOwner, "app-installation-owner", "", "github app installation owner (user or organization)")
|
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID")
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID")
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path")
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path")
|
||||||
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL")
|
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL")
|
||||||
@@ -72,22 +70,36 @@ func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
secretName := args[0]
|
secretName := args[0]
|
||||||
|
|
||||||
|
if secretGitHubAppArgs.appID == "" {
|
||||||
|
return fmt.Errorf("--app-id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretGitHubAppArgs.appInstallationID == "" {
|
||||||
|
return fmt.Errorf("--app-installation-id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if secretGitHubAppArgs.privateKeyFile == "" {
|
||||||
|
return fmt.Errorf("--app-private-key is required")
|
||||||
|
}
|
||||||
|
|
||||||
privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile)
|
privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to read private key file: %w", err)
|
return fmt.Errorf("unable to read private key file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := sourcesecret.Options{
|
opts := sourcesecret.Options{
|
||||||
Name: secretName,
|
Name: secretName,
|
||||||
Namespace: *kubeconfigArgs.Namespace,
|
Namespace: *kubeconfigArgs.Namespace,
|
||||||
GitHubAppID: secretGitHubAppArgs.appID,
|
GitHubAppID: secretGitHubAppArgs.appID,
|
||||||
GitHubAppInstallationOwner: secretGitHubAppArgs.appInstallationOwner,
|
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
|
||||||
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
|
GitHubAppPrivateKey: string(privateKey),
|
||||||
GitHubAppPrivateKey: string(privateKey),
|
|
||||||
GitHubAppBaseURL: secretGitHubAppArgs.baseURL,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := sourcesecret.GenerateGitHubApp(opts)
|
if secretGitHubAppArgs.baseURL != "" {
|
||||||
|
opts.GitHubAppBaseURL = secretGitHubAppArgs.baseURL
|
||||||
|
}
|
||||||
|
|
||||||
|
secret, err := sourcesecret.Generate(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,21 @@ func TestCreateSecretGitHubApp(t *testing.T) {
|
|||||||
args: "create secret githubapp",
|
args: "create secret githubapp",
|
||||||
assert: assertError("name is required"),
|
assert: assertError("name is required"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "create githubapp secret with missing app-id",
|
||||||
|
args: "create secret githubapp appinfo",
|
||||||
|
assert: assertError("--app-id is required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create githubapp secret with missing appInstallationID",
|
||||||
|
args: "create secret githubapp appinfo --app-id 1",
|
||||||
|
assert: assertError("--app-installation-id is required"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create githubapp secret with missing private key file",
|
||||||
|
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2",
|
||||||
|
assert: assertError("--app-private-key is required"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "create githubapp secret with private key file that does not exist",
|
name: "create githubapp secret with private key file that does not exist",
|
||||||
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem",
|
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem",
|
||||||
@@ -38,7 +53,7 @@ func TestCreateSecretGitHubApp(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "create githubapp secret with app info",
|
name: "create githubapp secret with app info",
|
||||||
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-owner my-org --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
|
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
|
||||||
assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"),
|
assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -83,12 +83,10 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var certFile, keyFile []byte
|
var certFile, keyFile []byte
|
||||||
if secretHelmArgs.tlsCrtFile != "" {
|
if secretHelmArgs.tlsCrtFile != "" && secretHelmArgs.tlsKeyFile != "" {
|
||||||
if certFile, err = os.ReadFile(secretHelmArgs.tlsCrtFile); err != nil {
|
if certFile, err = os.ReadFile(secretHelmArgs.tlsCrtFile); err != nil {
|
||||||
return fmt.Errorf("failed to read cert file: %w", err)
|
return fmt.Errorf("failed to read cert file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if secretHelmArgs.tlsKeyFile != "" {
|
|
||||||
if keyFile, err = os.ReadFile(secretHelmArgs.tlsKeyFile); err != nil {
|
if keyFile, err = os.ReadFile(secretHelmArgs.tlsKeyFile); err != nil {
|
||||||
return fmt.Errorf("failed to read key file: %w", err)
|
return fmt.Errorf("failed to read key file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -104,7 +102,7 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
TLSCrt: certFile,
|
TLSCrt: certFile,
|
||||||
TLSKey: keyFile,
|
TLSKey: keyFile,
|
||||||
}
|
}
|
||||||
secret, err := sourcesecret.GenerateHelm(opts)
|
secret, err := sourcesecret.Generate(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ func createSecretNotationCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
VerificationCrts: caCerts,
|
VerificationCrts: caCerts,
|
||||||
TrustPolicy: policy,
|
TrustPolicy: policy,
|
||||||
}
|
}
|
||||||
secret, err := sourcesecret.GenerateNotation(opts)
|
secret, err := sourcesecret.Generate(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func createSecretOCICmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
Username: secretOCIArgs.username,
|
Username: secretOCIArgs.username,
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := sourcesecret.GenerateOCI(opts)
|
secret, err := sourcesecret.Generate(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func createSecretProxyCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
Username: secretProxyArgs.username,
|
Username: secretProxyArgs.username,
|
||||||
Password: secretProxyArgs.password,
|
Password: secretProxyArgs.password,
|
||||||
}
|
}
|
||||||
secret, err := sourcesecret.GenerateProxy(opts)
|
secret, err := sourcesecret.Generate(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if opts.TLSCrt, err = os.ReadFile(secretTLSArgs.tlsCrtFile); err != nil {
|
||||||
return fmt.Errorf("failed to read cert file: %w", err)
|
return fmt.Errorf("failed to read cert file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if secretTLSArgs.tlsKeyFile != "" {
|
|
||||||
if opts.TLSKey, err = os.ReadFile(secretTLSArgs.tlsKeyFile); err != nil {
|
if opts.TLSKey, err = os.ReadFile(secretTLSArgs.tlsKeyFile); err != nil {
|
||||||
return fmt.Errorf("failed to read key file: %w", err)
|
return fmt.Errorf("failed to read key file: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := sourcesecret.GenerateTLS(opts)
|
secret, err := sourcesecret.Generate(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -113,6 +114,12 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
var ignorePaths *string
|
var ignorePaths *string
|
||||||
if len(sourceBucketArgs.ignorePaths) > 0 {
|
if len(sourceBucketArgs.ignorePaths) > 0 {
|
||||||
ignorePathsStr := strings.Join(sourceBucketArgs.ignorePaths, "\n")
|
ignorePathsStr := strings.Join(sourceBucketArgs.ignorePaths, "\n")
|
||||||
|
|||||||
@@ -44,26 +44,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type sourceGitFlags struct {
|
type sourceGitFlags struct {
|
||||||
url string
|
url string
|
||||||
branch string
|
branch string
|
||||||
tag string
|
tag string
|
||||||
semver string
|
semver string
|
||||||
refName string
|
refName string
|
||||||
commit string
|
commit string
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
keyAlgorithm flags.PublicKeyAlgorithm
|
keyAlgorithm flags.PublicKeyAlgorithm
|
||||||
keyRSABits flags.RSAKeyBits
|
keyRSABits flags.RSAKeyBits
|
||||||
keyECDSACurve flags.ECDSACurve
|
keyECDSACurve flags.ECDSACurve
|
||||||
secretRef string
|
secretRef string
|
||||||
proxySecretRef string
|
proxySecretRef string
|
||||||
provider flags.SourceGitProvider
|
provider flags.SourceGitProvider
|
||||||
caFile string
|
caFile string
|
||||||
privateKeyFile string
|
privateKeyFile string
|
||||||
recurseSubmodules bool
|
recurseSubmodules bool
|
||||||
silent bool
|
silent bool
|
||||||
ignorePaths []string
|
ignorePaths []string
|
||||||
sparseCheckoutPaths []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var createSourceGitCmd = &cobra.Command{
|
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")
|
"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().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.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)
|
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")
|
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()
|
sourceLabels, err := parseLabels()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -216,7 +220,6 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
RecurseSubmodules: sourceGitArgs.recurseSubmodules,
|
RecurseSubmodules: sourceGitArgs.recurseSubmodules,
|
||||||
Reference: &sourcev1.GitRepositoryRef{},
|
Reference: &sourcev1.GitRepositoryRef{},
|
||||||
Ignore: ignorePaths,
|
Ignore: ignorePaths,
|
||||||
SparseCheckout: sourceGitArgs.sparseCheckoutPaths,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +302,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
secretOpts.Username = sourceGitArgs.username
|
secretOpts.Username = sourceGitArgs.username
|
||||||
secretOpts.Password = sourceGitArgs.password
|
secretOpts.Password = sourceGitArgs.password
|
||||||
}
|
}
|
||||||
secret, err := sourcesecret.GenerateGit(secretOpts)
|
secret, err := sourcesecret.Generate(secretOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ func (r *reconciler) conditionFunc() (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateSourceGitExport(t *testing.T) {
|
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 {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -101,7 +101,7 @@ func TestCreateSourceGitExport(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no args",
|
name: "no args",
|
||||||
args: "create source git --url=https://github.com/stefanprodan/podinfo",
|
args: "create secret git",
|
||||||
assert: assertError("name is required"),
|
assert: assertError("name is required"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -204,13 +204,12 @@ func TestCreateSourceGit(t *testing.T) {
|
|||||||
ObservedGeneration: repo.GetGeneration(),
|
ObservedGeneration: repo.GetGeneration(),
|
||||||
}
|
}
|
||||||
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
|
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
|
||||||
repo.Status.Artifact = &meta.Artifact{
|
repo.Status.Artifact = &sourcev1.Artifact{
|
||||||
Path: "some-path",
|
Path: "some-path",
|
||||||
Revision: "v1",
|
Revision: "v1",
|
||||||
LastUpdateTime: metav1.Time{
|
LastUpdateTime: metav1.Time{
|
||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
},
|
},
|
||||||
Digest: "sha256:1234567890abcdef",
|
|
||||||
}
|
}
|
||||||
repo.Status.ObservedGeneration = repo.GetGeneration()
|
repo.Status.ObservedGeneration = repo.GetGeneration()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -114,6 +114,12 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tmpDir, err := os.MkdirTemp("", name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
if _, err := url.Parse(sourceHelmArgs.url); err != nil {
|
if _, err := url.Parse(sourceHelmArgs.url); err != nil {
|
||||||
return fmt.Errorf("url parse failed: %w", err)
|
return fmt.Errorf("url parse failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -196,7 +202,7 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
TLSKey: keyFile,
|
TLSKey: keyFile,
|
||||||
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
|
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
|
||||||
}
|
}
|
||||||
secret, err := sourcesecret.GenerateHelm(secretOpts)
|
secret, err := sourcesecret.Generate(secretOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
@@ -31,8 +32,6 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/validation"
|
"k8s.io/apimachinery/pkg/util/validation"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/yaml"
|
"sigs.k8s.io/yaml"
|
||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var createTenantCmd = &cobra.Command{
|
var createTenantCmd = &cobra.Command{
|
||||||
@@ -58,10 +57,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type tenantFlags struct {
|
type tenantFlags struct {
|
||||||
namespaces []string
|
namespaces []string
|
||||||
clusterRole string
|
clusterRole string
|
||||||
account string
|
|
||||||
skipNamespace bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var tenantArgs tenantFlags
|
var tenantArgs tenantFlags
|
||||||
@@ -69,8 +66,6 @@ var tenantArgs tenantFlags
|
|||||||
func init() {
|
func init() {
|
||||||
createTenantCmd.Flags().StringSliceVar(&tenantArgs.namespaces, "with-namespace", nil, "namespace belonging to this tenant")
|
createTenantCmd.Flags().StringSliceVar(&tenantArgs.namespaces, "with-namespace", nil, "namespace belonging to this tenant")
|
||||||
createTenantCmd.Flags().StringVar(&tenantArgs.clusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding")
|
createTenantCmd.Flags().StringVar(&tenantArgs.clusterRole, "cluster-role", "cluster-admin", "cluster role of the tenant role binding")
|
||||||
createTenantCmd.Flags().StringVar(&tenantArgs.account, "with-service-account", "", "service account belonging to this tenant")
|
|
||||||
createTenantCmd.Flags().BoolVar(&tenantArgs.skipNamespace, "skip-namespace", false, "skip namespace creation (namespace must exist already)")
|
|
||||||
createCmd.AddCommand(createTenantCmd)
|
createCmd.AddCommand(createTenantCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,17 +107,9 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
namespaces = append(namespaces, namespace)
|
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{
|
account := corev1.ServiceAccount{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: accountName,
|
Name: tenant,
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
Labels: objLabels,
|
Labels: objLabels,
|
||||||
},
|
},
|
||||||
@@ -144,7 +131,7 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Kind: "ServiceAccount",
|
Kind: "ServiceAccount",
|
||||||
Name: accountName,
|
Name: tenant,
|
||||||
Namespace: ns,
|
Namespace: ns,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -159,7 +146,7 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if createArgs.export {
|
if createArgs.export {
|
||||||
for i := range tenantArgs.namespaces {
|
for i := range tenantArgs.namespaces {
|
||||||
if err := exportTenant(namespaces[i], accounts[i], roleBindings[i], tenantArgs.skipNamespace); err != nil {
|
if err := exportTenant(namespaces[i], accounts[i], roleBindings[i]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,11 +162,9 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := range tenantArgs.namespaces {
|
for i := range tenantArgs.namespaces {
|
||||||
if !tenantArgs.skipNamespace {
|
logger.Actionf("applying namespace %s", namespaces[i].Name)
|
||||||
logger.Actionf("applying namespace %s", namespaces[i].Name)
|
if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil {
|
||||||
if err := upsertNamespace(ctx, kubeClient, namespaces[i]); err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Actionf("applying service account %s", accounts[i].Name)
|
logger.Actionf("applying service account %s", accounts[i].Name)
|
||||||
@@ -288,24 +273,19 @@ func upsertRoleBinding(ctx context.Context, kubeClient client.Client, roleBindin
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding, skipNamespace bool) error {
|
func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, roleBinding rbacv1.RoleBinding) error {
|
||||||
var data []byte
|
namespace.TypeMeta = metav1.TypeMeta{
|
||||||
var err error
|
APIVersion: "v1",
|
||||||
|
Kind: "Namespace",
|
||||||
if !skipNamespace {
|
|
||||||
namespace.TypeMeta = metav1.TypeMeta{
|
|
||||||
APIVersion: "v1",
|
|
||||||
Kind: "Namespace",
|
|
||||||
}
|
|
||||||
data, err = yaml.Marshal(namespace)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
|
|
||||||
|
|
||||||
printlnStdout("---")
|
|
||||||
printlnStdout(resourceToString(data))
|
|
||||||
}
|
}
|
||||||
|
data, err := yaml.Marshal(namespace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("---")
|
||||||
|
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
|
||||||
|
fmt.Println(resourceToString(data))
|
||||||
|
|
||||||
account.TypeMeta = metav1.TypeMeta{
|
account.TypeMeta = metav1.TypeMeta{
|
||||||
APIVersion: "v1",
|
APIVersion: "v1",
|
||||||
@@ -315,10 +295,10 @@ func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, rol
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
|
|
||||||
|
|
||||||
printlnStdout("---")
|
fmt.Println("---")
|
||||||
printlnStdout(resourceToString(data))
|
data = bytes.Replace(data, []byte("spec: {}\n"), []byte(""), 1)
|
||||||
|
fmt.Println(resourceToString(data))
|
||||||
|
|
||||||
roleBinding.TypeMeta = metav1.TypeMeta{
|
roleBinding.TypeMeta = metav1.TypeMeta{
|
||||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||||
@@ -329,8 +309,8 @@ func exportTenant(namespace corev1.Namespace, account corev1.ServiceAccount, rol
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
printlnStdout("---")
|
fmt.Println("---")
|
||||||
printlnStdout(resourceToString(data))
|
fmt.Println(resourceToString(data))
|
||||||
|
|
||||||
return nil
|
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
|
flux debug hr podinfo --show-status
|
||||||
|
|
||||||
# Export the final values of a Helm release composed from referred ConfigMaps and Secrets
|
# Export the final values of a Helm release composed from referred ConfigMaps and Secrets
|
||||||
flux debug hr podinfo --show-values > values.yaml
|
flux debug hr podinfo --show-values > values.yaml`,
|
||||||
|
|
||||||
# Print the reconciliation history of a Helm release
|
|
||||||
flux debug hr podinfo --show-history`,
|
|
||||||
RunE: debugHelmReleaseCmdRun,
|
RunE: debugHelmReleaseCmdRun,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
|
||||||
}
|
}
|
||||||
|
|
||||||
type debugHelmReleaseFlags struct {
|
type debugHelmReleaseFlags struct {
|
||||||
showStatus bool
|
showStatus bool
|
||||||
showValues bool
|
showValues bool
|
||||||
showHistory bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugHelmReleaseArgs debugHelmReleaseFlags
|
var debugHelmReleaseArgs debugHelmReleaseFlags
|
||||||
@@ -60,25 +56,15 @@ var debugHelmReleaseArgs debugHelmReleaseFlags
|
|||||||
func init() {
|
func init() {
|
||||||
debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showStatus, "show-status", false, "print the status of the Helm release")
|
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.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)
|
debugCmd.AddCommand(debugHelmReleaseCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
name := args[0]
|
name := args[0]
|
||||||
|
|
||||||
flagsSet := 0
|
if (!debugHelmReleaseArgs.showStatus && !debugHelmReleaseArgs.showValues) ||
|
||||||
if debugHelmReleaseArgs.showStatus {
|
(debugHelmReleaseArgs.showStatus && debugHelmReleaseArgs.showValues) {
|
||||||
flagsSet++
|
return fmt.Errorf("either --show-status or --show-values must be set")
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
||||||
@@ -123,20 +109,5 @@ func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
rootCmd.Print(string(values))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,18 +56,6 @@ func TestDebugHelmRelease(t *testing.T) {
|
|||||||
"testdata/debug_helmrelease/values-from.golden.yaml",
|
"testdata/debug_helmrelease/values-from.golden.yaml",
|
||||||
tmpl,
|
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 {
|
for _, tt := range cases {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||||
"github.com/fluxcd/pkg/apis/meta"
|
|
||||||
"github.com/fluxcd/pkg/kustomize"
|
"github.com/fluxcd/pkg/kustomize"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"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
|
flux debug ks podinfo --show-status
|
||||||
|
|
||||||
# Export the final variables used for post-build substitutions composed from referred ConfigMaps and Secrets
|
# Export the final variables used for post-build substitutions composed from referred ConfigMaps and Secrets
|
||||||
flux debug ks podinfo --show-vars > vars.env
|
flux debug ks podinfo --show-vars > vars.env`,
|
||||||
|
|
||||||
# Print the reconciliation history of a Flux Kustomization
|
|
||||||
flux debug ks podinfo --show-history`,
|
|
||||||
RunE: debugKustomizationCmdRun,
|
RunE: debugKustomizationCmdRun,
|
||||||
Args: cobra.ExactArgs(1),
|
Args: cobra.ExactArgs(1),
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
||||||
}
|
}
|
||||||
|
|
||||||
type debugKustomizationFlags struct {
|
type debugKustomizationFlags struct {
|
||||||
showStatus bool
|
showStatus bool
|
||||||
showVars bool
|
showVars bool
|
||||||
showHistory bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugKustomizationArgs debugKustomizationFlags
|
var debugKustomizationArgs debugKustomizationFlags
|
||||||
@@ -65,25 +60,15 @@ var debugKustomizationArgs debugKustomizationFlags
|
|||||||
func init() {
|
func init() {
|
||||||
debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showStatus, "show-status", false, "print the status of the Flux Kustomization")
|
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.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)
|
debugCmd.AddCommand(debugKustomizationCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func debugKustomizationCmdRun(cmd *cobra.Command, args []string) error {
|
func debugKustomizationCmdRun(cmd *cobra.Command, args []string) error {
|
||||||
name := args[0]
|
name := args[0]
|
||||||
|
|
||||||
flagsSet := 0
|
if (!debugKustomizationArgs.showStatus && !debugKustomizationArgs.showVars) ||
|
||||||
if debugKustomizationArgs.showStatus {
|
(debugKustomizationArgs.showStatus && debugKustomizationArgs.showVars) {
|
||||||
flagsSet++
|
return fmt.Errorf("either --show-status or --show-vars must be set")
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,17 +55,6 @@ func TestDebugKustomization(t *testing.T) {
|
|||||||
"debug ks test-from --show-vars --show-status=false",
|
"debug ks test-from --show-vars --show-status=false",
|
||||||
"testdata/debug_kustomization/vars-from.golden.env",
|
"testdata/debug_kustomization/vars-from.golden.env",
|
||||||
tmpl,
|
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 (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"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{
|
var deleteImagePolicyCmd = &cobra.Command{
|
||||||
Use: "policy [name]",
|
Use: "policy [name]",
|
||||||
Short: "Delete an ImagePolicy object",
|
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
|
Example: ` # Delete an image policy
|
||||||
flux delete image policy alpine3.x`,
|
flux delete image policy alpine3.x`,
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)),
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"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{
|
var deleteImageRepositoryCmd = &cobra.Command{
|
||||||
Use: "repository [name]",
|
Use: "repository [name]",
|
||||||
Short: "Delete an ImageRepository object",
|
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
|
Example: ` # Delete an image repository
|
||||||
flux delete image repository alpine`,
|
flux delete image repository alpine`,
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)),
|
||||||
|
|||||||
@@ -19,13 +19,13 @@ package main
|
|||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"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{
|
var deleteImageUpdateCmd = &cobra.Command{
|
||||||
Use: "update [name]",
|
Use: "update [name]",
|
||||||
Short: "Delete an ImageUpdateAutomation object",
|
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
|
Example: ` # Delete an image update automation
|
||||||
flux delete image update latest-images`,
|
flux delete image update latest-images`,
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)),
|
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 {
|
if diffArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
||||||
logger.Actionf("logging in to registry with provider credentials")
|
logger.Actionf("logging in to registry with provider credentials")
|
||||||
opt, _, err := loginWithProvider(ctx, url, diffArtifactArgs.provider.String())
|
opt, err := loginWithProvider(ctx, url, diffArtifactArgs.provider.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error during login with provider: %w", err)
|
return fmt.Errorf("error during login with provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,6 @@ type diffKsFlags struct {
|
|||||||
strictSubst bool
|
strictSubst bool
|
||||||
recursive bool
|
recursive bool
|
||||||
localSources map[string]string
|
localSources map[string]string
|
||||||
inMemoryBuild bool
|
|
||||||
ignoreNotFound bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var diffKsArgs diffKsFlags
|
var diffKsArgs diffKsFlags
|
||||||
@@ -77,10 +75,6 @@ func init() {
|
|||||||
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
|
||||||
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
|
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
|
||||||
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
|
||||||
diffKsCmd.Flags().BoolVar(&diffKsArgs.inMemoryBuild, "in-memory-build", true,
|
|
||||||
"Use in-memory filesystem during build.")
|
|
||||||
diffKsCmd.Flags().BoolVar(&diffKsArgs.ignoreNotFound, "ignore-not-found", false,
|
|
||||||
"Ignore Kustomization not found errors on the cluster when diffing.")
|
|
||||||
diffCmd.AddCommand(diffKsCmd)
|
diffCmd.AddCommand(diffKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,8 +113,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
build.WithRecursive(diffKsArgs.recursive),
|
build.WithRecursive(diffKsArgs.recursive),
|
||||||
build.WithLocalSources(diffKsArgs.localSources),
|
build.WithLocalSources(diffKsArgs.localSources),
|
||||||
build.WithSingleKustomization(),
|
build.WithSingleKustomization(),
|
||||||
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
|
|
||||||
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
builder, err = build.NewBuilder(name, diffKsArgs.path,
|
builder, err = build.NewBuilder(name, diffKsArgs.path,
|
||||||
@@ -132,8 +124,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
build.WithRecursive(diffKsArgs.recursive),
|
build.WithRecursive(diffKsArgs.recursive),
|
||||||
build.WithLocalSources(diffKsArgs.localSources),
|
build.WithLocalSources(diffKsArgs.localSources),
|
||||||
build.WithSingleKustomization(),
|
build.WithSingleKustomization(),
|
||||||
build.WithInMemoryBuild(diffKsArgs.inMemoryBuild),
|
|
||||||
build.WithIgnoreNotFound(diffKsArgs.ignoreNotFound),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import (
|
|||||||
|
|
||||||
"github.com/fluxcd/flux2/v2/internal/build"
|
"github.com/fluxcd/flux2/v2/internal/build"
|
||||||
"github.com/fluxcd/pkg/ssa"
|
"github.com/fluxcd/pkg/ssa"
|
||||||
"github.com/fluxcd/pkg/ssa/normalize"
|
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ func TestDiffKustomization(t *testing.T) {
|
|||||||
name: "diff nothing deployed",
|
name: "diff nothing deployed",
|
||||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false",
|
||||||
objectFile: "",
|
objectFile: "",
|
||||||
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "diff with a deployment object",
|
name: "diff with a deployment object",
|
||||||
@@ -96,7 +95,7 @@ func TestDiffKustomization(t *testing.T) {
|
|||||||
name: "diff where kustomization file has multiple objects with the same name",
|
name: "diff where kustomization file has multiple objects with the same name",
|
||||||
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
|
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml",
|
||||||
objectFile: "",
|
objectFile: "",
|
||||||
assert: assertGoldenFile("./testdata/diff-kustomization/diff-new-kustomization.golden"),
|
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "diff with recursive",
|
name: "diff with recursive",
|
||||||
@@ -138,118 +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 {
|
func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured {
|
||||||
buf, err := os.ReadFile(objectFile)
|
buf, err := os.ReadFile(objectFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -264,7 +151,7 @@ func createObjectFromFile(objectFile string, templateValues map[string]string, t
|
|||||||
t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err)
|
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)
|
t.Fatalf("Error setting native kinds defaults for '%s': %v", objectFile, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -39,13 +40,12 @@ import (
|
|||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||||
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
|
||||||
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
|
||||||
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
|
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
|
||||||
sourcev1 "github.com/fluxcd/source-controller/api/v1"
|
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/internal/utils"
|
||||||
"github.com/fluxcd/flux2/v2/pkg/printers"
|
"github.com/fluxcd/flux2/v2/pkg/printers"
|
||||||
@@ -112,12 +112,7 @@ func eventsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var diffRefNs bool
|
var diffRefNs bool
|
||||||
// Build the base list options. When --all-namespaces is set we must NOT constrain the
|
clientListOpts := []client.ListOption{client.InNamespace(*kubeconfigArgs.Namespace)}
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
var refListOpts [][]client.ListOption
|
var refListOpts [][]client.ListOption
|
||||||
if eventArgs.forSelector != "" {
|
if eventArgs.forSelector != "" {
|
||||||
kind, name := getKindNameFromSelector(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 {
|
func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
|
||||||
listOpts := &metav1.ListOptions{}
|
listOpts := &metav1.ListOptions{}
|
||||||
|
clientListOpts = append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
||||||
err := runtimeresource.FollowContinue(listOpts,
|
err := runtimeresource.FollowContinue(listOpts,
|
||||||
func(options metav1.ListOptions) (runtime.Object, error) {
|
func(options metav1.ListOptions) (runtime.Object, error) {
|
||||||
newEvents := &corev1.EventList{}
|
newEvents := &corev1.EventList{}
|
||||||
opts := append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
|
if err := kubeclient.List(ctx, newEvents, clientListOpts...); err != nil {
|
||||||
if options.Continue != "" {
|
|
||||||
opts = append(opts, client.Continue(options.Continue))
|
|
||||||
}
|
|
||||||
if err := kubeclient.List(ctx, newEvents, opts...); err != nil {
|
|
||||||
return nil, fmt.Errorf("error getting events: %w", err)
|
return nil, fmt.Errorf("error getting events: %w", err)
|
||||||
}
|
}
|
||||||
el.Items = append(el.Items, newEvents.Items...)
|
el.Items = append(el.Items, newEvents.Items...)
|
||||||
@@ -254,7 +246,7 @@ func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpt
|
|||||||
hdr = getHeaders(showNs)
|
hdr = getHeaders(showNs)
|
||||||
firstIteration = false
|
firstIteration = false
|
||||||
}
|
}
|
||||||
return printers.TablePrinter(hdr).Print(rootCmd.OutOrStdout(), [][]string{rows})
|
return printers.TablePrinter(hdr).Print(os.Stdout, [][]string{rows})
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, refOpts := range refListOpts {
|
for _, refOpts := range refListOpts {
|
||||||
@@ -458,7 +450,6 @@ var fluxKindMap = refMap{
|
|||||||
sourcev1.HelmRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)},
|
sourcev1.HelmRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)},
|
||||||
autov1.ImageUpdateAutomationKind: {gvk: autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)},
|
autov1.ImageUpdateAutomationKind: {gvk: autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)},
|
||||||
imagev1.ImageRepositoryKind: {gvk: imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)},
|
imagev1.ImageRepositoryKind: {gvk: imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)},
|
||||||
swapi.ArtifactGeneratorKind: {gvk: swapi.GroupVersion.WithKind(swapi.ArtifactGeneratorKind)},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ignoreEvent(e corev1.Event) bool {
|
func ignoreEvent(e corev1.Event) bool {
|
||||||
|
|||||||
@@ -20,13 +20,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
@@ -142,7 +140,7 @@ spec:
|
|||||||
address: https://hooks.slack.com/services/mock
|
address: https://hooks.slack.com/services/mock
|
||||||
type: slack
|
type: slack
|
||||||
---
|
---
|
||||||
apiVersion: image.toolkit.fluxcd.io/v1
|
apiVersion: image.toolkit.fluxcd.io/v1beta2
|
||||||
kind: ImagePolicy
|
kind: ImagePolicy
|
||||||
metadata:
|
metadata:
|
||||||
name: podinfo
|
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 {
|
func kindNameIndexer(obj client.Object) []string {
|
||||||
e, ok := obj.(*corev1.Event)
|
e, ok := obj.(*corev1.Event)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -109,13 +109,13 @@ func (export exportCommand) run(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func printExport(export any) error {
|
func printExport(export interface{}) error {
|
||||||
data, err := yaml.Marshal(export)
|
data, err := yaml.Marshal(export)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
printlnStdout("---")
|
rootCmd.Println("---")
|
||||||
printlnStdout(resourceToString(data))
|
rootCmd.Println(resourceToString(data))
|
||||||
return nil
|
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"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
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{
|
var exportImagePolicyCmd = &cobra.Command{
|
||||||
Use: "policy [name]",
|
Use: "policy [name]",
|
||||||
Short: "Export ImagePolicy resources in YAML format",
|
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
|
Example: ` # Export all ImagePolicy resources
|
||||||
flux export image policy --all > image-policies.yaml
|
flux export image policy --all > image-policies.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
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{
|
var exportImageRepositoryCmd = &cobra.Command{
|
||||||
Use: "repository [name]",
|
Use: "repository [name]",
|
||||||
Short: "Export ImageRepository resources in YAML format",
|
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
|
Example: ` # Export all ImageRepository resources
|
||||||
flux export image repository --all > image-repositories.yaml
|
flux export image repository --all > image-repositories.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var exportImageUpdateCmd = &cobra.Command{
|
var exportImageUpdateCmd = &cobra.Command{
|
||||||
Use: "update [name]",
|
Use: "update [name]",
|
||||||
Short: "Export ImageUpdateAutomation resources in YAML format",
|
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
|
Example: ` # Export all ImageUpdateAutomation resources
|
||||||
flux export image update --all > updates.yaml
|
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",
|
"testdata/export/bucket.yaml",
|
||||||
tmpl,
|
tmpl,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"source external",
|
|
||||||
"export source external flux-system",
|
|
||||||
"testdata/export/external-artifact.yaml",
|
|
||||||
tmpl,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range cases {
|
for _, tt := range cases {
|
||||||
|
|||||||
@@ -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"
|
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type getHelmReleaseFlags struct {
|
|
||||||
showSource bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var getHrArgs getHelmReleaseFlags
|
|
||||||
|
|
||||||
var getHelmReleaseCmd = &cobra.Command{
|
var getHelmReleaseCmd = &cobra.Command{
|
||||||
Use: "helmreleases",
|
Use: "helmreleases",
|
||||||
Aliases: []string{"hr", "helmrelease"},
|
Aliases: []string{"hr", "helmrelease"},
|
||||||
Short: "Get HelmRelease statuses",
|
Short: "Get HelmRelease statuses",
|
||||||
Long: "The get helmreleases command prints the statuses of the resources.",
|
Long: "The get helmreleases command prints the statuses of the resources.",
|
||||||
Example: ` # List all Helm releases and their status
|
Example: ` # List all Helm releases and their status
|
||||||
flux get helmreleases
|
flux get helmreleases`,
|
||||||
|
|
||||||
# List all Helm releases with source information
|
|
||||||
flux get helmreleases --show-source`,
|
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
get := getCommand{
|
get := getCommand{
|
||||||
@@ -78,7 +69,6 @@ var getHelmReleaseCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
getHelmReleaseCmd.Flags().BoolVar(&getHrArgs.showSource, "show-source", false, "show the source reference for each helmrelease")
|
|
||||||
getCmd.AddCommand(getHelmReleaseCmd)
|
getCmd.AddCommand(getHelmReleaseCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,45 +79,16 @@ func getHelmReleaseRevision(helmRelease helmv2.HelmRelease) string {
|
|||||||
return helmRelease.Status.LastAttemptedRevision
|
return helmRelease.Status.LastAttemptedRevision
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHelmReleaseSource(item helmv2.HelmRelease) string {
|
|
||||||
if item.Spec.ChartRef != nil {
|
|
||||||
ns := item.Spec.ChartRef.Namespace
|
|
||||||
if ns == "" {
|
|
||||||
ns = item.GetNamespace()
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s/%s/%s",
|
|
||||||
item.Spec.ChartRef.Kind,
|
|
||||||
ns,
|
|
||||||
item.Spec.ChartRef.Name)
|
|
||||||
}
|
|
||||||
ns := item.Spec.Chart.Spec.SourceRef.Namespace
|
|
||||||
if ns == "" {
|
|
||||||
ns = item.GetNamespace()
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s/%s/%s",
|
|
||||||
item.Spec.Chart.Spec.SourceRef.Kind,
|
|
||||||
ns,
|
|
||||||
item.Spec.Chart.Spec.SourceRef.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||||
item := a.Items[i]
|
item := a.Items[i]
|
||||||
revision := getHelmReleaseRevision(item)
|
revision := getHelmReleaseRevision(item)
|
||||||
status, msg := statusAndMessage(item.Status.Conditions)
|
status, msg := statusAndMessage(item.Status.Conditions)
|
||||||
row := nameColumns(&item, includeNamespace, includeKind)
|
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||||
if getHrArgs.showSource {
|
|
||||||
row = append(row, getHelmReleaseSource(item))
|
|
||||||
}
|
|
||||||
return append(row,
|
|
||||||
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a helmReleaseListAdapter) headers(includeNamespace bool) []string {
|
func (a helmReleaseListAdapter) headers(includeNamespace bool) []string {
|
||||||
headers := []string{"Name"}
|
headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"}
|
||||||
if getHrArgs.showSource {
|
|
||||||
headers = append(headers, "Source")
|
|
||||||
}
|
|
||||||
headers = append(headers, "Revision", "Suspended", "Ready", "Message")
|
|
||||||
if includeNamespace {
|
if includeNamespace {
|
||||||
headers = append([]string{"Namespace"}, headers...)
|
headers = append([]string{"Namespace"}, headers...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var getImageAllCmd = &cobra.Command{
|
var getImageAllCmd = &cobra.Command{
|
||||||
Use: "all",
|
Use: "all",
|
||||||
Short: "Get all image statuses",
|
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
|
Example: ` # List all image objects in a namespace
|
||||||
flux get images all --namespace=flux-system
|
flux get images all --namespace=flux-system
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"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{
|
var getImagePolicyCmd = &cobra.Command{
|
||||||
Use: "policy",
|
Use: "policy",
|
||||||
Short: "Get ImagePolicy status",
|
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
|
Example: ` # List all image policies and their status
|
||||||
flux get image policy
|
flux get image policy
|
||||||
|
|
||||||
@@ -74,16 +74,11 @@ func init() {
|
|||||||
func (s imagePolicyListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
func (s imagePolicyListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
|
||||||
item := s.Items[i]
|
item := s.Items[i]
|
||||||
status, msg := statusAndMessage(item.Status.Conditions)
|
status, msg := statusAndMessage(item.Status.Conditions)
|
||||||
var image, tag string
|
return append(nameColumns(&item, includeNamespace, includeKind), item.Status.LatestImage, status, msg)
|
||||||
if ref := item.Status.LatestRef; ref != nil {
|
|
||||||
image = ref.Name
|
|
||||||
tag = ref.Tag
|
|
||||||
}
|
|
||||||
return append(nameColumns(&item, includeNamespace, includeKind), image, tag, status, msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s imagePolicyListAdapter) headers(includeNamespace bool) []string {
|
func (s imagePolicyListAdapter) headers(includeNamespace bool) []string {
|
||||||
headers := []string{"Name", "Image", "Tag", "Ready", "Message"}
|
headers := []string{"Name", "Latest image", "Ready", "Message"}
|
||||||
if includeNamespace {
|
if includeNamespace {
|
||||||
return append(namespaceHeader, headers...)
|
return append(namespaceHeader, headers...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"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{
|
var getImageRepositoryCmd = &cobra.Command{
|
||||||
Use: "repository",
|
Use: "repository",
|
||||||
Short: "Get ImageRepository status",
|
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
|
Example: ` # List all image repositories and their status
|
||||||
flux get image repository
|
flux get image repository
|
||||||
|
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ import (
|
|||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"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{
|
var getImageUpdateCmd = &cobra.Command{
|
||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "Get ImageUpdateAutomation status",
|
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
|
Example: ` # List all image update automation object and their status
|
||||||
flux get image update
|
flux get image update
|
||||||
|
|
||||||
|
|||||||
@@ -30,22 +30,13 @@ import (
|
|||||||
"github.com/fluxcd/flux2/v2/internal/utils"
|
"github.com/fluxcd/flux2/v2/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type getKustomizationFlags struct {
|
|
||||||
showSource bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var getKsArgs getKustomizationFlags
|
|
||||||
|
|
||||||
var getKsCmd = &cobra.Command{
|
var getKsCmd = &cobra.Command{
|
||||||
Use: "kustomizations",
|
Use: "kustomizations",
|
||||||
Aliases: []string{"ks", "kustomization"},
|
Aliases: []string{"ks", "kustomization"},
|
||||||
Short: "Get Kustomization statuses",
|
Short: "Get Kustomization statuses",
|
||||||
Long: `The get kustomizations command prints the statuses of the resources.`,
|
Long: `The get kustomizations command prints the statuses of the resources.`,
|
||||||
Example: ` # List all kustomizations and their status
|
Example: ` # List all kustomizations and their status
|
||||||
flux get kustomizations
|
flux get kustomizations`,
|
||||||
|
|
||||||
# List all kustomizations with source information
|
|
||||||
flux get kustomizations --show-source`,
|
|
||||||
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
get := getCommand{
|
get := getCommand{
|
||||||
@@ -83,7 +74,6 @@ var getKsCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
getKsCmd.Flags().BoolVar(&getKsArgs.showSource, "show-source", false, "show the source reference for each kustomization")
|
|
||||||
getCmd.AddCommand(getKsCmd)
|
getCmd.AddCommand(getKsCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,27 +83,12 @@ func (a kustomizationListAdapter) summariseItem(i int, includeNamespace bool, in
|
|||||||
status, msg := statusAndMessage(item.Status.Conditions)
|
status, msg := statusAndMessage(item.Status.Conditions)
|
||||||
revision = utils.TruncateHex(revision)
|
revision = utils.TruncateHex(revision)
|
||||||
msg = utils.TruncateHex(msg)
|
msg = utils.TruncateHex(msg)
|
||||||
row := nameColumns(&item, includeNamespace, includeKind)
|
return append(nameColumns(&item, includeNamespace, includeKind),
|
||||||
if getKsArgs.showSource {
|
|
||||||
sourceNs := item.Spec.SourceRef.Namespace
|
|
||||||
if sourceNs == "" {
|
|
||||||
sourceNs = item.GetNamespace()
|
|
||||||
}
|
|
||||||
row = append(row, fmt.Sprintf("%s/%s/%s",
|
|
||||||
item.Spec.SourceRef.Kind,
|
|
||||||
sourceNs,
|
|
||||||
item.Spec.SourceRef.Name))
|
|
||||||
}
|
|
||||||
return append(row,
|
|
||||||
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a kustomizationListAdapter) headers(includeNamespace bool) []string {
|
func (a kustomizationListAdapter) headers(includeNamespace bool) []string {
|
||||||
headers := []string{"Name"}
|
headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"}
|
||||||
if getKsArgs.showSource {
|
|
||||||
headers = append(headers, "Source")
|
|
||||||
}
|
|
||||||
headers = append(headers, "Revision", "Suspended", "Ready", "Message")
|
|
||||||
if includeNamespace {
|
if includeNamespace {
|
||||||
headers = append([]string{"Namespace"}, headers...)
|
headers = append([]string{"Namespace"}, headers...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ var getSourceAllCmd = &cobra.Command{
|
|||||||
apiType: helmChartType,
|
apiType: helmChartType,
|
||||||
list: &helmChartListAdapter{&sourcev1.HelmChartList{}},
|
list: &helmChartListAdapter{&sourcev1.HelmChartList{}},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
apiType: externalArtifactType,
|
|
||||||
list: &externalArtifactListAdapter{&sourcev1.ExternalArtifactList{}},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range allSourceCmd {
|
for _, c := range allSourceCmd {
|
||||||
|
|||||||
@@ -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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
autov1 "github.com/fluxcd/image-automation-controller/api/v1"
|
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
|
||||||
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1"
|
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These are general-purpose adapters for attaching methods to, for
|
// These are general-purpose adapters for attaching methods to, for
|
||||||
@@ -79,34 +77,6 @@ func (a imagePolicyAdapter) asClientObject() client.Object {
|
|||||||
return a.ImagePolicy
|
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
|
// imagev1.ImagePolicyList
|
||||||
|
|
||||||
type imagePolicyListAdapter struct {
|
type imagePolicyListAdapter struct {
|
||||||
@@ -121,18 +91,6 @@ func (a imagePolicyListAdapter) len() int {
|
|||||||
return len(a.ImagePolicyList.Items)
|
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
|
// autov1.ImageUpdateAutomation
|
||||||
|
|
||||||
var imageUpdateAutomationType = apiType{
|
var imageUpdateAutomationType = apiType{
|
||||||
|
|||||||
@@ -53,18 +53,6 @@ func TestImageScanning(t *testing.T) {
|
|||||||
"get image policy podinfo-regex",
|
"get image policy podinfo-regex",
|
||||||
"testdata/image/get_image_policy_regex.golden",
|
"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 {
|
for _, tc := range cases {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ type installFlags struct {
|
|||||||
force bool
|
force bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var installArgs = newInstallFlags()
|
var installArgs = NewInstallFlags()
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
installCmd.Flags().BoolVar(&installArgs.export, "export", false,
|
installCmd.Flags().BoolVar(&installArgs.export, "export", false,
|
||||||
@@ -93,7 +93,7 @@ func init() {
|
|||||||
installCmd.Flags().StringSliceVar(&installArgs.defaultComponents, "components", rootArgs.defaults.Components,
|
installCmd.Flags().StringSliceVar(&installArgs.defaultComponents, "components", rootArgs.defaults.Components,
|
||||||
"list of components, accepts comma-separated values")
|
"list of components, accepts comma-separated values")
|
||||||
installCmd.Flags().StringSliceVar(&installArgs.extraComponents, "components-extra", nil,
|
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.manifestsPath, "manifests", "", "path to the manifest directory")
|
||||||
installCmd.Flags().StringVar(&installArgs.registry, "registry", rootArgs.defaults.Registry,
|
installCmd.Flags().StringVar(&installArgs.registry, "registry", rootArgs.defaults.Registry,
|
||||||
"container registry where the toolkit images are published")
|
"container registry where the toolkit images are published")
|
||||||
@@ -115,14 +115,9 @@ func init() {
|
|||||||
rootCmd.AddCommand(installCmd)
|
rootCmd.AddCommand(installCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newInstallFlags() installFlags {
|
func NewInstallFlags() installFlags {
|
||||||
return installFlags{
|
return installFlags{
|
||||||
logLevel: flags.LogLevel(rootArgs.defaults.LogLevel),
|
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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,13 +195,10 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if installArgs.export {
|
if installArgs.export {
|
||||||
_, err = rootCmd.OutOrStdout().Write([]byte(manifest.Content))
|
fmt.Print(manifest.Content)
|
||||||
return err
|
return nil
|
||||||
} else if rootArgs.verbose {
|
} else if rootArgs.verbose {
|
||||||
_, err = rootCmd.OutOrStdout().Write([]byte(manifest.Content))
|
fmt.Print(manifest.Content)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Successf("manifests build completed")
|
logger.Successf("manifests build completed")
|
||||||
@@ -246,7 +238,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
return fmt.Errorf("install failed: %w", err)
|
return fmt.Errorf("install failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rootCmd.Println(applyOutput)
|
fmt.Fprintln(os.Stderr, applyOutput)
|
||||||
|
|
||||||
if opts.ImagePullSecret != "" && opts.RegistryCredential != "" {
|
if opts.ImagePullSecret != "" && opts.RegistryCredential != "" {
|
||||||
logger.Actionf("generating image pull secret %s", opts.ImagePullSecret)
|
logger.Actionf("generating image pull secret %s", opts.ImagePullSecret)
|
||||||
@@ -258,7 +250,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
Username: credentials[0],
|
Username: credentials[0],
|
||||||
Password: credentials[1],
|
Password: credentials[1],
|
||||||
}
|
}
|
||||||
imagePullSecret, err := sourcesecret.GenerateOCI(secretOpts)
|
imagePullSecret, err := sourcesecret.Generate(secretOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("install failed: %w", err)
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@@ -16,17 +16,7 @@ limitations under the License.
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "testing"
|
||||||
"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"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInstall(t *testing.T) {
|
func TestInstall(t *testing.T) {
|
||||||
// The pointer to kubeconfigArgs.Namespace is shared across
|
// 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.
|
Long: `The list command fetches the tags and their metadata from a remote OCI repository.
|
||||||
The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`,
|
The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`,
|
||||||
Example: ` # List the artifacts stored in an OCI repository
|
Example: ` # List the artifacts stored in an OCI repository
|
||||||
flux list artifacts oci://ghcr.io/org/config/app
|
flux list artifact oci://ghcr.io/org/config/app
|
||||||
`,
|
`,
|
||||||
RunE: listArtifactsCmdRun,
|
RunE: listArtifactsCmdRun,
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ func listArtifactsCmdRun(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
if listArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
if listArtifactArgs.provider.String() != sourcev1.GenericOCIProvider {
|
||||||
logger.Actionf("logging in to registry with provider credentials")
|
logger.Actionf("logging in to registry with provider credentials")
|
||||||
ociOpt, _, err := loginWithProvider(ctx, url, listArtifactArgs.provider.String())
|
ociOpt, err := loginWithProvider(ctx, url, listArtifactArgs.provider.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error during login with provider: %w", err)
|
return fmt.Errorf("error during login with provider: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,16 +100,6 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
|
|||||||
# Uninstall Flux and delete CRDs
|
# Uninstall Flux and delete CRDs
|
||||||
flux uninstall`,
|
flux uninstall`,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// If opted in via --ns-follows-kube-context flag or
|
|
||||||
// FLUX_NS_FOLLOWS_KUBE_CONTEXT env var, and --namespace was not
|
|
||||||
// explicitly set, respect the namespace from the kubeconfig context.
|
|
||||||
if !cmd.Flags().Changed("namespace") &&
|
|
||||||
(rootArgs.nsFollowsKubeContext || os.Getenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT") != "") {
|
|
||||||
if ctxNs := getKubeconfigContextNamespace(kubeconfigArgs); ctxNs != "" {
|
|
||||||
*kubeconfigArgs.Namespace = ctxNs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ns, err := cmd.Flags().GetString("namespace")
|
ns, err := cmd.Flags().GetString("namespace")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error getting namespace: %w", err)
|
return fmt.Errorf("error getting namespace: %w", err)
|
||||||
@@ -126,11 +116,10 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
|
|||||||
var logger = stderrLogger{stderr: os.Stderr}
|
var logger = stderrLogger{stderr: os.Stderr}
|
||||||
|
|
||||||
type rootFlags struct {
|
type rootFlags struct {
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
verbose bool
|
verbose bool
|
||||||
pollInterval time.Duration
|
pollInterval time.Duration
|
||||||
nsFollowsKubeContext bool
|
defaults install.Options
|
||||||
defaults install.Options
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestError is a custom error type that wraps an error returned by the flux api.
|
// RequestError is a custom error type that wraps an error returned by the flux api.
|
||||||
@@ -150,8 +139,6 @@ var kubeclientOptions = new(runclient.Options)
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation")
|
rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation")
|
||||||
rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects")
|
rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects")
|
||||||
rootCmd.PersistentFlags().BoolVar(&rootArgs.nsFollowsKubeContext, "ns-follows-kube-context", false,
|
|
||||||
"use the namespace from the kubeconfig context instead of the default flux-system namespace, can also be set via FLUX_NS_FOLLOWS_KUBE_CONTEXT env var")
|
|
||||||
|
|
||||||
configureDefaultNamespace()
|
configureDefaultNamespace()
|
||||||
kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag
|
kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag
|
||||||
@@ -193,14 +180,12 @@ func main() {
|
|||||||
|
|
||||||
// This is required because controller-runtime expects its consumers to
|
// This is required because controller-runtime expects its consumers to
|
||||||
// set a logger through log.SetLogger within 30 seconds of the program's
|
// set a logger through log.SetLogger within 30 seconds of the program's
|
||||||
// initialization. If not set, the entire debug stack is printed as an
|
// initalization. If not set, the entire debug stack is printed as an
|
||||||
// error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59
|
// error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59
|
||||||
// Since we have our own logging and don't care about controller-runtime's
|
// Since we have our own logging and don't care about controller-runtime's
|
||||||
// logger, we configure it's logger to do nothing.
|
// logger, we configure it's logger to do nothing.
|
||||||
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{}))
|
||||||
|
|
||||||
registerPlugins()
|
|
||||||
|
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
|
||||||
if err, ok := err.(*RequestError); ok {
|
if err, ok := err.(*RequestError); ok {
|
||||||
@@ -218,26 +203,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getKubeconfigContextNamespace returns the namespace from the current
|
|
||||||
// kubeconfig context, or an empty string if it cannot be determined.
|
|
||||||
func getKubeconfigContextNamespace(cf *genericclioptions.ConfigFlags) string {
|
|
||||||
rawConfig, err := cf.ToRawKubeConfigLoader().RawConfig()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
currentContext := rawConfig.CurrentContext
|
|
||||||
if cf.Context != nil && *cf.Context != "" {
|
|
||||||
currentContext = *cf.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx, ok := rawConfig.Contexts[currentContext]; ok {
|
|
||||||
return ctx.Namespace
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func configureDefaultNamespace() {
|
func configureDefaultNamespace() {
|
||||||
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
|
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
|
||||||
fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE")
|
fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE")
|
||||||
@@ -260,9 +225,7 @@ func configureDefaultNamespace() {
|
|||||||
func readPasswordFromStdin(prompt string) (string, error) {
|
func readPasswordFromStdin(prompt string) (string, error) {
|
||||||
var out string
|
var out string
|
||||||
var err error
|
var err error
|
||||||
if _, err := fmt.Fprint(os.Stdout, prompt); err != nil {
|
fmt.Fprint(os.Stdout, prompt)
|
||||||
return "", fmt.Errorf("failed to write prompt: %w", err)
|
|
||||||
}
|
|
||||||
stdinFD := int(os.Stdin.Fd())
|
stdinFD := int(os.Stdin.Fd())
|
||||||
if term.IsTerminal(stdinFD) {
|
if term.IsTerminal(stdinFD) {
|
||||||
var inBytes []byte
|
var inBytes []byte
|
||||||
@@ -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.`
|
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)
|
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
|
// in subsequent executions which causes tests to fail that rely on the value
|
||||||
// of "Changed".
|
// of "Changed".
|
||||||
resumeCmd.PersistentFlags().Lookup("wait").Changed = false
|
resumeCmd.PersistentFlags().Lookup("wait").Changed = false
|
||||||
// Reset the help flag value and Changed state so that a prior
|
|
||||||
// "--help" invocation does not leak into subsequent test runs.
|
|
||||||
if hf := rootCmd.Flags().Lookup("help"); hf != nil {
|
|
||||||
hf.Value.Set("false")
|
|
||||||
hf.Changed = false
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
args, err := shellwords.Parse(cmd)
|
args, err := shellwords.Parse(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -453,16 +447,13 @@ func resetCmdArgs() {
|
|||||||
imagePolicyArgs = imagePolicyFlags{}
|
imagePolicyArgs = imagePolicyFlags{}
|
||||||
imageRepoArgs = imageRepoFlags{}
|
imageRepoArgs = imageRepoFlags{}
|
||||||
imageUpdateArgs = imageUpdateFlags{}
|
imageUpdateArgs = imageUpdateFlags{}
|
||||||
installArgs = newInstallFlags()
|
|
||||||
kustomizationArgs = NewKustomizationFlags()
|
kustomizationArgs = NewKustomizationFlags()
|
||||||
receiverArgs = receiverFlags{}
|
receiverArgs = receiverFlags{}
|
||||||
resumeArgs = ResumeFlags{}
|
resumeArgs = ResumeFlags{}
|
||||||
rhrArgs = reconcileHelmReleaseFlags{}
|
rhrArgs = reconcileHelmReleaseFlags{}
|
||||||
rksArgs = reconcileKsFlags{}
|
rksArgs = reconcileKsFlags{}
|
||||||
secretGitArgs = NewSecretGitFlags()
|
secretGitArgs = NewSecretGitFlags()
|
||||||
secretGitHubAppArgs = secretGitHubAppFlags{}
|
|
||||||
secretProxyArgs = secretProxyFlags{}
|
secretProxyArgs = secretProxyFlags{}
|
||||||
secretReceiverArgs = secretReceiverFlags{}
|
|
||||||
secretHelmArgs = secretHelmFlags{}
|
secretHelmArgs = secretHelmFlags{}
|
||||||
secretTLSArgs = secretTLSFlags{}
|
secretTLSArgs = secretTLSFlags{}
|
||||||
sourceBucketArgs = sourceBucketFlags{}
|
sourceBucketArgs = sourceBucketFlags{}
|
||||||
|
|||||||
@@ -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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/google/go-containerregistry/pkg/authn"
|
|
||||||
"github.com/google/go-containerregistry/pkg/crane"
|
"github.com/google/go-containerregistry/pkg/crane"
|
||||||
|
|
||||||
"github.com/fluxcd/pkg/auth"
|
"github.com/fluxcd/pkg/auth"
|
||||||
@@ -29,14 +29,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// loginWithProvider gets a crane authentication option for the given provider and URL.
|
// loginWithProvider gets a crane authentication option for the given provider and URL.
|
||||||
func loginWithProvider(ctx context.Context, url, provider string) (crane.Option, authn.Authenticator, error) {
|
func loginWithProvider(ctx context.Context, url, provider string) (crane.Option, error) {
|
||||||
var opts []auth.Option
|
var opts []auth.Option
|
||||||
if provider == azure.ProviderName {
|
if provider == azure.ProviderName {
|
||||||
opts = append(opts, auth.WithAllowShellOut())
|
opts = append(opts, auth.WithAllowShellOut())
|
||||||
}
|
}
|
||||||
authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, url, opts...)
|
authenticator, err := authutils.GetArtifactRegistryCredentials(ctx, provider, url, opts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 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