Compare commits

..

No commits in common. 'main' and 'v2.1.0' have entirely different histories.
main ... v2.1.0

@ -4,16 +4,19 @@ pkgbase = flux-bin
pkgrel = ${PKGREL}
url = https://fluxcd.io/
arch = x86_64
arch = armv6h
arch = armv7h
arch = aarch64
license = APACHE
optdepends = bash-completion: auto-completion for flux in Bash
optdepends = zsh-completions: auto-completion for flux in ZSH
source_x86_64 = flux-bin-${PKGVER}_linux_amd64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_amd64.tar.gz
source_x86_64 = flux-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_amd64.tar.gz
sha256sums_x86_64 = ${SHA256SUM_AMD64}
source_armv7h = flux-bin-${PKGVER}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm.tar.gz
source_armv6h = flux-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm.tar.gz
sha256sums_armv6h = ${SHA256SUM_ARM}
source_armv7h = flux-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm.tar.gz
sha256sums_armv7h = ${SHA256SUM_ARM}
source_aarch64 = flux-bin-${PKGVER}_linux_arm64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm64.tar.gz
source_aarch64 = flux-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm64.tar.gz
sha256sums_aarch64 = ${SHA256SUM_ARM64}
pkgname = flux-bin

@ -8,22 +8,28 @@ _srcname=flux
_srcver=${VERSION}
pkgdesc="Open and extensible continuous delivery solution for Kubernetes"
url="https://fluxcd.io/"
arch=("x86_64" "armv7h" "aarch64")
arch=("x86_64" "armv6h" "armv7h" "aarch64")
license=("APACHE")
optdepends=('bash-completion: auto-completion for flux in Bash'
'zsh-completions: auto-completion for flux in ZSH')
source_x86_64=(
"${pkgname}-${pkgver}_linux_amd64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_amd64.tar.gz"
"${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_amd64.tar.gz"
)
source_armv6h=(
"${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm.tar.gz"
)
source_armv7h=(
"${pkgname}-${pkgver}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm.tar.gz"
"${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm.tar.gz"
)
source_aarch64=(
"${pkgname}-${pkgver}_linux_arm64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm64.tar.gz"
"${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm64.tar.gz"
)
sha256sums_x86_64=(
${SHA256SUM_AMD64}
)
sha256sums_armv6h=(
${SHA256SUM_ARM}
)
sha256sums_armv7h=(
${SHA256SUM_ARM}
)

@ -4,6 +4,7 @@ pkgbase = flux-go
pkgrel = ${PKGREL}
url = https://fluxcd.io/
arch = x86_64
arch = armv6h
arch = armv7h
arch = aarch64
license = APACHE

@ -8,7 +8,7 @@ _srcname=flux
_srcver=${VERSION}
pkgdesc="Open and extensible continuous delivery solution for Kubernetes"
url="https://fluxcd.io/"
arch=("x86_64" "armv7h" "aarch64")
arch=("x86_64" "armv6h" "armv7h" "aarch64")
license=("APACHE")
provides=("flux-bin")
conflicts=("flux-bin")
@ -41,7 +41,7 @@ check() {
aarch64)
export ENVTEST_ARCH=arm64
;;
armv7h)
armv6h|armv7h)
export ENVTEST_ARCH=arm
;;
esac

@ -4,6 +4,7 @@ pkgbase = flux-scm
pkgrel = ${PKGREL}
url = https://fluxcd.io/
arch = x86_64
arch = armv6h
arch = armv7h
arch = aarch64
license = APACHE

@ -7,7 +7,7 @@ pkgrel=${PKGREL}
_srcname=flux
pkgdesc="Open and extensible continuous delivery solution for Kubernetes"
url="https://fluxcd.io/"
arch=("x86_64" "armv7h" "aarch64")
arch=("x86_64" "armv6h" "armv7h" "aarch64")
license=("APACHE")
provides=("flux-bin")
conflicts=("flux-bin")
@ -42,7 +42,7 @@ check() {
aarch64)
export ENVTEST_ARCH=arm64
;;
armv7h)
armv6h|armv7h)
export ENVTEST_ARCH=arm
;;
esac

@ -1,9 +1,5 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
networking:
disableDefaultCNI: true # disable kindnet
podSubnet: 192.168.0.0/16 # set to Calico's default subnet

@ -44,12 +44,6 @@
description: Feature request proposals in the RFC format
color: '#D621C3'
aliases: ['area/RFC']
- name: backport:release/v2.3.x
description: To be backported to release/v2.3.x
color: '#ffd700'
- name: backport:release/v2.4.x
description: To be backported to release/v2.4.x
color: '#ffd700'
- name: backport:release/v2.5.x
description: To be backported to release/v2.5.x
- name: backport:release/v2.0.x
description: To be backported to release/v2.0.x
color: '#ffd700'

@ -5,17 +5,15 @@ The Flux ARM64 end-to-end tests run on Equinix Metal instances provisioned with
## Current instances
| Repository | Runner | Instance | Location |
|-----------------------------|------------------|----------------|---------------|
| flux2 | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| flux2 | equinix-arm-dc-2 | flux-arm-dc-01 | Washington DC |
| flux2 | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| flux2 | equinix-arm-da-2 | flux-arm-da-01 | Dallas |
| flux-benchmark | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| flux-benchmark | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| source-controller | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| source-controller | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| image-automation-controller | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| image-automation-controller | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
|-----------------------------|------------------|------------------------|---------------|
| flux2 | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC |
| flux2 | equinix-arm-dc-2 | flux-equinix-arm-dc-01 | Washington DC |
| flux2 | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas |
| flux2 | equinix-arm-da-2 | flux-equinix-arm-da-01 | Dallas |
| source-controller | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC |
| source-controller | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas |
| image-automation-controller | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC |
| image-automation-controller | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas |
Instance spec:
- Ampere Altra Q80-30 80-core processor @ 2.8GHz

@ -18,11 +18,11 @@
set -eu
KIND_VERSION=0.22.0
KUBECTL_VERSION=1.29.0
KUSTOMIZE_VERSION=5.3.0
HELM_VERSION=3.14.1
GITHUB_RUNNER_VERSION=2.313.0
KIND_VERSION=0.17.0
KUBECTL_VERSION=1.24.0
KUSTOMIZE_VERSION=4.5.7
HELM_VERSION=3.10.1
GITHUB_RUNNER_VERSION=2.298.2
PACKAGES="apt-transport-https ca-certificates software-properties-common build-essential libssl-dev gnupg lsb-release jq pkg-config"
# install prerequisites

@ -22,7 +22,7 @@ RUNNER_NAME=$1
REPOSITORY_TOKEN=$2
REPOSITORY_URL=${3:-https://github.com/fluxcd/flux2}
GITHUB_RUNNER_VERSION=2.313.0
GITHUB_RUNNER_VERSION=2.298.2
# download runner
curl -o actions-runner-linux-arm64.tar.gz -L https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/actions-runner-linux-arm64-${GITHUB_RUNNER_VERSION}.tar.gz \

@ -24,6 +24,6 @@ jobs:
name: action on ${{ matrix.version }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup flux
uses: ./action

@ -4,9 +4,6 @@ on:
pull_request_target:
types: [closed, labeled]
permissions:
contents: read
jobs:
pull-request:
runs-on: ubuntu-latest
@ -16,11 +13,11 @@ jobs:
if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@be567af183754f6a5d831ae90f648954763f17f5 # v3.1.0
uses: korthout/backport-action@bd68141f079bd036e45ea8149bc9d174d5a04703 # v1.4.0
# xref: https://github.com/korthout/backport-action#inputs
with:
# Use token to allow workflows to be triggered for the created PR

@ -1,256 +0,0 @@
name: conformance
on:
workflow_dispatch:
push:
branches: [ 'main', 'update-components', 'release/**', 'conform*' ]
permissions:
contents: read
env:
GO_VERSION: 1.23.x
jobs:
conform-kubernetes:
runs-on:
group: "ARM64"
strategy:
matrix:
# Keep this list up-to-date with https://endoflife.date/kubernetes
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
KUBERNETES_VERSION: [1.30.9, 1.31.5, 1.32.1 ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Prepare
id: prep
run: |
ID=${GITHUB_SHA:0:7}-${{ matrix.KUBERNETES_VERSION }}-$(date +%s)
echo "CLUSTER=arm64-${ID}" >> $GITHUB_OUTPUT
- name: Build
run: |
make build
- name: Setup Kubernetes
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
with:
version: v0.22.0
cluster_name: ${{ steps.prep.outputs.CLUSTER }}
node_image: ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
- name: Run e2e tests
run: TEST_KUBECONFIG=$HOME/.kube/config make e2e
- name: Run multi-tenancy tests
run: |
./bin/flux install
./bin/flux create source git flux-system \
--interval=15m \
--url=https://github.com/fluxcd/flux2-multi-tenancy \
--branch=main \
--ignore-paths="./clusters/**/flux-system/"
./bin/flux create kustomization flux-system \
--interval=15m \
--source=flux-system \
--path=./clusters/staging
kubectl -n flux-system wait kustomization/tenants --for=condition=ready --timeout=5m
kubectl -n apps wait kustomization/dev-team --for=condition=ready --timeout=1m
kubectl -n apps wait helmrelease/podinfo --for=condition=ready --timeout=1m
- name: Debug failure
if: failure()
run: |
kubectl -n flux-system get all
kubectl -n flux-system describe po
kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller
conform-k3s:
runs-on: ubuntu-latest
strategy:
matrix:
# Keep this list up-to-date with https://endoflife.date/kubernetes
# Available versions can be found with "replicated cluster versions"
K3S_VERSION: [ 1.30.9, 1.31.5, 1.32.1 ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Prepare
id: prep
run: |
ID=${GITHUB_SHA:0:7}-${{ matrix.K3S_VERSION }}-$(date +%s)
PSEUDO_RAND_SUFFIX=$(echo "${ID}" | shasum | awk '{print $1}')
echo "cluster=flux2-k3s-${PSEUDO_RAND_SUFFIX}" >> $GITHUB_OUTPUT
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
- name: Build
run: make build-dev
- name: Create repository
run: |
gh repo create --private --add-readme fluxcd-testing/${{ steps.prep.outputs.cluster }}
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Create cluster
id: create-cluster
uses: replicatedhq/replicated-actions/create-cluster@c98ab3b97925af5db9faf3f9676df7a9c6736985 # v1.17.0
with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
kubernetes-distribution: "k3s"
kubernetes-version: ${{ matrix.K3S_VERSION }}
ttl: 20m
cluster-name: "${{ steps.prep.outputs.cluster }}"
kubeconfig-path: ${{ steps.prep.outputs.kubeconfig-path }}
export-kubeconfig: true
- name: Run e2e tests
run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e
- name: Run flux bootstrap
run: |
./bin/flux bootstrap git --manifests ./manifests/install/ \
--components-extra=image-reflector-controller,image-automation-controller \
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
--branch=main \
--path=clusters/k3s \
--token-auth
env:
GIT_PASSWORD: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Run flux check
run: |
./bin/flux check
- name: Run flux reconcile
run: |
./bin/flux reconcile ks flux-system --with-source
./bin/flux get all
./bin/flux events
- name: Collect reconcile logs
if: ${{ always() }}
continue-on-error: true
run: |
kubectl -n flux-system get all
kubectl -n flux-system describe pods
kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller
kubectl -n flux-system logs deploy/notification-controller
- name: Delete flux
run: |
./bin/flux uninstall -s --keep-namespace
kubectl delete ns flux-system --wait
- name: Delete cluster
if: ${{ always() }}
uses: replicatedhq/replicated-actions/remove-cluster@c98ab3b97925af5db9faf3f9676df7a9c6736985 # v1.17.0
continue-on-error: true
with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
cluster-id: ${{ steps.create-cluster.outputs.cluster-id }}
- name: Delete repository
if: ${{ always() }}
continue-on-error: true
run: |
gh repo delete fluxcd-testing/${{ steps.prep.outputs.cluster }} --yes
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
conform-openshift:
runs-on: ubuntu-latest
strategy:
matrix:
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
OPENSHIFT_VERSION: [ 4.17.0-okd ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Prepare
id: prep
run: |
ID=${GITHUB_SHA:0:7}-${{ matrix.OPENSHIFT_VERSION }}-$(date +%s)
PSEUDO_RAND_SUFFIX=$(echo "${ID}" | shasum | awk '{print $1}')
echo "cluster=flux2-openshift-${PSEUDO_RAND_SUFFIX}" >> $GITHUB_OUTPUT
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
- name: Build
run: make build-dev
- name: Create repository
run: |
gh repo create --private --add-readme fluxcd-testing/${{ steps.prep.outputs.cluster }}
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Create cluster
id: create-cluster
uses: replicatedhq/replicated-actions/create-cluster@c98ab3b97925af5db9faf3f9676df7a9c6736985 # v1.17.0
with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
kubernetes-distribution: "openshift"
kubernetes-version: ${{ matrix.OPENSHIFT_VERSION }}
ttl: 20m
cluster-name: "${{ steps.prep.outputs.cluster }}"
kubeconfig-path: ${{ steps.prep.outputs.kubeconfig-path }}
export-kubeconfig: true
- name: Run flux bootstrap
run: |
./bin/flux bootstrap git --manifests ./manifests/openshift/ \
--components-extra=image-reflector-controller,image-automation-controller \
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
--branch=main \
--path=clusters/openshift \
--token-auth
env:
GIT_PASSWORD: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Run flux check
run: |
./bin/flux check
- name: Run flux reconcile
run: |
./bin/flux reconcile ks flux-system --with-source
./bin/flux get all
./bin/flux events
- name: Collect reconcile logs
if: ${{ always() }}
continue-on-error: true
run: |
kubectl -n flux-system get all
kubectl -n flux-system describe pods
kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller
kubectl -n flux-system logs deploy/notification-controller
- name: Delete flux
run: |
./bin/flux uninstall -s --keep-namespace
kubectl delete ns flux-system --wait
- name: Delete cluster
if: ${{ always() }}
uses: replicatedhq/replicated-actions/remove-cluster@c98ab3b97925af5db9faf3f9676df7a9c6736985 # v1.17.0
continue-on-error: true
with:
api-token: ${{ secrets.REPLICATED_API_TOKEN }}
cluster-id: ${{ steps.create-cluster.outputs.cluster-id }}
- name: Delete repository
if: ${{ always() }}
continue-on-error: true
run: |
gh repo delete fluxcd-testing/${{ steps.prep.outputs.cluster }} --yes
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}

@ -0,0 +1,106 @@
name: e2e-arm64
on:
workflow_dispatch:
push:
branches: [ 'main', 'update-components', 'e2e-*', 'release/**' ]
permissions:
contents: read
jobs:
e2e-arm64-kubernetes:
# Hosted on Equinix
# Docs: https://github.com/fluxcd/flux2/tree/main/.github/runners
runs-on: [self-hosted, Linux, ARM64, equinix]
strategy:
matrix:
# Keep this list up-to-date with https://endoflife.date/kubernetes
# Check which versions are available on DockerHub with 'crane ls kindest/node'
KUBERNETES_VERSION: [ 1.25.11, 1.26.6, 1.27.3, 1.28.0 ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version: 1.20.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Prepare
id: prep
run: |
ID=${GITHUB_SHA:0:7}-${{ matrix.KUBERNETES_VERSION }}-$(date +%s)
echo "CLUSTER=arm64-${ID}" >> $GITHUB_OUTPUT
- name: Build
run: |
make build
- name: Setup Kubernetes Kind
run: |
kind create cluster \
--wait 5m \
--name ${{ steps.prep.outputs.CLUSTER }} \
--kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }} \
--image=kindest/node:v${{ matrix.KUBERNETES_VERSION }}
- name: Run e2e tests
run: TEST_KUBECONFIG=/tmp/${{ steps.prep.outputs.CLUSTER }} make e2e
- name: Run multi-tenancy tests
env:
KUBECONFIG: /tmp/${{ steps.prep.outputs.CLUSTER }}
run: |
./bin/flux install
./bin/flux create source git flux-system \
--interval=15m \
--url=https://github.com/fluxcd/flux2-multi-tenancy \
--branch=main \
--ignore-paths="./clusters/**/flux-system/"
./bin/flux create kustomization flux-system \
--interval=15m \
--source=flux-system \
--path=./clusters/staging
kubectl -n flux-system wait kustomization/tenants --for=condition=ready --timeout=5m
kubectl -n apps wait kustomization/dev-team --for=condition=ready --timeout=1m
kubectl -n apps wait helmrelease/podinfo --for=condition=ready --timeout=1m
- name: Run monitoring tests
# Keep this test in sync with https://fluxcd.io/flux/guides/monitoring/
env:
KUBECONFIG: /tmp/${{ steps.prep.outputs.CLUSTER }}
run: |
./bin/flux create source git flux-monitoring \
--interval=30m \
--url=https://github.com/fluxcd/flux2 \
--branch=${GITHUB_REF#refs/heads/}
./bin/flux create kustomization kube-prometheus-stack \
--interval=1h \
--prune \
--source=flux-monitoring \
--path="./manifests/monitoring/kube-prometheus-stack" \
--health-check-timeout=5m \
--wait
./bin/flux create kustomization monitoring-config \
--depends-on=kube-prometheus-stack \
--interval=1h \
--prune=true \
--source=flux-monitoring \
--path="./manifests/monitoring/monitoring-config" \
--health-check-timeout=1m \
--wait
kubectl -n flux-system wait kustomization/kube-prometheus-stack --for=condition=ready --timeout=5m
kubectl -n flux-system wait kustomization/monitoring-config --for=condition=ready --timeout=5m
kubectl -n monitoring wait helmrelease/kube-prometheus-stack --for=condition=ready --timeout=1m
- name: Debug failure
if: failure()
env:
KUBECONFIG: /tmp/${{ steps.prep.outputs.CLUSTER }}
run: |
kubectl -n flux-system get all
kubectl -n flux-system describe po
kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller
- name: Cleanup
if: always()
run: |
kind delete cluster --name ${{ steps.prep.outputs.CLUSTER }}
rm /tmp/${{ steps.prep.outputs.CLUSTER }}

@ -21,73 +21,49 @@ permissions:
contents: read
jobs:
e2e-aks:
e2e-amd64-aks:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./tests/integration
# This job is currently disabled. Remove the false check when Azure subscription is enabled.
# This job is currently disabled since if always evaluates to false. Remove the false check when Azure subscription is enabled
if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps:
- name: CheckoutD
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Checkout
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version: 1.23.x
cache-dependency-path: tests/integration/go.sum
- name: Setup Terraform
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
go-version: 1.20.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Flux CLI
run: make build
working-directory: ./
run: |
make build
mkdir -p $HOME/.local/bin
mv ./bin/flux $HOME/.local/bin
- name: Setup SOPS
run: |
wget https://github.com/mozilla/sops/releases/download/v3.7.1/sops-v3.7.1.linux
chmod +x sops-v3.7.1.linux
mkdir -p $HOME/.local/bin
wget -O $HOME/.local/bin/sops https://github.com/mozilla/sops/releases/download/v$SOPS_VER/sops-v$SOPS_VER.linux
chmod +x $HOME/.local/bin/sops
env:
SOPS_VER: 3.7.1
- name: Authenticate to Azure
uses: Azure/login@a65d910e8af852a8061c627c456678983e180302 # v1.4.6
mv sops-v3.7.1.linux $HOME/.local/bin/sops
- name: Setup Terraform
uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2
with:
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
terraform_version: 1.2.8
terraform_wrapper: false
- name: Setup Azure CLI
run: |
cat > .env <<EOF
export TF_VAR_tags='{ "environment"="github", "ci"="true", "repo"="flux2", "createdat"="$(date -u +x%Y-%m-%d_%Hh%Mm%Ss)" }'
EOF
- name: Print .env for dynamic tag value reference
run: cat .env
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- name: Run Azure e2e tests
env:
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
TF_VAR_location: ${{ vars.TF_VAR_azure_location }}
GITREPO_SSH_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_CONTENTS }}
GITREPO_SSH_PUB_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_PUB_CONTENTS }}
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
run: |
source .env
mkdir -p ./build/ssh
touch ./build/ssh/key
echo $GITREPO_SSH_CONTENTS | base64 -d > build/ssh/key
export GITREPO_SSH_PATH=build/ssh/key
touch ./build/ssh/key.pub
echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
make test-azure
- name: Ensure resource cleanup
if: ${{ always() }}
env:
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
TF_VAR_location: ${{ vars.TF_VAR_azure_location }}
run: source .env && make destroy-azure
echo $HOME
echo $PATH
ls $HOME/.local/bin
az login --service-principal -u ${ARM_CLIENT_ID} -p ${ARM_CLIENT_SECRET} -t ${ARM_TENANT_ID}
cd ./tests/azure
go test -v -coverprofile cover.out -timeout 60m .

@ -17,29 +17,29 @@ jobs:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version: 1.23.x
go-version: 1.20.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Kubernetes
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0
with:
version: v0.24.0
version: v0.20.0
cluster_name: kind
# The versions below should target the newest Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes
node_image: ghcr.io/fluxcd/kindest/node:v1.31.0-amd64
kubectl_version: v1.31.0
node_image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c
kubectl_version: v1.28.0
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
- name: Setup yq
uses: fluxcd/pkg/actions/yq@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
uses: fluxcd/pkg/actions/kustomize@main
- name: Build
run: make build-dev
run: |
make cmd/flux/.manifests.done
go build -o /tmp/flux ./cmd/flux
- name: Set outputs
id: vars
run: |
@ -51,24 +51,18 @@ jobs:
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
- name: bootstrap init
run: |
./bin/flux bootstrap github --manifests ./manifests/install/ \
/tmp/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--image-pull-secret=ghcr-auth \
--registry-creds=fluxcd:$GITHUB_TOKEN \
--repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \
--path=test-cluster \
--team=team-z
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: verify image pull secret
run: |
kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson
- name: bootstrap no-op
run: |
./bin/flux bootstrap github --manifests ./manifests/install/ \
/tmp/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--image-pull-secret=ghcr-auth \
--repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \
--path=test-cluster \
@ -78,7 +72,7 @@ jobs:
- name: bootstrap customize
run: |
make setup-bootstrap-patch
./bin/flux bootstrap github --manifests ./manifests/install/ \
/tmp/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \
@ -93,31 +87,46 @@ jobs:
GITHUB_ORG_NAME: fluxcd-testing
- name: uninstall
run: |
./bin/flux uninstall -s --keep-namespace
/tmp/flux uninstall -s --keep-namespace
kubectl delete ns flux-system --timeout=10m --wait=true
- name: test image automation
run: |
make setup-image-automation
./bin/flux bootstrap github --manifests ./manifests/install/ \
/tmp/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \
--path=test-cluster \
--read-write-key
./bin/flux reconcile image repository podinfo
./bin/flux reconcile image update flux-system
./bin/flux get images all
kubectl -n flux-system get -o yaml ImageUpdateAutomation flux-system | \
yq '.status.lastPushCommit | length > 1' | grep 'true'
/tmp/flux reconcile image repository podinfo
/tmp/flux get images all
retries=10
count=0
ok=false
until ${ok}; do
/tmp/flux get image update flux-system | grep 'commit' && ok=true || ok=false
count=$(($count + 1))
if [[ ${count} -eq ${retries} ]]; then
echo "No more retries left"
exit 1
fi
sleep 6
/tmp/flux reconcile image update flux-system
done
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }}
GITHUB_ORG_NAME: fluxcd-testing
- name: delete repository
if: ${{ always() }}
continue-on-error: true
run: |
gh repo delete fluxcd-testing/${{ steps.vars.outputs.test_repo_name }} --yes
curl \
-X DELETE \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token ${GITHUB_TOKEN}" \
--fail --silent \
https://api.github.com/repos/fluxcd-testing/${{ steps.vars.outputs.test_repo_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Debug failure

