Merge branch 'main' into add-reason-for-suspend

pull/4232/head
Travis Mattera 8 months ago committed by GitHub
commit 80f73504bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

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

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

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

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

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

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

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

@ -50,3 +50,6 @@
- name: backport:release/v2.1.x - name: backport:release/v2.1.x
description: To be backported to release/v2.1.x description: To be backported to release/v2.1.x
color: '#ffd700' color: '#ffd700'
- name: backport:release/v2.2.x
description: To be backported to release/v2.2.x
color: '#ffd700'

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

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

@ -22,7 +22,7 @@ RUNNER_NAME=$1
REPOSITORY_TOKEN=$2 REPOSITORY_TOKEN=$2
REPOSITORY_URL=${3:-https://github.com/fluxcd/flux2} REPOSITORY_URL=${3:-https://github.com/fluxcd/flux2}
GITHUB_RUNNER_VERSION=2.298.2 GITHUB_RUNNER_VERSION=2.313.0
# download runner # 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 \ 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 }} name: action on ${{ matrix.version }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup flux - name: Setup flux
uses: ./action uses: ./action

@ -13,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)) if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
with: with:
ref: ${{ github.event.pull_request.head.sha }} ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs - name: Create backport PRs
uses: korthout/backport-action@bd68141f079bd036e45ea8149bc9d174d5a04703 # v1.4.0 uses: korthout/backport-action@ef20d86abccbac3ee3a73cb2efbdc06344c390e5 # v2.5.0
# xref: https://github.com/korthout/backport-action#inputs # xref: https://github.com/korthout/backport-action#inputs
with: with:
# Use token to allow workflows to be triggered for the created PR # Use token to allow workflows to be triggered for the created PR

@ -16,16 +16,16 @@ jobs:
strategy: strategy:
matrix: matrix:
# Keep this list up-to-date with https://endoflife.date/kubernetes # Keep this list up-to-date with https://endoflife.date/kubernetes
# Check which versions are available on DockerHub with 'crane ls kindest/node' # Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
KUBERNETES_VERSION: [ 1.25.11, 1.26.6, 1.27.3, 1.28.0 ] KUBERNETES_VERSION: [ 1.28.9, 1.29.4, 1.30.0 ]
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup Go - name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.20.x go-version: 1.22.x
cache-dependency-path: | cache-dependency-path: |
**/go.sum **/go.sum
**/go.mod **/go.mod
@ -43,7 +43,7 @@ jobs:
--wait 5m \ --wait 5m \
--name ${{ steps.prep.outputs.CLUSTER }} \ --name ${{ steps.prep.outputs.CLUSTER }} \
--kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }} \ --kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }} \
--image=kindest/node:v${{ matrix.KUBERNETES_VERSION }} --image=ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
- name: Run e2e tests - name: Run e2e tests
run: TEST_KUBECONFIG=/tmp/${{ steps.prep.outputs.CLUSTER }} make e2e run: TEST_KUBECONFIG=/tmp/${{ steps.prep.outputs.CLUSTER }} make e2e
- name: Run multi-tenancy tests - name: Run multi-tenancy tests
@ -63,33 +63,6 @@ jobs:
kubectl -n flux-system wait kustomization/tenants --for=condition=ready --timeout=5m 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 kustomization/dev-team --for=condition=ready --timeout=1m
kubectl -n apps wait helmrelease/podinfo --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 - name: Debug failure
if: failure() if: failure()
env: env:

@ -21,52 +21,7 @@ permissions:
contents: read contents: read
jobs: jobs:
e2e-amd64-aks: e2e-aks:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./tests/azure
# This job is currently disabled. Remove the false check when Azure subscription is enabled.
if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
with:
go-version: 1.20.x
cache-dependency-path: tests/azure/go.sum
- name: Setup Flux CLI
run: |
make build
mkdir -p $HOME/.local/bin
mv ./bin/flux $HOME/.local/bin
working-directory: ./
- name: Setup SOPS
run: |
mkdir -p $HOME/.local/bin
wget https://github.com/mozilla/sops/releases/download/v3.7.1/sops-v3.7.1.linux -O $HOME/.local/bin/sops
chmod +x $HOME/.local/bin/sops
- name: Setup Terraform
uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2
with:
terraform_version: 1.2.8
terraform_wrapper: false
- name: Setup Azure CLI
run: |
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- name: Run Azure e2e tests
env:
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
run: |
ls $HOME/.local/bin
az login --service-principal -u ${ARM_CLIENT_ID} -p ${ARM_CLIENT_SECRET} -t ${ARM_TENANT_ID}
go test -v -coverprofile cover.out -timeout 60m .
refactored-e2e-amd64-aks:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
defaults: defaults:
run: run:
@ -75,11 +30,11 @@ jobs:
if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps: steps:
- name: CheckoutD - name: CheckoutD
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup Go - name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.20.x go-version: 1.22.x
cache-dependency-path: tests/integration/go.sum cache-dependency-path: tests/integration/go.sum
- name: Setup Flux CLI - name: Setup Flux CLI
run: make build run: make build
@ -92,7 +47,7 @@ jobs:
env: env:
SOPS_VER: 3.7.1 SOPS_VER: 3.7.1
- name: Authenticate to Azure - name: Authenticate to Azure
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.6 uses: Azure/login@6b2456866fc08b011acb422a92a4aa20e2c4de32 # v1.4.6
with: 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 }}"}' creds: '{"clientId":"${{ secrets.AZ_ARM_CLIENT_ID }}","clientSecret":"${{ secrets.AZ_ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZ_ARM_TENANT_ID }}"}'
- name: Set dynamic variables in .env - name: Set dynamic variables in .env
@ -123,3 +78,14 @@ jobs:
echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
make test-azure make test-azure
- name: Ensure resource cleanup
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

@ -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]' if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup Go - name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.20.x go-version: 1.22.x
cache-dependency-path: | cache-dependency-path: |
**/go.sum **/go.sum
**/go.mod **/go.mod
- name: Setup Kubernetes - name: Setup Kubernetes
uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0 uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0
with: with:
version: v0.20.0 version: v0.22.0
cluster_name: kind cluster_name: kind
# The versions below should target the newest Kubernetes version # The versions below should target the newest Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes # Keep this up-to-date with https://endoflife.date/kubernetes
node_image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c node_image: ghcr.io/fluxcd/kindest/node:v1.30.0-amd64
kubectl_version: v1.28.0 kubectl_version: v1.30.0
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@main uses: fluxcd/pkg/actions/kustomize@main
- name: Setup yq
uses: fluxcd/pkg/actions/yq@main
- name: Build - name: Build
run: | run: make build-dev
make cmd/flux/.manifests.done
go build -o /tmp/flux ./cmd/flux
- name: Set outputs - name: Set outputs
id: vars id: vars
run: | run: |
@ -51,18 +51,24 @@ jobs:
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
- name: bootstrap init - name: bootstrap init
run: | run: |
/tmp/flux bootstrap github --manifests ./manifests/install/ \ ./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \ --owner=fluxcd-testing \
--image-pull-secret=ghcr-auth \
--registry-creds=fluxcd:$GITHUB_TOKEN \
--repository=${{ steps.vars.outputs.test_repo_name }} \ --repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \ --branch=main \
--path=test-cluster \ --path=test-cluster \
--team=team-z --team=team-z
env: env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} 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 - name: bootstrap no-op
run: | run: |
/tmp/flux bootstrap github --manifests ./manifests/install/ \ ./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \ --owner=fluxcd-testing \
--image-pull-secret=ghcr-auth \
--repository=${{ steps.vars.outputs.test_repo_name }} \ --repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \ --branch=main \
--path=test-cluster \ --path=test-cluster \
@ -72,7 +78,7 @@ jobs:
- name: bootstrap customize - name: bootstrap customize
run: | run: |
make setup-bootstrap-patch make setup-bootstrap-patch
/tmp/flux bootstrap github --manifests ./manifests/install/ \ ./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \ --owner=fluxcd-testing \
--repository=${{ steps.vars.outputs.test_repo_name }} \ --repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \ --branch=main \
@ -87,46 +93,31 @@ jobs:
GITHUB_ORG_NAME: fluxcd-testing GITHUB_ORG_NAME: fluxcd-testing
- name: uninstall - name: uninstall
run: | run: |
/tmp/flux uninstall -s --keep-namespace ./bin/flux uninstall -s --keep-namespace
kubectl delete ns flux-system --timeout=10m --wait=true kubectl delete ns flux-system --timeout=10m --wait=true
- name: test image automation - name: test image automation
run: | run: |
make setup-image-automation make setup-image-automation
/tmp/flux bootstrap github --manifests ./manifests/install/ \ ./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \ --owner=fluxcd-testing \
--repository=${{ steps.vars.outputs.test_repo_name }} \ --repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \ --branch=main \
--path=test-cluster \ --path=test-cluster \
--read-write-key --read-write-key
/tmp/flux reconcile image repository podinfo ./bin/flux reconcile image repository podinfo
/tmp/flux get images all ./bin/flux reconcile image update flux-system
./bin/flux get images all
retries=10 kubectl -n flux-system get -o yaml ImageUpdateAutomation flux-system | \
count=0 yq '.status.lastPushCommit | length > 1' | grep 'true'
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: env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }} GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }}
GITHUB_ORG_NAME: fluxcd-testing GITHUB_ORG_NAME: fluxcd-testing
- name: delete repository - name: delete repository
if: ${{ always() }} if: ${{ always() }}
continue-on-error: true
run: | run: |
curl \ gh repo delete fluxcd-testing/${{ steps.vars.outputs.test_repo_name }} --yes
-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: env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Debug failure - name: Debug failure

@ -29,11 +29,11 @@ jobs:
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup Go - name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.20.x go-version: 1.22.x
cache-dependency-path: tests/integration/go.sum cache-dependency-path: tests/integration/go.sum
- name: Setup Flux CLI - name: Setup Flux CLI
run: make build run: make build
@ -46,19 +46,19 @@ jobs:
env: env:
SOPS_VER: 3.7.1 SOPS_VER: 3.7.1
- name: Authenticate to Google Cloud - name: Authenticate to Google Cloud
uses: google-github-actions/auth@35b0e87d162680511bf346c299f71c9c5c379033 # v1.1.1 uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2
id: 'auth' id: 'auth'
with: with:
credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}' credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}'
token_format: 'access_token' token_format: 'access_token'
- name: Setup gcloud - name: Setup gcloud
uses: google-github-actions/setup-gcloud@e30db14379863a8c79331b04a9969f4c1e225e0b # v1.1.1 uses: google-github-actions/setup-gcloud@98ddc00a17442e89a24bbf282954a3b65ce6d200 # v2.1.0
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
- name: Setup Docker Buildx - name: Setup Docker Buildx
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
- name: Log into us-central1-docker.pkg.dev - name: Log into us-central1-docker.pkg.dev
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
with: with:
registry: us-central1-docker.pkg.dev registry: us-central1-docker.pkg.dev
username: oauth2accesstoken username: oauth2accesstoken
@ -90,3 +90,13 @@ jobs:
echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
make test-gcp 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

@ -0,0 +1,101 @@
name: e2e-openshift
on:
workflow_dispatch:
push:
branches: [ 'main', 'update-components', 'openshift-*', 'release/**' ]
permissions:
contents: read
jobs:
e2e-openshift:
runs-on: ubuntu-latest
strategy:
matrix:
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
OPENSHIFT_VERSION: [ 4.14.0-okd, 4.15.0-okd ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: 1.22.x
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@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/compatibility-actions/create-cluster@v1
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@v1
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 }}

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

@ -19,16 +19,16 @@ jobs:
actions: read actions: read
contents: read contents: read
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Run analysis - name: Run analysis
uses: ossf/scorecard-action@08b4669551908b1024bb425080c797723083c031 # v2.2.0 uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
publish_results: true publish_results: true
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif

@ -20,33 +20,33 @@ jobs:
packages: write # needed for ghcr access packages: write # needed for ghcr access
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Unshallow - name: Unshallow
run: git fetch --prune --unshallow run: git fetch --prune --unshallow
- name: Setup Go - name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.20.x go-version: 1.22.x
cache: false cache: false
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
- name: Setup Docker Buildx - name: Setup Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0 uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
- name: Setup Syft - name: Setup Syft
uses: anchore/sbom-action/download-syft@78fc58e266e87a38d4194b2137a3d4e9bcaf7ca1 # v0.14.3 uses: anchore/sbom-action/download-syft@7ccf588e3cf3cc2611714c2eeae48550fbc17552 # v0.15.11
- name: Setup Cosign - name: Setup Cosign
uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2 uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@main uses: fluxcd/pkg/actions/kustomize@main
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.GHCR_TOKEN }} password: ${{ secrets.GHCR_TOKEN }}
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
with: with:
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@ -79,7 +79,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser - name: Run GoReleaser
id: run-goreleaser id: run-goreleaser
uses: goreleaser/goreleaser-action@5fdedb94abba051217030cc86d4523cf3f02243d # v4.6.0 uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0
with: with:
version: latest version: latest
args: release --release-notes=output/notes.md --skip-validate args: release --release-notes=output/notes.md --skip-validate
@ -110,7 +110,7 @@ jobs:
id-token: write id-token: write
packages: write packages: write
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@main uses: fluxcd/pkg/actions/kustomize@main
- name: Setup Flux CLI - name: Setup Flux CLI
@ -121,13 +121,13 @@ jobs:
VERSION=$(flux version --client | awk '{ print $NF }') VERSION=$(flux version --client | awk '{ print $NF }')
echo "version=${VERSION}" >> $GITHUB_OUTPUT echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
with: with:
registry: ghcr.io registry: ghcr.io
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.GHCR_TOKEN }} password: ${{ secrets.GHCR_TOKEN }}
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 uses: docker/login-action@e92390c5fb421da1463c202d546fed0ec5c39f20 # v3.1.0
with: with:
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@ -155,7 +155,7 @@ jobs:
--path="./flux-system" \ --path="./flux-system" \
--source=${{ github.repositoryUrl }} \ --source=${{ github.repositoryUrl }} \
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}" --revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
- uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2 - uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
- name: Sign manifests - name: Sign manifests
env: env:
COSIGN_EXPERIMENTAL: 1 COSIGN_EXPERIMENTAL: 1
@ -176,7 +176,7 @@ jobs:
actions: read # for detecting the Github Actions environment. actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing. id-token: write # for creating OIDC tokens for signing.
contents: write # for uploading attestations to GitHub releases. contents: write # for uploading attestations to GitHub releases.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.9.0 uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with: with:
provenance-name: "provenance.intoto.jsonl" provenance-name: "provenance.intoto.jsonl"
base64-subjects: "${{ needs.release-flux-cli.outputs.hashes }}" base64-subjects: "${{ needs.release-flux-cli.outputs.hashes }}"
@ -188,7 +188,7 @@ jobs:
actions: read # for detecting the Github Actions environment. actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing. id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations. packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0 uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with: with:
image: ${{ needs.release-flux-cli.outputs.image_url }} image: ${{ needs.release-flux-cli.outputs.image_url }}
digest: ${{ needs.release-flux-cli.outputs.image_digest }} digest: ${{ needs.release-flux-cli.outputs.image_digest }}
@ -202,7 +202,7 @@ jobs:
actions: read # for detecting the Github Actions environment. actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing. id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations. packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0 uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with: with:
image: ghcr.io/${{ needs.release-flux-cli.outputs.image_url }} image: ghcr.io/${{ needs.release-flux-cli.outputs.image_url }}
digest: ${{ needs.release-flux-cli.outputs.image_digest }} digest: ${{ needs.release-flux-cli.outputs.image_digest }}

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

@ -17,8 +17,8 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- uses: EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 # v2.3.2 - uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
with: with:
# Configuration file # Configuration file
config-file: | config-file: |

@ -18,11 +18,11 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
- name: Setup Go - name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.20.x go-version: 1.22.x
cache-dependency-path: | cache-dependency-path: |
**/go.sum **/go.sum
**/go.mod **/go.mod
@ -84,7 +84,7 @@ jobs:
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2 uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5
with: with:
token: ${{ secrets.BOT_GITHUB_TOKEN }} token: ${{ secrets.BOT_GITHUB_TOKEN }}
commit-message: | commit-message: |

@ -1,15 +1,15 @@
FROM alpine:3.18 as builder FROM alpine:3.19 as builder
RUN apk add --no-cache ca-certificates curl RUN apk add --no-cache ca-certificates curl
ARG ARCH=linux/amd64 ARG ARCH=linux/amd64
ARG KUBECTL_VER=1.27.3 ARG KUBECTL_VER=1.28.6
RUN curl -sL https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/${ARCH}/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 && \ -o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl && \
kubectl version --client=true kubectl version --client=true
FROM alpine:3.18 as flux-cli FROM alpine:3.19 as flux-cli
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates

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