@ -1,104 +0,0 @@
name: e2e-gcp
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'
push:
branches:
- main
paths:
- 'tests/**'
- '.github/workflows/e2e-gcp.yaml'
pull_request:
branches:
- main
paths:
- 'tests/**'
- '.github/workflows/e2e-gcp.yaml'
permissions:
contents: read
jobs:
e2e-gcp:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./tests/integration
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: 1.23.x
cache-dependency-path: tests/integration/go.sum
- name: Setup Terraform
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
- name: Setup Flux CLI
run: make build
working-directory: ./
- name: Setup SOPS
run: |
mkdir -p $HOME/.local/bin
wget -O $HOME/.local/bin/sops https://github.com/mozilla/sops/releases/download/v$SOPS_VER/sops-v$SOPS_VER.linux
chmod +x $HOME/.local/bin/sops
env:
SOPS_VER: 3.7.1
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
id: 'auth'
with:
credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}'
token_format: 'access_token'
- name: Setup gcloud
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4
- name: Setup QEMU
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3.4.0
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: Log into us-central1-docker.pkg.dev
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: us-central1-docker.pkg.dev
username: oauth2accesstoken
password: ${{ steps.auth.outputs.access_token }}
- name: Set dynamic variables in .env
run: |
cat > .env <<EOF
export TF_VAR_tags='{ "environment"="github", "ci"="true", "repo"="flux2", "createdat"="$(date -u +x%Y-%m-%d_%Hh%Mm%Ss)" }'
EOF
- name: Print .env for dynamic tag value reference
run: cat .env
- name: Run GCP e2e tests
env:
TF_VAR_gcp_project_id: ${{ vars.TF_VAR_gcp_project_id }}
TF_VAR_gcp_region: ${{ vars.TF_VAR_gcp_region }}
TF_VAR_gcp_zone: ${{ vars.TF_VAR_gcp_zone }}
TF_VAR_gcp_email: ${{ secrets.TF_VAR_gcp_email }}
TF_VAR_gcp_keyring: ${{ secrets.TF_VAR_gcp_keyring }}
TF_VAR_gcp_crypto_key: ${{ secrets.TF_VAR_gcp_crypto_key }}
GITREPO_SSH_CONTENTS: ${{ secrets.GCP_GITREPO_SSH_CONTENTS }}
GITREPO_SSH_PUB_CONTENTS: ${{ secrets.GCP_GITREPO_SSH_PUB_CONTENTS }}
run: |
source .env
mkdir -p ./build/ssh
touch ./build/ssh/key
echo $GITREPO_SSH_CONTENTS | base64 -d > build/ssh/key
export GITREPO_SSH_PATH=build/ssh/key
touch ./build/ssh/key.pub
echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
make test-gcp
- name: Ensure resource cleanup
if: ${{ always() }}
env:
TF_VAR_gcp_project_id: ${{ vars.TF_VAR_gcp_project_id }}
TF_VAR_gcp_region: ${{ vars.TF_VAR_gcp_region }}
TF_VAR_gcp_zone: ${{ vars.TF_VAR_gcp_zone }}
TF_VAR_gcp_email: ${{ secrets.TF_VAR_gcp_email }}
TF_VAR_gcp_keyring: ${{ secrets.TF_VAR_gcp_keyring }}
TF_VAR_gcp_crypto_key: ${{ secrets.TF_VAR_gcp_crypto_key }}
run: source .env && make destroy-gcp

@ -13,9 +13,7 @@ permissions:
jobs:
e2e-amd64-kubernetes:
runs-on:
group: "Default Larger Runners"
labels: ubuntu-latest-16-cores
runs-on: ubuntu-latest
services:
registry:
image: registry:2
@ -23,30 +21,30 @@ jobs:
- 5000:5000
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version: 1.23.x
go-version: 1.20.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Kubernetes
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0
with:
version: v0.24.0
version: v0.20.0
cluster_name: kind
wait: 5s
config: .github/kind/config.yaml # disable KIND-net
# The versions below should target the oldest supported Kubernetes version
# The versions below should target the newest Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes
node_image: ghcr.io/fluxcd/kindest/node:v1.30.9-amd64
kubectl_version: v1.30.9
node_image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c
kubectl_version: v1.28.0
- name: Setup Calico for network policy
run: |
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
kubectl apply -f https://docs.projectcalico.org/v3.25/manifests/calico.yaml
kubectl -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
uses: fluxcd/pkg/actions/kustomize@main
- name: Run tests
run: make test
- name: Run e2e tests
@ -59,43 +57,44 @@ jobs:
exit 1
fi
- name: Build
run: make build-dev
run: |
go build -o /tmp/flux ./cmd/flux
- name: flux check --pre
run: |
./bin/flux check --pre
/tmp/flux check --pre
- name: flux install --manifests
run: |
./bin/flux install --manifests ./manifests/install/
/tmp/flux install --manifests ./manifests/install/
- name: flux create secret
run: |
./bin/flux create secret git git-ssh-test \
/tmp/flux create secret git git-ssh-test \
--url ssh://git@github.com/stefanprodan/podinfo
./bin/flux create secret git git-https-test \
/tmp/flux create secret git git-https-test \
--url https://github.com/stefanprodan/podinfo \
--username=test --password=test
./bin/flux create secret helm helm-test \
/tmp/flux create secret helm helm-test \
--username=test --password=test
- name: flux create source git
run: |
./bin/flux create source git podinfo \
/tmp/flux create source git podinfo \
--url https://github.com/stefanprodan/podinfo \
--tag-semver=">=6.3.5"
- name: flux create source git export apply
run: |
./bin/flux create source git podinfo-export \
/tmp/flux create source git podinfo-export \
--url https://github.com/stefanprodan/podinfo \
--tag-semver=">=6.3.5" \
--export | kubectl apply -f -
./bin/flux delete source git podinfo-export --silent
/tmp/flux delete source git podinfo-export --silent
- name: flux get sources git
run: |
./bin/flux get sources git
/tmp/flux get sources git
- name: flux get sources git --all-namespaces
run: |
./bin/flux get sources git --all-namespaces
/tmp/flux get sources git --all-namespaces
- name: flux create kustomization
run: |
./bin/flux create kustomization podinfo \
/tmp/flux create kustomization podinfo \
--source=podinfo \
--path="./deploy/overlays/dev" \
--prune=true \
@ -105,89 +104,89 @@ jobs:
--health-check-timeout=3m
- name: flux trace
run: |
./bin/flux trace frontend \
/tmp/flux trace frontend \
--kind=deployment \
--api-version=apps/v1 \
--namespace=dev
- name: flux reconcile kustomization --with-source
run: |
./bin/flux reconcile kustomization podinfo --with-source
/tmp/flux reconcile kustomization podinfo --with-source
- name: flux get kustomizations
run: |
./bin/flux get kustomizations
/tmp/flux get kustomizations
- name: flux get kustomizations --all-namespaces
run: |
./bin/flux get kustomizations --all-namespaces
/tmp/flux get kustomizations --all-namespaces
- name: flux suspend kustomization
run: |
./bin/flux suspend kustomization podinfo
/tmp/flux suspend kustomization podinfo
- name: flux resume kustomization
run: |
./bin/flux resume kustomization podinfo
/tmp/flux resume kustomization podinfo
- name: flux export
run: |
./bin/flux export source git --all
./bin/flux export kustomization --all
/tmp/flux export source git --all
/tmp/flux export kustomization --all
- name: flux delete kustomization
run: |
./bin/flux delete kustomization podinfo --silent
/tmp/flux delete kustomization podinfo --silent
- name: flux create source helm
run: |
./bin/flux create source helm podinfo \
/tmp/flux create source helm podinfo \
--url https://stefanprodan.github.io/podinfo
- name: flux create helmrelease --source=HelmRepository/podinfo
run: |
./bin/flux create hr podinfo-helm \
/tmp/flux create hr podinfo-helm \
--target-namespace=default \
--source=HelmRepository/podinfo.flux-system \
--chart=podinfo \
--chart-version=">6.0.0 <7.0.0"
- name: flux create helmrelease --source=GitRepository/podinfo
run: |
./bin/flux create hr podinfo-git \
/tmp/flux create hr podinfo-git \
--target-namespace=default \
--source=GitRepository/podinfo \
--chart=./charts/podinfo
- name: flux reconcile helmrelease --with-source
run: |
./bin/flux reconcile helmrelease podinfo-git --with-source
/tmp/flux reconcile helmrelease podinfo-git --with-source
- name: flux get helmreleases
run: |
./bin/flux get helmreleases
/tmp/flux get helmreleases
- name: flux get helmreleases --all-namespaces
run: |
./bin/flux get helmreleases --all-namespaces
/tmp/flux get helmreleases --all-namespaces
- name: flux export helmrelease
run: |
./bin/flux export hr --all
/tmp/flux export hr --all
- name: flux delete helmrelease podinfo-helm
run: |
./bin/flux delete hr podinfo-helm --silent
/tmp/flux delete hr podinfo-helm --silent
- name: flux delete helmrelease podinfo-git
run: |
./bin/flux delete hr podinfo-git --silent
/tmp/flux delete hr podinfo-git --silent
- name: flux delete source helm
run: |
./bin/flux delete source helm podinfo --silent
/tmp/flux delete source helm podinfo --silent
- name: flux delete source git
run: |
./bin/flux delete source git podinfo --silent
/tmp/flux delete source git podinfo --silent
- name: flux oci artifacts
run: |
./bin/flux push artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \
/tmp/flux push artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \
--path="./manifests" \
--source="${{ github.repositoryUrl }}" \
--revision="${{ github.ref }}@sha1:${{ github.sha }}"
./bin/flux tag artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \
/tmp/flux tag artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \
--tag latest
./bin/flux list artifacts oci://localhost:5000/fluxcd/flux
/tmp/flux list artifacts oci://localhost:5000/fluxcd/flux
- name: flux oci repositories
run: |
./bin/flux create source oci podinfo-oci \
/tmp/flux create source oci podinfo-oci \
--url oci://ghcr.io/stefanprodan/manifests/podinfo \
--tag-semver 6.3.x \
--interval 10m
./bin/flux create kustomization podinfo-oci \
/tmp/flux create kustomization podinfo-oci \
--source=OCIRepository/podinfo-oci \
--path="./" \
--prune=true \
@ -195,31 +194,31 @@ jobs:
--target-namespace=default \
--wait=true \
--health-check-timeout=3m
./bin/flux reconcile source oci podinfo-oci
./bin/flux suspend source oci podinfo-oci
./bin/flux get sources oci
./bin/flux resume source oci podinfo-oci
./bin/flux export source oci podinfo-oci
./bin/flux delete ks podinfo-oci --silent
./bin/flux delete source oci podinfo-oci --silent
/tmp/flux reconcile source oci podinfo-oci
/tmp/flux suspend source oci podinfo-oci
/tmp/flux get sources oci
/tmp/flux resume source oci podinfo-oci
/tmp/flux export source oci podinfo-oci
/tmp/flux delete ks podinfo-oci --silent
/tmp/flux delete source oci podinfo-oci --silent
- name: flux create tenant
run: |
./bin/flux create tenant dev-team --with-namespace=apps
./bin/flux -n apps create source helm podinfo \
/tmp/flux create tenant dev-team --with-namespace=apps
/tmp/flux -n apps create source helm podinfo \
--url https://stefanprodan.github.io/podinfo
./bin/flux -n apps create hr podinfo-helm \
/tmp/flux -n apps create hr podinfo-helm \
--source=HelmRepository/podinfo \
--chart=podinfo \
--chart-version="6.3.x" \
--service-account=dev-team
- name: flux2-kustomize-helm-example
run: |
./bin/flux create source git flux-system \
/tmp/flux create source git flux-system \
--url=https://github.com/fluxcd/flux2-kustomize-helm-example \
--branch=main \
--ignore-paths="./clusters/**/flux-system/" \
--recurse-submodules
./bin/flux create kustomization flux-system \
/tmp/flux create kustomization flux-system \
--source=flux-system \
--path=./clusters/staging
kubectl -n flux-system wait kustomization/infra-controllers --for=condition=ready --timeout=5m
@ -227,23 +226,13 @@ jobs:
kubectl -n podinfo wait helmrelease/podinfo --for=condition=ready --timeout=5m
- name: flux tree
run: |
./bin/flux tree kustomization flux-system | grep Service/podinfo
- name: flux events
run: |
./bin/flux -n flux-system events --for Kustomization/apps | grep 'HelmRelease/podinfo'
./bin/flux -n podinfo events --for HelmRelease/podinfo | grep 'podinfo.v1'
- name: flux stats
run: |
./bin/flux stats -A
/tmp/flux tree kustomization flux-system | grep Service/podinfo
- name: flux check
run: |
./bin/flux check
- name: flux version
run: |
./bin/flux version
/tmp/flux check
- name: flux uninstall
run: |
./bin/flux uninstall --silent
/tmp/flux uninstall --silent
- name: Debug failure
if: failure()
run: |

@ -19,21 +19,21 @@ jobs:
actions: read
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Run analysis
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0
with:
results_file: results.sarif
results_format: sarif
repo_token: ${{ secrets.GITHUB_TOKEN }}
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
with:
sarif_file: results.sarif

@ -2,7 +2,7 @@ name: release
on:
push:
tags: ["v*"]
tags: [ 'v*' ]
permissions:
contents: read
@ -20,33 +20,33 @@ jobs:
packages: write # needed for ghcr access
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Unshallow
run: git fetch --prune --unshallow
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version: 1.23.x
go-version: 1.20.x
cache: false
- name: Setup QEMU
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3.4.0
uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
uses: docker/setup-buildx-action@4c0219f9ac95b02789c1075625400b2acbff50b1 # v2.9.1
- name: Setup Syft
uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
uses: anchore/sbom-action/download-syft@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1 # v0.14.3
- name: Setup Cosign
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3.8.0
uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 # v3.1.1
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
uses: fluxcd/pkg/actions/kustomize@main
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@ -59,19 +59,30 @@ jobs:
run: |
kustomize build manifests/crds > all-crds.yaml
- name: Generate OpenAPI JSON schemas from CRDs
uses: fluxcd/pkg/actions/crdjsonschema@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
uses: fluxcd/pkg/actions/crdjsonschema@main
with:
crd: all-crds.yaml
output: schemas
- name: Archive the OpenAPI JSON schemas
run: |
tar -czvf ./output/crd-schemas.tar.gz -C schemas .
- name: Download release notes utility
env:
GH_REL_URL: https://github.com/buchanae/github-release-notes/releases/download/0.2.0/github-release-notes-linux-amd64-0.2.0.tar.gz
run: cd /tmp && curl -sSL ${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/
- name: Generate release notes
run: |
NOTES="./output/notes.md"
echo '## CLI Changelog' > ${NOTES}
github-release-notes -org fluxcd -repo flux2 -since-latest-release -include-author >> ${NOTES}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
id: run-goreleaser
uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0
uses: goreleaser/goreleaser-action@3fa32b8bb5620a2c1afe798654bbad59f9da4906 # v4.4.0
with:
version: latest
args: release --skip=validate
args: release --release-notes=output/notes.md --skip-validate
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
@ -99,9 +110,9 @@ jobs:
id-token: write
packages: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
uses: fluxcd/pkg/actions/kustomize@main
- name: Setup Flux CLI
uses: ./action/
- name: Prepare
@ -110,13 +121,13 @@ jobs:
VERSION=$(flux version --client | awk '{ print $NF }')
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Login to GHCR
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.GHCR_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0
with:
username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@ -144,7 +155,7 @@ jobs:
--path="./flux-system" \
--source=${{ github.repositoryUrl }} \
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
- uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3.8.0
- uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 # v3.1.1
- name: Sign manifests
env:
COSIGN_EXPERIMENTAL: 1
@ -165,7 +176,7 @@ jobs:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
contents: write # for uploading attestations to GitHub releases.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.8.0
with:
provenance-name: "provenance.intoto.jsonl"
base64-subjects: "${{ needs.release-flux-cli.outputs.hashes }}"
@ -177,7 +188,7 @@ jobs:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.8.0
with:
image: ${{ needs.release-flux-cli.outputs.image_url }}
digest: ${{ needs.release-flux-cli.outputs.image_digest }}
@ -191,10 +202,10 @@ jobs:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.8.0
with:
image: ghcr.io/${{ needs.release-flux-cli.outputs.image_url }}
digest: ${{ needs.release-flux-cli.outputs.image_digest }}
registry-username: fluxcdbot
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}
registry-password: ${{ secrets.GHCR_TOKEN }}

@ -17,9 +17,9 @@ jobs:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Run FOSSA scan and upload build data
uses: fossa-contrib/fossa-action@cdc5065bcdee31a32e47d4585df72d66e8e941c2 # v3.0.0
uses: fossa-contrib/fossa-action@6728dc6fe9a068c648d080c33829ffbe56565023 # v2.0.0
with:
# FOSSA Push-Only API Token
fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de
@ -31,13 +31,13 @@ jobs:
security-events: write
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
uses: fluxcd/pkg/actions/kustomize@main
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version-file: 'go.mod'
go-version: 1.20.x
cache-dependency-path: |
**/go.sum
**/go.mod
@ -49,12 +49,11 @@ jobs:
- name: Run Snyk to check for vulnerabilities
continue-on-error: true
run: |
snyk test --all-projects --sarif-file-output=snyk.sarif
snyk test --sarif-file-output=snyk.sarif
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Upload result to GitHub Code Scanning
continue-on-error: true
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
with:
sarif_file: snyk.sarif
@ -65,22 +64,22 @@ jobs:
if: github.actor != 'dependabot[bot]'
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version-file: 'go.mod'
go-version: 1.20.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Initialize CodeQL
uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
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@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
uses: github/codeql-action/autobuild@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4

@ -17,8 +17,8 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
- uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- uses: EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 # v2.3.2
with:
# Configuration file
config-file: |

@ -18,11 +18,11 @@ jobs:
pull-requests: write
steps:
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version: 1.23.x
go-version: 1.20.x
cache-dependency-path: |
**/go.sum
**/go.mod
@ -84,7 +84,7 @@ jobs:
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
commit-message: |

@ -1,6 +1,4 @@
project_name: flux
changelog:
use: github-native
builds:
- <<: &build_defaults
binary: flux
@ -17,7 +15,7 @@ builds:
- arm64
- arm
goarm:
- "7"
- 7
- <<: *build_defaults
id: darwin
goos:
@ -75,11 +73,11 @@ signs:
output: true
brews:
- name: flux
repository:
tap:
owner: fluxcd
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
directory: Formula
folder: Formula
homepage: "https://fluxcd.io/"
description: "Flux CLI"
install: |

@ -1,5 +0,0 @@
annotations:
- checks:
- dangerous-workflow
reasons:
- reason: not-applicable # This workflow does not run untrusted code, the bot will only backport a code if the a PR was approved and merged into main.

@ -1,16 +1,15 @@
FROM alpine:3.21 AS builder
FROM alpine:3.18 as builder
RUN apk add --no-cache ca-certificates curl
ARG ARCH=linux/amd64
ARG KUBECTL_VER=1.32.2
ARG KUBECTL_VER=1.27.3
RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \
-o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl
RUN curl -sL https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \
-o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl && \
kubectl version --client=true
RUN kubectl version --client=true
FROM alpine:3.21 AS flux-cli
FROM alpine:3.18 as flux-cli
RUN apk add --no-cache ca-certificates

@ -17,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2
all: test build
tidy:
go mod tidy -compat=1.23
cd tests/integration && go mod tidy -compat=1.23
go mod tidy -compat=1.20
cd tests/azure && go mod tidy -compat=1.20
fmt:
go fmt ./...