@ -5,6 +5,7 @@
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2/badge)](https://api.securityscorecards.dev/projects/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) [![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) [![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 Flux is a tool for keeping Kubernetes clusters in sync with sources of
configuration (like Git repositories and OCI artifacts), configuration (like Git repositories and OCI artifacts),

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

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

@ -17,11 +17,16 @@ limitations under the License.
package main package main
import ( import (
"context"
"crypto/elliptic" "crypto/elliptic"
"fmt" "fmt"
"strings" "strings"
"github.com/fluxcd/pkg/git"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra" "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/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
@ -48,17 +53,19 @@ type bootstrapFlags struct {
extraComponents []string extraComponents []string
requiredComponents []string requiredComponents []string
registry string registry string
imagePullSecret string registryCredential string
imagePullSecret string
secretName string secretName string
tokenAuth bool tokenAuth bool
keyAlgorithm flags.PublicKeyAlgorithm keyAlgorithm flags.PublicKeyAlgorithm
keyRSABits flags.RSAKeyBits keyRSABits flags.RSAKeyBits
keyECDSACurve flags.ECDSACurve keyECDSACurve flags.ECDSACurve
sshHostname string sshHostname string
caFile string caFile string
privateKeyFile string privateKeyFile string
sshHostKeyAlgorithms []string
watchAllNamespaces bool watchAllNamespaces bool
networkPolicy bool networkPolicy bool
@ -72,6 +79,8 @@ type bootstrapFlags struct {
gpgPassphrase string gpgPassphrase string
gpgKeyID string gpgKeyID string
force bool
commitMessageAppendix string commitMessageAppendix string
} }
@ -92,6 +101,8 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd", bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd",
"container registry where the Flux controller images are published") "container registry where the Flux controller images are published")
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", "", bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.imagePullSecret, "image-pull-secret", "",
"Kubernetes secret name used for pulling the controller images from a private registry") "Kubernetes secret name used for pulling the controller images from a private registry")
@ -115,6 +126,7 @@ 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().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.keyAlgorithm, "ssh-key-algorithm", bootstrapArgs.keyAlgorithm.Description())
bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyRSABits, "ssh-rsa-bits", bootstrapArgs.keyRSABits.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().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.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") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates")
@ -129,6 +141,7 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
bootstrapCmd.PersistentFlags().MarkHidden("manifests") bootstrapCmd.PersistentFlags().MarkHidden("manifests")
rootCmd.AddCommand(bootstrapCmd) rootCmd.AddCommand(bootstrapCmd)
@ -174,6 +187,18 @@ func bootstrapValidate() error {
return err 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 return nil
} }
@ -188,3 +213,27 @@ func mapTeamSlice(s []string, defaultPermission string) map[string]string {
return m 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 # 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 flux bootstrap bitbucket-server --owner=<user> --repository=<repository name> --private=false --personal --hostname=<domain> --token-auth --path=clusters/my-cluster
# Run bootstrap for a an existing repository with a branch named main # Run bootstrap for 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`, flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --branch=main --hostname=<domain> --token-auth --path=clusters/my-cluster`,
RunE: bootstrapBServerCmdRun, RunE: bootstrapBServerCmdRun,
} }
@ -124,6 +124,13 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base // Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil { if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err return err
@ -189,6 +196,7 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(), Components: bootstrapComponents(),
Registry: bootstrapArgs.registry, Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret, ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy, NetworkPolicy: bootstrapArgs.networkPolicy,

@ -28,6 +28,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/bootstrap" "github.com/fluxcd/flux2/v2/pkg/bootstrap"
@ -35,8 +38,6 @@ import (
"github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
) )
var bootstrapGitCmd = &cobra.Command{ var bootstrapGitCmd = &cobra.Command{
@ -65,7 +66,10 @@ 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 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 # Run bootstrap for a Git repository on Azure Devops
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 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
`, `,
RunE: bootstrapGitCmdRun, RunE: bootstrapGitCmdRun,
} }
@ -78,6 +82,7 @@ type gitFlags struct {
password string password string
silent bool silent bool
insecureHttpAllowed bool insecureHttpAllowed bool
withBearerToken bool
} }
const ( const (
@ -94,11 +99,16 @@ func init() {
bootstrapGitCmd.Flags().StringVarP(&gitArgs.password, "password", "p", "", "basic authentication password") 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().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.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) bootstrapCmd.AddCommand(bootstrapGitCmd)
} }
func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
if gitArgs.withBearerToken {
bootstrapArgs.tokenAuth = true
}
gitPassword := os.Getenv(gitPasswordEnvVar) gitPassword := os.Getenv(gitPasswordEnvVar)
if gitPassword != "" && gitArgs.password == "" { if gitPassword != "" && gitArgs.password == "" {
gitArgs.password = gitPassword gitArgs.password = gitPassword
@ -146,6 +156,13 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base // Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil { if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err return err
@ -194,6 +211,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(), Components: bootstrapComponents(),
Registry: bootstrapArgs.registry, Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret, ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy, NetworkPolicy: bootstrapArgs.networkPolicy,
@ -216,9 +234,15 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
TargetPath: gitArgs.path.String(), TargetPath: gitArgs.path.String(),
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
} }
if bootstrapArgs.tokenAuth { if bootstrapArgs.tokenAuth {
secretOpts.Username = gitArgs.username if gitArgs.withBearerToken {
secretOpts.Password = gitArgs.password secretOpts.BearerToken = gitArgs.password
} else {
secretOpts.Username = gitArgs.username
secretOpts.Password = gitArgs.password
}
secretOpts.CAFile = caBundle secretOpts.CAFile = caBundle
// Remove port of the given host when not syncing over HTTP/S to not assume port for protocol // Remove port of the given host when not syncing over HTTP/S to not assume port for protocol
@ -311,18 +335,28 @@ func getAuthOpts(u *url.URL, caBundle []byte) (*git.AuthOptions, error) {
if !gitArgs.insecureHttpAllowed { if !gitArgs.insecureHttpAllowed {
return nil, fmt.Errorf("scheme http is insecure, pass --allow-insecure-http=true to allow it") return nil, fmt.Errorf("scheme http is insecure, pass --allow-insecure-http=true to allow it")
} }
return &git.AuthOptions{ httpAuth := git.AuthOptions{
Transport: git.HTTP, Transport: git.HTTP,
Username: gitArgs.username, }
Password: gitArgs.password, if gitArgs.withBearerToken {
}, nil httpAuth.BearerToken = gitArgs.password
} else {
httpAuth.Username = gitArgs.username
httpAuth.Password = gitArgs.password
}
return &httpAuth, nil
case "https": case "https":
return &git.AuthOptions{ httpsAuth := git.AuthOptions{
Transport: git.HTTPS, Transport: git.HTTPS,
Username: gitArgs.username,
Password: gitArgs.password,
CAFile: caBundle, CAFile: caBundle,
}, nil }
if gitArgs.withBearerToken {
httpsAuth.BearerToken = gitArgs.password
} else {
httpsAuth.Username = gitArgs.username
httpsAuth.Password = gitArgs.password
}
return &httpsAuth, nil
case "ssh": case "ssh":
authOpts := &git.AuthOptions{ authOpts := &git.AuthOptions{
Transport: git.SSH, Transport: git.SSH,

@ -0,0 +1,276 @@
/*
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.CAFile = 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,6 +128,13 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base // Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil { if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err return err
@ -184,6 +191,7 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(), Components: bootstrapComponents(),
Registry: bootstrapArgs.registry, Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret, ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy, NetworkPolicy: bootstrapArgs.networkPolicy,

@ -64,7 +64,7 @@ the bootstrap command will perform an upgrade if needed.`,
# Run bootstrap for a private repository hosted on a GitLab server # Run bootstrap for a private repository hosted on a GitLab server
flux bootstrap gitlab --owner=<group> --repository=<repository name> --hostname=<domain> --token-auth flux bootstrap gitlab --owner=<group> --repository=<repository name> --hostname=<domain> --token-auth
# Run bootstrap for a an existing repository with a branch named main # Run bootstrap for an existing repository with a branch named main
flux bootstrap gitlab --owner=<organization> --repository=<repository name> --branch=main --token-auth flux bootstrap gitlab --owner=<organization> --repository=<repository name> --branch=main --token-auth
# Run bootstrap for a private repository using Deploy Token authentication # Run bootstrap for a private repository using Deploy Token authentication
@ -145,6 +145,13 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base // Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil { if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err return err
@ -209,6 +216,7 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Components: bootstrapComponents(), Components: bootstrapComponents(),
Registry: bootstrapArgs.registry, Registry: bootstrapArgs.registry,
RegistryCredential: bootstrapArgs.registryCredential,
ImagePullSecret: bootstrapArgs.imagePullSecret, ImagePullSecret: bootstrapArgs.imagePullSecret,
WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces,
NetworkPolicy: bootstrapArgs.networkPolicy, NetworkPolicy: bootstrapArgs.networkPolicy,

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

@ -21,10 +21,10 @@ import (
"os" "os"
"os/signal" "os/signal"
"github.com/fluxcd/pkg/ssa"
"github.com/spf13/cobra" "github.com/spf13/cobra"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
ssautil "github.com/fluxcd/pkg/ssa/utils"
"github.com/fluxcd/flux2/v2/internal/build" "github.com/fluxcd/flux2/v2/internal/build"
) )
@ -63,6 +63,7 @@ type buildKsFlags struct {
path string path string
ignorePaths []string ignorePaths []string
dryRun bool dryRun bool
strictSubst bool
} }
var buildKsArgs buildKsFlags var buildKsArgs buildKsFlags
@ -72,6 +73,8 @@ func init() {
buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") 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().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.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.")
buildCmd.AddCommand(buildKsCmd) buildCmd.AddCommand(buildKsCmd)
} }
@ -107,6 +110,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
build.WithDryRun(buildKsArgs.dryRun), build.WithDryRun(buildKsArgs.dryRun),
build.WithNamespace(*kubeconfigArgs.Namespace), build.WithNamespace(*kubeconfigArgs.Namespace),
build.WithIgnore(buildKsArgs.ignorePaths), build.WithIgnore(buildKsArgs.ignorePaths),
build.WithStrictSubstitute(buildKsArgs.strictSubst),
) )
} else { } else {
builder, err = build.NewBuilder(name, buildKsArgs.path, builder, err = build.NewBuilder(name, buildKsArgs.path,
@ -114,6 +118,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
build.WithTimeout(rootArgs.timeout), build.WithTimeout(rootArgs.timeout),
build.WithKustomizationFile(buildKsArgs.kustomizationFile), build.WithKustomizationFile(buildKsArgs.kustomizationFile),
build.WithIgnore(buildKsArgs.ignorePaths), build.WithIgnore(buildKsArgs.ignorePaths),
build.WithStrictSubstitute(buildKsArgs.strictSubst),
) )
} }
@ -132,7 +137,7 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
errChan <- err errChan <- err
} }
manifests, err := ssa.ObjectsToYAML(objects) manifests, err := ssautil.ObjectsToYAML(objects)
if err != nil { if err != nil {
errChan <- err errChan <- err
} }

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

@ -0,0 +1,126 @@
/*
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"
}

@ -0,0 +1,141 @@
/*
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))
})
}
}

@ -131,8 +131,8 @@ func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) e
} }
logger.Waitingf("waiting for %s reconciliation", names.kind) logger.Waitingf("waiting for %s reconciliation", names.kind)
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isReady(ctx, kubeClient, namespacedName, object)); err != nil { isObjectReadyConditionFunc(kubeClient, namespacedName, object.asClientObject())); err != nil {
return err return err
} }
logger.Successf("%s reconciliation completed", names.kind) logger.Successf("%s reconciliation completed", names.kind)
@ -165,6 +165,6 @@ func parseLabels() (map[string]string, error) {
} }
func validateObjectName(name string) bool { 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) return r.MatchString(name)
} }

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

@ -24,22 +24,22 @@ import (
"strings" "strings"
"time" "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/apis/meta"
"github.com/fluxcd/pkg/runtime/transform" "github.com/fluxcd/pkg/runtime/transform"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" helmv2 "github.com/fluxcd/helm-controller/api/v2beta2"
) )
var createHelmReleaseCmd = &cobra.Command{ var createHelmReleaseCmd = &cobra.Command{
@ -303,8 +303,8 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for HelmRelease reconciliation") logger.Waitingf("waiting for HelmRelease reconciliation")
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isHelmReleaseReady(ctx, kubeClient, namespacedName, &helmRelease)); err != nil { isObjectReadyConditionFunc(kubeClient, namespacedName, &helmRelease)); err != nil {
return err return err
} }
logger.Successf("HelmRelease %s is ready", name) logger.Successf("HelmRelease %s is ready", name)
@ -344,23 +344,6 @@ func upsertHelmRelease(ctx context.Context, kubeClient client.Client,
return namespacedName, nil 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 { func validateStrategy(input string) bool {
allowedStrategy := []string{"Revision", "ChartVersion"} allowedStrategy := []string{"Revision", "ChartVersion"}

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

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

@ -24,13 +24,12 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" helmv2 "github.com/fluxcd/helm-controller/api/v2beta2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
@ -263,8 +262,8 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for Kustomization reconciliation") logger.Waitingf("waiting for Kustomization reconciliation")
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isKustomizationReady(ctx, kubeClient, namespacedName, &kustomization)); err != nil { isObjectReadyConditionFunc(kubeClient, namespacedName, &kustomization)); err != nil {
return err return err
} }
logger.Successf("Kustomization %s is ready", name) logger.Successf("Kustomization %s is ready", name)
@ -303,28 +302,3 @@ func upsertKustomization(ctx context.Context, kubeClient client.Client,
logger.Successf("Kustomization updated") logger.Successf("Kustomization updated")
return namespacedName, nil 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,7 +22,6 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
apimeta "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
@ -139,8 +138,8 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for Receiver reconciliation") logger.Waitingf("waiting for Receiver reconciliation")
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isReceiverReady(ctx, kubeClient, namespacedName, &receiver)); err != nil { isObjectReadyConditionFunc(kubeClient, namespacedName, &receiver)); err != nil {
return err return err
} }
logger.Successf("Receiver %s is ready", name) logger.Successf("Receiver %s is ready", name)
@ -179,23 +178,3 @@ func upsertReceiver(ctx context.Context, kubeClient client.Client,
logger.Successf("Receiver updated") logger.Successf("Receiver updated")
return namespacedName, nil 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
}
}

@ -32,7 +32,7 @@ import (
var createSecretHelmCmd = &cobra.Command{ var createSecretHelmCmd = &cobra.Command{
Use: "helm [name]", Use: "helm [name]",
Short: "Create or update a Kubernetes secret for Helm repository authentication", Short: "Create or update a Kubernetes secret for Helm repository authentication",
Long: withPreviewNote(`The create secret helm command generates a Kubernetes secret with basic authentication credentials.`), Long: `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 Example: ` # Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret helm repo-auth \ flux create secret helm repo-auth \
--namespace=my-namespace \ --namespace=my-namespace \

@ -0,0 +1,161 @@
/*
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
}

@ -0,0 +1,124 @@
/*
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: 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)
})
}
}

@ -31,7 +31,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
@ -204,8 +203,8 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for Bucket source reconciliation") logger.Waitingf("waiting for Bucket source reconciliation")
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isBucketReady(ctx, kubeClient, namespacedName, bucket)); err != nil { isObjectReadyConditionFunc(kubeClient, namespacedName, bucket)); err != nil {
return err return err
} }
logger.Successf("Bucket source reconciliation completed") logger.Successf("Bucket source reconciliation completed")
@ -247,30 +246,3 @@ func upsertBucket(ctx context.Context, kubeClient client.Client,
logger.Successf("Bucket source updated") logger.Successf("Bucket source updated")
return namespacedName, nil 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
}
}

@ -35,7 +35,6 @@ import (
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"github.com/fluxcd/pkg/apis/meta" "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/v1"
@ -325,8 +324,8 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for GitRepository source reconciliation") logger.Waitingf("waiting for GitRepository source reconciliation")
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isGitRepositoryReady(ctx, kubeClient, namespacedName, &gitRepository)); err != nil { isObjectReadyConditionFunc(kubeClient, namespacedName, &gitRepository)); err != nil {
return err return err
} }
logger.Successf("GitRepository source reconciliation completed") logger.Successf("GitRepository source reconciliation completed")
@ -368,30 +367,3 @@ func upsertGitRepository(ctx context.Context, kubeClient client.Client,
logger.Successf("GitRepository source updated") logger.Successf("GitRepository source updated")
return namespacedName, nil 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
}
}

@ -181,12 +181,21 @@ func TestCreateSourceGit(t *testing.T) {
Time: time.Now(), Time: time.Now(),
}, },
} }
repo.Status.ObservedGeneration = repo.GetGeneration()
}, },
}, { }, {
"Failed", "Failed",
command, command,
assertError("failed message"), assertError("failed message"),
func(repo *sourcev1.GitRepository) { 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{ newCondition := metav1.Condition{
Type: meta.ReadyCondition, Type: meta.ReadyCondition,
Status: metav1.ConditionFalse, Status: metav1.ConditionFalse,
@ -195,6 +204,7 @@ func TestCreateSourceGit(t *testing.T) {
ObservedGeneration: repo.GetGeneration(), ObservedGeneration: repo.GetGeneration(),
} }
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition) apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
repo.Status.ObservedGeneration = repo.GetGeneration()
}, },
}, { }, {
"NoArtifact", "NoArtifact",
@ -210,6 +220,7 @@ func TestCreateSourceGit(t *testing.T) {
ObservedGeneration: repo.GetGeneration(), ObservedGeneration: repo.GetGeneration(),
} }
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition) apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
repo.Status.ObservedGeneration = repo.GetGeneration()
}, },
}, },
} }

@ -22,8 +22,6 @@ import (
"net/url" "net/url"
"os" "os"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
@ -33,7 +31,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" "github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
@ -42,8 +41,8 @@ import (
var createSourceHelmCmd = &cobra.Command{ var createSourceHelmCmd = &cobra.Command{
Use: "helm [name]", Use: "helm [name]",
Short: "Create or update a HelmRepository source", Short: "Create or update a HelmRepository source",
Long: withPreviewNote(`The create source helm command generates a HelmRepository resource and waits for it to fetch the index. 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.`), For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`,
Example: ` # Create a source for an HTTPS public Helm repository Example: ` # Create a source for an HTTPS public Helm repository
flux create source helm podinfo \ flux create source helm podinfo \
--url=https://stefanprodan.github.io/podinfo \ --url=https://stefanprodan.github.io/podinfo \
@ -231,8 +230,12 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for HelmRepository source reconciliation") logger.Waitingf("waiting for HelmRepository source reconciliation")
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, readyConditionFunc := isObjectReadyConditionFunc(kubeClient, namespacedName, helmRepository)
isHelmRepositoryReady(ctx, kubeClient, namespacedName, helmRepository)); err != nil { 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 {
return err return err
} }
logger.Successf("HelmRepository source reconciliation completed") logger.Successf("HelmRepository source reconciliation completed")
@ -279,30 +282,3 @@ func upsertHelmRepository(ctx context.Context, kubeClient client.Client,
logger.Successf("source updated") logger.Successf("source updated")
return namespacedName, nil 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" "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" 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/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
@ -44,30 +44,43 @@ var createSourceOCIRepositoryCmd = &cobra.Command{
Example: ` # Create an OCIRepository for a public container image Example: ` # Create an OCIRepository for a public container image
flux create source oci podinfo \ flux create source oci podinfo \
--url=oci://ghcr.io/stefanprodan/manifests/podinfo \ --url=oci://ghcr.io/stefanprodan/manifests/podinfo \
--tag=6.1.6 \ --tag=6.6.2 \
--interval=10m --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, RunE: createSourceOCIRepositoryCmdRun,
} }
type sourceOCIRepositoryFlags struct { type sourceOCIRepositoryFlags struct {
url string url string
tag string tag string
semver string semver string
digest string digest string
secretRef string secretRef string
serviceAccount string serviceAccount string
certSecretRef string certSecretRef string
ignorePaths []string verifyProvider flags.SourceOCIVerifyProvider
provider flags.SourceOCIProvider verifySecretRef string
insecure bool verifyOIDCIssuer string
verifySubject string
ignorePaths []string
provider flags.SourceOCIProvider
insecure bool
} }
var sourceOCIRepositoryArgs = newSourceOCIFlags() var sourceOCIRepositoryArgs = newSourceOCIFlags()
func newSourceOCIFlags() sourceOCIRepositoryFlags { func newSourceOCIFlags() sourceOCIRepositoryFlags {
return sourceOCIRepositoryFlags{ return sourceOCIRepositoryFlags{
provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), provider: flags.SourceOCIProvider(sourcev1b2.GenericOCIProvider),
} }
} }
@ -80,6 +93,10 @@ func init() {
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')") createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')")
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.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().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().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)")
createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP") createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP")
@ -108,20 +125,20 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
ignorePaths = &ignorePathsStr ignorePaths = &ignorePathsStr
} }
repository := &sourcev1.OCIRepository{ repository := &sourcev1b2.OCIRepository{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Labels: sourceLabels, Labels: sourceLabels,
}, },
Spec: sourcev1.OCIRepositorySpec{ Spec: sourcev1b2.OCIRepositorySpec{
Provider: sourceOCIRepositoryArgs.provider.String(), Provider: sourceOCIRepositoryArgs.provider.String(),
URL: sourceOCIRepositoryArgs.url, URL: sourceOCIRepositoryArgs.url,
Insecure: sourceOCIRepositoryArgs.insecure, Insecure: sourceOCIRepositoryArgs.insecure,
Interval: metav1.Duration{ Interval: metav1.Duration{
Duration: createArgs.interval, Duration: createArgs.interval,
}, },
Reference: &sourcev1.OCIRepositoryRef{}, Reference: &sourcev1b2.OCIRepositoryRef{},
Ignore: ignorePaths, Ignore: ignorePaths,
}, },
} }
@ -156,6 +173,29 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
} }
} }
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 { if createArgs.export {
return printExport(exportOCIRepository(repository)) return printExport(exportOCIRepository(repository))
} }
@ -175,8 +215,8 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for OCIRepository reconciliation") logger.Waitingf("waiting for OCIRepository reconciliation")
if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isOCIRepositoryReady(ctx, kubeClient, namespacedName, repository)); err != nil { isObjectReadyConditionFunc(kubeClient, namespacedName, repository)); err != nil {
return err return err
} }
logger.Successf("OCIRepository reconciliation completed") logger.Successf("OCIRepository reconciliation completed")
@ -189,13 +229,13 @@ func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
} }
func upsertOCIRepository(ctx context.Context, kubeClient client.Client, func upsertOCIRepository(ctx context.Context, kubeClient client.Client,
ociRepository *sourcev1.OCIRepository) (types.NamespacedName, error) { ociRepository *sourcev1b2.OCIRepository) (types.NamespacedName, error) {
namespacedName := types.NamespacedName{ namespacedName := types.NamespacedName{
Namespace: ociRepository.GetNamespace(), Namespace: ociRepository.GetNamespace(),
Name: ociRepository.GetName(), Name: ociRepository.GetName(),
} }
var existing sourcev1.OCIRepository var existing sourcev1b2.OCIRepository
err := kubeClient.Get(ctx, namespacedName, &existing) err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
@ -218,30 +258,3 @@ func upsertOCIRepository(ctx context.Context, kubeClient client.Client,
logger.Successf("OCIRepository updated") logger.Successf("OCIRepository updated")
return namespacedName, nil 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,6 +36,36 @@ func TestCreateSourceOCI(t *testing.T) {
args: "create source oci podinfo", args: "create source oci podinfo",
assertFunc: assertError("url is required"), 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", name: "export manifest",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --export", args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --export",
@ -46,6 +76,11 @@ 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", 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"), 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 { for _, tt := range tests {

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

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

@ -19,7 +19,7 @@ package main
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" helmv2 "github.com/fluxcd/helm-controller/api/v2beta2"
) )
var deleteHelmReleaseCmd = &cobra.Command{ var deleteHelmReleaseCmd = &cobra.Command{

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

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

@ -23,8 +23,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/v2/internal/build"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/build"
) )
var diffKsCmd = &cobra.Command{ var diffKsCmd = &cobra.Command{
@ -53,6 +54,7 @@ type diffKsFlags struct {
path string path string
ignorePaths []string ignorePaths []string
progressBar bool progressBar bool
strictSubst bool
} }
var diffKsArgs diffKsFlags var diffKsArgs diffKsFlags
@ -62,6 +64,8 @@ func init() {
diffKsCmd.Flags().BoolVar(&diffKsArgs.progressBar, "progress-bar", true, "Boolean to set the progress bar. The default value is true.") 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().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().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.")
diffCmd.AddCommand(diffKsCmd) diffCmd.AddCommand(diffKsCmd)
} }
@ -96,6 +100,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithKustomizationFile(diffKsArgs.kustomizationFile), build.WithKustomizationFile(diffKsArgs.kustomizationFile),
build.WithProgressBar(), build.WithProgressBar(),
build.WithIgnore(diffKsArgs.ignorePaths), build.WithIgnore(diffKsArgs.ignorePaths),
build.WithStrictSubstitute(diffKsArgs.strictSubst),
) )
} else { } else {
builder, err = build.NewBuilder(name, diffKsArgs.path, builder, err = build.NewBuilder(name, diffKsArgs.path,
@ -103,6 +108,7 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error {
build.WithTimeout(rootArgs.timeout), build.WithTimeout(rootArgs.timeout),
build.WithKustomizationFile(diffKsArgs.kustomizationFile), build.WithKustomizationFile(diffKsArgs.kustomizationFile),
build.WithIgnore(diffKsArgs.ignorePaths), build.WithIgnore(diffKsArgs.ignorePaths),
build.WithStrictSubstitute(diffKsArgs.strictSubst),
) )
} }

@ -0,0 +1,74 @@
/*
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
}

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

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

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

@ -20,7 +20,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" helmv2 "github.com/fluxcd/helm-controller/api/v2beta2"
) )
var exportHelmReleaseCmd = &cobra.Command{ var exportHelmReleaseCmd = &cobra.Command{

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

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

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

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

@ -19,12 +19,14 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
) )
var getAlertCmd = &cobra.Command{ var getAlertCmd = &cobra.Command{
@ -76,8 +78,9 @@ func init() {
func (s alertListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { func (s alertListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
item := s.Items[i] item := s.Items[i]
status, msg := statusAndMessage(item.Status.Conditions) status, msg := string(metav1.ConditionTrue), "Alert is Ready"
return append(nameColumns(&item, includeNamespace, includeKind), strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) return append(nameColumns(&item, includeNamespace, includeKind),
cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (s alertListAdapter) headers(includeNamespace bool) []string { func (s alertListAdapter) headers(includeNamespace bool) []string {
@ -89,6 +92,5 @@ func (s alertListAdapter) headers(includeNamespace bool) []string {
} }
func (s alertListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { func (s alertListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
item := s.Items[i] return false
return statusMatches(conditionType, conditionStatus, item.Status.Conditions)
} }

@ -20,9 +20,10 @@ import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
) )
var getAlertProviderCmd = &cobra.Command{ var getAlertProviderCmd = &cobra.Command{
@ -74,7 +75,7 @@ func init() {
func (s alertProviderListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { func (s alertProviderListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
item := s.Items[i] item := s.Items[i]
status, msg := statusAndMessage(item.Status.Conditions) status, msg := string(metav1.ConditionTrue), "Provider is Ready"
return append(nameColumns(&item, includeNamespace, includeKind), status, msg) return append(nameColumns(&item, includeNamespace, includeKind), status, msg)
} }
@ -87,6 +88,5 @@ func (s alertProviderListAdapter) headers(includeNamespace bool) []string {
} }
func (s alertProviderListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { func (s alertProviderListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool {
item := s.Items[i] return false
return statusMatches(conditionType, conditionStatus, item.Status.Conditions)
} }

@ -17,14 +17,13 @@ limitations under the License.
package main package main
import ( import (
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
apimeta "k8s.io/apimachinery/pkg/api/meta"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" helmv2 "github.com/fluxcd/helm-controller/api/v2beta2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1"
notificationv1b2 "github.com/fluxcd/notification-controller/api/v1beta2" notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
) )
var getAllCmd = &cobra.Command{ var getAllCmd = &cobra.Command{
@ -63,11 +62,11 @@ var getAllCmd = &cobra.Command{
}, },
{ {
apiType: alertProviderType, apiType: alertProviderType,
list: alertProviderListAdapter{&notificationv1b2.ProviderList{}}, list: alertProviderListAdapter{&notificationv1b3.ProviderList{}},
}, },
{ {
apiType: alertType, apiType: alertType,
list: &alertListAdapter{&notificationv1b2.AlertList{}}, list: &alertListAdapter{&notificationv1b3.AlertList{}},
}, },
} }
@ -87,7 +86,7 @@ var getAllCmd = &cobra.Command{
} }
func logError(err error) { func logError(err error) {
if !strings.Contains(err.Error(), "no matches for kind") { if !apimeta.IsNoMatchError(err) {
logger.Failuref(err.Error()) logger.Failuref(err.Error())
} }
} }

@ -19,11 +19,13 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta2"
) )
var getHelmReleaseCmd = &cobra.Command{ var getHelmReleaseCmd = &cobra.Command{
@ -75,7 +77,7 @@ func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, incl
revision := item.Status.LastAppliedRevision revision := item.Status.LastAppliedRevision
status, msg := statusAndMessage(item.Status.Conditions) status, msg := statusAndMessage(item.Status.Conditions)
return append(nameColumns(&item, includeNamespace, includeKind), return append(nameColumns(&item, includeNamespace, includeKind),
revision, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a helmReleaseListAdapter) headers(includeNamespace bool) []string { func (a helmReleaseListAdapter) headers(includeNamespace bool) []string {

@ -19,7 +19,7 @@ package main
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
) )

@ -19,10 +19,11 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
@ -82,7 +83,7 @@ func (s imageRepositoryListAdapter) summariseItem(i int, includeNamespace bool,
lastScan = item.Status.LastScanResult.ScanTime.Time.Format(time.RFC3339) lastScan = item.Status.LastScanResult.ScanTime.Time.Format(time.RFC3339)
} }
return append(nameColumns(&item, includeNamespace, includeKind), return append(nameColumns(&item, includeNamespace, includeKind),
lastScan, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) lastScan, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (s imageRepositoryListAdapter) headers(includeNamespace bool) []string { func (s imageRepositoryListAdapter) headers(includeNamespace bool) []string {

@ -19,13 +19,14 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
) )
var getImageUpdateCmd = &cobra.Command{ var getImageUpdateCmd = &cobra.Command{
@ -81,7 +82,8 @@ func (s imageUpdateAutomationListAdapter) summariseItem(i int, includeNamespace
if item.Status.LastAutomationRunTime != nil { if item.Status.LastAutomationRunTime != nil {
lastRun = item.Status.LastAutomationRunTime.Time.Format(time.RFC3339) lastRun = item.Status.LastAutomationRunTime.Time.Format(time.RFC3339)
} }
return append(nameColumns(&item, includeNamespace, includeKind), lastRun, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) return append(nameColumns(&item, includeNamespace, includeKind), lastRun,
cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (s imageUpdateAutomationListAdapter) headers(includeNamespace bool) []string { func (s imageUpdateAutomationListAdapter) headers(includeNamespace bool) []string {

@ -19,9 +19,10 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
@ -83,7 +84,7 @@ func (a kustomizationListAdapter) summariseItem(i int, includeNamespace bool, in
revision = utils.TruncateHex(revision) revision = utils.TruncateHex(revision)
msg = utils.TruncateHex(msg) msg = utils.TruncateHex(msg)
return append(nameColumns(&item, includeNamespace, includeKind), return append(nameColumns(&item, includeNamespace, includeKind),
revision, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a kustomizationListAdapter) headers(includeNamespace bool) []string { func (a kustomizationListAdapter) headers(includeNamespace bool) []string {

@ -19,9 +19,10 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1"
@ -74,7 +75,8 @@ func init() {
func (s receiverListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { func (s receiverListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string {
item := s.Items[i] item := s.Items[i]
status, msg := statusAndMessage(item.Status.Conditions) status, msg := statusAndMessage(item.Status.Conditions)
return append(nameColumns(&item, includeNamespace, includeKind), strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) return append(nameColumns(&item, includeNamespace, includeKind),
cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (s receiverListAdapter) headers(includeNamespace bool) []string { func (s receiverListAdapter) headers(includeNamespace bool) []string {

@ -17,9 +17,8 @@ limitations under the License.
package main package main
import ( import (
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
apimeta "k8s.io/apimachinery/pkg/api/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
@ -55,17 +54,17 @@ var getSourceAllCmd = &cobra.Command{
}, },
{ {
apiType: helmRepositoryType, apiType: helmRepositoryType,
list: &helmRepositoryListAdapter{&sourcev1b2.HelmRepositoryList{}}, list: &helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{}},
}, },
{ {
apiType: helmChartType, apiType: helmChartType,
list: &helmChartListAdapter{&sourcev1b2.HelmChartList{}}, list: &helmChartListAdapter{&sourcev1.HelmChartList{}},
}, },
} }
for _, c := range allSourceCmd { for _, c := range allSourceCmd {
if err := c.run(cmd, args); err != nil { if err := c.run(cmd, args); err != nil {
if !strings.Contains(err.Error(), "no matches for kind") { if !apimeta.IsNoMatchError(err) {
logger.Failuref(err.Error()) logger.Failuref(err.Error())
} }
} }

@ -19,9 +19,10 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
@ -85,7 +86,7 @@ func (a *bucketListAdapter) summariseItem(i int, includeNamespace bool, includeK
revision = utils.TruncateHex(revision) revision = utils.TruncateHex(revision)
msg = utils.TruncateHex(msg) msg = utils.TruncateHex(msg)
return append(nameColumns(&item, includeNamespace, includeKind), return append(nameColumns(&item, includeNamespace, includeKind),
revision, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a bucketListAdapter) headers(includeNamespace bool) []string { func (a bucketListAdapter) headers(includeNamespace bool) []string {

@ -19,12 +19,13 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
@ -32,7 +33,7 @@ import (
var getSourceHelmChartCmd = &cobra.Command{ var getSourceHelmChartCmd = &cobra.Command{
Use: "chart", Use: "chart",
Short: "Get HelmChart statuses", Short: "Get HelmChart statuses",
Long: withPreviewNote("The get sources chart command prints the status of the HelmCharts."), Long: "The get sources chart command prints the status of the HelmCharts.",
Example: ` # List all Helm charts and their status Example: ` # List all Helm charts and their status
flux get sources chart flux get sources chart
@ -86,7 +87,7 @@ func (a *helmChartListAdapter) summariseItem(i int, includeNamespace bool, inclu
// Message may still contain reference of e.g. commit chart was build from // Message may still contain reference of e.g. commit chart was build from
msg = utils.TruncateHex(msg) msg = utils.TruncateHex(msg)
return append(nameColumns(&item, includeNamespace, includeKind), return append(nameColumns(&item, includeNamespace, includeKind),
revision, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a helmChartListAdapter) headers(includeNamespace bool) []string { func (a helmChartListAdapter) headers(includeNamespace bool) []string {

@ -19,9 +19,10 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
@ -85,7 +86,7 @@ func (a *gitRepositoryListAdapter) summariseItem(i int, includeNamespace bool, i
revision = utils.TruncateHex(revision) revision = utils.TruncateHex(revision)
msg = utils.TruncateHex(msg) msg = utils.TruncateHex(msg)
return append(nameColumns(&item, includeNamespace, includeKind), return append(nameColumns(&item, includeNamespace, includeKind),
revision, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a gitRepositoryListAdapter) headers(includeNamespace bool) []string { func (a gitRepositoryListAdapter) headers(includeNamespace bool) []string {

@ -19,12 +19,14 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
@ -32,7 +34,7 @@ import (
var getSourceHelmCmd = &cobra.Command{ var getSourceHelmCmd = &cobra.Command{
Use: "helm", Use: "helm",
Short: "Get HelmRepository source statuses", Short: "Get HelmRepository source statuses",
Long: withPreviewNote("The get sources helm command prints the status of the HelmRepository sources."), Long: "The get sources helm command prints the status of the HelmRepository sources.",
Example: ` # List all Helm repositories and their status Example: ` # List all Helm repositories and their status
flux get sources helm flux get sources helm
@ -81,11 +83,16 @@ func (a *helmRepositoryListAdapter) summariseItem(i int, includeNamespace bool,
if item.GetArtifact() != nil { if item.GetArtifact() != nil {
revision = item.GetArtifact().Revision revision = item.GetArtifact().Revision
} }
status, msg := statusAndMessage(item.Status.Conditions) var status, msg string
if item.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
status, msg = string(metav1.ConditionTrue), "Helm repository is Ready"
} else {
status, msg = statusAndMessage(item.Status.Conditions)
}
revision = utils.TruncateHex(revision) revision = utils.TruncateHex(revision)
msg = utils.TruncateHex(msg) msg = utils.TruncateHex(msg)
return append(nameColumns(&item, includeNamespace, includeKind), return append(nameColumns(&item, includeNamespace, includeKind),
revision, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a helmRepositoryListAdapter) headers(includeNamespace bool) []string { func (a helmRepositoryListAdapter) headers(includeNamespace bool) []string {

@ -19,9 +19,10 @@ package main
import ( import (
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
@ -85,7 +86,7 @@ func (a *ociRepositoryListAdapter) summariseItem(i int, includeNamespace bool, i
revision = utils.TruncateHex(revision) revision = utils.TruncateHex(revision)
msg = utils.TruncateHex(msg) msg = utils.TruncateHex(msg)
return append(nameColumns(&item, includeNamespace, includeKind), return append(nameColumns(&item, includeNamespace, includeKind),
revision, strings.Title(strconv.FormatBool(item.Spec.Suspend)), status, msg) revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg)
} }
func (a ociRepositoryListAdapter) headers(includeNamespace bool) []string { func (a ociRepositoryListAdapter) headers(includeNamespace bool) []string {

@ -19,7 +19,7 @@ package main
import ( import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" helmv2 "github.com/fluxcd/helm-controller/api/v2beta2"
) )
// helmv2.HelmRelease // helmv2.HelmRelease

@ -19,7 +19,7 @@ package main
import ( import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
) )

@ -21,19 +21,26 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/v2/pkg/status" "github.com/fluxcd/flux2/v2/pkg/status"
) )
var installCmd = &cobra.Command{ var installCmd = &cobra.Command{
Use: "install", Use: "install",
Args: cobra.NoArgs,
Short: "Install or upgrade Flux", Short: "Install or upgrade Flux",
Long: `The install command deploys Flux in the specified namespace. Long: `The install command deploys Flux in the specified namespace.
If a previous version is installed, then an in-place upgrade will be performed.`, If a previous version is installed, then an in-place upgrade will be performed.`,
@ -63,6 +70,7 @@ type installFlags struct {
defaultComponents []string defaultComponents []string
extraComponents []string extraComponents []string
registry string registry string
registryCredential string
imagePullSecret string imagePullSecret string
branch string branch string
watchAllNamespaces bool watchAllNamespaces bool
@ -72,6 +80,7 @@ type installFlags struct {
tokenAuth bool tokenAuth bool
clusterDomain string clusterDomain string
tolerationKeys []string tolerationKeys []string
force bool
} }
var installArgs = NewInstallFlags() var installArgs = NewInstallFlags()
@ -88,6 +97,8 @@ func init() {
installCmd.Flags().StringVar(&installArgs.manifestsPath, "manifests", "", "path to the manifest directory") installCmd.Flags().StringVar(&installArgs.manifestsPath, "manifests", "", "path to the manifest directory")
installCmd.Flags().StringVar(&installArgs.registry, "registry", rootArgs.defaults.Registry, installCmd.Flags().StringVar(&installArgs.registry, "registry", rootArgs.defaults.Registry,
"container registry where the toolkit images are published") "container registry where the toolkit images are published")
installCmd.Flags().StringVar(&installArgs.registryCredential, "registry-creds", "",
"container registry credentials in the format 'user:password', requires --image-pull-secret to be set")
installCmd.Flags().StringVar(&installArgs.imagePullSecret, "image-pull-secret", "", installCmd.Flags().StringVar(&installArgs.imagePullSecret, "image-pull-secret", "",
"Kubernetes secret name used for pulling the toolkit images from a private registry") "Kubernetes secret name used for pulling the toolkit images from a private registry")
installCmd.Flags().BoolVar(&installArgs.watchAllNamespaces, "watch-all-namespaces", rootArgs.defaults.WatchAllNamespaces, installCmd.Flags().BoolVar(&installArgs.watchAllNamespaces, "watch-all-namespaces", rootArgs.defaults.WatchAllNamespaces,
@ -98,6 +109,7 @@ func init() {
installCmd.Flags().StringVar(&installArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain") installCmd.Flags().StringVar(&installArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain")
installCmd.Flags().StringSliceVar(&installArgs.tolerationKeys, "toleration-keys", nil, installCmd.Flags().StringSliceVar(&installArgs.tolerationKeys, "toleration-keys", nil,
"list of toleration keys used to schedule the components pods onto nodes with matching taints") "list of toleration keys used to schedule the components pods onto nodes with matching taints")
installCmd.Flags().BoolVar(&installArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
installCmd.Flags().MarkHidden("manifests") installCmd.Flags().MarkHidden("manifests")
rootCmd.AddCommand(installCmd) rootCmd.AddCommand(installCmd)
@ -119,6 +131,14 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if installArgs.registryCredential != "" && installArgs.imagePullSecret == "" {
return fmt.Errorf("--registry-creds requires --image-pull-secret to be set")
}
if installArgs.registryCredential != "" && len(strings.Split(installArgs.registryCredential, ":")) != 2 {
return fmt.Errorf("invalid --registry-creds format, expected 'user:password'")
}
if ver, err := getVersion(installArgs.version); err != nil { if ver, err := getVersion(installArgs.version); err != nil {
return err return err
} else { } else {
@ -149,6 +169,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Components: components, Components: components,
Registry: installArgs.registry, Registry: installArgs.registry,
RegistryCredential: installArgs.registryCredential,
ImagePullSecret: installArgs.imagePullSecret, ImagePullSecret: installArgs.imagePullSecret,
WatchAllNamespaces: installArgs.watchAllNamespaces, WatchAllNamespaces: installArgs.watchAllNamespaces,
NetworkPolicy: installArgs.networkPolicy, NetworkPolicy: installArgs.networkPolicy,
@ -183,6 +204,35 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
logger.Successf("manifests build completed") logger.Successf("manifests build completed")
logger.Actionf("installing components in %s namespace", *kubeconfigArgs.Namespace) logger.Actionf("installing components in %s namespace", *kubeconfigArgs.Namespace)
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
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 info.bootstrapped {
return fmt.Errorf("this cluster has already been bootstrapped with Flux %s! Please use 'flux bootstrap' to upgrade",
info.version)
}
if installed && !installArgs.force {
err := confirmFluxInstallOverride(info)
if err != nil {
if err == promptui.ErrAbort {
return fmt.Errorf("installation cancelled")
}
return err
}
}
applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path)) applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path))
if err != nil { if err != nil {
return fmt.Errorf("install failed: %w", err) return fmt.Errorf("install failed: %w", err)
@ -190,6 +240,29 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
fmt.Fprintln(os.Stderr, applyOutput) fmt.Fprintln(os.Stderr, applyOutput)
if opts.ImagePullSecret != "" && opts.RegistryCredential != "" {
logger.Actionf("generating image pull secret %s", opts.ImagePullSecret)
credentials := strings.SplitN(opts.RegistryCredential, ":", 2)
secretOpts := sourcesecret.Options{
Name: opts.ImagePullSecret,
Namespace: opts.Namespace,
Registry: opts.Registry,
Username: credentials[0],
Password: credentials[1],
}
imagePullSecret, err := sourcesecret.Generate(secretOpts)
if err != nil {
return fmt.Errorf("install failed: %w", err)
}
var s corev1.Secret
if err := yaml.Unmarshal([]byte(imagePullSecret.Content), &s); err != nil {
return fmt.Errorf("install failed: %w", err)
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return fmt.Errorf("install failed: %w", err)
}
}
kubeConfig, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) kubeConfig, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return fmt.Errorf("install failed: %w", err) return fmt.Errorf("install failed: %w", err)

@ -37,6 +37,16 @@ func TestInstall(t *testing.T) {
args: "install --namespace='@#[]'", args: "install --namespace='@#[]'",
assert: assertError("namespace must be a valid DNS label: \"@#[]\""), assert: assertError("namespace must be a valid DNS label: \"@#[]\""),
}, },
{
name: "invalid sub-command",
args: "install unexpectedPosArg --namespace=example",
assert: assertError(`unknown command "unexpectedPosArg" for "flux install"`),
},
{
name: "missing image pull secret",
args: "install --registry-creds=fluxcd:test",
assert: assertError(`--registry-creds requires --image-pull-secret to be set`),
},
} }
for _, tt := range tests { for _, tt := range tests {

@ -74,7 +74,7 @@ type logsFlags struct {
fluxNamespace string fluxNamespace string
allNamespaces bool allNamespaces bool
sinceTime string sinceTime string
sinceSeconds time.Duration sinceDuration time.Duration
} }
var logsArgs = logsFlags{ var logsArgs = logsFlags{
@ -91,7 +91,7 @@ func init() {
logsCmd.Flags().Int64VarP(&logsArgs.tail, "tail", "", logsArgs.tail, "lines of recent log file to display") logsCmd.Flags().Int64VarP(&logsArgs.tail, "tail", "", logsArgs.tail, "lines of recent log file to display")
logsCmd.Flags().StringVarP(&logsArgs.fluxNamespace, "flux-namespace", "", rootArgs.defaults.Namespace, "the namespace where the Flux components are running") logsCmd.Flags().StringVarP(&logsArgs.fluxNamespace, "flux-namespace", "", rootArgs.defaults.Namespace, "the namespace where the Flux components are running")
logsCmd.Flags().BoolVarP(&logsArgs.allNamespaces, "all-namespaces", "A", false, "displays logs for objects across all namespaces") logsCmd.Flags().BoolVarP(&logsArgs.allNamespaces, "all-namespaces", "A", false, "displays logs for objects across all namespaces")
logsCmd.Flags().DurationVar(&logsArgs.sinceSeconds, "since", logsArgs.sinceSeconds, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") logsCmd.Flags().DurationVar(&logsArgs.sinceDuration, "since", logsArgs.sinceDuration, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.")
logsCmd.Flags().StringVar(&logsArgs.sinceTime, "since-time", logsArgs.sinceTime, "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.") logsCmd.Flags().StringVar(&logsArgs.sinceTime, "since-time", logsArgs.sinceTime, "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.")
rootCmd.AddCommand(logsCmd) rootCmd.AddCommand(logsCmd)
} }
@ -129,8 +129,8 @@ func logsCmdRun(cmd *cobra.Command, args []string) error {
logOpts.TailLines = &logsArgs.tail logOpts.TailLines = &logsArgs.tail
} }
if len(logsArgs.sinceTime) > 0 && logsArgs.sinceSeconds != 0 { if len(logsArgs.sinceTime) > 0 && logsArgs.sinceDuration != 0 {
return fmt.Errorf("at most one of `sinceTime` or `sinceSeconds` may be specified") return fmt.Errorf("at most one of `sinceTime` or `sinceDuration` may be specified")
} }
if len(logsArgs.sinceTime) > 0 { if len(logsArgs.sinceTime) > 0 {
@ -141,9 +141,9 @@ func logsCmdRun(cmd *cobra.Command, args []string) error {
logOpts.SinceTime = &t logOpts.SinceTime = &t
} }
if logsArgs.sinceSeconds != 0 { if logsArgs.sinceDuration != 0 {
// round up to the nearest second // round up to the nearest second
sec := int64(logsArgs.sinceSeconds.Round(time.Second).Seconds()) sec := int64(logsArgs.sinceDuration.Round(time.Second).Seconds())
logOpts.SinceSeconds = &sec logOpts.SinceSeconds = &sec
} }

@ -82,7 +82,7 @@ func TestLogsSinceTimeInvalid(t *testing.T) {
func TestLogsSinceOnlyOneAllowed(t *testing.T) { func TestLogsSinceOnlyOneAllowed(t *testing.T) {
cmd := cmdTestCase{ cmd := cmdTestCase{
args: "logs --since=2m --since-time=2021-08-06T14:26:25.546Z", args: "logs --since=2m --since-time=2021-08-06T14:26:25.546Z",
assert: assertError("at most one of `sinceTime` or `sinceSeconds` may be specified"), assert: assertError("at most one of `sinceTime` or `sinceDuration` may be specified"),
} }
cmd.runTestCmd(t) cmd.runTestCmd(t)
} }

@ -151,6 +151,11 @@ func init() {
apiServer := "" apiServer := ""
kubeconfigArgs.APIServer = &apiServer kubeconfigArgs.APIServer = &apiServer
rootCmd.PersistentFlags().StringVar(kubeconfigArgs.APIServer, "server", *kubeconfigArgs.APIServer, "The address and port of the Kubernetes API server") rootCmd.PersistentFlags().StringVar(kubeconfigArgs.APIServer, "server", *kubeconfigArgs.APIServer, "The address and port of the Kubernetes API server")
// Update the description for kubeconfig TLS flags so that user's don't mistake it for a Flux specific flag
rootCmd.Flag("insecure-skip-tls-verify").Usage = "If true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure"
rootCmd.Flag("client-certificate").Usage = "Path to a client certificate file for TLS authentication to the Kubernetes API server"
rootCmd.Flag("certificate-authority").Usage = "Path to a cert file for the certificate authority to authenticate the Kubernetes API server"
rootCmd.Flag("client-key").Usage = "Path to a client key file for TLS authentication to the Kubernetes API server"
kubeclientOptions.BindFlags(rootCmd.PersistentFlags()) kubeclientOptions.BindFlags(rootCmd.PersistentFlags())

@ -25,10 +25,15 @@ import (
"os" "os"
"testing" "testing"
"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/log"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
log.SetLogger(logr.New(log.NullLogSink{}))
// Ensure tests print consistent timestamps regardless of timezone // Ensure tests print consistent timestamps regardless of timezone
os.Setenv("TZ", "UTC") os.Setenv("TZ", "UTC")

@ -31,14 +31,16 @@ import (
"text/template" "text/template"
"time" "time"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/mattn/go-shellwords" "github.com/mattn/go-shellwords"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
k8syaml "k8s.io/apimachinery/pkg/util/yaml" k8syaml "k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest" "sigs.k8s.io/controller-runtime/pkg/envtest"
"github.com/fluxcd/flux2/v2/internal/utils"
) )
var nextNamespaceId int64 var nextNamespaceId int64
@ -112,7 +114,8 @@ func (m *testEnvKubeManager) CreateObjects(clientObjects []*unstructured.Unstruc
} }
obj.SetResourceVersion(createObj.GetResourceVersion()) obj.SetResourceVersion(createObj.GetResourceVersion())
err = m.client.Status().Update(context.Background(), obj) err = m.client.Status().Update(context.Background(), obj)
if err != nil { // Updating status of static objects results in not found error.
if err != nil && !errors.IsNotFound(err) {
return err return err
} }
} }
@ -182,7 +185,7 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager
} }
tmpFilename := filepath.Join("/tmp", "kubeconfig-"+time.Nanosecond.String()) tmpFilename := filepath.Join("/tmp", "kubeconfig-"+time.Nanosecond.String())
os.WriteFile(tmpFilename, kubeConfig, 0644) os.WriteFile(tmpFilename, kubeConfig, 0o600)
k8sClient, err := client.NewWithWatch(cfg, client.Options{ k8sClient, err := client.NewWithWatch(cfg, client.Options{
Scheme: utils.NewScheme(), Scheme: utils.NewScheme(),
}) })
@ -203,6 +206,9 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager
useExistingCluster := true useExistingCluster := true
config, err := clientcmd.BuildConfigFromFlags("", testKubeConfig) config, err := clientcmd.BuildConfigFromFlags("", testKubeConfig)
if err != nil {
return nil, err
}
testEnv := &envtest.Environment{ testEnv := &envtest.Environment{
UseExistingCluster: &useExistingCluster, UseExistingCluster: &useExistingCluster,
Config: config, Config: config,
@ -310,7 +316,7 @@ func assertGoldenTemplateFile(goldenFile string, templateValues map[string]strin
if len(templateValues) > 0 { if len(templateValues) > 0 {
fmt.Println("NOTE: -update flag passed but golden template files can't be updated, please update it manually") fmt.Println("NOTE: -update flag passed but golden template files can't be updated, please update it manually")
} else { } else {
if err := os.WriteFile(goldenFile, []byte(output), 0644); err != nil { if err := os.WriteFile(goldenFile, []byte(output), 0o600); err != nil {
return fmt.Errorf("failed to update golden file '%s': %v", goldenFile, err) return fmt.Errorf("failed to update golden file '%s': %v", goldenFile, err)
} }
return nil return nil
@ -337,8 +343,6 @@ type cmdTestCase struct {
// Tests use assertFunc to assert on an output, success or failure. This // Tests use assertFunc to assert on an output, success or failure. This
// can be a function defined by the test or existing function above. // can be a function defined by the test or existing function above.
assert assertFunc assert assertFunc
// Filename that contains yaml objects to load into Kubernetes
objectFile string
} }
func (cmd *cmdTestCase) runTestCmd(t *testing.T) { func (cmd *cmdTestCase) runTestCmd(t *testing.T) {
@ -390,6 +394,29 @@ func executeCommand(cmd string) (string, error) {
return result, err return result, err
} }
// Run the command while passing the string as input and return the captured output.
func executeCommandWithIn(cmd string, in io.Reader) (string, error) {
defer resetCmdArgs()
args, err := shellwords.Parse(cmd)
if err != nil {
return "", err
}
buf := new(bytes.Buffer)
rootCmd.SetOut(buf)
rootCmd.SetErr(buf)
rootCmd.SetArgs(args)
if in != nil {
rootCmd.SetIn(in)
}
_, err = rootCmd.ExecuteC()
result := buf.String()
return result, err
}
// resetCmdArgs resets the flags for various cmd // resetCmdArgs resets the flags for various cmd
// Note: this will also clear default value of the flags set in init() // Note: this will also clear default value of the flags set in init()
func resetCmdArgs() { func resetCmdArgs() {
@ -438,7 +465,7 @@ func resetCmdArgs() {
versionArgs = versionFlags{ versionArgs = versionFlags{
output: "yaml", output: "yaml",
} }
envsubstArgs = envsubstFlags{}
} }
func isChangeError(err error) bool { func isChangeError(err error) bool {

@ -22,10 +22,13 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"os" "os"
"testing" "testing"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/log"
) )
// The test environment is long running process shared between tests, initialized // The test environment is long running process shared between tests, initialized
@ -34,6 +37,8 @@ import (
var testEnv *testEnvKubeManager var testEnv *testEnvKubeManager
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
log.SetLogger(logr.New(log.NullLogSink{}))
// Ensure tests print consistent timestamps regardless of timezone // Ensure tests print consistent timestamps regardless of timezone
os.Setenv("TZ", "UTC") os.Setenv("TZ", "UTC")

@ -111,8 +111,12 @@ func pullArtifactCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
logger.Successf("source %s", meta.Source) if meta.Source != "" {
logger.Successf("revision %s", meta.Revision) logger.Successf("source %s", meta.Source)
}
if meta.Revision != "" {
logger.Successf("revision %s", meta.Revision)
}
logger.Successf("digest %s", meta.Digest) logger.Successf("digest %s", meta.Digest)
logger.Successf("artifact content extracted to %s", pullArtifactArgs.output) logger.Successf("artifact content extracted to %s", pullArtifactArgs.output)

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

Loading…
Cancel
Save