@ -2,10 +2,9 @@
[![release](https://img.shields.io/github/release/fluxcd/flux2/all.svg)](https://github.com/fluxcd/flux2/releases)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4782/badge)](https://bestpractices.coreinfrastructure.org/projects/4782)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2/badge)](https://scorecard.dev/viewer/?uri=github.com/fluxcd/flux2)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2/badge)](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2)
[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Ffluxcd%2Fflux2.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Ffluxcd%2Fflux2?ref=badge_shield)
[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/flux2)](https://artifacthub.io/packages/helm/fluxcd-community/flux2)
[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://fluxcd.io/flux/security/slsa-assessment)
Flux is a tool for keeping Kubernetes clusters in sync with sources of
configuration (like Git repositories and OCI artifacts),
@ -21,7 +20,7 @@ Flux v2 is constructed with the [GitOps Toolkit](#gitops-toolkit), a
set of composable APIs and specialized tools for building Continuous
Delivery on top of Kubernetes.
Flux is a Cloud Native Computing Foundation ([CNCF](https://www.cncf.io/)) graduated project, used in
Flux is a Cloud Native Computing Foundation ([CNCF](https://www.cncf.io/)) project, used in
production by various [organisations](https://fluxcd.io/adopters) and [cloud providers](https://fluxcd.io/ecosystem).
## Quickstart and documentation
@ -33,7 +32,7 @@ For more comprehensive documentation, see the following guides:
- [Ways of structuring your repositories](https://fluxcd.io/flux/guides/repository-structure/)
- [Manage Helm Releases](https://fluxcd.io/flux/guides/helmreleases/)
- [Automate image updates to Git](https://fluxcd.io/flux/guides/image-update/)
- [Manage Kubernetes secrets with Flux and SOPS](https://fluxcd.io/flux/guides/mozilla-sops/)
- [Manage Kubernetes secrets with Mozilla SOPS](https://fluxcd.io/flux/guides/mozilla-sops/)
If you need help, please refer to our **[Support page](https://fluxcd.io/support/)**.
@ -44,7 +43,7 @@ runtime for Flux v2. The APIs comprise Kubernetes custom resources,
which can be created and updated by a cluster user, or by other
automation tooling.
![overview](https://raw.githubusercontent.com/fluxcd/flux2/main/docs/diagrams/fluxcd-controllers.png)
![overview](https://fluxcd.io/img/diagrams/gitops-toolkit.png)
You can use the toolkit to extend Flux, or to build your own systems
for continuous delivery -- see [the developer
@ -59,13 +58,13 @@ guides](https://fluxcd.io/flux/gitops-toolkit/source-watcher/).
- [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/)
- [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/)
- [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/kustomization/)
- [Helm Controller](https://fluxcd.io/flux/components/helm/)
- [HelmRelease CRD](https://fluxcd.io/flux/components/helm/helmreleases/)
- [Notification Controller](https://fluxcd.io/flux/components/notification/)
- [Provider CRD](https://fluxcd.io/flux/components/notification/providers/)
- [Alert CRD](https://fluxcd.io/flux/components/notification/alerts/)
- [Receiver CRD](https://fluxcd.io/flux/components/notification/receivers/)
- [Provider CRD](https://fluxcd.io/flux/components/notification/provider/)
- [Alert CRD](https://fluxcd.io/flux/components/notification/alert/)
- [Receiver CRD](https://fluxcd.io/flux/components/notification/receiver/)
- [Image Automation Controllers](https://fluxcd.io/flux/components/image/)
- [ImageRepository CRD](https://fluxcd.io/flux/components/image/imagerepositories/)
- [ImagePolicy CRD](https://fluxcd.io/flux/components/image/imagepolicies/)

@ -18,5 +18,5 @@ The Flux GitHub Action can be used to automate various tasks in CI, such as:
- [Push Kubernetes manifests to container registries](https://fluxcd.io/flux/flux-gh-action/#push-kubernetes-manifests-to-container-registries)
- [Run end-to-end testing with Flux and Kubernetes Kind](https://fluxcd.io/flux/flux-gh-action/#end-to-end-testing)
For more information, please see the [Flux GitHub Action documentation](https://fluxcd.io/flux/flux-gh-action/).
For more information, please see the [Flux GitHub Action documentation](/flux/flux-gh-action.md).

@ -19,7 +19,7 @@ package main
import (
"sigs.k8s.io/controller-runtime/pkg/client"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2"
)
// notificationv1.Alert

@ -19,7 +19,7 @@ package main
import (
"sigs.k8s.io/controller-runtime/pkg/client"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2"
)
// notificationv1.Provider

@ -17,16 +17,11 @@ limitations under the License.
package main
import (
"context"
"crypto/elliptic"
"fmt"
"strings"
"github.com/fluxcd/pkg/git"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
@ -54,7 +49,6 @@ type bootstrapFlags struct {
requiredComponents []string
registry string
registryCredential string
imagePullSecret string
secretName string
@ -65,7 +59,6 @@ type bootstrapFlags struct {
sshHostname string
caFile string
privateKeyFile string
sshHostKeyAlgorithms []string
watchAllNamespaces bool
networkPolicy bool
@ -79,8 +72,6 @@ type bootstrapFlags struct {
gpgPassphrase string
gpgKeyID string
force bool
commitMessageAppendix string
}
@ -101,8 +92,6 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd",
"container registry where the Flux controller images are published")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registryCredential, "registry-creds", "",
"container registry credentials in the format 'user:password', requires --image-pull-secret to be set")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.imagePullSecret, "image-pull-secret", "",
"Kubernetes secret name used for pulling the controller images from a private registry")
@ -126,7 +115,6 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.secretName, "secret-name", rootArgs.defaults.Namespace, "name of the secret the sync credentials can be found in or stored to")
bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyAlgorithm, "ssh-key-algorithm", bootstrapArgs.keyAlgorithm.Description())
bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyRSABits, "ssh-rsa-bits", bootstrapArgs.keyRSABits.Description())
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.sshHostKeyAlgorithms, "ssh-hostkey-algos", nil, "list of host key algorithms to be used by the CLI for SSH connections")
bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyECDSACurve, "ssh-ecdsa-curve", bootstrapArgs.keyECDSACurve.Description())
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshHostname, "ssh-hostname", "", "SSH hostname, to be used when the SSH host differs from the HTTPS one")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates")
@ -141,7 +129,6 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
bootstrapCmd.PersistentFlags().MarkHidden("manifests")
rootCmd.AddCommand(bootstrapCmd)
@ -187,18 +174,6 @@ func bootstrapValidate() error {
return err
}
if bootstrapArgs.registryCredential != "" && bootstrapArgs.imagePullSecret == "" {
return fmt.Errorf("--registry-creds requires --image-pull-secret to be set")
}
if bootstrapArgs.registryCredential != "" && len(strings.Split(bootstrapArgs.registryCredential, ":")) != 2 {
return fmt.Errorf("invalid --registry-creds format, expected 'user:password'")
}
if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 {
git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms
}
return nil
}
@ -213,27 +188,3 @@ func mapTeamSlice(s []string, defaultPermission string) map[string]string {
return m
}
// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation.
// It returns a nil error if Flux is not installed or the user confirms overriding an existing installation
func confirmBootstrap(ctx context.Context, kubeClient client.Client) error {
installed := true
info, err := getFluxClusterInfo(ctx, kubeClient)
if err != nil {
if !errors.IsNotFound(err) {
return fmt.Errorf("cluster info unavailable: %w", err)
}
installed = false
}
if installed {
err = confirmFluxInstallOverride(info)
if err != nil {
if err == promptui.ErrAbort {
return fmt.Errorf("bootstrap cancelled")
}
return err
}
}
return nil
}

@ -56,7 +56,7 @@ the bootstrap command will perform an upgrade if needed.`,
# Run bootstrap for a public repository on a personal account
flux bootstrap bitbucket-server --owner=<user> --repository=<repository name> --private=false --personal --hostname=<domain> --token-auth --path=clusters/my-cluster
# Run bootstrap for an existing repository with a branch named main
# Run bootstrap for a an existing repository with a branch named main
flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --branch=main --hostname=<domain> --token-auth --path=clusters/my-cluster`,
RunE: bootstrapBServerCmdRun,
}
@ -124,13 +124,6 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
return err
}
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err
@ -196,7 +189,6 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(),
Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy,
@ -226,7 +218,7 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
secretOpts.Username = bServerArgs.username
}
secretOpts.Password = bitbucketToken
secretOpts.CACrt = caBundle
secretOpts.CAFile = caBundle
} else {
keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password)
if err != nil {

@ -28,9 +28,6 @@ import (
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/bootstrap"
@ -38,6 +35,8 @@ import (
"github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
)
var bootstrapGitCmd = &cobra.Command{
@ -66,10 +65,7 @@ command will perform an upgrade if needed.`,
flux bootstrap git --url=ssh://<SSH-Key-ID>@git-codecommit.<region>.amazonaws.com/v1/repos/<repository> --private-key-file=<path/to/private.key> --password=<SSH-passphrase> --path=clusters/my-cluster
# Run bootstrap for a Git repository on Azure Devops
flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/<org>/<project>/<repository> --private-key-file=<path/to/rsa-sha2-private.key> --ssh-hostkey-algos=rsa-sha2-512,rsa-sha2-256 --path=clusters/my-cluster
# Run bootstrap for a Git repository on Oracle VBS
flux bootstrap git --url=https://repository_url.git --with-bearer-token=true --password=<PAT> --path=clusters/my-cluster
flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/<org>/<project>/<repository> --ssh-key-algorithm=rsa --ssh-rsa-bits=4096 --path=clusters/my-cluster
`,
RunE: bootstrapGitCmdRun,
}
@ -82,7 +78,6 @@ type gitFlags struct {
password string
silent bool
insecureHttpAllowed bool
withBearerToken bool
}
const (
@ -99,16 +94,11 @@ func init() {
bootstrapGitCmd.Flags().StringVarP(&gitArgs.password, "password", "p", "", "basic authentication password")
bootstrapGitCmd.Flags().BoolVarP(&gitArgs.silent, "silent", "s", false, "assumes the deploy key is already setup, skips confirmation")
bootstrapGitCmd.Flags().BoolVar(&gitArgs.insecureHttpAllowed, "allow-insecure-http", false, "allows insecure HTTP connections")
bootstrapGitCmd.Flags().BoolVar(&gitArgs.withBearerToken, "with-bearer-token", false, "use password as bearer token for Authorization header")
bootstrapCmd.AddCommand(bootstrapGitCmd)
}
func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
if gitArgs.withBearerToken {
bootstrapArgs.tokenAuth = true
}
gitPassword := os.Getenv(gitPasswordEnvVar)
if gitPassword != "" && gitArgs.password == "" {
gitArgs.password = gitPassword
@ -156,13 +146,6 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
return err
}
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err
@ -211,7 +194,6 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(),
Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy,
@ -234,16 +216,10 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
TargetPath: gitArgs.path.String(),
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
}
if bootstrapArgs.tokenAuth {
if gitArgs.withBearerToken {
secretOpts.BearerToken = gitArgs.password
} else {
secretOpts.Username = gitArgs.username
secretOpts.Password = gitArgs.password
}
secretOpts.CACrt = caBundle
secretOpts.CAFile = caBundle
// Remove port of the given host when not syncing over HTTP/S to not assume port for protocol
// This _might_ be overwritten later on by e.g. --ssh-hostname
@ -335,28 +311,18 @@ func getAuthOpts(u *url.URL, caBundle []byte) (*git.AuthOptions, error) {
if !gitArgs.insecureHttpAllowed {
return nil, fmt.Errorf("scheme http is insecure, pass --allow-insecure-http=true to allow it")
}
httpAuth := git.AuthOptions{
return &git.AuthOptions{
Transport: git.HTTP,
}
if gitArgs.withBearerToken {
httpAuth.BearerToken = gitArgs.password
} else {
httpAuth.Username = gitArgs.username
httpAuth.Password = gitArgs.password
}
return &httpAuth, nil
Username: gitArgs.username,
Password: gitArgs.password,
}, nil
case "https":
httpsAuth := git.AuthOptions{
return &git.AuthOptions{
Transport: git.HTTPS,
Username: gitArgs.username,
Password: gitArgs.password,
CAFile: caBundle,
}
if gitArgs.withBearerToken {
httpsAuth.BearerToken = gitArgs.password
} else {
httpsAuth.Username = gitArgs.username
httpsAuth.Password = gitArgs.password
}
return &httpsAuth, nil
}, nil
case "ssh":
authOpts := &git.AuthOptions{
Transport: git.SSH,

@ -1,276 +0,0 @@
/*
Copyright 2023 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"
"os"
"time"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/bootstrap"
"github.com/fluxcd/flux2/v2/pkg/bootstrap/provider"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
)
var bootstrapGiteaCmd = &cobra.Command{
Use: "gitea",
Short: "Deploy Flux on a cluster connected to a Gitea repository",
Long: `The bootstrap gitea command creates the Gitea repository if it doesn't exists and
commits the Flux manifests to the specified branch.
Then it configures the target cluster to synchronize with that repository.
If the Flux components are present on the cluster,
the bootstrap command will perform an upgrade if needed.`,
Example: ` # Create a Gitea personal access token and export it as an env var
export GITEA_TOKEN=<my-token>
# Run bootstrap for a private repository owned by a Gitea organization
flux bootstrap gitea --owner=<organization> --repository=<repository name> --path=clusters/my-cluster
# Run bootstrap for a private repository and assign organization teams to it
flux bootstrap gitea --owner=<organization> --repository=<repository name> --team=<team1 slug> --team=<team2 slug> --path=clusters/my-cluster
# Run bootstrap for a private repository and assign organization teams with their access level(e.g maintain, admin) to it
flux bootstrap gitea --owner=<organization> --repository=<repository name> --team=<team1 slug>:<access-level> --path=clusters/my-cluster
# Run bootstrap for a public repository on a personal account
flux bootstrap gitea --owner=<user> --repository=<repository name> --private=false --personal=true --path=clusters/my-cluster
# Run bootstrap for a private repository hosted on Gitea Enterprise using SSH auth
flux bootstrap gitea --owner=<organization> --repository=<repository name> --hostname=<domain> --ssh-hostname=<domain> --path=clusters/my-cluster
# Run bootstrap for a private repository hosted on Gitea Enterprise using HTTPS auth
flux bootstrap gitea --owner=<organization> --repository=<repository name> --hostname=<domain> --token-auth --path=clusters/my-cluster
# Run bootstrap for an existing repository with a branch named main
flux bootstrap gitea --owner=<organization> --repository=<repository name> --branch=main --path=clusters/my-cluster`,
RunE: bootstrapGiteaCmdRun,
}
type giteaFlags struct {
owner string
repository string
interval time.Duration
personal bool
private bool
hostname string
path flags.SafeRelativePath
teams []string
readWriteKey bool
reconcile bool
}
const (
gtDefaultPermission = "maintain"
gtDefaultDomain = "gitea.com"
gtTokenEnvVar = "GITEA_TOKEN"
)
var giteaArgs giteaFlags
func init() {
bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.owner, "owner", "", "Gitea user or organization name")
bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.repository, "repository", "", "Gitea repository name")
bootstrapGiteaCmd.Flags().StringSliceVar(&giteaArgs.teams, "team", []string{}, "Gitea team and the access to be given to it(team:maintain). Defaults to maintainer access if no access level is specified (also accepts comma-separated values)")
bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.personal, "personal", false, "if true, the owner is assumed to be a Gitea user; otherwise an org")
bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.private, "private", true, "if true, the repository is setup or configured as private")
bootstrapGiteaCmd.Flags().DurationVar(&giteaArgs.interval, "interval", time.Minute, "sync interval")
bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.hostname, "hostname", gtDefaultDomain, "Gitea hostname")
bootstrapGiteaCmd.Flags().Var(&giteaArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions")
bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists")
bootstrapCmd.AddCommand(bootstrapGiteaCmd)
}
func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error {
gtToken := os.Getenv(gtTokenEnvVar)
if gtToken == "" {
var err error
gtToken, err = readPasswordFromStdin("Please enter your Gitea personal access token (PAT): ")
if err != nil {
return fmt.Errorf("could not read token: %w", err)
}
}
if err := bootstrapValidate(); err != nil {
return err
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
// Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err
} else {
bootstrapArgs.version = ver
}
manifestsBase, err := buildEmbeddedManifestBase()
if err != nil {
return err
}
defer os.RemoveAll(manifestsBase)
var caBundle []byte
if bootstrapArgs.caFile != "" {
var err error
caBundle, err = os.ReadFile(bootstrapArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
}
// Build Gitea provider
providerCfg := provider.Config{
Provider: provider.GitProviderGitea,
Hostname: giteaArgs.hostname,
Token: gtToken,
CaBundle: caBundle,
}
providerClient, err := provider.BuildGitProvider(providerCfg)
if err != nil {
return err
}
tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-")
if err != nil {
return fmt.Errorf("failed to create temporary working dir: %w", err)
}
defer os.RemoveAll(tmpDir)
clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()}
gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{
Transport: git.HTTPS,
Username: giteaArgs.owner,
Password: gtToken,
CAFile: caBundle,
}, clientOpts...)
if err != nil {
return fmt.Errorf("failed to create a Git client: %w", err)
}
// Install manifest config
installOptions := install.Options{
BaseURL: rootArgs.defaults.BaseURL,
Version: bootstrapArgs.version,
Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(),
Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy,
LogLevel: bootstrapArgs.logLevel.String(),
NotificationController: rootArgs.defaults.NotificationController,
ManifestFile: rootArgs.defaults.ManifestFile,
Timeout: rootArgs.timeout,
TargetPath: giteaArgs.path.ToSlash(),
ClusterDomain: bootstrapArgs.clusterDomain,
TolerationKeys: bootstrapArgs.tolerationKeys,
}
if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" {
installOptions.BaseURL = customBaseURL
}
// Source generation and secret config
secretOpts := sourcesecret.Options{
Name: bootstrapArgs.secretName,
Namespace: *kubeconfigArgs.Namespace,
TargetPath: giteaArgs.path.ToSlash(),
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
}
if bootstrapArgs.tokenAuth {
secretOpts.Username = "git"
secretOpts.Password = gtToken
secretOpts.CACrt = caBundle
} else {
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
secretOpts.SSHHostname = giteaArgs.hostname
if bootstrapArgs.sshHostname != "" {
secretOpts.SSHHostname = bootstrapArgs.sshHostname
}
}
// Sync manifest config
syncOpts := sync.Options{
Interval: giteaArgs.interval,
Name: *kubeconfigArgs.Namespace,
Namespace: *kubeconfigArgs.Namespace,
Branch: bootstrapArgs.branch,
Secret: bootstrapArgs.secretName,
TargetPath: giteaArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
}
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
if err != nil {
return err
}
// Bootstrap config
bootstrapOpts := []bootstrap.GitProviderOption{
bootstrap.WithProviderRepository(giteaArgs.owner, giteaArgs.repository, giteaArgs.personal),
bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithBootstrapTransportType("https"),
bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithProviderTeamPermissions(mapTeamSlice(giteaArgs.teams, gtDefaultPermission)),
bootstrap.WithReadWriteKeyPermissions(giteaArgs.readWriteKey),
bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions),
bootstrap.WithLogger(logger),
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
}
if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
}
if bootstrapArgs.tokenAuth {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https"))
}
if !giteaArgs.private {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public"))
}
if giteaArgs.reconcile {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
}
// Setup bootstrapper with constructed configs
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...)
if err != nil {
return err
}
// Run
return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout)
}

@ -128,13 +128,6 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
return err
}
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err
@ -191,7 +184,6 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(),
Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy,
@ -217,7 +209,7 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
if bootstrapArgs.tokenAuth {
secretOpts.Username = "git"
secretOpts.Password = ghToken
secretOpts.CACrt = caBundle
secretOpts.CAFile = caBundle
} else {
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)

@ -24,7 +24,6 @@ import (
"strings"
"time"
"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/spf13/cobra"
@ -59,14 +58,14 @@ the bootstrap command will perform an upgrade if needed.`,
# Run bootstrap for a repository path
flux bootstrap gitlab --owner=<group> --repository=<repository name> --path=dev-cluster
# Run bootstrap for a public repository
flux bootstrap gitlab --owner=<group> --repository=<repository name> --visibility=public --token-auth
# Run bootstrap for a public repository on a personal account
flux bootstrap gitlab --owner=<user> --repository=<repository name> --private=false --personal --token-auth
# Run bootstrap for a private repository hosted on a GitLab server
flux bootstrap gitlab --owner=<group> --repository=<repository name> --hostname=<gitlab_url> --token-auth
flux bootstrap gitlab --owner=<group> --repository=<repository name> --hostname=<domain> --token-auth
# Run bootstrap for an existing repository with a branch named main
flux bootstrap gitlab --owner=<group> --repository=<repository name> --branch=main --token-auth
# Run bootstrap for a an existing repository with a branch named main
flux bootstrap gitlab --owner=<organization> --repository=<repository name> --branch=main --token-auth
# Run bootstrap for a private repository using Deploy Token authentication
flux bootstrap gitlab --owner=<group> --repository=<repository name> --deploy-token-auth
@ -86,7 +85,6 @@ type gitlabFlags struct {
repository string
interval time.Duration
personal bool
visibility flags.GitLabVisibility
private bool
hostname string
path flags.SafeRelativePath
@ -96,13 +94,7 @@ type gitlabFlags struct {
deployTokenAuth bool
}
func NewGitlabFlags() gitlabFlags {
return gitlabFlags{
visibility: flags.GitLabVisibility(gitprovider.RepositoryVisibilityPrivate),
}
}
var gitlabArgs = NewGitlabFlags()
var gitlabArgs gitlabFlags
func init() {
bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.owner, "owner", "", "GitLab user or group name")
@ -110,8 +102,6 @@ func init() {
bootstrapGitLabCmd.Flags().StringSliceVar(&gitlabArgs.teams, "team", []string{}, "GitLab teams to be given maintainer access (also accepts comma-separated values)")
bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.personal, "personal", false, "if true, the owner is assumed to be a GitLab user; otherwise a group")
bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.private, "private", true, "if true, the repository is setup or configured as private")
bootstrapGitLabCmd.Flags().MarkDeprecated("private", "use --visibility instead")
bootstrapGitLabCmd.Flags().Var(&gitlabArgs.visibility, "visibility", gitlabArgs.visibility.Description())
bootstrapGitLabCmd.Flags().DurationVar(&gitlabArgs.interval, "interval", time.Minute, "sync interval")
bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.hostname, "hostname", glDefaultDomain, "GitLab hostname")
bootstrapGitLabCmd.Flags().Var(&gitlabArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
@ -143,11 +133,6 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("--token-auth and --deploy-token-auth cannot be set both.")
}
if !gitlabArgs.private {
gitlabArgs.visibility.Set(string(gitprovider.RepositoryVisibilityPublic))
cmd.Println("Using visibility public as --private=false")
}
if err := bootstrapValidate(); err != nil {
return err
}
@ -160,13 +145,6 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
return err
}
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err
@ -231,7 +209,6 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(),
Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy,
@ -257,10 +234,10 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
if bootstrapArgs.tokenAuth {
secretOpts.Username = "git"
secretOpts.Password = glToken
secretOpts.CACrt = caBundle
secretOpts.CAFile = caBundle
} else if gitlabArgs.deployTokenAuth {
// the actual deploy token will be reconciled later
secretOpts.CACrt = caBundle
secretOpts.CAFile = caBundle
} else {
keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password)
if err != nil {
@ -297,7 +274,6 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
// Bootstrap config
bootstrapOpts := []bootstrap.GitProviderOption{
bootstrap.WithProviderRepository(gitlabArgs.owner, gitlabArgs.repository, gitlabArgs.personal),
bootstrap.WithProviderVisibility(gitlabArgs.visibility.String()),
bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithBootstrapTransportType("https"),
bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
@ -317,6 +293,9 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
if gitlabArgs.deployTokenAuth {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithDeployTokenAuth())
}
if !gitlabArgs.private {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public"))
}
if gitlabArgs.reconcile {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
}

@ -89,7 +89,7 @@ func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
ociClient := oci.NewClient(oci.DefaultOptions())
if err := ociClient.Build(buildArtifactArgs.output, path, buildArtifactArgs.ignorePaths); err != nil {
return fmt.Errorf("building artifact failed, error: %w", err)
return fmt.Errorf("bulding artifact failed, error: %w", err)
}
logger.Successf("artifact created at %s", buildArtifactArgs.output)

@ -21,10 +21,10 @@ import (
"os"
"os/signal"
"github.com/fluxcd/pkg/ssa"
"github.com/spf13/cobra"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
ssautil "github.com/fluxcd/pkg/ssa/utils"
"github.com/fluxcd/flux2/v2/internal/build"
)
@ -53,12 +53,7 @@ flux build kustomization my-app --path ./path/to/local/manifests \
# Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat.
flux build kustomization my-app --path ./path/to/local/manifests \
--kustomization-file ./path/to/local/my-app.yaml \
--ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"
# Run recursively on all encountered Kustomizations
flux build kustomization my-app --path ./path/to/local/manifests \
--recursive \
--local-sources GitRepository/flux-system/my-repo=./path/to/local/git`,
--ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: buildKsCmdRun,
}
@ -68,9 +63,6 @@ type buildKsFlags struct {
path string
ignorePaths []string
dryRun bool
strictSubst bool
recursive bool
localSources map[string]string
}
var buildKsArgs buildKsFlags
@ -80,10 +72,6 @@ func init() {
buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.")
buildKsCmd.Flags().StringSliceVar(&buildKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format")
buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.")
buildKsCmd.Flags().BoolVar(&buildKsArgs.strictSubst, "strict-substitute", false,
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
buildCmd.AddCommand(buildKsCmd)
}
@ -119,9 +107,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
build.WithDryRun(buildKsArgs.dryRun),
build.WithNamespace(*kubeconfigArgs.Namespace),
build.WithIgnore(buildKsArgs.ignorePaths),
build.WithStrictSubstitute(buildKsArgs.strictSubst),
build.WithRecursive(buildKsArgs.recursive),
build.WithLocalSources(buildKsArgs.localSources),
)
} else {
builder, err = build.NewBuilder(name, buildKsArgs.path,
@ -129,9 +114,6 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
build.WithTimeout(rootArgs.timeout),
build.WithKustomizationFile(buildKsArgs.kustomizationFile),
build.WithIgnore(buildKsArgs.ignorePaths),
build.WithStrictSubstitute(buildKsArgs.strictSubst),
build.WithRecursive(buildKsArgs.recursive),
build.WithLocalSources(buildKsArgs.localSources),
)
}
@ -150,7 +132,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
errChan <- err
}
manifests, err := ssautil.ObjectsToYAML(objects)
manifests, err := ssa.ObjectsToYAML(objects)
if err != nil {
errChan <- err
}

@ -22,7 +22,6 @@ package main
import (
"bytes"
"os"
"path/filepath"
"testing"
"text/template"
)
@ -70,12 +69,6 @@ func TestBuildKustomization(t *testing.T) {
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with recursive",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
}
tmpl := map[string]string{
@ -125,8 +118,6 @@ spec:
cluster_region: "eu-central-1"
`
tmpFile := filepath.Join(t.TempDir(), "podinfo.yaml")
tests := []struct {
name string
args string
@ -141,40 +132,28 @@ spec:
},
{
name: "build podinfo",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo",
args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/podinfo",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build podinfo without service",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/delete-service",
resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build deployment and configmap with var substitution",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/var-substitution",
args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution",
resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build deployment and configmap with var substitution in dry-run mode",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/var-substitution --dry-run",
args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution --dry-run",
resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with recursive",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with recursive in dry-run mode",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --dry-run",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
}
tmpl := map[string]string{
@ -182,6 +161,8 @@ spec:
}
setup(t, tmpl)
testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t)
temp, err := template.New("podinfo").Parse(podinfo)
if err != nil {
t.Fatal(err)
@ -193,10 +174,11 @@ spec:
t.Fatal(err)
}
err = os.WriteFile(tmpFile, b.Bytes(), 0666)
err = os.WriteFile("./testdata/build-kustomization/podinfo.yaml", b.Bytes(), 0666)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Remove("./testdata/build-kustomization/podinfo.yaml") })
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

@ -18,7 +18,6 @@ package main
import (
"context"
"fmt"
"os"
"time"
@ -27,7 +26,6 @@ import (
v1 "k8s.io/api/apps/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/version"
@ -40,7 +38,6 @@ import (
var checkCmd = &cobra.Command{
Use: "check",
Args: cobra.NoArgs,
Short: "Check requirements and installation",
Long: withPreviewNote(`The check command will perform a series of checks to validate that
the local environment is configured correctly and if the installed components are healthy.`),
@ -60,7 +57,7 @@ type checkFlags struct {
}
var kubernetesConstraints = []string{
">=1.30.0-0",
">=1.25.0-0",
}
var checkArgs checkFlags
@ -83,20 +80,7 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
fluxCheck()
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
if err != nil {
return fmt.Errorf("Kubernetes client initialization failed: %s", err.Error())
}
kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()})
if err != nil {
return err
}
if !kubernetesCheck(cfg, kubernetesConstraints) {
if !kubernetesCheck(kubernetesConstraints) {
checkFailed = true
}
@ -108,18 +92,13 @@ func runCheckCmd(cmd *cobra.Command, args []string) error {
return nil
}
logger.Actionf("checking version in cluster")
if !fluxClusterVersionCheck(ctx, kubeClient) {
checkFailed = true
}
logger.Actionf("checking controllers")
if !componentsCheck(ctx, kubeClient) {
if !componentsCheck() {
checkFailed = true
}
logger.Actionf("checking crds")
if !crdsCheck(ctx, kubeClient) {
if !crdsCheck() {
checkFailed = true
}
@ -150,11 +129,17 @@ func fluxCheck() {
return
}
if latestSv.GreaterThan(curSv) {
logger.Failuref("flux %s <%s (new CLI version is available, please upgrade)", curSv, latestSv)
logger.Failuref("flux %s <%s (new version is available, please upgrade)", curSv, latestSv)
}
}
func kubernetesCheck(cfg *rest.Config, constraints []string) bool {
func kubernetesCheck(constraints []string) bool {
cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
if err != nil {
logger.Failuref("Kubernetes client initialization failed: %s", err.Error())
return false
}
clientSet, err := kubernetes.NewForConfig(cfg)
if err != nil {
logger.Failuref("Kubernetes client initialization failed: %s", err.Error())
@ -193,8 +178,21 @@ func kubernetesCheck(cfg *rest.Config, constraints []string) bool {
return true
}
func componentsCheck(ctx context.Context, kubeClient client.Client) bool {
statusChecker, err := status.NewStatusCheckerWithClient(kubeClient, checkArgs.pollInterval, rootArgs.timeout, logger)
func componentsCheck() bool {
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeConfig, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
if err != nil {
return false
}
statusChecker, err := status.NewStatusChecker(kubeConfig, checkArgs.pollInterval, rootArgs.timeout, logger)
if err != nil {
return false
}
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return false
}
@ -224,7 +222,15 @@ func componentsCheck(ctx context.Context, kubeClient client.Client) bool {
return ok
}
func crdsCheck(ctx context.Context, kubeClient client.Client) bool {
func crdsCheck() bool {
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return false
}
ok := true
selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue}
var list apiextensionsv1.CustomResourceDefinitionList
@ -247,17 +253,3 @@ func crdsCheck(ctx context.Context, kubeClient client.Client) bool {
}
return ok
}
func fluxClusterVersionCheck(ctx context.Context, kubeClient client.Client) bool {
clusterInfo, err := getFluxClusterInfo(ctx, kubeClient)
if err != nil {
logger.Failuref("checking failed: %s", err.Error())
return false
}
if clusterInfo.distribution() != "" {
logger.Successf("distribution: %s", clusterInfo.distribution())
}
logger.Successf("bootstrapped: %t", clusterInfo.bootstrapped)
return true
}

@ -1,126 +0,0 @@
/*
Copyright 2023 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/manifoldco/promptui"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
)
// bootstrapLabels are labels put on a resource by kustomize-controller. These labels on the CRD indicates
// that flux has been bootstrapped.
var bootstrapLabels = []string{
fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group),
fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group),
}
// fluxClusterInfo contains information about an existing flux installation on a cluster.
type fluxClusterInfo struct {
// bootstrapped indicates that Flux was installed using the `flux bootstrap` command.
bootstrapped bool
// managedBy is the name of the tool being used to manage the installation of Flux.
managedBy string
// partOf indicates which distribution the instance is a part of.
partOf string
// version is the Flux version number in semver format.
version string
}
// getFluxClusterInfo returns information on the Flux installation running on the cluster.
// If an error occurred, the returned error will be non-nil.
//
// This function retrieves the GitRepository CRD from the cluster and checks it
// for a set of labels used to determine the Flux version and how Flux was installed.
// It returns the NotFound error from the underlying library if it was unable to find
// the GitRepository CRD and this can be used to check if Flux is installed.
func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, error) {
var info fluxClusterInfo
crdMetadata := &metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{
APIVersion: apiextensionsv1.SchemeGroupVersion.String(),
Kind: "CustomResourceDefinition",
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("gitrepositories.%s", sourcev1.GroupVersion.Group),
},
}
if err := c.Get(ctx, client.ObjectKeyFromObject(crdMetadata), crdMetadata); err != nil {
return info, err
}
info.version = crdMetadata.Labels[manifestgen.VersionLabelKey]
var present bool
for _, l := range bootstrapLabels {
_, present = crdMetadata.Labels[l]
}
if present {
info.bootstrapped = true
}
// the `app.kubernetes.io/managed-by` label is not set by flux but might be set by other
// tools used to install Flux e.g Helm.
if manager, ok := crdMetadata.Labels["app.kubernetes.io/managed-by"]; ok {
info.managedBy = manager
}
if partOf, ok := crdMetadata.Labels[manifestgen.PartOfLabelKey]; ok {
info.partOf = partOf
}
return info, nil
}
// confirmFluxInstallOverride displays a prompt to the user so that they can confirm before overriding
// a Flux installation. It returns nil if the installation should continue,
// promptui.ErrAbort if the user doesn't confirm, or an error encountered.
func confirmFluxInstallOverride(info fluxClusterInfo) error {
// no need to display prompt if installation is managed by Flux
if installManagedByFlux(info.managedBy) {
return nil
}
display := fmt.Sprintf("Flux %s has been installed on this cluster with %s!", info.version, info.managedBy)
fmt.Fprintln(rootCmd.ErrOrStderr(), display)
prompt := promptui.Prompt{
Label: fmt.Sprintf("Are you sure you want to override the %s installation? Y/N", info.managedBy),
IsConfirm: true,
}
_, err := prompt.Run()
return err
}
func (info fluxClusterInfo) distribution() string {
distribution := info.version
if info.partOf != "" {
distribution = fmt.Sprintf("%s-%s", info.partOf, info.version)
}
return distribution
}
func installManagedByFlux(manager string) bool {
return manager == "" || manager == "flux"
}

@ -1,141 +0,0 @@
/*
Copyright 2023 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"
"os"
"testing"
. "github.com/onsi/gomega"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
ssautil "github.com/fluxcd/pkg/ssa/utils"
)
func Test_getFluxClusterInfo(t *testing.T) {
g := NewWithT(t)
f, err := os.Open("./testdata/cluster_info/gitrepositories.yaml")
g.Expect(err).To(BeNil())
objs, err := ssautil.ReadObjects(f)
g.Expect(err).To(Not(HaveOccurred()))
gitrepo := objs[0]
tests := []struct {
name string
labels map[string]string
wantErr bool
wantInfo fluxClusterInfo
}{
{
name: "no git repository CRD present",
wantErr: true,
},
{
name: "CRD with kustomize-controller labels",
labels: map[string]string{
fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): "flux-system",
fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system",
"app.kubernetes.io/version": "v2.1.0",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
bootstrapped: true,
},
},
{
name: "CRD with kustomize-controller labels and managed-by label",
labels: map[string]string{
fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): "flux-system",
fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system",
"app.kubernetes.io/version": "v2.1.0",
"app.kubernetes.io/managed-by": "flux",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
bootstrapped: true,
managedBy: "flux",
},
},
{
name: "CRD with only managed-by label",
labels: map[string]string{
"app.kubernetes.io/version": "v2.1.0",
"app.kubernetes.io/managed-by": "helm",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
managedBy: "helm",
},
},
{
name: "CRD with no labels",
labels: map[string]string{},
wantInfo: fluxClusterInfo{},
},
{
name: "CRD with only version label",
labels: map[string]string{
"app.kubernetes.io/version": "v2.1.0",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
},
},
{
name: "CRD with version and part-of labels",
labels: map[string]string{
"app.kubernetes.io/version": "v2.1.0",
"app.kubernetes.io/part-of": "flux",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
partOf: "flux",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
newscheme := runtime.NewScheme()
apiextensionsv1.AddToScheme(newscheme)
builder := fake.NewClientBuilder().WithScheme(newscheme)
if tt.labels != nil {
gitrepo.SetLabels(tt.labels)
builder = builder.WithRuntimeObjects(gitrepo)
}
client := builder.Build()
info, err := getFluxClusterInfo(context.Background(), client)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
g.Expect(errors.IsNotFound(err)).To(BeTrue())
} else {
g.Expect(err).To(Not(HaveOccurred()))
}
g.Expect(info).To(BeEquivalentTo(tt.wantInfo))
})
}
}

@ -125,14 +125,14 @@ func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) e
logger.Generatef("generating %s", names.kind)
logger.Actionf("applying %s", names.kind)
namespacedName, err := names.upsert(ctx, kubeClient, object, mutate)
namespacedName, err := imageRepositoryType.upsert(ctx, kubeClient, object, mutate)
if err != nil {
return err
}
logger.Waitingf("waiting for %s reconciliation", names.kind)
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isObjectReadyConditionFunc(kubeClient, namespacedName, object.asClientObject())); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isReady(ctx, kubeClient, namespacedName, object)); err != nil {
return err
}
logger.Successf("%s reconciliation completed", names.kind)
@ -165,6 +165,6 @@ func parseLabels() (map[string]string, error) {
}
func validateObjectName(name string) bool {
r := regexp.MustCompile(`^[a-z0-9]([a-z0-9\-]){0,61}[a-z0-9]$`)
r := regexp.MustCompile("^[a-z0-9]([a-z0-9\\-]){0,61}[a-z0-9]$")
return r.MatchString(name)
}

@ -22,13 +22,14 @@ import (
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1b2 "github.com/fluxcd/notification-controller/api/v1beta2"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/v2/internal/utils"
@ -96,13 +97,13 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error {
logger.Generatef("generating Alert")
}
alert := notificationv1b3.Alert{
alert := notificationv1b2.Alert{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: sourceLabels,
},
Spec: notificationv1b3.AlertSpec{
Spec: notificationv1b2.AlertSpec{
ProviderRef: meta.LocalObjectReference{
Name: alertArgs.providerRef,
},
@ -131,8 +132,8 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for Alert reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isStaticObjectReadyConditionFunc(kubeClient, namespacedName, &alert)); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isAlertReady(ctx, kubeClient, namespacedName, &alert)); err != nil {
return err
}
logger.Successf("Alert %s is ready", name)
@ -140,13 +141,13 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error {
}
func upsertAlert(ctx context.Context, kubeClient client.Client,
alert *notificationv1b3.Alert) (types.NamespacedName, error) {
alert *notificationv1b2.Alert) (types.NamespacedName, error) {
namespacedName := types.NamespacedName{
Namespace: alert.GetNamespace(),
Name: alert.GetName(),
}
var existing notificationv1b3.Alert
var existing notificationv1b2.Alert
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
@ -169,3 +170,23 @@ func upsertAlert(ctx context.Context, kubeClient client.Client,
logger.Successf("Alert updated")
return namespacedName, nil
}
func isAlertReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, alert *notificationv1b2.Alert) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, alert)
if err != nil {
return false, err
}
if c := apimeta.FindStatusCondition(alert.Status.Conditions, meta.ReadyCondition); c != nil {
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}

@ -22,12 +22,13 @@ import (
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/v2/internal/utils"
@ -126,8 +127,8 @@ func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for Provider reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isStaticObjectReadyConditionFunc(kubeClient, namespacedName, &provider)); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isAlertProviderReady(ctx, kubeClient, namespacedName, &provider)); err != nil {
return err
}
@ -166,3 +167,23 @@ func upsertAlertProvider(ctx context.Context, kubeClient client.Client,
logger.Successf("Provider updated")
return namespacedName, nil
}
func isAlertProviderReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, provider *notificationv1.Provider) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, provider)
if err != nil {
return false, err
}
if c := apimeta.FindStatusCondition(provider.Status.Conditions, meta.ReadyCondition); c != nil {
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}

@ -1,5 +1,5 @@
/*
Copyright 2024 The Flux authors
Copyright 2020 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.
@ -24,30 +24,29 @@ import (
"strings"
"time"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/transform"
"github.com/spf13/cobra"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/transform"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
)
var createHelmReleaseCmd = &cobra.Command{
Use: "helmrelease [name]",
Aliases: []string{"hr"},
Short: "Create or update a HelmRelease resource",
Long: `The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.`,
Long: withPreviewNote(`The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.`),
Example: ` # Create a HelmRelease with a chart from a HelmRepository source
flux create hr podinfo \
--interval=10m \
@ -106,17 +105,7 @@ var createHelmReleaseCmd = &cobra.Command{
--source=HelmRepository/podinfo \
--chart=podinfo \
--values=./values.yaml \
--export > podinfo-release.yaml
# Create a HelmRelease using a chart from a HelmChart resource
flux create hr podinfo \
--namespace=default \
--chart-ref=HelmChart/podinfo.flux-system \
# Create a HelmRelease using a chart from an OCIRepository resource
flux create hr podinfo \
--namespace=default \
--chart-ref=OCIRepository/podinfo.flux-system`,
--export > podinfo-release.yaml`,
RunE: createHelmReleaseCmdRun,
}
@ -126,7 +115,6 @@ type helmReleaseFlags struct {
dependsOn []string
chart string
chartVersion string
chartRef string
targetNamespace string
createNamespace bool
valuesFiles []string
@ -142,8 +130,6 @@ var helmReleaseArgs helmReleaseFlags
var supportedHelmReleaseValuesFromKinds = []string{"Secret", "ConfigMap"}
var supportedHelmReleaseReferenceKinds = []string{sourcev1b2.OCIRepositoryKind, sourcev1.HelmChartKind}
func init() {
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.name, "release-name", "", "name used for the Helm release, defaults to a composition of '[<target-namespace>-]<HelmRelease-name>'")
createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.source, "source", helmReleaseArgs.source.Description())
@ -159,15 +145,14 @@ func init() {
createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFrom, "values-from", nil, "a Kubernetes object reference that contains the values.yaml data key in the format '<kind>/<name>', where kind must be one of: (Secret,ConfigMap)")
createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.crds, "crds", helmReleaseArgs.crds.Description())
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.kubeConfigSecretRef, "kubeconfig-secret-ref", "", "the name of the Kubernetes Secret that contains a key with the kubeconfig file for connecting to a remote cluster")
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chartRef, "chart-ref", "", "the name of the HelmChart resource to use as source for the HelmRelease, in the format '<kind>/<name>.<namespace>', where kind must be one of: (OCIRepository,HelmChart)")
createCmd.AddCommand(createHelmReleaseCmd)
}
func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
if helmReleaseArgs.chart == "" && helmReleaseArgs.chartRef == "" {
return fmt.Errorf("chart or chart-ref is required")
if helmReleaseArgs.chart == "" {
return fmt.Errorf("chart name or path is required")
}
sourceLabels, err := parseLabels()
@ -197,13 +182,8 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
Duration: createArgs.interval,
},
TargetNamespace: helmReleaseArgs.targetNamespace,
Suspend: false,
},
}
switch {
case helmReleaseArgs.chart != "":
helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{
Chart: helmv2.HelmChartTemplate{
Spec: helmv2.HelmChartTemplateSpec{
Chart: helmReleaseArgs.chart,
Version: helmReleaseArgs.chartVersion,
@ -214,23 +194,9 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
},
ReconcileStrategy: helmReleaseArgs.reconcileStrategy,
},
}
if helmReleaseArgs.chartInterval != 0 {
helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{
Duration: helmReleaseArgs.chartInterval,
}
}
case helmReleaseArgs.chartRef != "":
kind, name, ns := utils.ParseObjectKindNameNamespace(helmReleaseArgs.chartRef)
if kind != sourcev1.HelmChartKind && kind != sourcev1b2.OCIRepositoryKind {
return fmt.Errorf("chart reference kind '%s' is not supported, must be one of: %s",
kind, strings.Join(supportedHelmReleaseReferenceKinds, ", "))
}
helmRelease.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{
Kind: kind,
Name: name,
Namespace: ns,
}
},
Suspend: false,
},
}
if helmReleaseArgs.kubeConfigSecretRef != "" {
@ -241,6 +207,12 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
}
}
if helmReleaseArgs.chartInterval != 0 {
helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{
Duration: helmReleaseArgs.chartInterval,
}
}
if helmReleaseArgs.createNamespace {
if helmRelease.Spec.Install == nil {
helmRelease.Spec.Install = &helmv2.Install{}
@ -331,13 +303,13 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for HelmRelease reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isObjectReadyConditionFunc(kubeClient, namespacedName, &helmRelease)); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isHelmReleaseReady(ctx, kubeClient, namespacedName, &helmRelease)); err != nil {
return err
}
logger.Successf("HelmRelease %s is ready", name)
logger.Successf("applied revision %s", getHelmReleaseRevision(helmRelease))
logger.Successf("applied revision %s", helmRelease.Status.LastAppliedRevision)
return nil
}
@ -372,6 +344,23 @@ func upsertHelmRelease(ctx context.Context, kubeClient client.Client,
return namespacedName, nil
}
func isHelmReleaseReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, helmRelease *helmv2.HelmRelease) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, helmRelease)
if err != nil {
return false, err
}
// Confirm the state we are observing is for the current generation
if helmRelease.Generation != helmRelease.Status.ObservedGeneration {
return false, nil
}
return apimeta.IsStatusConditionTrue(helmRelease.Status.Conditions, meta.ReadyCondition), nil
}
}
func validateStrategy(input string) bool {
allowedStrategy := []string{"Revision", "ChartVersion"}

@ -1,86 +0,0 @@
//go:build unit
// +build unit
/*
Copyright 2024 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 TestCreateHelmRelease(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupHRSource(t, tmpl)
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "missing name",
args: "create helmrelease --export",
assert: assertError("name is required"),
},
{
name: "missing chart template and chartRef",
args: "create helmrelease podinfo --export",
assert: assertError("chart or chart-ref is required"),
},
{
name: "unknown source kind",
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`),
},
{
name: "unknown chart reference kind",
args: "create helmrelease podinfo --chart-ref foobar/podinfo --export",
assert: assertError(`chart reference kind 'foobar' is not supported, must be one of: OCIRepository, HelmChart`),
},
{
name: "basic helmrelease",
args: "create helmrelease podinfo --source Helmrepository/podinfo --chart podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/basic.yaml", tmpl),
},
{
name: "chart with OCIRepository source",
args: "create helmrelease podinfo --chart-ref OCIRepository/podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/or_basic.yaml", tmpl),
},
{
name: "chart with HelmChart source",
args: "create helmrelease podinfo --chart-ref HelmChart/podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/hc_basic.yaml", tmpl),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
func setupHRSource(t *testing.T, tmpl map[string]string) {
t.Helper()
testEnv.CreateObjectFile("./testdata/create_hr/setup-source.yaml", tmpl, t)
}

@ -60,6 +60,7 @@ type imagePolicyFlags struct {
numeric string
filterRegex string
filterExtract string
filterNumerical string
}
var imagePolicyArgs = imagePolicyFlags{}
@ -182,6 +183,7 @@ func validateExtractStr(template string, capNames []string) error {
name, num, rest, ok := extract(template)
if !ok {
// Malformed extract string, assume user didn't want this
template = template[1:]
return fmt.Errorf("--filter-extract is malformed")
}
template = rest

@ -22,7 +22,7 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)

@ -24,12 +24,13 @@ import (
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta"
@ -262,8 +263,8 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for Kustomization reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isObjectReadyConditionFunc(kubeClient, namespacedName, &kustomization)); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isKustomizationReady(ctx, kubeClient, namespacedName, &kustomization)); err != nil {
return err
}
logger.Successf("Kustomization %s is ready", name)
@ -302,3 +303,28 @@ func upsertKustomization(ctx context.Context, kubeClient client.Client,
logger.Successf("Kustomization updated")
return namespacedName, nil
}
func isKustomizationReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, kustomization *kustomizev1.Kustomization) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, kustomization)
if err != nil {
return false, err
}
// Confirm the state we are observing is for the current generation
if kustomization.Generation != kustomization.Status.ObservedGeneration {
return false, nil
}
if c := apimeta.FindStatusCondition(kustomization.Status.Conditions, meta.ReadyCondition); c != nil {
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}

@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
@ -138,8 +139,8 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for Receiver reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isObjectReadyConditionFunc(kubeClient, namespacedName, &receiver)); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isReceiverReady(ctx, kubeClient, namespacedName, &receiver)); err != nil {
return err
}
logger.Successf("Receiver %s is ready", name)
@ -178,3 +179,23 @@ func upsertReceiver(ctx context.Context, kubeClient client.Client,
logger.Successf("Receiver updated")
return namespacedName, nil
}
func isReceiverReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, receiver *notificationv1.Receiver) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, receiver)
if err != nil {
return false, err
}
if c := apimeta.FindStatusCondition(receiver.Status.Conditions, meta.ReadyCondition); c != nil {
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}

@ -87,6 +87,7 @@ type secretGitFlags struct {
keyAlgorithm flags.PublicKeyAlgorithm
rsaBits flags.RSAKeyBits
ecdsaCurve flags.ECDSACurve
caFile string
caCrtFile string
privateKeyFile string
bearerToken string
@ -101,7 +102,8 @@ func init() {
createSecretGitCmd.Flags().Var(&secretGitArgs.keyAlgorithm, "ssh-key-algorithm", secretGitArgs.keyAlgorithm.Description())
createSecretGitCmd.Flags().Var(&secretGitArgs.rsaBits, "ssh-rsa-bits", secretGitArgs.rsaBits.Description())
createSecretGitCmd.Flags().Var(&secretGitArgs.ecdsaCurve, "ssh-ecdsa-curve", secretGitArgs.ecdsaCurve.Description())
createSecretGitCmd.Flags().StringVar(&secretGitArgs.caCrtFile, "ca-crt-file", "", "path to TLS CA certificate file used for validating self-signed certificates")
createSecretGitCmd.Flags().StringVar(&secretGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates")
createSecretGitCmd.Flags().StringVar(&secretGitArgs.caCrtFile, "ca-crt-file", "", "path to TLS CA certificate file used for validating self-signed certificates; takes precedence over --ca-file")
createSecretGitCmd.Flags().StringVar(&secretGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server")
createSecretGitCmd.Flags().StringVar(&secretGitArgs.bearerToken, "bearer-token", "", "bearer authentication token")
@ -167,6 +169,11 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
} else if secretGitArgs.caFile != "" {
opts.CAFile, err = os.ReadFile(secretGitArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
}
default:
return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme)

@ -1,128 +0,0 @@
/*
Copyright 2024 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"
"os"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
var createSecretGitHubAppCmd = &cobra.Command{
Use: "githubapp [name]",
Short: "Create or update a github app secret",
Long: withPreviewNote(`The create secret githubapp command generates a Kubernetes secret that can be used for GitRepository authentication with github app`),
Example: ` # Create a githubapp authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret githubapp podinfo-auth \
--app-id="1" \
--app-installation-id="2" \
--app-private-key=./private-key-file.pem \
--export > githubapp-auth.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place githubapp-auth.yaml
`,
RunE: createSecretGitHubAppCmdRun,
}
type secretGitHubAppFlags struct {
appID string
appInstallationID string
privateKeyFile string
baseURL string
}
var secretGitHubAppArgs = secretGitHubAppFlags{}
func init() {
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL")
createSecretCmd.AddCommand(createSecretGitHubAppCmd)
}
func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("name is required")
}
secretName := args[0]
if secretGitHubAppArgs.appID == "" {
return fmt.Errorf("--app-id is required")
}
if secretGitHubAppArgs.appInstallationID == "" {
return fmt.Errorf("--app-installation-id is required")
}
if secretGitHubAppArgs.privateKeyFile == "" {
return fmt.Errorf("--app-private-key is required")
}
privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile)
if err != nil {
return fmt.Errorf("unable to read private key file: %w", err)
}
opts := sourcesecret.Options{
Name: secretName,
Namespace: *kubeconfigArgs.Namespace,
GitHubAppID: secretGitHubAppArgs.appID,
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
GitHubAppPrivateKey: string(privateKey),
}
if secretGitHubAppArgs.baseURL != "" {
opts.GitHubAppBaseURL = secretGitHubAppArgs.baseURL
}
secret, err := sourcesecret.Generate(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("githubapp secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace)
return nil
}

@ -1,74 +0,0 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 TestCreateSecretGitHubApp(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "create githubapp secret with missing name",
args: "create secret githubapp",
assert: assertError("name is required"),
},
{
name: "create githubapp secret with missing app-id",
args: "create secret githubapp appinfo",
assert: assertError("--app-id is required"),
},
{
name: "create githubapp secret with missing appInstallationID",
args: "create secret githubapp appinfo --app-id 1",
assert: assertError("--app-installation-id is required"),
},
{
name: "create githubapp secret with missing private key file",
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2",
assert: assertError("--app-private-key is required"),
},
{
name: "create githubapp secret with private key file that does not exist",
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem",
assert: assertError("unable to read private key file: open pk.pem: no such file or directory"),
},
{
name: "create githubapp secret with app info",
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"),
},
{
name: "create githubapp secret with appinfo and base url",
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 --app-base-url www.example.com/api/v3 --export",
assert: assertGoldenFile("testdata/create_secret/githubapp/secret-with-baseurl.yaml"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

@ -32,7 +32,7 @@ import (
var createSecretHelmCmd = &cobra.Command{
Use: "helm [name]",
Short: "Create or update a Kubernetes secret for Helm repository authentication",
Long: `The create secret helm command generates a Kubernetes secret with basic authentication credentials.`,
Long: withPreviewNote(`The create secret helm command generates a Kubernetes secret with basic authentication credentials.`),
Example: ` # Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret helm repo-auth \
--namespace=my-namespace \
@ -58,9 +58,12 @@ func init() {
flags := createSecretHelmCmd.Flags()
flags.StringVarP(&secretHelmArgs.username, "username", "u", "", "basic authentication username")
flags.StringVarP(&secretHelmArgs.password, "password", "p", "", "basic authentication password")
flags.StringVar(&secretHelmArgs.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path")
flags.StringVar(&secretHelmArgs.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path")
flags.StringVar(&secretHelmArgs.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path")
initSecretDeprecatedTLSFlags(flags, &secretHelmArgs.secretTLSFlags)
deprecationMsg := "please use the command `flux create secret tls` to generate TLS secrets"
flags.MarkDeprecated("cert-file", deprecationMsg)
flags.MarkDeprecated("key-file", deprecationMsg)
flags.MarkDeprecated("ca-file", deprecationMsg)
createSecretCmd.AddCommand(createSecretHelmCmd)
}
@ -74,20 +77,20 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
}
caBundle := []byte{}
if secretHelmArgs.caCrtFile != "" {
if secretHelmArgs.caFile != "" {
var err error
caBundle, err = os.ReadFile(secretHelmArgs.caCrtFile)
caBundle, err = os.ReadFile(secretHelmArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
}
var certFile, keyFile []byte
if secretHelmArgs.tlsCrtFile != "" && secretHelmArgs.tlsKeyFile != "" {
if certFile, err = os.ReadFile(secretHelmArgs.tlsCrtFile); err != nil {
if secretHelmArgs.certFile != "" && secretHelmArgs.keyFile != "" {
if certFile, err = os.ReadFile(secretHelmArgs.certFile); err != nil {
return fmt.Errorf("failed to read cert file: %w", err)
}
if keyFile, err = os.ReadFile(secretHelmArgs.tlsKeyFile); err != nil {
if keyFile, err = os.ReadFile(secretHelmArgs.keyFile); err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
}
@ -98,9 +101,9 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
Labels: labels,
Username: secretHelmArgs.username,
Password: secretHelmArgs.password,
CACrt: caBundle,
TLSCrt: certFile,
TLSKey: keyFile,
CAFile: caBundle,
CertFile: certFile,
KeyFile: keyFile,
}
secret, err := sourcesecret.Generate(opts)
if err != nil {

@ -1,161 +0,0 @@
/*
Copyright 2024 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"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
var createSecretNotationCmd = &cobra.Command{
Use: "notation [name]",
Short: "Create or update a Kubernetes secret for verifications of artifacts signed by Notation",
Long: withPreviewNote(`The create secret notation command generates a Kubernetes secret with root ca certificates and trust policy.`),
Example: ` # Create a Notation configuration secret on disk and encrypt it with Mozilla SOPS
flux create secret notation my-notation-cert \
--namespace=my-namespace \
--trust-policy-file=./my-trust-policy.json \
--ca-cert-file=./my-cert.crt \
--export > my-notation-cert.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place my-notation-cert.yaml`,
RunE: createSecretNotationCmdRun,
}
type secretNotationFlags struct {
trustPolicyFile string
caCrtFile []string
}
var secretNotationArgs secretNotationFlags
func init() {
createSecretNotationCmd.Flags().StringVar(&secretNotationArgs.trustPolicyFile, "trust-policy-file", "", "notation trust policy file path")
createSecretNotationCmd.Flags().StringSliceVar(&secretNotationArgs.caCrtFile, "ca-cert-file", []string{}, "root ca cert file path")
createSecretCmd.AddCommand(createSecretNotationCmd)
}
func createSecretNotationCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("name is required")
}
if secretNotationArgs.caCrtFile == nil || len(secretNotationArgs.caCrtFile) == 0 {
return fmt.Errorf("--ca-cert-file is required")
}
if secretNotationArgs.trustPolicyFile == "" {
return fmt.Errorf("--trust-policy-file is required")
}
name := args[0]
labels, err := parseLabels()
if err != nil {
return err
}
policy, err := os.ReadFile(secretNotationArgs.trustPolicyFile)
if err != nil {
return fmt.Errorf("unable to read trust policy file: %w", err)
}
var doc trustpolicy.Document
if err := json.Unmarshal(policy, &doc); err != nil {
return fmt.Errorf("failed to unmarshal trust policy %s: %w", secretNotationArgs.trustPolicyFile, err)
}
if err := doc.Validate(); err != nil {
return fmt.Errorf("invalid trust policy: %w", err)
}
var (
caCerts []sourcesecret.VerificationCrt
fileErr error
)
for _, caCrtFile := range secretNotationArgs.caCrtFile {
fileName := filepath.Base(caCrtFile)
if !strings.HasSuffix(fileName, ".crt") && !strings.HasSuffix(fileName, ".pem") {
fileErr = errors.Join(fileErr, fmt.Errorf("%s must end with either .crt or .pem", fileName))
continue
}
caBundle, err := os.ReadFile(caCrtFile)
if err != nil {
fileErr = errors.Join(fileErr, fmt.Errorf("unable to read TLS CA file: %w", err))
continue
}
caCerts = append(caCerts, sourcesecret.VerificationCrt{Name: fileName, CACrt: caBundle})
}
if fileErr != nil {
return fileErr
}
if len(caCerts) == 0 {
return fmt.Errorf("no CA certs found")
}
opts := sourcesecret.Options{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: labels,
VerificationCrts: caCerts,
TrustPolicy: policy,
}
secret, err := sourcesecret.Generate(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("notation configuration secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
return nil
}

@ -1,124 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"os"
"path/filepath"
"testing"
)
const (
trustPolicy = "./testdata/create_secret/notation/test-trust-policy.json"
invalidTrustPolicy = "./testdata/create_secret/notation/invalid-trust-policy.json"
invalidJson = "./testdata/create_secret/notation/invalid.json"
testCertFolder = "./testdata/create_secret/notation"
)
func TestCreateNotationSecret(t *testing.T) {
crt, err := os.Create(filepath.Join(t.TempDir(), "ca.crt"))
if err != nil {
t.Fatal("could not create ca.crt file")
}
pem, err := os.Create(filepath.Join(t.TempDir(), "ca.pem"))
if err != nil {
t.Fatal("could not create ca.pem file")
}
invalidCert, err := os.Create(filepath.Join(t.TempDir(), "ca.p12"))
if err != nil {
t.Fatal("could not create ca.p12 file")
}
_, err = crt.Write([]byte("ca-data-crt"))
if err != nil {
t.Fatal("could not write to crt certificate file")
}
_, err = pem.Write([]byte("ca-data-pem"))
if err != nil {
t.Fatal("could not write to pem certificate file")
}
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "no args",
args: "create secret notation",
assert: assertError("name is required"),
},
{
name: "no trust policy",
args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s", testCertFolder),
assert: assertError("--trust-policy-file is required"),
},
{
name: "no cert",
args: fmt.Sprintf("create secret notation notation-config --trust-policy-file=%s", trustPolicy),
assert: assertError("--ca-cert-file is required"),
},
{
name: "non pem and crt cert",
args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", invalidCert.Name(), trustPolicy),
assert: assertError("ca.p12 must end with either .crt or .pem"),
},
{
name: "invalid trust policy",
args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", t.TempDir(), invalidTrustPolicy),
assert: assertError("invalid trust policy: trust policy: a trust policy statement is missing a name, every statement requires a name"),
},
{
name: "invalid trust policy json",
args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", t.TempDir(), invalidJson),
assert: assertError(fmt.Sprintf("failed to unmarshal trust policy %s: json: cannot unmarshal string into Go value of type trustpolicy.Document", invalidJson)),
},
{
name: "crt secret",
args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", crt.Name(), trustPolicy),
assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-crt.yaml"),
},
{
name: "pem secret",
args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", pem.Name(), trustPolicy),
assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-pem.yaml"),
},
{
name: "multi secret",
args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", crt.Name(), pem.Name(), trustPolicy),
assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-multi.yaml"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
secretNotationArgs = secretNotationFlags{}
}()
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

@ -1,112 +0,0 @@
/*
Copyright 2024 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"
"errors"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
)
var createSecretProxyCmd = &cobra.Command{
Use: "proxy [name]",
Short: "Create or update a Kubernetes secret for proxy authentication",
Long: `The create secret proxy command generates a Kubernetes secret with the
proxy address and the basic authentication credentials.`,
Example: ` # Create a proxy secret on disk and encrypt it with SOPS
flux create secret proxy my-proxy \
--namespace=my-namespace \
--address=https://my-proxy.com \
--username=my-username \
--password=my-password \
--export > proxy.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place proxy.yaml`,
RunE: createSecretProxyCmdRun,
}
type secretProxyFlags struct {
address string
username string
password string
}
var secretProxyArgs secretProxyFlags
func init() {
createSecretProxyCmd.Flags().StringVar(&secretProxyArgs.address, "address", "", "proxy address")
createSecretProxyCmd.Flags().StringVarP(&secretProxyArgs.username, "username", "u", "", "basic authentication username")
createSecretProxyCmd.Flags().StringVarP(&secretProxyArgs.password, "password", "p", "", "basic authentication password")
createSecretCmd.AddCommand(createSecretProxyCmd)
}
func createSecretProxyCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
labels, err := parseLabels()
if err != nil {
return err
}
if secretProxyArgs.address == "" {
return errors.New("address is required")
}
opts := sourcesecret.Options{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: labels,
Address: secretProxyArgs.address,
Username: secretProxyArgs.username,
Password: secretProxyArgs.password,
}
secret, err := sourcesecret.Generate(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("proxy secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
return nil
}

@ -1,47 +0,0 @@
/*
Copyright 2024 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 TestCreateProxySecret(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
args: "create secret proxy proxy-secret",
assert: assertError("address is required"),
},
{
args: "create secret proxy proxy-secret --address=https://my-proxy.com --username=my-username --password=my-password --namespace=my-namespace --export",
assert: assertGoldenFile("testdata/create_secret/proxy/secret-proxy.yaml"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

@ -22,6 +22,7 @@ import (
"os"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
@ -32,8 +33,8 @@ import (
var createSecretTLSCmd = &cobra.Command{
Use: "tls [name]",
Short: "Create or update a Kubernetes secret with TLS certificates",
Long: `The create secret tls command generates a Kubernetes secret with certificates for use with TLS.`,
Example: ` # Create a TLS secret on disk and encrypt it with SOPS.
Long: withPreviewNote(`The create secret tls command generates a Kubernetes secret with certificates for use with TLS.`),
Example: ` # Create a TLS secret on disk and encrypt it with Mozilla SOPS.
# Files are expected to be PEM-encoded.
flux create secret tls certs \
--namespace=my-namespace \
@ -48,6 +49,9 @@ var createSecretTLSCmd = &cobra.Command{
}
type secretTLSFlags struct {
certFile string
keyFile string
caFile string
caCrtFile string
tlsKeyFile string
tlsCrtFile string
@ -55,10 +59,26 @@ type secretTLSFlags struct {
var secretTLSArgs secretTLSFlags
func initSecretDeprecatedTLSFlags(flags *pflag.FlagSet, args *secretTLSFlags) {
flags.StringVar(&args.certFile, "cert-file", "", "TLS authentication cert file path")
flags.StringVar(&args.keyFile, "key-file", "", "TLS authentication key file path")
flags.StringVar(&args.caFile, "ca-file", "", "TLS authentication CA file path")
}
func initSecretTLSFlags(flags *pflag.FlagSet, args *secretTLSFlags) {
flags.StringVar(&args.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path")
flags.StringVar(&args.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path")
flags.StringVar(&args.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path")
}
func init() {
createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path")
createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path")
createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path")
flags := createSecretTLSCmd.Flags()
initSecretDeprecatedTLSFlags(flags, &secretTLSArgs)
initSecretTLSFlags(flags, &secretTLSArgs)
flags.MarkDeprecated("cert-file", "please use --tls-crt-file instead")
flags.MarkDeprecated("key-file", "please use --tls-key-file instead")
flags.MarkDeprecated("ca-file", "please use --ca-crt-file instead")
createSecretCmd.AddCommand(createSecretTLSCmd)
}
@ -82,6 +102,11 @@ func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
} else if secretTLSArgs.caFile != "" {
opts.CAFile, err = os.ReadFile(secretTLSArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
}
if secretTLSArgs.tlsCrtFile != "" && secretTLSArgs.tlsKeyFile != "" {
@ -91,6 +116,13 @@ func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error {
if opts.TLSKey, err = os.ReadFile(secretTLSArgs.tlsKeyFile); err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
} else if secretTLSArgs.certFile != "" && secretTLSArgs.keyFile != "" {
if opts.CertFile, err = os.ReadFile(secretTLSArgs.certFile); err != nil {
return fmt.Errorf("failed to read cert file: %w", err)
}
if opts.KeyFile, err = os.ReadFile(secretTLSArgs.keyFile); err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
}
secret, err := sourcesecret.Generate(opts)

@ -18,6 +18,10 @@ func TestCreateTlsSecret(t *testing.T) {
args: "create secret tls certs --namespace=my-namespace --tls-crt-file=./testdata/create_secret/tls/test-cert.pem --tls-key-file=./testdata/create_secret/tls/test-key.pem --ca-crt-file=./testdata/create_secret/tls/test-ca.pem --export",
assert: assertGoldenFile("testdata/create_secret/tls/secret-tls.yaml"),
},
{
args: "create secret tls certs --namespace=my-namespace --cert-file=./testdata/create_secret/tls/test-cert.pem --key-file=./testdata/create_secret/tls/test-key.pem --ca-file=./testdata/create_secret/tls/test-ca.pem --export",
assert: assertGoldenFile("testdata/create_secret/tls/deprecated-secret-tls.yaml"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

@ -31,8 +31,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
@ -41,8 +42,8 @@ import (
var createSourceBucketCmd = &cobra.Command{
Use: "bucket [name]",
Short: "Create or update a Bucket source",
Long: `The create source bucket command generates a Bucket resource and waits for it to be downloaded.
For Buckets with static authentication, the credentials are stored in a Kubernetes secret.`,
Long: withPreviewNote(`The create source bucket command generates a Bucket resource and waits for it to be downloaded.
For Buckets with static authentication, the credentials are stored in a Kubernetes secret.`),
Example: ` # Create a source for a Bucket using static authentication
flux create source bucket podinfo \
--bucket-name=podinfo \
@ -71,7 +72,6 @@ type sourceBucketFlags struct {
region string
insecure bool
secretRef string
proxySecretRef string
ignorePaths []string
}
@ -86,7 +86,6 @@ func init() {
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.region, "region", "", "the bucket region")
createSourceBucketCmd.Flags().BoolVar(&sourceBucketArgs.insecure, "insecure", false, "for when connecting to a non-TLS S3 HTTP endpoint")
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.secretRef, "secret-ref", "", "the name of an existing secret containing credentials")
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.proxySecretRef, "proxy-secret-ref", "", "the name of an existing secret containing the proxy address and credentials")
createSourceBucketCmd.Flags().StringSliceVar(&sourceBucketArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in bucket resource (can specify multiple paths with commas: path1,path2)")
createSourceCmd.AddCommand(createSourceBucketCmd)
@ -94,7 +93,7 @@ func init() {
func newSourceBucketFlags() sourceBucketFlags {
return sourceBucketFlags{
provider: flags.SourceBucketProvider(sourcev1.BucketProviderGeneric),
provider: flags.SourceBucketProvider(sourcev1.GenericBucketProvider),
}
}
@ -155,12 +154,6 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
}
}
if sourceBucketArgs.proxySecretRef != "" {
bucket.Spec.ProxySecretRef = &meta.LocalObjectReference{
Name: sourceBucketArgs.proxySecretRef,
}
}
if createArgs.export {
return printExport(exportBucket(bucket))
}
@ -211,8 +204,8 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for Bucket source reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isObjectReadyConditionFunc(kubeClient, namespacedName, bucket)); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isBucketReady(ctx, kubeClient, namespacedName, bucket)); err != nil {
return err
}
logger.Successf("Bucket source reconciliation completed")
@ -254,3 +247,30 @@ func upsertBucket(ctx context.Context, kubeClient client.Client,
logger.Successf("Bucket source updated")
return namespacedName, nil
}
func isBucketReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, bucket *sourcev1.Bucket) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, bucket)
if err != nil {
return false, err
}
if c := conditions.Get(bucket, meta.ReadyCondition); c != nil {
// Confirm the Ready condition we are observing is for the
// current generation
if c.ObservedGeneration != bucket.GetGeneration() {
return false, nil
}
// Further check the Status
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}

@ -1,217 +0,0 @@
/*
Copyright 2024 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"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
)
var createSourceChartCmd = &cobra.Command{
Use: "chart [name]",
Short: "Create or update a HelmChart source",
Long: `The create source chart command generates a HelmChart resource and waits for the chart to be available.`,
Example: ` # Create a source for a chart residing in a HelmRepository
flux create source chart podinfo \
--source=HelmRepository/podinfo \
--chart=podinfo \
--chart-version=6.x
# Create a source for a chart residing in a Git repository
flux create source chart podinfo \
--source=GitRepository/podinfo \
--chart=./charts/podinfo
# Create a source for a chart residing in a S3 Bucket
flux create source chart podinfo \
--source=Bucket/podinfo \
--chart=./charts/podinfo
# Create a source for a chart from OCI and verify its signature
flux create source chart podinfo \
--source HelmRepository/podinfo \
--chart podinfo \
--chart-version=6.6.2 \
--verify-provider=cosign \
--verify-issuer=https://token.actions.githubusercontent.com \
--verify-subject=https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2`,
RunE: createSourceChartCmdRun,
}
type sourceChartFlags struct {
chart string
chartVersion string
source flags.LocalHelmChartSource
reconcileStrategy string
verifyProvider flags.SourceOCIVerifyProvider
verifySecretRef string
verifyOIDCIssuer string
verifySubject string
}
var sourceChartArgs sourceChartFlags
func init() {
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.chart, "chart", "", "Helm chart name or path")
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.chartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)")
createSourceChartCmd.Flags().Var(&sourceChartArgs.source, "source", sourceChartArgs.source.Description())
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.reconcileStrategy, "reconcile-strategy", "ChartVersion", "the reconcile strategy for helm chart (accepted values: Revision and ChartRevision)")
createSourceChartCmd.Flags().Var(&sourceChartArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description())
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification")
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification")
createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification")
createSourceCmd.AddCommand(createSourceChartCmd)
}
func createSourceChartCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
if sourceChartArgs.source.Kind == "" || sourceChartArgs.source.Name == "" {
return fmt.Errorf("chart source is required")
}
if sourceChartArgs.chart == "" {
return fmt.Errorf("chart name or path is required")
}
logger.Generatef("generating HelmChart source")
sourceLabels, err := parseLabels()
if err != nil {
return err
}
helmChart := &sourcev1.HelmChart{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: sourceLabels,
},
Spec: sourcev1.HelmChartSpec{
Chart: sourceChartArgs.chart,
Version: sourceChartArgs.chartVersion,
Interval: metav1.Duration{
Duration: createArgs.interval,
},
ReconcileStrategy: sourceChartArgs.reconcileStrategy,
SourceRef: sourcev1.LocalHelmChartSourceReference{
Kind: sourceChartArgs.source.Kind,
Name: sourceChartArgs.source.Name,
},
},
}
if provider := sourceChartArgs.verifyProvider.String(); provider != "" {
helmChart.Spec.Verify = &sourcev1.OCIRepositoryVerification{
Provider: provider,
}
if secretName := sourceChartArgs.verifySecretRef; secretName != "" {
helmChart.Spec.Verify.SecretRef = &meta.LocalObjectReference{
Name: secretName,
}
}
verifyIssuer := sourceChartArgs.verifyOIDCIssuer
verifySubject := sourceChartArgs.verifySubject
if verifyIssuer != "" || verifySubject != "" {
helmChart.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{
Issuer: verifyIssuer,
Subject: verifySubject,
}}
}
} else if sourceChartArgs.verifySecretRef != "" {
return fmt.Errorf("a verification provider must be specified when a secret is specified")
} else if sourceChartArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" {
return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified")
}
if createArgs.export {
return printExport(exportHelmChart(helmChart))
}
logger.Actionf("applying HelmChart source")
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
namespacedName, err := upsertHelmChart(ctx, kubeClient, helmChart)
if err != nil {
return err
}
logger.Waitingf("waiting for HelmChart source reconciliation")
readyConditionFunc := isObjectReadyConditionFunc(kubeClient, namespacedName, helmChart)
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil {
return err
}
logger.Successf("HelmChart source reconciliation completed")
if helmChart.Status.Artifact == nil {
return fmt.Errorf("HelmChart source reconciliation completed but no artifact was found")
}
logger.Successf("fetched revision: %s", helmChart.Status.Artifact.Revision)
return nil
}
func upsertHelmChart(ctx context.Context, kubeClient client.Client,
helmChart *sourcev1.HelmChart) (types.NamespacedName, error) {
namespacedName := types.NamespacedName{
Namespace: helmChart.GetNamespace(),
Name: helmChart.GetName(),
}
var existing sourcev1.HelmChart
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
if err := kubeClient.Create(ctx, helmChart); err != nil {
return namespacedName, err
} else {
logger.Successf("source created")
return namespacedName, nil
}
}
return namespacedName, err
}
existing.Labels = helmChart.Labels
existing.Spec = helmChart.Spec
if err := kubeClient.Update(ctx, &existing); err != nil {
return namespacedName, err
}
helmChart = &existing
logger.Successf("source updated")
return namespacedName, nil
}

@ -1,91 +0,0 @@
//go:build unit
// +build unit
/*
Copyright 2024 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 TestCreateSourceChart(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupSourceChart(t, tmpl)
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "missing name",
args: "create source chart --export",
assert: assertError("name is required"),
},
{
name: "missing source reference",
args: "create source chart podinfo --export ",
assert: assertError("chart source is required"),
},
{
name: "missing chart name",
args: "create source chart podinfo --source helmrepository/podinfo --export",
assert: assertError("chart name or path is required"),
},
{
name: "unknown source kind",
args: "create source chart podinfo --source foobar/podinfo --export",
assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`),
},
{
name: "basic chart",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/basic.yaml", tmpl),
},
{
name: "chart with basic signature verification",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_basic.yaml", tmpl),
},
{
name: "unknown signature verification provider",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider foobar --export",
assert: assertError(`invalid argument "foobar" for "--verify-provider" flag: source OCI verify provider 'foobar' is not supported, must be one of: cosign`),
},
{
name: "chart with complete signature verification",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --verify-issuer foo --verify-subject bar --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_complete.yaml", tmpl),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
func setupSourceChart(t *testing.T, tmpl map[string]string) {
t.Helper()
testEnv.CreateObjectFile("./testdata/create_source_chart/setup-source.yaml", tmpl, t)
}

@ -35,6 +35,7 @@ import (
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
@ -56,8 +57,6 @@ type sourceGitFlags struct {
keyRSABits flags.RSAKeyBits
keyECDSACurve flags.ECDSACurve
secretRef string
proxySecretRef string
provider flags.SourceGitProvider
caFile string
privateKeyFile string
recurseSubmodules bool
@ -121,13 +120,7 @@ For private Git repositories, the basic authentication credentials are stored in
--url=https://github.com/stefanprodan/podinfo \
--branch=master \
--username=username \
--password=password
# Create a source for a Git repository using azure provider
flux create source git podinfo \
--url=https://dev.azure.com/foo/bar/_git/podinfo \
--branch=master \
--provider=azure`,
--password=password`,
RunE: createSourceGitCmdRun,
}
@ -138,16 +131,14 @@ func init() {
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.branch, "branch", "", "git branch")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.tag, "tag", "", "git tag")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.semver, "tag-semver", "", "git tag semver range")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.refName, "ref-name", "", "git reference name")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.refName, "ref-name", "", " git reference name")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.commit, "commit", "", "git commit")
createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.username, "username", "u", "", "basic authentication username")
createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.password, "password", "p", "", "basic authentication password")
createSourceGitCmd.Flags().Var(&sourceGitArgs.keyAlgorithm, "ssh-key-algorithm", sourceGitArgs.keyAlgorithm.Description())
createSourceGitCmd.Flags().Var(&sourceGitArgs.keyRSABits, "ssh-rsa-bits", sourceGitArgs.keyRSABits.Description())
createSourceGitCmd.Flags().Var(&sourceGitArgs.keyECDSACurve, "ssh-ecdsa-curve", sourceGitArgs.keyECDSACurve.Description())
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.secretRef, "secret-ref", "", "the name of an existing secret containing SSH or basic credentials or github app authentication")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.proxySecretRef, "proxy-secret-ref", "", "the name of an existing secret containing the proxy address and credentials")
createSourceGitCmd.Flags().Var(&sourceGitArgs.provider, "provider", sourceGitArgs.provider.Description())
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.secretRef, "secret-ref", "", "the name of an existing secret containing SSH or basic credentials")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server")
createSourceGitCmd.Flags().BoolVar(&sourceGitArgs.recurseSubmodules, "recurse-submodules", false,
@ -246,16 +237,6 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
}
}
if sourceGitArgs.proxySecretRef != "" {
gitRepository.Spec.ProxySecretRef = &meta.LocalObjectReference{
Name: sourceGitArgs.proxySecretRef,
}
}
if provider := sourceGitArgs.provider.String(); provider != "" {
gitRepository.Spec.Provider = provider
}
if createArgs.export {
return printExport(exportGit(&gitRepository))
}
@ -293,7 +274,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
secretOpts.CACrt = caBundle
secretOpts.CAFile = caBundle
}
secretOpts.Username = sourceGitArgs.username
secretOpts.Password = sourceGitArgs.password
@ -344,8 +325,8 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for GitRepository source reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isObjectReadyConditionFunc(kubeClient, namespacedName, &gitRepository)); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isGitRepositoryReady(ctx, kubeClient, namespacedName, &gitRepository)); err != nil {
return err
}
logger.Successf("GitRepository source reconciliation completed")
@ -387,3 +368,30 @@ func upsertGitRepository(ctx context.Context, kubeClient client.Client,
logger.Successf("GitRepository source updated")
return namespacedName, nil
}
func isGitRepositoryReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, gitRepository *sourcev1.GitRepository) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, gitRepository)
if err != nil {
return false, err
}
if c := conditions.Get(gitRepository, meta.ReadyCondition); c != nil {
// Confirm the Ready condition we are observing is for the
// current generation
if c.ObservedGeneration != gitRepository.GetGeneration() {
return false, nil
}
// Further check the Status
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}

@ -134,36 +134,6 @@ func TestCreateSourceGitExport(t *testing.T) {
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --branch=test --interval=1m0s --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-branch.yaml"),
},
{
name: "source with generic provider",
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --provider generic --branch=test --interval=1m0s --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-provider-generic.yaml"),
},
{
name: "source with azure provider",
args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider azure --branch=test --interval=1m0s --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-provider-azure.yaml"),
},
{
name: "source with github provider",
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --provider github --branch=test --interval=1m0s --secret-ref appinfo --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-provider-github.yaml"),
},
{
name: "source with invalid provider",
args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider dummy --branch=test --interval=1m0s --export",
assert: assertError("invalid argument \"dummy\" for \"--provider\" flag: source Git provider 'dummy' is not supported, must be one of: generic|azure|github"),
},
{
name: "source with empty provider",
args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --provider \"\" --branch=test --interval=1m0s --export",
assert: assertError("invalid argument \"\" for \"--provider\" flag: no source Git provider given, please specify the Git provider name"),
},
{
name: "source with no provider",
args: "create source git podinfo --namespace=flux-system --url=https://dev.azure.com/foo/bar/_git/podinfo --branch=test --interval=1m0s --export --provider",
assert: assertError("flag needs an argument: --provider"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@ -211,21 +181,12 @@ func TestCreateSourceGit(t *testing.T) {
Time: time.Now(),
},
}
repo.Status.ObservedGeneration = repo.GetGeneration()
},
}, {
"Failed",
command,
assertError("failed message"),
func(repo *sourcev1.GitRepository) {
stalledCondition := metav1.Condition{
Type: meta.StalledCondition,
Status: metav1.ConditionTrue,
Reason: sourcev1.URLInvalidReason,
Message: "failed message",
ObservedGeneration: repo.GetGeneration(),
}
apimeta.SetStatusCondition(&repo.Status.Conditions, stalledCondition)
newCondition := metav1.Condition{
Type: meta.ReadyCondition,
Status: metav1.ConditionFalse,
@ -234,7 +195,6 @@ func TestCreateSourceGit(t *testing.T) {
ObservedGeneration: repo.GetGeneration(),
}
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
repo.Status.ObservedGeneration = repo.GetGeneration()
},
}, {
"NoArtifact",
@ -250,7 +210,6 @@ func TestCreateSourceGit(t *testing.T) {
ObservedGeneration: repo.GetGeneration(),
}
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
repo.Status.ObservedGeneration = repo.GetGeneration()
},
},
}

@ -22,6 +22,8 @@ import (
"net/url"
"os"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
@ -31,8 +33,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
@ -41,8 +42,8 @@ import (
var createSourceHelmCmd = &cobra.Command{
Use: "helm [name]",
Short: "Create or update a HelmRepository source",
Long: `The create source helm command generates a HelmRepository resource and waits for it to fetch the index.
For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`,
Long: withPreviewNote(`The create source helm command generates a HelmRepository resource and waits for it to fetch the index.
For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`),
Example: ` # Create a source for an HTTPS public Helm repository
flux create source helm podinfo \
--url=https://stefanprodan.github.io/podinfo \
@ -197,9 +198,9 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace,
Username: sourceHelmArgs.username,
Password: sourceHelmArgs.password,
CACrt: caBundle,
TLSCrt: certFile,
TLSKey: keyFile,
CAFile: caBundle,
CertFile: certFile,
KeyFile: keyFile,
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
}
secret, err := sourcesecret.Generate(secretOpts)
@ -230,12 +231,8 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for HelmRepository source reconciliation")
readyConditionFunc := isObjectReadyConditionFunc(kubeClient, namespacedName, helmRepository)
if helmRepository.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
// HelmRepository type OCI is a static object.
readyConditionFunc = isStaticObjectReadyConditionFunc(kubeClient, namespacedName, helmRepository)
}
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isHelmRepositoryReady(ctx, kubeClient, namespacedName, helmRepository)); err != nil {
return err
}
logger.Successf("HelmRepository source reconciliation completed")
@ -282,3 +279,30 @@ func upsertHelmRepository(ctx context.Context, kubeClient client.Client,
logger.Successf("source updated")
return namespacedName, nil
}
func isHelmRepositoryReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, helmRepository *sourcev1.HelmRepository) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, helmRepository)
if err != nil {
return false, err
}
if c := conditions.Get(helmRepository, meta.ReadyCondition); c != nil {
// Confirm the Ready condition we are observing is for the
// current generation
if c.ObservedGeneration != helmRepository.GetGeneration() {
return false, nil
}
// Further check the Status
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}

@ -29,9 +29,9 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
@ -44,17 +44,8 @@ var createSourceOCIRepositoryCmd = &cobra.Command{
Example: ` # Create an OCIRepository for a public container image
flux create source oci podinfo \
--url=oci://ghcr.io/stefanprodan/manifests/podinfo \
--tag=6.6.2 \
--tag=6.1.6 \
--interval=10m
# Create an OCIRepository with OIDC signature verification
flux create source oci podinfo \
--url=oci://ghcr.io/stefanprodan/manifests/podinfo \
--tag=6.6.2 \
--interval=10m \
--verify-provider=cosign \
--verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \
--verify-issuer="^https://token.actions.githubusercontent.com$"
`,
RunE: createSourceOCIRepositoryCmdRun,
}
@ -65,13 +56,8 @@ type sourceOCIRepositoryFlags struct {
semver string
digest string
secretRef string
proxySecretRef string
serviceAccount string
certSecretRef string
verifyProvider flags.SourceOCIVerifyProvider
verifySecretRef string
verifyOIDCIssuer string
verifySubject string
ignorePaths []string
provider flags.SourceOCIProvider
insecure bool
@ -81,7 +67,7 @@ var sourceOCIRepositoryArgs = newSourceOCIFlags()
func newSourceOCIFlags() sourceOCIRepositoryFlags {
return sourceOCIRepositoryFlags{
provider: flags.SourceOCIProvider(sourcev1b2.GenericOCIProvider),
provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider),
}
}
@ -92,13 +78,8 @@ func init() {
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.semver, "tag-semver", "", "the OCI artifact tag semver range")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.digest, "digest", "", "the OCI artifact digest")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.proxySecretRef, "proxy-secret-ref", "", "the name of an existing secret containing the proxy address and credentials")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.serviceAccount, "service-account", "", "the name of the Kubernetes service account that refers to an image pull secret")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.certSecretRef, "cert-ref", "", "the name of a secret to use for TLS certificates")
createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description())
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification")
createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)")
createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP")
@ -127,20 +108,20 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
ignorePaths = &ignorePathsStr
}
repository := &sourcev1b2.OCIRepository{
repository := &sourcev1.OCIRepository{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: sourceLabels,
},
Spec: sourcev1b2.OCIRepositorySpec{
Spec: sourcev1.OCIRepositorySpec{
Provider: sourceOCIRepositoryArgs.provider.String(),
URL: sourceOCIRepositoryArgs.url,
Insecure: sourceOCIRepositoryArgs.insecure,
Interval: metav1.Duration{
Duration: createArgs.interval,
},
Reference: &sourcev1b2.OCIRepositoryRef{},
Reference: &sourcev1.OCIRepositoryRef{},
Ignore: ignorePaths,
},
}
@ -169,41 +150,12 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
}
}
if secretName := sourceOCIRepositoryArgs.proxySecretRef; secretName != "" {
repository.Spec.ProxySecretRef = &meta.LocalObjectReference{
Name: secretName,
}
}
if secretName := sourceOCIRepositoryArgs.certSecretRef; secretName != "" {
repository.Spec.CertSecretRef = &meta.LocalObjectReference{
Name: secretName,
}
}
if provider := sourceOCIRepositoryArgs.verifyProvider.String(); provider != "" {
repository.Spec.Verify = &sourcev1.OCIRepositoryVerification{
Provider: provider,
}
if secretName := sourceOCIRepositoryArgs.verifySecretRef; secretName != "" {
repository.Spec.Verify.SecretRef = &meta.LocalObjectReference{
Name: secretName,
}
}
verifyIssuer := sourceOCIRepositoryArgs.verifyOIDCIssuer
verifySubject := sourceOCIRepositoryArgs.verifySubject
if verifyIssuer != "" || verifySubject != "" {
repository.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{
Issuer: verifyIssuer,
Subject: verifySubject,
}}
}
} else if sourceOCIRepositoryArgs.verifySecretRef != "" {
return fmt.Errorf("a verification provider must be specified when a secret is specified")
} else if sourceOCIRepositoryArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" {
return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified")
}
if createArgs.export {
return printExport(exportOCIRepository(repository))
}
@ -223,8 +175,8 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
}
logger.Waitingf("waiting for OCIRepository reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isObjectReadyConditionFunc(kubeClient, namespacedName, repository)); err != nil {
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout,
isOCIRepositoryReady(ctx, kubeClient, namespacedName, repository)); err != nil {
return err
}
logger.Successf("OCIRepository reconciliation completed")
@ -237,13 +189,13 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
}
func upsertOCIRepository(ctx context.Context, kubeClient client.Client,
ociRepository *sourcev1b2.OCIRepository) (types.NamespacedName, error) {
ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) {
namespacedName := types.NamespacedName{
Namespace: ociRepository.GetNamespace(),
Name: ociRepository.GetName(),
}
var existing sourcev1b2.OCIRepository
var existing sourcev1.OCIRepository
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
@ -266,3 +218,30 @@ func upsertOCIRepository(ctx context.Context, kubeClient client.Client,
logger.Successf("OCIRepository updated")
return namespacedName, nil
}
func isOCIRepositoryReady(ctx context.Context, kubeClient client.Client,
namespacedName types.NamespacedName, ociRepository *sourcev1.OCIRepository) wait.ConditionFunc {
return func() (bool, error) {
err := kubeClient.Get(ctx, namespacedName, ociRepository)
if err != nil {
return false, err
}
if c := conditions.Get(ociRepository, meta.ReadyCondition); c != nil {
// Confirm the Ready condition we are observing is for the
// current generation
if c.ObservedGeneration != ociRepository.GetGeneration() {
return false, nil
}
// Further check the Status
switch c.Status {
case metav1.ConditionTrue:
return true, nil
case metav1.ConditionFalse:
return false, fmt.Errorf(c.Message)
}
}
return false, nil
}
}

@ -36,36 +36,6 @@ func TestCreateSourceOCI(t *testing.T) {
args: "create source oci podinfo",
assertFunc: assertError("url is required"),
},
{
name: "verify secret specified but provider missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-secret-ref=cosign-pub",
assertFunc: assertError("a verification provider must be specified when a secret is specified"),
},
{
name: "verify issuer specified but provider missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github.com",
assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"),
},
{
name: "verify identity specified but provider missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=developer",
assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"),
},
{
name: "verify issuer specified but subject missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github --verify-provider=cosign --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"),
},
{
name: "all verify fields set",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github verify-subject=stefanprodan --verify-provider=cosign --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"),
},
{
name: "verify subject specified but issuer missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=stefanprodan --verify-provider=cosign --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_subject.golden"),
},
{
name: "export manifest",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --export",
@ -76,11 +46,6 @@ func TestCreateSourceOCI(t *testing.T) {
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --secret-ref=creds --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_secret.golden"),
},
{
name: "export manifest with verify secret",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"),
},
}
for _, tt := range tests {

@ -1,31 +0,0 @@
/*
Copyright 2024 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 debugCmd = &cobra.Command{
Use: "debug",
Short: "Debug a flux resource",
Long: `The debug command can be used to troubleshoot failing resource reconciliations.`,
}
func init() {
rootCmd.AddCommand(debugCmd)
}

@ -1,113 +0,0 @@
/*
Copyright 2024 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"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
"github.com/fluxcd/pkg/chartutil"
"github.com/go-logr/logr"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/v2/internal/utils"
)
var debugHelmReleaseCmd = &cobra.Command{
Use: "helmrelease [name]",
Aliases: []string{"hr"},
Short: "Debug a HelmRelease resource",
Long: withPreviewNote(`The debug helmrelease command can be used to troubleshoot failing Helm release reconciliations.
WARNING: This command will print sensitive information if Kubernetes Secrets are referenced in the HelmRelease .spec.valuesFrom field.`),
Example: ` # Print the status of a Helm release
flux debug hr podinfo --show-status
# Export the final values of a Helm release composed from referred ConfigMaps and Secrets
flux debug hr podinfo --show-values > values.yaml`,
RunE: debugHelmReleaseCmdRun,
Args: cobra.ExactArgs(1),
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),
}
type debugHelmReleaseFlags struct {
showStatus bool
showValues bool
}
var debugHelmReleaseArgs debugHelmReleaseFlags
func init() {
debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showStatus, "show-status", false, "print the status of the Helm release")
debugHelmReleaseCmd.Flags().BoolVar(&debugHelmReleaseArgs.showValues, "show-values", false, "print the final values of the Helm release")
debugCmd.AddCommand(debugHelmReleaseCmd)
}
func debugHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
if (!debugHelmReleaseArgs.showStatus && !debugHelmReleaseArgs.showValues) ||
(debugHelmReleaseArgs.showStatus && debugHelmReleaseArgs.showValues) {
return fmt.Errorf("either --show-status or --show-values must be set")
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
hr := &helmv2.HelmRelease{}
hrName := types.NamespacedName{Namespace: *kubeconfigArgs.Namespace, Name: name}
if err := kubeClient.Get(ctx, hrName, hr); err != nil {
return err
}
if debugHelmReleaseArgs.showStatus {
status, err := yaml.Marshal(hr.Status)
if err != nil {
return err
}
rootCmd.Println("# Status documentation: https://fluxcd.io/flux/components/helm/helmreleases/#helmrelease-status")
rootCmd.Print(string(status))
return nil
}
if debugHelmReleaseArgs.showValues {
finalValues, err := chartutil.ChartValuesFromReferences(ctx,
logr.Discard(),
kubeClient,
hr.GetNamespace(),
hr.GetValues(),
hr.Spec.ValuesFrom...)
if err != nil {
return err
}
values, err := yaml.Marshal(finalValues)
if err != nil {
return err
}
rootCmd.Print(string(values))
}
return nil
}

@ -1,71 +0,0 @@
//go:build unit
// +build unit
/*
Copyright 2024 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 TestDebugHelmRelease(t *testing.T) {
namespace := allocateNamespace("debug")
objectFile := "testdata/debug_helmrelease/objects.yaml"
tmpl := map[string]string{
"fluxns": namespace,
}
testEnv.CreateObjectFile(objectFile, tmpl, t)
cases := []struct {
name string
arg string
goldenFile string
tmpl map[string]string
}{
{
"debug status",
"debug helmrelease test-values-inline --show-status --show-values=false",
"testdata/debug_helmrelease/status.golden.yaml",
tmpl,
},
{
"debug values",
"debug helmrelease test-values-inline --show-values --show-status=false",
"testdata/debug_helmrelease/values-inline.golden.yaml",
tmpl,
},
{
"debug values from",
"debug helmrelease test-values-from --show-values --show-status=false",
"testdata/debug_helmrelease/values-from.golden.yaml",
tmpl,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.arg + " -n=" + namespace,
assert: assertGoldenTemplateFile(tt.goldenFile, tmpl),
}
cmd.runTestCmd(t)
})
}
}

@ -1,134 +0,0 @@
/*
Copyright 2024 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"
"errors"
"fmt"
"sort"
"strings"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/pkg/kustomize"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/v2/internal/utils"
)
var debugKustomizationCmd = &cobra.Command{
Use: "kustomization [name]",
Aliases: []string{"ks"},
Short: "Debug a Flux Kustomization resource",
Long: withPreviewNote(`The debug kustomization command can be used to troubleshoot failing Flux Kustomization reconciliations.
WARNING: This command will print sensitive information if Kubernetes Secrets are referenced in the Kustomization .spec.postBuild.substituteFrom field.`),
Example: ` # Print the status of a Flux Kustomization
flux debug ks podinfo --show-status
# Export the final variables used for post-build substitutions composed from referred ConfigMaps and Secrets
flux debug ks podinfo --show-vars > vars.env`,
RunE: debugKustomizationCmdRun,
Args: cobra.ExactArgs(1),
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
}
type debugKustomizationFlags struct {
showStatus bool
showVars bool
}
var debugKustomizationArgs debugKustomizationFlags
func init() {
debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showStatus, "show-status", false, "print the status of the Flux Kustomization")
debugKustomizationCmd.Flags().BoolVar(&debugKustomizationArgs.showVars, "show-vars", false, "print the final vars of the Flux Kustomization in dot env format")
debugCmd.AddCommand(debugKustomizationCmd)
}
func debugKustomizationCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
if (!debugKustomizationArgs.showStatus && !debugKustomizationArgs.showVars) ||
(debugKustomizationArgs.showStatus && debugKustomizationArgs.showVars) {
return fmt.Errorf("either --show-status or --show-vars must be set")
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
ks := &kustomizev1.Kustomization{}
ksName := types.NamespacedName{Namespace: *kubeconfigArgs.Namespace, Name: name}
if err := kubeClient.Get(ctx, ksName, ks); err != nil {
return err
}
if debugKustomizationArgs.showStatus {
status, err := yaml.Marshal(ks.Status)
if err != nil {
return err
}
rootCmd.Println("# Status documentation: https://fluxcd.io/flux/components/kustomize/kustomizations/#kustomization-status")
rootCmd.Print(string(status))
return nil
}
if debugKustomizationArgs.showVars {
if ks.Spec.PostBuild == nil {
return errors.New("no post build substitutions found")
}
ksObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(ks)
if err != nil {
return err
}
finalVars, err := kustomize.LoadVariables(ctx, kubeClient, unstructured.Unstructured{Object: ksObj})
if err != nil {
return err
}
if len(ks.Spec.PostBuild.Substitute) > 0 {
for k, v := range ks.Spec.PostBuild.Substitute {
// Remove new lines from the values as they are not supported.
// Replicates the controller behavior from
// https://github.com/fluxcd/pkg/blob/main/kustomize/kustomize_varsub.go
finalVars[k] = strings.ReplaceAll(v, "\n", "")
}
}
keys := make([]string, 0, len(finalVars))
for k := range finalVars {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
rootCmd.Println(k + "=" + finalVars[k])
}
}
return nil
}

@ -1,71 +0,0 @@
//go:build unit
// +build unit
/*
Copyright 2024 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 TestDebugKustomization(t *testing.T) {
namespace := allocateNamespace("debug")
objectFile := "testdata/debug_kustomization/objects.yaml"
tmpl := map[string]string{
"fluxns": namespace,
}
testEnv.CreateObjectFile(objectFile, tmpl, t)
cases := []struct {
name string
arg string
goldenFile string
tmpl map[string]string
}{
{
"debug status",
"debug ks test --show-status --show-vars=false",
"testdata/debug_kustomization/status.golden.yaml",
tmpl,
},
{
"debug vars",
"debug ks test --show-vars --show-status=false",
"testdata/debug_kustomization/vars.golden.env",
tmpl,
},
{
"debug vars from",
"debug ks test-from --show-vars --show-status=false",
"testdata/debug_kustomization/vars-from.golden.env",
tmpl,
},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.arg + " -n=" + namespace,
assert: assertGoldenTemplateFile(tt.goldenFile, tmpl),
}
cmd.runTestCmd(t)
})
}
}

@ -19,7 +19,7 @@ package main
import (
"github.com/spf13/cobra"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2"
)
var deleteAlertCmd = &cobra.Command{

@ -19,7 +19,7 @@ package main
import (
"github.com/spf13/cobra"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2"
)
var deleteAlertProviderCmd = &cobra.Command{

@ -1,5 +1,5 @@
/*
Copyright 2024 The Flux authors
Copyright 2020 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.
@ -19,14 +19,14 @@ package main
import (
"github.com/spf13/cobra"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
)
var deleteHelmReleaseCmd = &cobra.Command{
Use: "helmrelease [name]",
Aliases: []string{"hr"},
Short: "Delete a HelmRelease resource",
Long: "The delete helmrelease command removes the given HelmRelease from the cluster.",
Long: withPreviewNote("The delete helmrelease command removes the given HelmRelease from the cluster."),
Example: ` # Delete a Helm release and the Kubernetes resources created by it
flux delete hr podinfo`,
ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)),

@ -19,7 +19,7 @@ package main
import (
"github.com/spf13/cobra"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
)
var deleteImageUpdateCmd = &cobra.Command{

@ -19,13 +19,13 @@ package main
import (
"github.com/spf13/cobra"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)
var deleteSourceBucketCmd = &cobra.Command{
Use: "bucket [name]",
Short: "Delete a Bucket source",
Long: "The delete source bucket command deletes the given Bucket from the cluster.",
Long: withPreviewNote("The delete source bucket command deletes the given Bucket from the cluster."),
Example: ` # Delete a Bucket source
flux delete source bucket podinfo`,
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)),

@ -1,40 +0,0 @@
/*
Copyright 2024 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"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)
var deleteSourceChartCmd = &cobra.Command{
Use: "chart [name]",
Short: "Delete a HelmChart source",
Long: "The delete source chart command deletes the given HelmChart from the cluster.",
Example: ` # Delete a HelmChart
flux delete source chart podinfo`,
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)),
RunE: deleteCommand{
apiType: helmChartType,
object: universalAdapter{&sourcev1.HelmChart{}},
}.run,
}
func init() {
deleteSourceCmd.AddCommand(deleteSourceChartCmd)
}

@ -19,13 +19,13 @@ package main
import (
"github.com/spf13/cobra"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)
var deleteSourceHelmCmd = &cobra.Command{
Use: "helm [name]",
Short: "Delete a HelmRepository source",
Long: "The delete source helm command deletes the given HelmRepository from the cluster.",
Long: withPreviewNote("The delete source helm command deletes the given HelmRepository from the cluster."),
Example: ` # Delete a Helm repository
flux delete source helm podinfo`,
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)),

@ -23,7 +23,6 @@ import (
oci "github.com/fluxcd/pkg/oci/client"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/google/go-containerregistry/pkg/crane"
"github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/flags"
@ -43,7 +42,6 @@ type diffArtifactFlags struct {
creds string
provider flags.SourceOCIProvider
ignorePaths []string
insecure bool
}
var diffArtifactArgs = newDiffArtifactArgs()
@ -59,7 +57,6 @@ func init() {
diffArtifactCmd.Flags().StringVar(&diffArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format <username>[:<password>] if --provider is generic")
diffArtifactCmd.Flags().Var(&diffArtifactArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description())
diffArtifactCmd.Flags().StringSliceVar(&diffArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
diffArtifactCmd.Flags().BoolVar(&diffArtifactArgs.insecure, "insecure-registry", false, "allows the remote artifact to be pulled without TLS")
diffCmd.AddCommand(diffArtifactCmd)
}
@ -85,13 +82,7 @@ func diffArtifactCmdRun(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
opts := oci.DefaultOptions()
if diffArtifactArgs.insecure {
opts = append(opts, crane.Insecure)
}
ociClient := oci.NewClient(opts)
ociClient := oci.NewClient(oci.DefaultOptions())
if diffArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && diffArtifactArgs.creds != "" {
logger.Actionf("logging in to registry with credentials")

@ -23,9 +23,8 @@ import (
"github.com/spf13/cobra"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/build"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
)
var diffKsCmd = &cobra.Command{
@ -44,12 +43,7 @@ flux diff kustomization my-app --path ./path/to/local/manifests \
# Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat.
flux diff kustomization my-app --path ./path/to/local/manifests \
--kustomization-file ./path/to/local/my-app.yaml \
--ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"
# Run recursively on all encountered Kustomizations
flux diff kustomization my-app --path ./path/to/local/manifests \
--recursive \
--local-sources GitRepository/flux-system/my-repo=./path/to/local/git`,
--ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: diffKsCmdRun,
}
@ -59,9 +53,6 @@ type diffKsFlags struct {
path string
ignorePaths []string
progressBar bool
strictSubst bool
recursive bool
localSources map[string]string
}
var diffKsArgs diffKsFlags
@ -71,10 +62,6 @@ func init() {
diffKsCmd.Flags().BoolVar(&diffKsArgs.progressBar, "progress-bar", true, "Boolean to set the progress bar. The default value is true.")
diffKsCmd.Flags().StringSliceVar(&diffKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format")
diffKsCmd.Flags().StringVar(&diffKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.")
diffKsCmd.Flags().BoolVar(&diffKsArgs.strictSubst, "strict-substitute", false,
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
diffKsCmd.Flags().BoolVarP(&diffKsArgs.recursive, "recursive", "r", false, "Recursively diff Kustomizations")
diffKsCmd.Flags().StringToStringVar(&diffKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
diffCmd.AddCommand(diffKsCmd)
}
@ -109,10 +96,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithKustomizationFile(diffKsArgs.kustomizationFile),
build.WithProgressBar(),
build.WithIgnore(diffKsArgs.ignorePaths),
build.WithStrictSubstitute(diffKsArgs.strictSubst),
build.WithRecursive(diffKsArgs.recursive),
build.WithLocalSources(diffKsArgs.localSources),
build.WithSingleKustomization(),
)
} else {
builder, err = build.NewBuilder(name, diffKsArgs.path,
@ -120,10 +103,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithTimeout(rootArgs.timeout),
build.WithKustomizationFile(diffKsArgs.kustomizationFile),
build.WithIgnore(diffKsArgs.ignorePaths),
build.WithStrictSubstitute(diffKsArgs.strictSubst),
build.WithRecursive(diffKsArgs.recursive),
build.WithLocalSources(diffKsArgs.localSources),
build.WithSingleKustomization(),
)
}
@ -153,12 +132,6 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
select {
case <-sigc:
if diffKsArgs.progressBar {
err := builder.StopSpinner()
if err != nil {
return err
}
}
fmt.Println("Build cancelled... exiting.")
return builder.Cancel()
case err := <-errChan:

@ -97,12 +97,6 @@ func TestDiffKustomization(t *testing.T) {
objectFile: "",
assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"),
},
{
name: "diff with recursive",
args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --progress-bar=false --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
objectFile: "./testdata/diff-kustomization/my-app.yaml",
assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-recursive.golden"),
},
}
tmpl := map[string]string{

@ -1,74 +0,0 @@
/*
Copyright 2024 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 (
"bufio"
"fmt"
"github.com/fluxcd/pkg/envsubst"
"github.com/spf13/cobra"
)
var envsubstCmd = &cobra.Command{
Use: "envsubst",
Args: cobra.NoArgs,
Short: "envsubst substitutes the values of environment variables",
Long: withPreviewNote(`The envsubst command substitutes the values of environment variables
in the string piped as standard input and writes the result to the standard output. This command can be used
to replicate the behavior of the Flux Kustomization post-build substitutions.`),
Example: ` # Run env var substitutions on the kustomization build output
export cluster_region=eu-central-1
kustomize build . | flux envsubst
# Run env var substitutions and error out if a variable is not set
kustomize build . | flux envsubst --strict
`,
RunE: runEnvsubstCmd,
}
type envsubstFlags struct {
strict bool
}
var envsubstArgs envsubstFlags
func init() {
envsubstCmd.Flags().BoolVar(&envsubstArgs.strict, "strict", false,
"fail if a variable without a default value is declared in the input but is missing from the environment")
rootCmd.AddCommand(envsubstCmd)
}
func runEnvsubstCmd(cmd *cobra.Command, args []string) error {
stdin := bufio.NewScanner(rootCmd.InOrStdin())
stdout := bufio.NewWriter(rootCmd.OutOrStdout())
for stdin.Scan() {
line, err := envsubst.EvalEnv(stdin.Text(), envsubstArgs.strict)
if err != nil {
return err
}
_, err = fmt.Fprintln(stdout, line)
if err != nil {
return err
}
err = stdout.Flush()
if err != nil {
return err
}
}
return nil
}

@ -1,50 +0,0 @@
/*
Copyright 2024 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"
"os"
"testing"
. "github.com/onsi/gomega"
)
func TestEnvsubst(t *testing.T) {
g := NewWithT(t)
input, err := os.ReadFile("testdata/envsubst/file.yaml")
g.Expect(err).NotTo(HaveOccurred())
t.Setenv("REPO_NAME", "test")
output, err := executeCommandWithIn("envsubst", bytes.NewReader(input))
g.Expect(err).NotTo(HaveOccurred())
expected, err := os.ReadFile("testdata/envsubst/file.gold")
g.Expect(err).NotTo(HaveOccurred())
g.Expect(output).To(Equal(string(expected)))
}
func TestEnvsubst_Strinct(t *testing.T) {
g := NewWithT(t)
input, err := os.ReadFile("testdata/envsubst/file.yaml")
g.Expect(err).NotTo(HaveOccurred())
_, err = executeCommandWithIn("envsubst --strict", bytes.NewReader(input))
g.Expect(err).To(HaveOccurred())
g.Expect(err.Error()).To(ContainSubstring("variable not set (strict mode)"))
}

@ -39,12 +39,12 @@ import (
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"sigs.k8s.io/controller-runtime/pkg/client"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1b2 "github.com/fluxcd/notification-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
@ -62,14 +62,8 @@ var eventsCmd = &cobra.Command{
# Display events for flux resources in all namespaces
flux events -A
# Display events for a Kustomization named podinfo
# Display events for flux resources
flux events --for Kustomization/podinfo
# Display events for all Kustomizations in default namespace
flux events --for Kustomization -n default
# Display warning events for alert resources
flux events --for Alert/podinfo --types warning
`,
RunE: eventsCmdRun,
}
@ -90,7 +84,7 @@ func init() {
"indicate if the events should be streamed")
eventsCmd.Flags().StringVar(&eventArgs.forSelector, "for", "",
"get events for a particular object")
eventsCmd.Flags().StringSliceVar(&eventArgs.filterTypes, "types", []string{}, "filter events for certain types (valid types are: Normal, Warning)")
eventsCmd.Flags().StringSliceVar(&eventArgs.filterTypes, "types", []string{}, "filter events for certain types")
rootCmd.AddCommand(eventsCmd)
}
@ -98,10 +92,6 @@ func eventsCmdRun(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
if err := validateEventTypes(eventArgs.filterTypes); err != nil {
return err
}
kubeclient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
@ -113,33 +103,21 @@ func eventsCmdRun(cmd *cobra.Command, args []string) error {
}
var diffRefNs bool
clientListOpts := []client.ListOption{client.InNamespace(*kubeconfigArgs.Namespace)}
clientListOpts := getListOpt(namespace, eventArgs.forSelector)
var refListOpts [][]client.ListOption
if eventArgs.forSelector != "" {
kind, name := getKindNameFromSelector(eventArgs.forSelector)
if kind == "" {
return fmt.Errorf("--for selector must be of format <kind>[/<name>]")
}
refInfoKind, err := fluxKindMap.getRefInfo(kind)
if err != nil {
return err
}
clientListOpts = append(clientListOpts, getListOpt(refInfoKind.gvk.Kind, name))
if name != "" {
refs, err := getObjectRef(ctx, kubeclient, refInfoKind, name, *kubeconfigArgs.Namespace)
refs, err := getObjectRef(ctx, kubeclient, eventArgs.forSelector, *kubeconfigArgs.Namespace)
if err != nil {
return err
}
for _, ref := range refs {
refKind, refName, refNs := utils.ParseObjectKindNameNamespace(ref)
kind, name, refNs := utils.ParseObjectKindNameNamespace(ref)
if refNs != namespace {
diffRefNs = true
}
refOpt := []client.ListOption{getListOpt(refKind, refName), client.InNamespace(refNs)}
refListOpts = append(refListOpts, refOpt)
}
refSelector := fmt.Sprintf("%s/%s", kind, name)
refListOpts = append(refListOpts, getListOpt(refNs, refSelector))
}
}
@ -162,7 +140,8 @@ func eventsCmdRun(cmd *cobra.Command, args []string) error {
return nil
}
headers := getHeaders(showNamespace)
return printers.TablePrinter(headers).Print(cmd.OutOrStdout(), rows)
err = printers.TablePrinter(headers).Print(cmd.OutOrStdout(), rows)
return err
}
func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []client.ListOption, refListOpts [][]client.ListOption, showNs bool) ([][]string, error) {
@ -192,11 +171,11 @@ func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []cli
func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error {
listOpts := &metav1.ListOptions{}
clientListOpts = append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize))
err := runtimeresource.FollowContinue(listOpts,
func(options metav1.ListOptions) (runtime.Object, error) {
newEvents := &corev1.EventList{}
if err := kubeclient.List(ctx, newEvents, clientListOpts...); err != nil {
err := kubeclient.List(ctx, newEvents, clientListOpts...)
if err != nil {
return nil, fmt.Errorf("error getting events: %w", err)
}
el.Items = append(el.Items, newEvents.Items...)
@ -206,22 +185,21 @@ func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.E
return err
}
func getListOpt(kind, name string) client.ListOption {
var sel fields.Selector
if name == "" {
sel = fields.OneTermEqualSelector("involvedObject.kind", kind)
} else {
sel = fields.AndSelectors(
func getListOpt(namespace, selector string) []client.ListOption {
clientListOpts := []client.ListOption{client.Limit(cmdutil.DefaultChunkSize), client.InNamespace(namespace)}
if selector != "" {
kind, name := utils.ParseObjectKindName(selector)
sel := fields.AndSelectors(
fields.OneTermEqualSelector("involvedObject.kind", kind),
fields.OneTermEqualSelector("involvedObject.name", name))
clientListOpts = append(clientListOpts, client.MatchingFieldsSelector{Selector: sel})
}
return client.MatchingFieldsSelector{Selector: sel}
return clientListOpts
}
func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpts []client.ListOption, refListOpts [][]client.ListOption, showNs bool) error {
event := &corev1.EventList{}
listOpts = append(listOpts, client.Limit(cmdutil.DefaultChunkSize))
eventWatch, err := kubeclient.Watch(ctx, event, listOpts...)
if err != nil {
return err
@ -247,7 +225,12 @@ func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpt
hdr = getHeaders(showNs)
firstIteration = false
}
return printers.TablePrinter(hdr).Print(os.Stdout, [][]string{rows})
err = printers.TablePrinter(hdr).Print(os.Stdout, [][]string{rows})
if err != nil {
return err
}
return nil
}
for _, refOpts := range refListOpts {
@ -256,7 +239,8 @@ func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpt
return err
}
go func() {
if err := receiveEventChan(ctx, refEventWatch, handleEvent); err != nil {
err := receiveEventChan(ctx, refEventWatch, handleEvent)
if err != nil {
logger.Failuref("error watching events: %s", err.Error())
}
}()
@ -305,7 +289,13 @@ func getEventRow(e corev1.Event, showNs bool) []string {
// getObjectRef is used to get the metadata of a resource that the selector(in the format <kind/name>) references.
// It returns an empty string if the resource doesn't reference any resource
// and a string with the format `<kind>/<name>.<namespace>` if it does.
func getObjectRef(ctx context.Context, kubeclient client.Client, ref refInfo, name, ns string) ([]string, error) {
func getObjectRef(ctx context.Context, kubeclient client.Client, selector string, ns string) ([]string, error) {
kind, name := utils.ParseObjectKindName(selector)
ref, err := fluxKindMap.getRefInfo(kind)
if err != nil {
return nil, fmt.Errorf("error getting groupversion: %w", err)
}
// the resource has no source ref
if len(ref.field) == 0 {
return nil, nil
@ -313,30 +303,31 @@ func getObjectRef(ctx context.Context, kubeclient client.Client, ref refInfo, na
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(schema.GroupVersionKind{
Kind: ref.gvk.Kind,
Version: ref.gvk.Version,
Group: ref.gvk.Group,
Kind: kind,
Version: ref.gv.Version,
Group: ref.gv.Group,
})
objName := types.NamespacedName{
Namespace: ns,
Name: name,
}
if err := kubeclient.Get(ctx, objName, obj); err != nil {
err = kubeclient.Get(ctx, objName, obj)
if err != nil {
return nil, err
}
var ok bool
refKind := ref.kind
if refKind == "" {
kindField := append(ref.field, "kind")
specKind, ok, err := unstructured.NestedString(obj.Object, kindField...)
refKind, ok, err = unstructured.NestedString(obj.Object, kindField...)
if err != nil {
return nil, err
}
if !ok {
return nil, fmt.Errorf("field '%s' for '%s' not found", strings.Join(kindField, "."), objName)
}
refKind = specKind
}
nameField := append(ref.field, "name")
@ -386,71 +377,49 @@ func (r refMap) hasKind(kind string) bool {
return err == nil
}
// validateEventTypes checks that the event types passed into the function
// is either equal to `Normal` or `Warning` which are currently the two supported types.
// https://github.com/kubernetes/kubernetes/blob/a8a1abc25cad87333840cd7d54be2efaf31a3177/staging/src/k8s.io/api/core/v1/types.go#L6212
func validateEventTypes(eventTypes []string) error {
for _, t := range eventTypes {
if !strings.EqualFold(corev1.EventTypeWarning, t) && !strings.EqualFold(corev1.EventTypeNormal, t) {
return fmt.Errorf("type '%s' not supported. Supported types are Normal, Warning", t)
}
}
return nil
}
type refInfo struct {
// gvk is the group version kind of the resource
gvk schema.GroupVersionKind
// kind is the kind that the resource references if it's not static
gv schema.GroupVersion
kind string
// crossNamespaced indicates if this resource uses cross namespaced references
crossNamespaced bool
// otherRefs returns other reference that might not be directly accessible
// from the spec of the object
otherRefs func(namespace, name string) []string
field []string
}
var fluxKindMap = refMap{
kustomizev1.KustomizationKind: {
gvk: kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind),
gv: kustomizev1.GroupVersion,
crossNamespaced: true,
field: []string{"spec", "sourceRef"},
},
helmv2.HelmReleaseKind: {
gvk: helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind),
gv: helmv2.GroupVersion,
crossNamespaced: true,
otherRefs: func(namespace, name string) []string {
return []string{fmt.Sprintf("%s/%s-%s", sourcev1.HelmChartKind, namespace, name)}
return []string{fmt.Sprintf("%s/%s-%s", sourcev1b2.HelmChartKind, namespace, name)}
},
field: []string{"spec", "chart", "spec", "sourceRef"},
},
notificationv1b3.AlertKind: {
gvk: notificationv1b3.GroupVersion.WithKind(notificationv1b3.AlertKind),
kind: notificationv1b3.ProviderKind,
notificationv1b2.AlertKind: {
gv: notificationv1b2.GroupVersion,
kind: notificationv1b2.ProviderKind,
crossNamespaced: false,
field: []string{"spec", "providerRef"},
},
notificationv1.ReceiverKind: {gvk: notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)},
notificationv1b3.ProviderKind: {gvk: notificationv1b3.GroupVersion.WithKind(notificationv1b3.ProviderKind)},
notificationv1.ReceiverKind: {gv: notificationv1.GroupVersion},
notificationv1b2.ProviderKind: {gv: notificationv1b2.GroupVersion},
imagev1.ImagePolicyKind: {
gvk: imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind),
gv: imagev1.GroupVersion,
kind: imagev1.ImageRepositoryKind,
crossNamespaced: true,
field: []string{"spec", "imageRepositoryRef"},
},
sourcev1.HelmChartKind: {
gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind),
crossNamespaced: true,
field: []string{"spec", "sourceRef"},
},
sourcev1.GitRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)},
sourcev1b2.OCIRepositoryKind: {gvk: sourcev1b2.GroupVersion.WithKind(sourcev1b2.OCIRepositoryKind)},
sourcev1.BucketKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)},
sourcev1.HelmRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)},
autov1.ImageUpdateAutomationKind: {gvk: autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)},
imagev1.ImageRepositoryKind: {gvk: imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)},
sourcev1.GitRepositoryKind: {gv: sourcev1.GroupVersion},
sourcev1b2.OCIRepositoryKind: {gv: sourcev1b2.GroupVersion},
sourcev1b2.BucketKind: {gv: sourcev1b2.GroupVersion},
sourcev1b2.HelmRepositoryKind: {gv: sourcev1b2.GroupVersion},
sourcev1b2.HelmChartKind: {gv: sourcev1b2.GroupVersion},
autov1.ImageUpdateAutomationKind: {gv: autov1.GroupVersion},
imagev1.ImageRepositoryKind: {gv: imagev1.GroupVersion},
}
func ignoreEvent(e corev1.Event) bool {
@ -468,19 +437,7 @@ func ignoreEvent(e corev1.Event) bool {
return false
}
func getKindNameFromSelector(selector string) (string, string) {
kind, name := utils.ParseObjectKindName(selector)
// if there's no slash in the selector utils.ParseObjectKindName returns the
// input string as the name but here we want it as the kind instead
if kind == "" && name != "" {
kind = name
name = ""
}
return kind, name
}
// The functions below are copied from: https://github.com/kubernetes/kubectl/blob/4ecd7bd0f0799f191335a331ca3c6a397a888233/pkg/cmd/events/events.go#L294
// The functions below are copied from: https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/events/events.go#L347
// SortableEvents implements sort.Interface for []api.Event by time
type SortableEvents []corev1.Event

@ -27,11 +27,21 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1"
notificationv1b2 "github.com/fluxcd/notification-controller/api/v1beta2"
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
ssautil "github.com/fluxcd/pkg/ssa/utils"
"github.com/fluxcd/pkg/ssa"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/internal/utils"
)
@ -78,7 +88,7 @@ spec:
timeout: 1m0s
url: ssh://git@github.com/example/repo
---
apiVersion: helm.toolkit.fluxcd.io/v2
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: podinfo
@ -95,7 +105,7 @@ spec:
version: '*'
interval: 5m0s
---
apiVersion: source.toolkit.fluxcd.io/v1
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: podinfo
@ -104,7 +114,7 @@ spec:
interval: 1m0s
url: https://stefanprodan.github.io/podinfo
---
apiVersion: source.toolkit.fluxcd.io/v1
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmChart
metadata:
name: default-podinfo
@ -118,7 +128,7 @@ spec:
name: podinfo-chart
version: '*'
---
apiVersion: notification.toolkit.fluxcd.io/v1beta3
apiVersion: notification.toolkit.fluxcd.io/v1beta2
kind: Alert
metadata:
name: webapp
@ -131,7 +141,7 @@ spec:
providerRef:
name: slack
---
apiVersion: notification.toolkit.fluxcd.io/v1beta3
apiVersion: notification.toolkit.fluxcd.io/v1beta2
kind: Provider
metadata:
name: slack
@ -160,10 +170,10 @@ metadata:
func Test_getObjectRef(t *testing.T) {
g := NewWithT(t)
objs, err := ssautil.ReadObjects(strings.NewReader(objects))
objs, err := ssa.ReadObjects(strings.NewReader(objects))
g.Expect(err).To(Not(HaveOccurred()))
builder := fake.NewClientBuilder().WithScheme(utils.NewScheme())
builder := fake.NewClientBuilder().WithScheme(getScheme())
for _, obj := range objs {
builder = builder.WithObjects(obj)
}
@ -206,12 +216,6 @@ func Test_getObjectRef(t *testing.T) {
namespace: "default",
want: []string{"ImageRepository/acr-podinfo.flux-system"},
},
{
name: "Source Ref for ImagePolicy (lowercased)",
selector: "imagepolicy/podinfo",
namespace: "default",
want: []string{"ImageRepository/acr-podinfo.flux-system"},
},
{
name: "Empty Ref for Provider",
selector: "Provider/slack",
@ -228,13 +232,11 @@ func Test_getObjectRef(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
kind, name := getKindNameFromSelector(tt.selector)
infoRef, err := fluxKindMap.getRefInfo(kind)
got, err := getObjectRef(context.Background(), c, tt.selector, tt.namespace)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
return
}
got, err := getObjectRef(context.Background(), c, infoRef, name, tt.namespace)
g.Expect(err).To(Not(HaveOccurred()))
g.Expect(got).To(Equal(tt.want))
@ -244,10 +246,10 @@ func Test_getObjectRef(t *testing.T) {
func Test_getRows(t *testing.T) {
g := NewWithT(t)
objs, err := ssautil.ReadObjects(strings.NewReader(objects))
objs, err := ssa.ReadObjects(strings.NewReader(objects))
g.Expect(err).To(Not(HaveOccurred()))
builder := fake.NewClientBuilder().WithScheme(utils.NewScheme())
builder := fake.NewClientBuilder().WithScheme(getScheme())
for _, obj := range objs {
builder = builder.WithObjects(obj)
}
@ -259,7 +261,6 @@ func Test_getRows(t *testing.T) {
}
builder = builder.WithLists(eventList)
builder.WithIndex(&corev1.Event{}, "involvedObject.kind/name", kindNameIndexer)
builder.WithIndex(&corev1.Event{}, "involvedObject.kind", kindIndexer)
c := builder.Build()
tests := []struct {
@ -319,16 +320,6 @@ func Test_getRows(t *testing.T) {
{"flux-system", "<unknown>", "info", "Info Reason", "GitRepository/flux-system", "Info Message"},
},
},
{
name: "All Kustomization (lowercased selector)",
selector: "kustomization",
expected: [][]string{
{"default", "<unknown>", "error", "Error Reason", "Kustomization/podinfo", "Error Message"},
{"default", "<unknown>", "info", "Info Reason", "Kustomization/podinfo", "Info Message"},
{"flux-system", "<unknown>", "error", "Error Reason", "Kustomization/flux-system", "Error Message"},
{"flux-system", "<unknown>", "info", "Info Reason", "Kustomization/flux-system", "Info Message"},
},
},
{
name: "HelmRelease with crossnamespaced HelmRepository",
selector: "HelmRelease/podinfo",
@ -342,19 +333,6 @@ func Test_getRows(t *testing.T) {
{"flux-system", "<unknown>", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"},
},
},
{
name: "HelmRelease with crossnamespaced HelmRepository (lowercased)",
selector: "helmrelease/podinfo",
namespace: "default",
expected: [][]string{
{"default", "<unknown>", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"},
{"default", "<unknown>", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"},
{"flux-system", "<unknown>", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"},
{"flux-system", "<unknown>", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"},
{"flux-system", "<unknown>", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"},
{"flux-system", "<unknown>", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"},
},
},
}
for _, tt := range tests {
@ -363,49 +341,59 @@ func Test_getRows(t *testing.T) {
var refs []string
var refNs, refKind, refName string
var clientOpts = []client.ListOption{client.InNamespace(tt.namespace)}
if tt.selector != "" {
kind, name := getKindNameFromSelector(tt.selector)
infoRef, err := fluxKindMap.getRefInfo(kind)
clientOpts = append(clientOpts, getTestListOpt(infoRef.gvk.Kind, name))
if name != "" {
g.Expect(err).To(Not(HaveOccurred()))
refs, err = getObjectRef(context.Background(), c, infoRef, name, tt.namespace)
refs, err = getObjectRef(context.Background(), c, tt.selector, tt.namespace)
g.Expect(err).To(Not(HaveOccurred()))
}
}
g.Expect(err).To(Not(HaveOccurred()))
clientOpts := getTestListOpt(tt.namespace, tt.selector)
var refOpts [][]client.ListOption
for _, ref := range refs {
refKind, refName, refNs = utils.ParseObjectKindNameNamespace(ref)
refOpts = append(refOpts, []client.ListOption{client.InNamespace(refNs), getTestListOpt(refKind, refName)})
refSelector := fmt.Sprintf("%s/%s", refKind, refName)
refOpts = append(refOpts, getTestListOpt(refNs, refSelector))
}
showNs := tt.namespace == "" || (refNs != "" && refNs != tt.namespace)
rows, err := getRows(context.Background(), c, clientOpts, refOpts, showNs)
g.Expect(err).To(Not(HaveOccurred()))
g.Expect(rows).To(ConsistOf(tt.expected))
g.Expect(rows).To(Equal(tt.expected))
})
}
}
func getTestListOpt(kind, name string) client.ListOption {
var sel fields.Selector
if name == "" {
sel = fields.OneTermEqualSelector("involvedObject.kind", kind)
} else {
sel = fields.OneTermEqualSelector("involvedObject.kind/name", fmt.Sprintf("%s/%s", kind, name))
func getTestListOpt(namespace, selector string) []client.ListOption {
clientListOpts := []client.ListOption{client.Limit(cmdutil.DefaultChunkSize), client.InNamespace(namespace)}
if selector != "" {
sel := fields.OneTermEqualSelector("involvedObject.kind/name", selector)
clientListOpts = append(clientListOpts, client.MatchingFieldsSelector{Selector: sel})
}
return client.MatchingFieldsSelector{Selector: sel}
return clientListOpts
}
func getScheme() *runtime.Scheme {
newscheme := runtime.NewScheme()
corev1.AddToScheme(newscheme)
kustomizev1.AddToScheme(newscheme)
helmv2beta1.AddToScheme(newscheme)
notificationv1.AddToScheme(newscheme)
notificationv1b2.AddToScheme(newscheme)
imagev1.AddToScheme(newscheme)
autov1.AddToScheme(newscheme)
sourcev1.AddToScheme(newscheme)
sourcev1b2.AddToScheme(newscheme)
return newscheme
}
func createEvent(obj client.Object, eventType, msg, reason string) corev1.Event {
return corev1.Event{
ObjectMeta: metav1.ObjectMeta{
Namespace: obj.GetNamespace(),
// name of event needs to be unique
// name of event needs to be unique so fak
Name: obj.GetNamespace() + obj.GetNamespace() + obj.GetObjectKind().GroupVersionKind().Kind + eventType,
},
Reason: reason,
@ -427,12 +415,3 @@ func kindNameIndexer(obj client.Object) []string {
return []string{fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name)}
}
func kindIndexer(obj client.Object) []string {
e, ok := obj.(*corev1.Event)
if !ok {
panic(fmt.Sprintf("Expected a Event, got %T", e))
}
return []string{e.InvolvedObject.Kind}
}

@ -20,7 +20,7 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2"
)
var exportAlertCmd = &cobra.Command{

@ -20,7 +20,7 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2"
)
var exportAlertProviderCmd = &cobra.Command{

@ -1,5 +1,5 @@
/*
Copyright 2024 The Flux authors
Copyright 2020 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.
@ -20,14 +20,14 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
)
var exportHelmReleaseCmd = &cobra.Command{
Use: "helmrelease [name]",
Aliases: []string{"hr"},
Short: "Export HelmRelease resources in YAML format",
Long: "The export helmrelease command exports one or all HelmRelease resources in YAML format.",
Long: withPreviewNote("The export helmrelease command exports one or all HelmRelease resources in YAML format."),
Example: ` # Export all HelmRelease resources
flux export helmrelease --all > kustomizations.yaml

@ -20,7 +20,7 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1"
)
var exportImageUpdateCmd = &cobra.Command{

@ -46,6 +46,7 @@ type exportableWithSecretList interface {
}
type exportWithSecretCommand struct {
apiType
object exportableWithSecret
list exportableWithSecretList
}

@ -21,13 +21,13 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)
var exportSourceBucketCmd = &cobra.Command{
Use: "bucket [name]",
Short: "Export Bucket sources in YAML format",
Long: "The export source git command exports one or all Bucket sources in YAML format.",
Long: withPreviewNote("The export source git command exports one or all Bucket sources in YAML format."),
Example: ` # Export all Bucket sources
flux export source bucket --all > sources.yaml

@ -1,67 +0,0 @@
/*
Copyright 2024 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"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)
var exportSourceChartCmd = &cobra.Command{
Use: "chart [name]",
Short: "Export HelmChart sources in YAML format",
Long: withPreviewNote("The export source chart command exports one or all HelmChart sources in YAML format."),
Example: ` # Export all chart sources
flux export source chart --all > sources.yaml`,
ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)),
RunE: exportCommand{
list: helmChartListAdapter{&sourcev1.HelmChartList{}},
object: helmChartAdapter{&sourcev1.HelmChart{}},
}.run,
}
func init() {
exportSourceCmd.AddCommand(exportSourceChartCmd)
}
func exportHelmChart(source *sourcev1.HelmChart) interface{} {
gvk := sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)
export := sourcev1.HelmChart{
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 (ex helmChartAdapter) export() interface{} {
return exportHelmChart(ex.HelmChart)
}
func (ex helmChartListAdapter) exportItem(i int) interface{} {
return exportHelmChart(&ex.HelmChartList.Items[i])
}

@ -21,13 +21,13 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
)
var exportSourceHelmCmd = &cobra.Command{
Use: "helm [name]",
Short: "Export HelmRepository sources in YAML format",
Long: "The export source git command exports one or all HelmRepository sources in YAML format.",
Long: withPreviewNote("The export source git command exports one or all HelmRepository sources in YAML format."),
Example: ` # Export all HelmRepository sources
flux export source helm --all > sources.yaml

@ -1,22 +1,6 @@
//go:build unit
// +build unit
/*
Copyright 2024 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 (
@ -74,12 +58,6 @@ func TestExport(t *testing.T) {
"testdata/export/git-repo.yaml",
tmpl,
},
{
"source chart",
"export source chart flux-system",
"testdata/export/helm-chart.yaml",
tmpl,
},
{
"source helm",
"export source helm flux-system",

@ -18,6 +18,7 @@ package main
import (
"context"
"errors"
"fmt"
"os"
"strings"
@ -27,6 +28,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
watchtools "k8s.io/client-go/tools/watch"
"sigs.k8s.io/controller-runtime/pkg/client"
@ -176,7 +178,8 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error {
err = kubeClient.List(ctx, get.list.asClientList(), listOpts...)
if err != nil {
if getAll && apimeta.IsNoMatchError(err) {
var discErr *discovery.ErrGroupDiscoveryFailed
if getAll && (strings.Contains(err.Error(), "no matches for kind") || errors.As(err, &discErr)) {
return nil
}
return err

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

Loading…
Cancel
Save