Merge branch 'main' into rfc0005

pull/2222/head
gregoireW 7 months ago committed by GitHub
commit c9361ca55f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -4,13 +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
source_x86_64 = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_linux_amd64.tar.gz optdepends = bash-completion: auto-completion for flux in Bash
source_armv6h = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_linux_arm.tar.gz optdepends = zsh-completions: auto-completion for flux in ZSH
source_armv7h = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_linux_arm.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
source_aarch64 = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_linux_arm64.tar.gz sha256sums_x86_64 = ${SHA256SUM_AMD64}
source_armv7h = flux-bin-${PKGVER}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm.tar.gz
sha256sums_armv7h = ${SHA256SUM_ARM}
source_aarch64 = flux-bin-${PKGVER}_linux_arm64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm64.tar.gz
sha256sums_aarch64 = ${SHA256SUM_ARM64}
pkgname = flux-bin pkgname = flux-bin

@ -4,37 +4,32 @@
pkgname=flux-bin pkgname=flux-bin
pkgver=${PKGVER} pkgver=${PKGVER}
pkgrel=${PKGREL} pkgrel=${PKGREL}
_srcname=flux
_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${pkgver}/flux_${pkgver}_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${pkgver}/flux_${pkgver}_linux_arm.tar.gz"
) )
source_armv7h=( source_armv7h=(
"${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${pkgver}/flux_${pkgver}_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${pkgver}/flux_${pkgver}_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}
) )
sha256sums_aarch64=( sha256sums_aarch64=(
${SHA256SUM_ARM64} ${SHA256SUM_ARM64}
) )
_srcname=flux
package() { package() {
install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}"

@ -28,6 +28,7 @@ git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1
CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }')
CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }')
# Transform pre-release to AUR compatible version format
export PKGVER=${VERSION/-/} export PKGVER=${VERSION/-/}
if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then
@ -36,12 +37,12 @@ else
export PKGREL=1 export PKGREL=1
fi fi
export SHA256SUM_ARM=$(sha256sum ${ROOT}/dist/flux_${PKGVER}_linux_arm.tar.gz | awk '{ print $1 }') export SHA256SUM_ARM=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_arm.tar.gz | awk '{ print $1 }')
export SHA256SUM_ARM64=$(sha256sum ${ROOT}/dist/flux_${PKGVER}_linux_arm64.tar.gz | awk '{ print $1 }') export SHA256SUM_ARM64=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_arm64.tar.gz | awk '{ print $1 }')
export SHA256SUM_AMD64=$(sha256sum ${ROOT}/dist/flux_${PKGVER}_linux_amd64.tar.gz | awk '{ print $1 }') export SHA256SUM_AMD64=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_amd64.tar.gz | awk '{ print $1 }')
envsubst '$PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < .SRCINFO.template > $GITDIR/.SRCINFO envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < .SRCINFO.template > $GITDIR/.SRCINFO
envsubst '$PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < PKGBUILD.template > $GITDIR/PKGBUILD envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < PKGBUILD.template > $GITDIR/PKGBUILD
cd $GITDIR cd $GITDIR
git config user.name "fluxcdbot" git config user.name "fluxcdbot"

@ -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
@ -13,6 +12,6 @@ pkgbase = flux-go
provides = flux-bin provides = flux-bin
conflicts = flux-bin conflicts = flux-bin
replaces = flux-cli replaces = flux-cli
source = flux-go-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/archive/v${PKGVER}.tar.gz source = flux-go-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/archive/v${VERSION}.tar.gz
pkgname = flux-go pkgname = flux-go

@ -4,43 +4,44 @@
pkgname=flux-go pkgname=flux-go
pkgver=${PKGVER} pkgver=${PKGVER}
pkgrel=${PKGREL} pkgrel=${PKGREL}
_srcname=flux
_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")
replaces=("flux-cli") replaces=("flux-cli")
depends=("glibc") depends=("glibc")
makedepends=('go>=1.17', 'kustomize>=3.0') makedepends=('go>=1.20', 'kustomize>=5.0')
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=( source=(
"${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/archive/v${pkgver}.tar.gz" "${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/archive/v${_srcver}.tar.gz"
) )
sha256sums=( sha256sums=(
${SHA256SUM} ${SHA256SUM}
) )
_srcname=flux
build() { build() {
cd "flux2-${pkgver}" cd "flux2-${_srcver}"
export CGO_LDFLAGS="$LDFLAGS" export CGO_LDFLAGS="$LDFLAGS"
export CGO_CFLAGS="$CFLAGS" export CGO_CFLAGS="$CFLAGS"
export CGO_CXXFLAGS="$CXXFLAGS" export CGO_CXXFLAGS="$CXXFLAGS"
export CGO_CPPFLAGS="$CPPFLAGS" export CGO_CPPFLAGS="$CPPFLAGS"
export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw" export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw"
make cmd/flux/.manifests.done make cmd/flux/.manifests.done
go build -ldflags "-linkmode=external -X main.VERSION=${pkgver}" -o ${_srcname} ./cmd/flux go build -ldflags "-linkmode=external -X main.VERSION=${_srcver}" -o ${_srcname} ./cmd/flux
} }
check() { check() {
cd "flux2-${pkgver}" cd "flux2-${_srcver}"
case $CARCH in case $CARCH in
aarch64) aarch64)
export ENVTEST_ARCH=arm64 export ENVTEST_ARCH=arm64
;; ;;
armv6h|armv7h) armv7h)
export ENVTEST_ARCH=arm export ENVTEST_ARCH=arm
;; ;;
esac esac
@ -48,7 +49,7 @@ check() {
} }
package() { package() {
cd "flux2-${pkgver}" cd "flux2-${_srcver}"
install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}"
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"

@ -28,6 +28,7 @@ git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1
CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }')
CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }')
# Transform pre-release to AUR compatible version format
export PKGVER=${VERSION/-/} export PKGVER=${VERSION/-/}
if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then
@ -36,10 +37,10 @@ else
export PKGREL=1 export PKGREL=1
fi fi
export SHA256SUM=$(curl -sL https://github.com/fluxcd/flux2/archive/v$PKGVER.tar.gz | sha256sum | awk '{ print $1 }') export SHA256SUM=$(curl -sL https://github.com/fluxcd/flux2/archive/v${VERSION}.tar.gz | sha256sum | awk '{ print $1 }')
envsubst '$PKGVER $PKGREL $SHA256SUM' < .SRCINFO.template > $GITDIR/.SRCINFO envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM' < .SRCINFO.template > $GITDIR/.SRCINFO
envsubst '$PKGVER $PKGREL $SHA256SUM' < PKGBUILD.template > $GITDIR/PKGBUILD envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM' < PKGBUILD.template > $GITDIR/PKGBUILD
cd $GITDIR cd $GITDIR
git config user.name "fluxcdbot" git config user.name "fluxcdbot"

@ -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

@ -4,21 +4,21 @@
pkgname=flux-scm pkgname=flux-scm
pkgver=${PKGVER} pkgver=${PKGVER}
pkgrel=${PKGREL} pkgrel=${PKGREL}
_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")
depends=("glibc") depends=("glibc")
makedepends=('go>=1.17', 'kustomize>=3.0', 'git') makedepends=('go>=1.20', 'kustomize>=5.0', 'git')
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=( source=(
"git+https://github.com/fluxcd/flux2.git" "git+https://github.com/fluxcd/flux2.git"
) )
md5sums=('SKIP') md5sums=('SKIP')
_srcname=flux
pkgver() { pkgver() {
cd "flux2" cd "flux2"
@ -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

@ -28,6 +28,7 @@ git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1
CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }')
CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }')
# Transform pre-release to AUR compatible version format
export PKGVER=${VERSION/-/} export PKGVER=${VERSION/-/}
if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then

@ -0,0 +1,16 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
labels: ["area/ci", "dependencies"]
groups:
# Group all updates together, so that they are all applied in a single PR.
# Grouped updates are currently in beta and is subject to change.
# xref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups
ci:
patterns:
- "*"
schedule:
# By default, this will be on a monday.
interval: "weekly"

@ -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

@ -0,0 +1,58 @@
# Configuration file to declaratively configure labels
# Ref: https://github.com/EndBug/label-sync#Config-files
- name: area/bootstrap
description: Bootstrap related issues and pull requests
color: '#86efc9'
- name: area/install
description: Install and uninstall related issues and pull requests
color: '#86efc9'
- name: area/diff
description: Diff related issues and pull requests
color: '#BA4192'
- name: area/bucket
description: Bucket related issues and pull requests
color: '#00b140'
- name: area/git
description: Git related issues and pull requests
color: '#863faf'
- name: area/oci
description: OCI related issues and pull requests
color: '#c739ff'
- name: area/kustomization
description: Kustomization related issues and pull requests
color: '#00e54d'
- name: area/helm
description: Helm related issues and pull requests
color: '#1673b6'
- name: area/image-automation
description: Automated image updates related issues and pull requests
color: '#c5def5'
- name: area/monitoring
description: Monitoring related issues and pull requests
color: '#dd75ae'
- name: area/multi-tenancy
description: Multi-tenancy related issues and pull requests
color: '#72CDBD'
- name: area/notification
description: Notification API related issues and pull requests
color: '#434ec1'
- name: area/source
description: Source API related issues and pull requests
color: '#863faf'
- name: area/rfc
description: Feature request proposals in the RFC format
color: '#D621C3'
aliases: ['area/RFC']
- name: backport:release/v2.0.x
description: To be backported to release/v2.0.x
color: '#ffd700'
- name: backport:release/v2.1.x
description: To be backported to release/v2.1.x
color: '#ffd700'
- name: backport:release/v2.2.x
description: To be backported to release/v2.2.x
color: '#ffd700'
- name: backport:release/v2.3.x
description: To be backported to release/v2.3.x
color: '#ffd700'

@ -1,24 +1,34 @@
# Flux ARM64 GitHub runners # Flux ARM64 GitHub runners
The Flux ARM64 end-to-end tests run on Equinix instances provisioned with Docker and GitHub self-hosted runners. The Flux ARM64 end-to-end tests run on Equinix Metal instances provisioned with Docker and GitHub self-hosted runners.
## Current instances ## Current instances
| Runner | Instance | Region | | Repository | Runner | Instance | Location |
|---------------|---------------------|--------| |-----------------------------|------------------|----------------|---------------|
| equinix-arm-1 | flux-equinix-arm-01 | AMS1 | | flux2 | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| equinix-arm-2 | flux-equinix-arm-01 | AMS1 | | flux2 | equinix-arm-dc-2 | flux-arm-dc-01 | Washington DC |
| equinix-arm-3 | flux-equinix-arm-01 | AMS1 | | flux2 | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| equinix-arm-4 | flux-equinix-arm-02 | DFW2 | | flux2 | equinix-arm-da-2 | flux-arm-da-01 | Dallas |
| equinix-arm-5 | flux-equinix-arm-02 | DFW2 | | flux-benchmark | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| equinix-arm-6 | flux-equinix-arm-02 | DFW2 | | flux-benchmark | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| source-controller | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| source-controller | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| image-automation-controller | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| image-automation-controller | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
Instance spec:
- Ampere Altra Q80-30 80-core processor @ 2.8GHz
- 2 x 960GB NVME
- 256GB RAM
- 2 x 25Gbps
## Instance setup ## Instance setup
In order to add a new runner to the GitHub Actions pool, In order to add a new runner to the GitHub Actions pool,
first create a server on Equinix with the following configuration: first create a server on Equinix with the following configuration:
- Type: c2.large.arm - Type: `c3.large.arm64`
- OS: Ubuntu 20.04 - OS: `Ubuntu 22.04 LTS`
### Install prerequisites ### Install prerequisites
@ -54,14 +64,14 @@ sudo ./prereq.sh
- Retrieve the GitHub runner token from the repository [settings page](https://github.com/fluxcd/flux2/settings/actions/runners/new?arch=arm64&os=linux) - Retrieve the GitHub runner token from the repository [settings page](https://github.com/fluxcd/flux2/settings/actions/runners/new?arch=arm64&os=linux)
- Create 3 directories `runner1`, `runner2`, `runner3` - Create two directories `flux2-01`, `flux2-02`
- In each dir run: - In each dir run:
```shell ```shell
curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/runner-setup.sh > runner-setup.sh \ curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/runner-setup.sh > runner-setup.sh \
&& chmod +x ./runner-setup.sh && chmod +x ./runner-setup.sh
./runner-setup.sh equinix-arm-<NUMBER> <TOKEN> ./runner-setup.sh equinix-arm-<NUMBER> <TOKEN> <REPO>
``` ```
- Reboot the instance - Reboot the instance

@ -18,12 +18,12 @@
set -eu set -eu
KIND_VERSION=0.11.1 KIND_VERSION=0.22.0
KUBECTL_VERSION=1.21.2 KUBECTL_VERSION=1.29.0
KUSTOMIZE_VERSION=4.1.3 KUSTOMIZE_VERSION=5.3.0
HELM_VERSION=3.7.2 HELM_VERSION=3.14.1
GITHUB_RUNNER_VERSION=2.285.1 GITHUB_RUNNER_VERSION=2.313.0
PACKAGES="apt-transport-https ca-certificates software-properties-common build-essential libssl-dev gnupg lsb-release jq" PACKAGES="apt-transport-https ca-certificates software-properties-common build-essential libssl-dev gnupg lsb-release jq pkg-config"
# install prerequisites # install prerequisites
apt-get update \ apt-get update \
@ -31,6 +31,10 @@ apt-get update \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# fix Kubernetes DNS resolution
rm /etc/resolv.conf
cat "/run/systemd/resolve/stub-resolv.conf" | sed '/search/d' > /etc/resolv.conf
# install docker # install docker
curl -fsSL https://get.docker.com -o get-docker.sh \ curl -fsSL https://get.docker.com -o get-docker.sh \
&& chmod +x get-docker.sh && chmod +x get-docker.sh

@ -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.285.1 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 \

@ -0,0 +1,50 @@
# Flux GitHub Workflows
## End-to-end Testing
The e2e workflows run a series of tests to ensure that the Flux CLI and
the GitOps Toolkit controllers work well all together.
The tests are written in Go, Bash, Make and Terraform.
| Workflow | Jobs | Runner | Role |
|--------------------|----------------------|----------------|-----------------------------------------------|
| e2e.yaml | e2e-amd64-kubernetes | GitHub Ubuntu | integration testing with Kubernetes Kind<br/> |
| e2e-arm64.yaml | e2e-arm64-kubernetes | Equinix Ubuntu | integration testing with Kubernetes Kind<br/> |
| e2e-bootstrap.yaml | e2e-boostrap-github | GitHub Ubuntu | integration testing with GitHub API<br/> |
| e2e-azure.yaml | e2e-amd64-aks | GitHub Ubuntu | integration testing with Azure API<br/> |
| scan.yaml | scan-fossa | GitHub Ubuntu | license scanning<br/> |
| scan.yaml | scan-snyk | GitHub Ubuntu | vulnerability scanning<br/> |
| scan.yaml | scan-codeql | GitHub Ubuntu | vulnerability scanning<br/> |
## Components Update
The components update workflow scans the GitOps Toolkit controller repositories for new releases,
amd when it finds a new controller version, the workflow performs the following steps:
- Updates the controller API package version in `go.mod`.
- Patches the controller CRDs version in the `manifests/crds` overlay.
- Patches the controller Deployment version in `manifests/bases` overlay.
- Opens a Pull Request against the `main` branch.
- Triggers the e2e test suite to run for the opened PR.
| Workflow | Jobs | Runner | Role |
|-------------|-------------------|---------------|-----------------------------------------------------|
| update.yaml | update-components | GitHub Ubuntu | update the GitOps Toolkit APIs and controllers<br/> |
## Release
The release workflow is triggered by a semver Git tag and performs the following steps:
- Generates the Flux install manifests (YAML).
- Generates the OpenAPI validation schemas for the GitOps Toolkit CRDs (JSON).
- Generates a Software Bill of Materials (SPDX JSON).
- Builds the Flux CLI binaries and the multi-arch container images.
- Pushes the container images to GitHub Container Registry and DockerHub.
- Signs the sbom, the binaries checksum and the container images with Cosign and GitHub OIDC.
- Uploads the sbom, binaries, checksums and install manifests to GitHub Releases.
- Pushes the install manifests as OCI artifacts to GitHub Container Registry and DockerHub.
- Signs the OCI artifacts with Cosign and GitHub OIDC.
| Workflow | Jobs | Runner | Role |
|--------------|------------------------|---------------|------------------------------------------------------|
| release.yaml | release-flux-cli | GitHub Ubuntu | build, push and sign the CLI release artifacts<br/> |
| release.yaml | release-flux-manifests | GitHub Ubuntu | build, push and sign the Flux install manifests<br/> |

@ -0,0 +1,29 @@
name: test-gh-action
on:
pull_request:
paths:
- 'action/**'
push:
paths:
- 'action/**'
branches:
- 'main'
- 'release/**'
permissions: read-all
jobs:
actions:
strategy:
fail-fast: false
matrix:
version: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.version }}
name: action on ${{ matrix.version }}
steps:
- name: Checkout
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup flux
uses: ./action

@ -0,0 +1,31 @@
name: backport
on:
pull_request_target:
types: [closed, labeled]
jobs:
pull-request:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name))
steps:
- name: Checkout
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@bd410d37cdcae80be6d969823ff5a225fe5c833f # v3.0.2
# xref: https://github.com/korthout/backport-action#inputs
with:
# Use token to allow workflows to be triggered for the created PR
github_token: ${{ secrets.BOT_GITHUB_TOKEN }}
# Match labels with a pattern `backport:<target-branch>`
label_pattern: '^backport:([^ ]+)$'
# A bit shorter pull-request title than the default
pull_title: '[${target_branch}] ${pull_title}'
# Simpler PR description than default
pull_description: |-
Automated backport to `${target_branch}`, triggered by a label in #${pull_number}.

@ -0,0 +1,267 @@
name: conformance
on:
workflow_dispatch:
push:
branches: [ 'main', 'update-components', 'release/**', 'conform*' ]
permissions:
contents: read
env:
GO_VERSION: 1.22.x
jobs:
conform-kubernetes:
# Hosted on Equinix
# Docs: https://github.com/fluxcd/flux2/tree/main/.github/runners
runs-on: [self-hosted, Linux, ARM64, equinix]
strategy:
matrix:
# Keep this list up-to-date with https://endoflife.date/kubernetes
# Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml
KUBERNETES_VERSION: [ 1.28.9, 1.29.4, 1.30.0 ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Prepare
id: prep
run: |
ID=${GITHUB_SHA:0:7}-${{ matrix.KUBERNETES_VERSION }}-$(date +%s)
echo "CLUSTER=arm64-${ID}" >> $GITHUB_OUTPUT
- name: Build
run: |
make build
- name: Setup Kubernetes Kind
run: |
kind create cluster \
--wait 5m \
--name ${{ steps.prep.outputs.CLUSTER }} \
--kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }} \
--image=ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64
- name: Run e2e tests
run: TEST_KUBECONFIG=/tmp/${{ steps.prep.outputs.CLUSTER }} make e2e
- name: Run multi-tenancy tests
env:
KUBECONFIG: /tmp/${{ steps.prep.outputs.CLUSTER }}
run: |
./bin/flux install
./bin/flux create source git flux-system \
--interval=15m \
--url=https://github.com/fluxcd/flux2-multi-tenancy \
--branch=main \
--ignore-paths="./clusters/**/flux-system/"
./bin/flux create kustomization flux-system \
--interval=15m \
--source=flux-system \
--path=./clusters/staging
kubectl -n flux-system wait kustomization/tenants --for=condition=ready --timeout=5m
kubectl -n apps wait kustomization/dev-team --for=condition=ready --timeout=1m
kubectl -n apps wait helmrelease/podinfo --for=condition=ready --timeout=1m
- name: Debug failure
if: failure()
env:
KUBECONFIG: /tmp/${{ steps.prep.outputs.CLUSTER }}
run: |
kubectl -n flux-system get all
kubectl -n flux-system describe po
kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller
- name: Cleanup
if: always()
run: |
kind delete cluster --name ${{ steps.prep.outputs.CLUSTER }}
rm /tmp/${{ steps.prep.outputs.CLUSTER }}
conform-k3s:
runs-on: ubuntu-latest
strategy:
matrix:
# Keep this list up-to-date with https://endoflife.date/kubernetes
# Available versions can be found with "replicated cluster versions"
K3S_VERSION: [ 1.28.7, 1.29.2 ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Prepare
id: prep
run: |
ID=${GITHUB_SHA:0:7}-${{ matrix.K3S_VERSION }}-$(date +%s)
PSEUDO_RAND_SUFFIX=$(echo "${ID}" | shasum | awk '{print $1}')
echo "cluster=flux2-k3s-${PSEUDO_RAND_SUFFIX}" >> $GITHUB_OUTPUT
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@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: "k3s"
kubernetes-version: ${{ matrix.K3S_VERSION }}
ttl: 20m
cluster-name: "${{ steps.prep.outputs.cluster }}"
kubeconfig-path: ${{ steps.prep.outputs.kubeconfig-path }}
export-kubeconfig: true
- name: Run e2e tests
run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e
- name: Run flux bootstrap
run: |
./bin/flux bootstrap git --manifests ./manifests/install/ \
--components-extra=image-reflector-controller,image-automation-controller \
--url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \
--branch=main \
--path=clusters/k3s \
--token-auth
env:
GIT_PASSWORD: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Run flux check
run: |
./bin/flux check
- name: Run flux reconcile
run: |
./bin/flux reconcile ks flux-system --with-source
./bin/flux get all
./bin/flux events
- name: Collect reconcile logs
if: ${{ always() }}
continue-on-error: true
run: |
kubectl -n flux-system get all
kubectl -n flux-system describe pods
kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller
kubectl -n flux-system logs deploy/notification-controller
- name: Delete flux
run: |
./bin/flux uninstall -s --keep-namespace
kubectl delete ns flux-system --wait
- name: Delete cluster
if: ${{ always() }}
uses: replicatedhq/replicated-actions/remove-cluster@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 }}
conform-openshift:
runs-on: ubuntu-latest
strategy:
matrix:
# Keep this list up-to-date with https://endoflife.date/red-hat-openshift
OPENSHIFT_VERSION: [ 4.15.0-okd ]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Prepare
id: prep
run: |
ID=${GITHUB_SHA:0:7}-${{ matrix.OPENSHIFT_VERSION }}-$(date +%s)
PSEUDO_RAND_SUFFIX=$(echo "${ID}" | shasum | awk '{print $1}')
echo "cluster=flux2-openshift-${PSEUDO_RAND_SUFFIX}" >> $GITHUB_OUTPUT
KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml"
echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@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 }}

@ -1,37 +0,0 @@
name: e2e-arm64
on:
workflow_dispatch:
push:
branches: [ main, update-components, equinix-runners ]
jobs:
test:
# Hosted on Equinix
# Docs: https://github.com/fluxcd/flux2/tree/main/.github/runners
runs-on: [self-hosted, Linux, ARM64, equinix]
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.17.x
- name: Prepare
id: prep
run: |
echo ::set-output name=CLUSTER::arm64-${GITHUB_SHA:0:7}-$(date +%s)
echo ::set-output name=CONTEXT::kind-arm64-${GITHUB_SHA:0:7}-$(date +%s)
- name: Build
run: |
make build
- name: Setup Kubernetes Kind
run: |
kind create cluster --name ${{ steps.prep.outputs.CLUSTER }} --kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }}
- name: Run e2e tests
run: TEST_KUBECONFIG=/tmp/${{ steps.prep.outputs.CLUSTER }} make e2e
- name: Cleanup
if: always()
run: |
kind delete cluster --name ${{ steps.prep.outputs.CLUSTER }}
rm /tmp/${{ steps.prep.outputs.CLUSTER }}

@ -3,64 +3,89 @@ name: e2e-azure
on: on:
workflow_dispatch: workflow_dispatch:
schedule: schedule:
- cron: '0 6 * * *' - cron: '0 6 * * *'
push: push:
branches: [ azure* ] branches:
- main
paths:
- 'tests/**'
- '.github/workflows/e2e-azure.yaml'
pull_request:
branches:
- main
paths:
- 'tests/**'
- '.github/workflows/e2e-azure.yaml'
permissions:
contents: read
jobs: jobs:
e2e: e2e-aks:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./tests/integration
# This job is currently disabled. Remove the false check when Azure subscription is enabled.
if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps: steps:
- name: Checkout - name: CheckoutD
uses: actions/checkout@v2 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Restore Go cache
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go1.17-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go1.17-
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.17.x go-version: 1.22.x
- name: Install libgit2 cache-dependency-path: tests/integration/go.sum
run: |
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 648ACFD622F3D138
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 0E98404D386FA1D9
echo "deb http://deb.debian.org/debian unstable main" | sudo tee -a /etc/apt/sources.list
echo "deb-src http://deb.debian.org/debian unstable main" | sudo tee -a /etc/apt/sources.list
sudo apt-get update
sudo apt-get install -y --allow-downgrades libgit2-dev/unstable zlib1g-dev/unstable libssh2-1-dev/unstable libpcre3-dev/unstable
- name: Setup Flux CLI - name: Setup Flux CLI
run: | run: make build
make build working-directory: ./
mkdir -p $HOME/.local/bin
mv ./bin/flux $HOME/.local/bin
- name: Setup SOPS - name: Setup SOPS
run: | run: |
wget https://github.com/mozilla/sops/releases/download/v3.7.1/sops-v3.7.1.linux
chmod +x sops-v3.7.1.linux
mkdir -p $HOME/.local/bin mkdir -p $HOME/.local/bin
mv sops-v3.7.1.linux $HOME/.local/bin/sops wget -O $HOME/.local/bin/sops https://github.com/mozilla/sops/releases/download/v$SOPS_VER/sops-v$SOPS_VER.linux
- name: Setup Terraform chmod +x $HOME/.local/bin/sops
uses: hashicorp/setup-terraform@v1 env:
SOPS_VER: 3.7.1
- name: Authenticate to Azure
uses: Azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v1.4.6
with: with:
terraform_version: 1.0.7 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 }}"}'
terraform_wrapper: false - name: Set dynamic variables in .env
- name: Setup Azure CLI
run: | run: |
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash cat > .env <<EOF
export TF_VAR_tags='{ "environment"="github", "ci"="true", "repo"="flux2", "createdat"="$(date -u +x%Y-%m-%d_%Hh%Mm%Ss)" }'
EOF
- name: Print .env for dynamic tag value reference
run: cat .env
- name: Run Azure e2e tests - name: Run Azure e2e tests
env: env:
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
TF_VAR_location: ${{ vars.TF_VAR_azure_location }}
GITREPO_SSH_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_CONTENTS }}
GITREPO_SSH_PUB_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_PUB_CONTENTS }}
run: | run: |
echo $HOME source .env
echo $PATH mkdir -p ./build/ssh
ls $HOME/.local/bin touch ./build/ssh/key
az login --service-principal -u ${ARM_CLIENT_ID} -p ${ARM_CLIENT_SECRET} -t ${ARM_TENANT_ID} echo $GITREPO_SSH_CONTENTS | base64 -d > build/ssh/key
cd ./tests/azure export GITREPO_SSH_PATH=build/ssh/key
go test -v -coverprofile cover.out -timeout 60m . touch ./build/ssh/key.pub
echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
make test-azure
- name: Ensure resource cleanup
if: ${{ always() }}
env:
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
TF_VAR_location: ${{ vars.TF_VAR_azure_location }}
run: source .env && make destroy-azure

@ -1,40 +1,45 @@
name: bootstrap name: e2e-bootstrap
on: on:
workflow_dispatch:
push: push:
branches: [ main ] branches: [ 'main', 'release/**' ]
pull_request: pull_request:
branches: [ main ] branches: [ 'main', 'release/**' ]
paths-ignore: [ 'docs/**', 'rfcs/**' ]
permissions:
contents: read
jobs: jobs:
github: e2e-boostrap-github:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 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@v2 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Restore Go cache
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go1.17-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go1.17-
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.17.x go-version: 1.22.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Kubernetes - name: Setup Kubernetes
uses: engineerd/setup-kind@v0.5.0 uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0
with: with:
version: v0.11.1 version: v0.22.0
image: kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6 cluster_name: kind
# The versions below should target the newest Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes
node_image: ghcr.io/fluxcd/kindest/node:v1.30.0-amd64
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: |
@ -43,21 +48,27 @@ jobs:
COMMIT_SHA=$(git rev-parse HEAD) COMMIT_SHA=$(git rev-parse HEAD)
PSEUDO_RAND_SUFFIX=$(echo "${BRANCH_NAME}-${COMMIT_SHA}" | shasum | awk '{print $1}') PSEUDO_RAND_SUFFIX=$(echo "${BRANCH_NAME}-${COMMIT_SHA}" | shasum | awk '{print $1}')
TEST_REPO_NAME="${REPOSITORY_NAME}-${PSEUDO_RAND_SUFFIX}" TEST_REPO_NAME="${REPOSITORY_NAME}-${PSEUDO_RAND_SUFFIX}"
echo "::set-output name=test_repo_name::$TEST_REPO_NAME" 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 \
@ -67,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 \
@ -80,56 +91,33 @@ jobs:
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: libgit2
run: |
/tmp/flux create source git test-libgit2 \
--url=ssh://git@github.com/fluxcd-testing/${{ steps.vars.outputs.test_repo_name }} \
--git-implementation=libgit2 \
--secret-ref=flux-system \
--branch=main
- 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 reconcile image update flux-system ./bin/flux reconcile image update flux-system
/tmp/flux get images all ./bin/flux get images all
kubectl -n flux-system get -o yaml ImageUpdateAutomation flux-system | \
retries=10 yq '.status.lastPushCommit | length > 1' | grep 'true'
count=0
ok=false
until ${ok}; do
/tmp/flux get image update flux-system | grep 'commit' && ok=true || ok=false
count=$(($count + 1))
if [[ ${count} -eq ${retries} ]]; then
echo "No more retries left"
exit 1
fi
sleep 6
/tmp/flux reconcile image update flux-system
done
env: 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

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

@ -1,40 +1,52 @@
name: e2e name: e2e
on: on:
workflow_dispatch:
push: push:
branches: [ main, e2e* ] branches: [ 'main', 'release/**' ]
pull_request: pull_request:
branches: [ main ] branches: [ 'main', 'release/**' ]
paths-ignore: [ 'docs/**', 'rfcs/**' ]
permissions:
contents: read
jobs: jobs:
kind: e2e-amd64-kubernetes:
runs-on: ubuntu-latest runs-on:
group: "Default Larger Runners"
labels: ubuntu-latest-16-cores
services:
registry:
image: registry:2
ports:
- 5000:5000
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Restore Go cache
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go1.17-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go1.17-
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.17.x go-version: 1.22.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Kubernetes - name: Setup Kubernetes
uses: engineerd/setup-kind@v0.5.0 uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0
with: with:
version: v0.11.1 version: v0.22.0
image: kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729 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 oldest supported Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes
node_image: ghcr.io/fluxcd/kindest/node:v1.28.9-amd64
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.20/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
run: make test run: make test
- name: Run e2e tests - name: Run e2e tests
@ -47,51 +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=">=3.2.3" --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=">=3.2.3" \ --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 create source git libgit2 semver
run: |
/tmp/flux create source git podinfo-libgit2 \
--url https://github.com/stefanprodan/podinfo \
--tag-semver=">=3.2.3" \
--git-implementation=libgit2
/tmp/flux delete source git podinfo-libgit2 --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 \
@ -101,106 +105,145 @@ 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=">4.0.0 <5.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
run: |
./bin/flux push artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \
--path="./manifests" \
--source="${{ github.repositoryUrl }}" \
--revision="${{ github.ref }}@sha1:${{ github.sha }}"
./bin/flux tag artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \
--tag latest
./bin/flux list artifacts oci://localhost:5000/fluxcd/flux
- name: flux oci repositories
run: |
./bin/flux create source oci podinfo-oci \
--url oci://ghcr.io/stefanprodan/manifests/podinfo \
--tag-semver 6.3.x \
--interval 10m
./bin/flux create kustomization podinfo-oci \
--source=OCIRepository/podinfo-oci \
--path="./" \
--prune=true \
--interval=5m \
--target-namespace=default \
--wait=true \
--health-check-timeout=3m
./bin/flux reconcile source oci podinfo-oci
./bin/flux suspend source oci podinfo-oci
./bin/flux get sources oci
./bin/flux resume source oci podinfo-oci
./bin/flux export source oci podinfo-oci
./bin/flux delete ks podinfo-oci --silent
./bin/flux delete source oci podinfo-oci --silent
- 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="5.0.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/" \
--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/infrastructure --for=condition=ready --timeout=5m kubectl -n flux-system wait kustomization/infra-controllers --for=condition=ready --timeout=5m
kubectl -n flux-system wait kustomization/apps --for=condition=ready --timeout=5m kubectl -n flux-system wait kustomization/apps --for=condition=ready --timeout=5m
kubectl -n nginx wait helmrelease/nginx --for=condition=ready --timeout=5m
kubectl -n redis wait helmrelease/redis --for=condition=ready --timeout=5m
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: |

@ -0,0 +1,39 @@
name: ossf
on:
workflow_dispatch:
push:
branches:
- main
schedule:
# Weekly on Saturdays.
- cron: '30 1 * * 6'
permissions: read-all
jobs:
scorecard:
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
actions: read
contents: read
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Run analysis
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
with:
results_file: results.sarif
results_format: sarif
repo_token: ${{ secrets.GITHUB_TOKEN }}
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
with:
sarif_file: results.sarif

@ -1,21 +0,0 @@
name: rebase
on:
pull_request:
types: [ opened ]
issue_comment:
types: [ created ]
jobs:
rebase:
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') && (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Automatic Rebase
uses: cirrus-actions/rebase@1.3.1
env:
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}

@ -5,41 +5,48 @@ on:
tags: [ 'v*' ] tags: [ 'v*' ]
permissions: permissions:
contents: write # needed to write releases contents: read
id-token: write # needed for keyless signing
packages: write # needed for ghcr access
jobs: jobs:
goreleaser: release-flux-cli:
outputs:
hashes: ${{ steps.slsa.outputs.hashes }}
image_url: ${{ steps.slsa.outputs.image_url }}
image_digest: ${{ steps.slsa.outputs.image_digest }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write # needed to write releases
id-token: write # needed for keyless signing
packages: write # needed for ghcr access
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Unshallow - name: Unshallow
run: git fetch --prune --unshallow run: git fetch --prune --unshallow
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.17.x go-version: 1.22.x
cache: false
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v1 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@v1 uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0
- name: Setup Syft - name: Setup Syft
uses: anchore/sbom-action/download-syft@v0 uses: anchore/sbom-action/download-syft@e8d2a6937ecead383dfe75190d104edd1f9c5751 # v0.16.0
- name: Setup Cosign - name: Setup Cosign
uses: sigstore/cosign-installer@main 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@v1 uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.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@v1 uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
with: with:
username: fluxcdbot username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
@ -51,10 +58,8 @@ jobs:
- name: Build CRDs - name: Build CRDs
run: | run: |
kustomize build manifests/crds > all-crds.yaml kustomize build manifests/crds > all-crds.yaml
# Pinned to commit before https://github.com/fluxcd/pkg/pull/189 due to
# introduction faulty behavior.
- name: Generate OpenAPI JSON schemas from CRDs - name: Generate OpenAPI JSON schemas from CRDs
uses: fluxcd/pkg//actions/crdjsonschema@49e26aa2ee9e734c3233c560253fd9542afe18ae uses: fluxcd/pkg/actions/crdjsonschema@main
with: with:
crd: all-crds.yaml crd: all-crds.yaml
output: schemas output: schemas
@ -73,11 +78,134 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v1 id: run-goreleaser
uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.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
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
AUR_BOT_SSH_PRIVATE_KEY: ${{ secrets.AUR_BOT_SSH_PRIVATE_KEY }} AUR_BOT_SSH_PRIVATE_KEY: ${{ secrets.AUR_BOT_SSH_PRIVATE_KEY }}
- name: Generate SLSA metadata
id: slsa
env:
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
run: |
set -euo pipefail
hashes=$(echo -E $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0)
echo "hashes=$hashes" >> $GITHUB_OUTPUT
image_url=fluxcd/flux-cli:$GITHUB_REF_NAME
echo "image_url=$image_url" >> $GITHUB_OUTPUT
image_digest=$(docker buildx imagetools inspect ${image_url} --format '{{json .}}' | jq -r .manifest.digest)
echo "image_digest=$image_digest" >> $GITHUB_OUTPUT
release-flux-manifests:
runs-on: ubuntu-latest
needs: release-flux-cli
permissions:
id-token: write
packages: write
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@main
- name: Setup Flux CLI
uses: ./action/
- name: Prepare
id: prep
run: |
VERSION=$(flux version --client | awk '{ print $NF }')
echo "version=${VERSION}" >> $GITHUB_OUTPUT
- name: Login to GHCR
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GHCR_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
with:
username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
- name: Push manifests to GHCR
run: |
mkdir -p ./ghcr.io/flux-system
flux install --registry=ghcr.io/fluxcd \
--components-extra=image-reflector-controller,image-automation-controller \
--export > ./ghcr.io/flux-system/gotk-components.yaml
cd ./ghcr.io && flux push artifact \
oci://ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \
--path="./flux-system" \
--source=${{ github.repositoryUrl }} \
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
- name: Push manifests to DockerHub
run: |
mkdir -p ./docker.io/flux-system
flux install --registry=docker.io/fluxcd \
--components-extra=image-reflector-controller,image-automation-controller \
--export > ./docker.io/flux-system/gotk-components.yaml
cd ./docker.io && flux push artifact \
oci://docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \
--path="./flux-system" \
--source=${{ github.repositoryUrl }} \
--revision="${{ github.ref_name }}@sha1:${{ github.sha }}"
- uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
- name: Sign manifests
env:
COSIGN_EXPERIMENTAL: 1
run: |
cosign sign --yes ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }}
cosign sign --yes docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }}
- name: Tag manifests
run: |
flux tag artifact oci://ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \
--tag latest
flux tag artifact oci://docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \
--tag latest
release-provenance:
needs: [release-flux-cli]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
contents: write # for uploading attestations to GitHub releases.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
provenance-name: "provenance.intoto.jsonl"
base64-subjects: "${{ needs.release-flux-cli.outputs.hashes }}"
upload-assets: true
dockerhub-provenance:
needs: [release-flux-cli]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with:
image: ${{ needs.release-flux-cli.outputs.image_url }}
digest: ${{ needs.release-flux-cli.outputs.image_digest }}
registry-username: fluxcdbot
secrets:
registry-password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
ghcr-provenance:
needs: [release-flux-cli]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations.
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with:
image: ghcr.io/${{ needs.release-flux-cli.outputs.image_url }}
digest: ${{ needs.release-flux-cli.outputs.image_digest }}
registry-username: fluxcdbot
secrets:
registry-password: ${{ secrets.GHCR_TOKEN }}

@ -1,60 +1,86 @@
name: Scan name: scan
on: on:
workflow_dispatch:
push: push:
branches: [ main ] branches: [ 'main', 'release/**' ]
pull_request: pull_request:
branches: [ main ] branches: [ 'main', 'release/**' ]
schedule: schedule:
- cron: '18 10 * * 3' - cron: '18 10 * * 3'
permissions:
contents: read
jobs: jobs:
fossa: scan-fossa:
name: FOSSA
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Run FOSSA scan and upload build data - name: Run FOSSA scan and upload build data
uses: fossa-contrib/fossa-action@v1 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
github-token: ${{ github.token }} github-token: ${{ github.token }}
snyk: scan-snyk:
name: Snyk
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository permissions:
security-events: write
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@v2 - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg//actions/kustomize@main uses: fluxcd/pkg/actions/kustomize@main
- name: Build manifests - name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version-file: 'go.mod'
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Download modules and build manifests
run: | run: |
make tidy
make cmd/flux/.manifests.done make cmd/flux/.manifests.done
- name: Run Snyk to check for vulnerabilities - uses: snyk/actions/setup@b98d498629f1c368650224d6d212bf7dfa89e4bf
uses: snyk/actions/golang@master - name: Run Snyk to check for vulnerabilities
continue-on-error: true continue-on-error: true
run: |
snyk test --all-projects --sarif-file-output=snyk.sarif
env: env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --sarif-file-output=snyk.sarif
- name: Upload result to GitHub Code Scanning - name: Upload result to GitHub Code Scanning
uses: github/codeql-action/upload-sarif@v1 continue-on-error: true
uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
with: with:
sarif_file: snyk.sarif sarif_file: snyk.sarif
codeql: scan-codeql:
name: CodeQL
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
security-events: write
if: github.actor != 'dependabot[bot]'
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v2 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup Go
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version-file: 'go.mod'
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v1 uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
with: with:
languages: go languages: go
# xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# xref: https://codeql.github.com/codeql-query-help/go/
queries: security-and-quality
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v1 uses: github/codeql-action/autobuild@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1 uses: github/codeql-action/analyze@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8

@ -0,0 +1,28 @@
name: sync-labels
on:
workflow_dispatch:
push:
branches:
- main
paths:
- .github/labels.yaml
permissions:
contents: read
jobs:
labels:
name: Run sync
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
with:
# Configuration file
config-file: |
https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml
.github/labels.yaml
# Strictly declarative
delete-other-labels: true

@ -1,4 +1,4 @@
name: Update Components name: update
on: on:
workflow_dispatch: workflow_dispatch:
@ -7,20 +7,29 @@ on:
push: push:
branches: [main] branches: [main]
permissions:
contents: read
jobs: jobs:
update-components: update-components:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v2 uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v2 uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with: with:
go-version: 1.17.x go-version: 1.22.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Update component versions - name: Update component versions
id: update id: update
run: | run: |
PR_BODY="" PR_BODY=$(mktemp)
bump_version() { bump_version() {
local LATEST_VERSION=$(curl -s https://api.github.com/repos/fluxcd/$1/releases | jq -r 'sort_by(.published_at) | .[-1] | .tag_name') local LATEST_VERSION=$(curl -s https://api.github.com/repos/fluxcd/$1/releases | jq -r 'sort_by(.published_at) | .[-1] | .tag_name')
@ -42,13 +51,13 @@ jobs:
if [[ "${MOD_VERSION}" != "${LATEST_VERSION}" ]]; then if [[ "${MOD_VERSION}" != "${LATEST_VERSION}" ]]; then
go mod edit -require="github.com/fluxcd/$1/api@${LATEST_VERSION}" go mod edit -require="github.com/fluxcd/$1/api@${LATEST_VERSION}"
rm go.sum make tidy
go mod tidy
changed=true changed=true
fi fi
if [[ "$changed" == true ]]; then if [[ "$changed" == true ]]; then
PR_BODY="$PR_BODY- $1 to ${LATEST_VERSION}%0A https://github.com/fluxcd/$1/blob/${LATEST_VERSION}/CHANGELOG.md%0A" echo "- $1 to ${LATEST_VERSION}" >> $PR_BODY
echo " https://github.com/fluxcd/$1/blob/${LATEST_VERSION}/CHANGELOG.md" >> $PR_BODY
fi fi
} }
@ -65,12 +74,17 @@ jobs:
git diff git diff
# export PR_BODY for PR and commit # export PR_BODY for PR and commit
echo "::set-output name=pr_body::$PR_BODY" # NB: this may look strange but it is the way it should be done to
# maintain our precious newlines
# Ref: https://github.com/github/docs/issues/21529
echo 'pr_body<<EOF' >> $GITHUB_OUTPUT
cat $PR_BODY >> $GITHUB_OUTPUT
echo 'EOF' >> $GITHUB_OUTPUT
} }
- name: Create Pull Request - name: Create Pull Request
id: cpr id: cpr
uses: peter-evans/create-pull-request@v3 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: |
@ -85,7 +99,7 @@ jobs:
body: | body: |
${{ steps.update.outputs.pr_body }} ${{ steps.update.outputs.pr_body }}
labels: | labels: |
area/build dependencies
reviewers: ${{ secrets.ASSIGNEES }} reviewers: ${{ secrets.ASSIGNEES }}
- name: Check output - name: Check output

@ -15,7 +15,7 @@ builds:
- arm64 - arm64
- arm - arm
goarm: goarm:
- 7 - "7"
- <<: *build_defaults - <<: *build_defaults
id: darwin id: darwin
goos: goos:
@ -65,6 +65,7 @@ signs:
certificate: '${artifact}.pem' certificate: '${artifact}.pem'
args: args:
- sign-blob - sign-blob
- "--yes"
- '--output-certificate=${certificate}' - '--output-certificate=${certificate}'
- '--output-signature=${signature}' - '--output-signature=${signature}'
- '${artifact}' - '${artifact}'
@ -72,24 +73,17 @@ signs:
output: true output: true
brews: brews:
- name: flux - name: flux
tap: repository:
owner: fluxcd owner: fluxcd
name: homebrew-tap name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
folder: Formula directory: Formula
homepage: "https://fluxcd.io/" homepage: "https://fluxcd.io/"
description: "Flux CLI" description: "Flux CLI"
install: | install: |
bin.install "flux" bin.install "flux"
bash_output = Utils.safe_popen_read(bin/"flux", "completion", "bash") generate_completions_from_executable(bin/"flux", "completion")
(bash_completion/"flux").write bash_output
zsh_output = Utils.safe_popen_read(bin/"flux", "completion", "zsh")
(zsh_completion/"_flux").write zsh_output
fish_output = Utils.safe_popen_read(bin/"flux", "completion", "fish")
(fish_completion/"flux.fish").write fish_output
test: | test: |
system "#{bin}/flux --version" system "#{bin}/flux --version"
publishers: publishers:
@ -175,6 +169,7 @@ docker_signs:
- COSIGN_EXPERIMENTAL=1 - COSIGN_EXPERIMENTAL=1
args: args:
- sign - sign
- "--yes"
- '${artifact}' - '${artifact}'
artifacts: all artifacts: all
output: true output: true

@ -59,7 +59,7 @@ This project is composed of:
### Understanding the code ### Understanding the code
To get started with developing controllers, you might want to review To get started with developing controllers, you might want to review
[our guide](https://fluxcd.io/docs/gitops-toolkit/source-watcher/) which [our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which
walks you through writing a short and concise controller that watches out walks you through writing a short and concise controller that watches out
for source changes. for source changes.
@ -67,9 +67,10 @@ for source changes.
Prerequisites: Prerequisites:
* go >= 1.16 * go >= 1.20
* kubectl >= 1.19 * kubectl >= 1.24
* kustomize >= 4.0 * kustomize >= 5.0
* coreutils (on Mac OS)
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with: Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
@ -96,6 +97,25 @@ Then you can run the end-to-end tests with:
make e2e make e2e
``` ```
When the output of the Flux CLI changes, to automatically update the golden
files used in the test, pass `-update` flag to the test as:
```bash
make e2e TEST_ARGS="-update"
```
Since not all packages use golden files for testing, `-update` argument must be
passed only for the packages that use golden files. Use the variables
`TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the
path of the target test package:
```bash
# Unit test
make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
# e2e test
make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
```
Teardown the e2e environment with: Teardown the e2e environment with:
```bash ```bash

@ -1,23 +1,20 @@
FROM alpine:3.15 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.23.1 ARG KUBECTL_VER=1.30.0
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.15 as flux-cli FROM alpine:3.19 as flux-cli
# Create minimal nsswitch.conf file to prioritize the usage of /etc/hosts over DNS queries.
# https://github.com/gliderlabs/docker-alpine/issues/367#issuecomment-354316460
RUN [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf
RUN apk add --no-cache ca-certificates RUN apk add --no-cache ca-certificates
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/ COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/
COPY --chmod=755 flux /usr/local/bin/ COPY --chmod=755 flux /usr/local/bin/
USER 65534:65534
ENTRYPOINT [ "flux" ] ENTRYPOINT [ "flux" ]

@ -2,19 +2,7 @@ The maintainers are generally available in Slack at
https://cloud-native.slack.com in #flux (https://cloud-native.slack.com/messages/CLAJ40HV3) https://cloud-native.slack.com in #flux (https://cloud-native.slack.com/messages/CLAJ40HV3)
(obtain an invitation at https://slack.cncf.io/). (obtain an invitation at https://slack.cncf.io/).
These maintainers are shared with other Flux v2-related git The Flux2 maintainers team is identical with the core maintainers of the project
repositories under https://github.com/fluxcd, as noted in their as listed in
respective MAINTAINERS files.
For convenience, they are reflected in the GitHub team https://github.com/fluxcd/community/blob/main/CORE-MAINTAINERS
@fluxcd/flux2-maintainers -- if the list here changes, that team also
should.
In alphabetical order:
Aurel Canciu, NexHealth <aurel.canciu@nexhealth.com> (github: @relu, slack: relu)
Hidde Beydals, Weaveworks <hidde@weave.works> (github: @hiddeco, slack: hidde)
Max Jonas Werner, D2iQ <max@e13.dev> (github: @makkes, slack: max)
Philip Laine, Xenit <philip.laine@xenit.se> (github: @phillebaba, slack: phillebaba)
Stefan Prodan, Weaveworks <stefan@weave.works> (github: @stefanprodan, slack: stefanprodan)
Sunny, Weaveworks <sunny@weave.works> (github: @darkowlzz, slack: darkowlzz)

@ -1,4 +1,5 @@
VERSION?=$(shell grep 'VERSION' cmd/flux/main.go | awk '{ print $$4 }' | head -n 1 | tr -d '"') VERSION?=$(shell grep 'VERSION' cmd/flux/main.go | awk '{ print $$4 }' | head -n 1 | tr -d '"')
DEV_VERSION?=0.0.0-$(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --short HEAD)-$(shell date +%s)
EMBEDDED_MANIFESTS_TARGET=cmd/flux/.manifests.done EMBEDDED_MANIFESTS_TARGET=cmd/flux/.manifests.done
TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig
# Architecture to use envtest with # Architecture to use envtest with
@ -16,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2
all: test build all: test build
tidy: tidy:
go mod tidy go mod tidy -compat=1.22
cd tests/azure && go mod tidy cd tests/integration && go mod tidy -compat=1.22
fmt: fmt:
go fmt ./... go fmt ./...
@ -35,11 +36,13 @@ cleanup-kind:
rm $(TEST_KUBECONFIG) rm $(TEST_KUBECONFIG)
KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)" KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)"
TEST_PKG_PATH="./..."
test: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet install-envtest test: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet install-envtest
KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" go test ./... -coverprofile cover.out --tags=unit KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" go test $(TEST_PKG_PATH) -coverprofile cover.out --tags=unit $(TEST_ARGS)
E2E_TEST_PKG_PATH="./cmd/flux/..."
e2e: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet e2e: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet
TEST_KUBECONFIG=$(TEST_KUBECONFIG) go test ./cmd/flux/... -coverprofile e2e.cover.out --tags=e2e -v -failfast TEST_KUBECONFIG=$(TEST_KUBECONFIG) go test $(E2E_TEST_PKG_PATH) -coverprofile e2e.cover.out --tags=e2e -v -failfast $(TEST_ARGS)
test-with-kind: install-envtest test-with-kind: install-envtest
make setup-kind make setup-kind
@ -53,6 +56,9 @@ $(EMBEDDED_MANIFESTS_TARGET): $(call rwildcard,manifests/,*.yaml *.json)
build: $(EMBEDDED_MANIFESTS_TARGET) build: $(EMBEDDED_MANIFESTS_TARGET)
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(VERSION)" -o ./bin/flux ./cmd/flux CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(VERSION)" -o ./bin/flux ./cmd/flux
build-dev: $(EMBEDDED_MANIFESTS_TARGET)
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(DEV_VERSION)" -o ./bin/flux ./cmd/flux
.PHONY: install .PHONY: install
install: install:
CGO_ENABLED=0 go install ./cmd/flux CGO_ENABLED=0 go install ./cmd/flux

@ -1,14 +1,15 @@
# Flux version 2 # Flux version 2
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4782/badge)](https://bestpractices.coreinfrastructure.org/projects/4782)
[![e2e](https://github.com/fluxcd/flux2/workflows/e2e/badge.svg)](https://github.com/fluxcd/flux2/actions)
[![report](https://goreportcard.com/badge/github.com/fluxcd/flux2)](https://goreportcard.com/report/github.com/fluxcd/flux2)
[![license](https://img.shields.io/github/license/fluxcd/flux2.svg)](https://github.com/fluxcd/flux2/blob/main/LICENSE)
[![release](https://img.shields.io/github/release/fluxcd/flux2/all.svg)](https://github.com/fluxcd/flux2/releases) [![release](https://img.shields.io/github/release/fluxcd/flux2/all.svg)](https://github.com/fluxcd/flux2/releases)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4782/badge)](https://bestpractices.coreinfrastructure.org/projects/4782)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2/badge)](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2)
[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Ffluxcd%2Fflux2.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Ffluxcd%2Fflux2?ref=badge_shield)
[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/flux2)](https://artifacthub.io/packages/helm/fluxcd-community/flux2)
[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://fluxcd.io/flux/security/slsa-assessment)
Flux is a tool for keeping Kubernetes clusters in sync with sources of Flux is a tool for keeping Kubernetes clusters in sync with sources of
configuration (like Git repositories), and automating updates to configuration (like Git repositories and OCI artifacts),
configuration when there is new code to deploy. and automating updates to configuration when there is new code to deploy.
Flux version 2 ("v2") is built from the ground up to use Kubernetes' Flux version 2 ("v2") is built from the ground up to use Kubernetes'
API extension system, and to integrate with Prometheus and other core API extension system, and to integrate with Prometheus and other core
@ -20,18 +21,19 @@ Flux v2 is constructed with the [GitOps Toolkit](#gitops-toolkit), a
set of composable APIs and specialized tools for building Continuous set of composable APIs and specialized tools for building Continuous
Delivery on top of Kubernetes. Delivery on top of Kubernetes.
Flux is a Cloud Native Computing Foundation ([CNCF](https://www.cncf.io/)) project. Flux is a Cloud Native Computing Foundation ([CNCF](https://www.cncf.io/)) graduated project, used in
production by various [organisations](https://fluxcd.io/adopters) and [cloud providers](https://fluxcd.io/ecosystem).
## Quickstart and documentation ## Quickstart and documentation
To get started check out this [guide](https://fluxcd.io/docs/get-started/) To get started check out this [guide](https://fluxcd.io/flux/get-started/)
on how to bootstrap Flux on Kubernetes and deploy a sample application in a GitOps manner. on how to bootstrap Flux on Kubernetes and deploy a sample application in a GitOps manner.
For more comprehensive documentation, see the following guides: For more comprehensive documentation, see the following guides:
- [Ways of structuring your repositories](https://fluxcd.io/docs/guides/repository-structure/) - [Ways of structuring your repositories](https://fluxcd.io/flux/guides/repository-structure/)
- [Manage Helm Releases](https://fluxcd.io/docs/guides/helmreleases/) - [Manage Helm Releases](https://fluxcd.io/flux/guides/helmreleases/)
- [Automate image updates to Git](https://fluxcd.io/docs/guides/image-update/) - [Automate image updates to Git](https://fluxcd.io/flux/guides/image-update/)
- [Manage Kubernetes secrets with Mozilla SOPS](https://fluxcd.io/docs/guides/mozilla-sops/) - [Manage Kubernetes secrets with Flux and SOPS](https://fluxcd.io/flux/guides/mozilla-sops/)
If you need help, please refer to our **[Support page](https://fluxcd.io/support/)**. If you need help, please refer to our **[Support page](https://fluxcd.io/support/)**.
@ -42,31 +44,32 @@ runtime for Flux v2. The APIs comprise Kubernetes custom resources,
which can be created and updated by a cluster user, or by other which can be created and updated by a cluster user, or by other
automation tooling. automation tooling.
![overview](docs/_files/gitops-toolkit.png) ![overview](https://raw.githubusercontent.com/fluxcd/flux2/main/docs/diagrams/fluxcd-controllers.png)
You can use the toolkit to extend Flux, or to build your own systems You can use the toolkit to extend Flux, or to build your own systems
for continuous delivery -- see [the developer for continuous delivery -- see [the developer
guides](https://fluxcd.io/docs/gitops-toolkit/source-watcher/). guides](https://fluxcd.io/flux/gitops-toolkit/source-watcher/).
### Components ### Components
- [Source Controller](https://fluxcd.io/docs/components/source/) - [Source Controller](https://fluxcd.io/flux/components/source/)
- [GitRepository CRD](https://fluxcd.io/docs/components/source/gitrepositories/) - [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/)
- [HelmRepository CRD](https://fluxcd.io/docs/components/source/helmrepositories/) - [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/)
- [HelmChart CRD](https://fluxcd.io/docs/components/source/helmcharts/) - [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/)
- [Bucket CRD](https://fluxcd.io/docs/components/source/buckets/) - [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/)
- [Kustomize Controller](https://fluxcd.io/docs/components/kustomize/) - [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/)
- [Kustomization CRD](https://fluxcd.io/docs/components/kustomize/kustomization/) - [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/)
- [Helm Controller](https://fluxcd.io/docs/components/helm/) - [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/)
- [HelmRelease CRD](https://fluxcd.io/docs/components/helm/helmreleases/) - [Helm Controller](https://fluxcd.io/flux/components/helm/)
- [Notification Controller](https://fluxcd.io/docs/components/notification/) - [HelmRelease CRD](https://fluxcd.io/flux/components/helm/helmreleases/)
- [Provider CRD](https://fluxcd.io/docs/components/notification/provider/) - [Notification Controller](https://fluxcd.io/flux/components/notification/)
- [Alert CRD](https://fluxcd.io/docs/components/notification/alert/) - [Provider CRD](https://fluxcd.io/flux/components/notification/providers/)
- [Receiver CRD](https://fluxcd.io/docs/components/notification/receiver/) - [Alert CRD](https://fluxcd.io/flux/components/notification/alerts/)
- [Image Automation Controllers](https://fluxcd.io/docs/components/image/) - [Receiver CRD](https://fluxcd.io/flux/components/notification/receivers/)
- [ImageRepository CRD](https://fluxcd.io/docs/components/image/imagerepositories/) - [Image Automation Controllers](https://fluxcd.io/flux/components/image/)
- [ImagePolicy CRD](https://fluxcd.io/docs/components/image/imagepolicies/) - [ImageRepository CRD](https://fluxcd.io/flux/components/image/imagerepositories/)
- [ImageUpdateAutomation CRD](https://fluxcd.io/docs/components/image/imageupdateautomations/) - [ImagePolicy CRD](https://fluxcd.io/flux/components/image/imagepolicies/)
- [ImageUpdateAutomation CRD](https://fluxcd.io/flux/components/image/imageupdateautomations/)
## Community ## Community
@ -74,18 +77,19 @@ Need help or want to contribute? Please see the links below. The Flux project is
new contributors and there are a multitude of ways to get involved. new contributors and there are a multitude of ways to get involved.
- Getting Started? - Getting Started?
- Look at our [Get Started guide](https://fluxcd.io/docs/get-started/) and give us feedback - Look at our [Get Started guide](https://fluxcd.io/flux/get-started/) and give us feedback
- Need help? - Need help?
- First: Ask questions on our [GH Discussions page](https://github.com/fluxcd/flux2/discussions) - First: Ask questions on our [GH Discussions page](https://github.com/fluxcd/flux2/discussions).
- Second: Talk to us in the #flux channel on [CNCF Slack](https://slack.cncf.io/) - Second: Talk to us in the #flux channel on [CNCF Slack](https://slack.cncf.io/).
- Please follow our [Support Guidelines](https://fluxcd.io/support/) - Please follow our [Support Guidelines](https://fluxcd.io/support/)
(in short: be nice, be respectful of volunteers' time, understand that maintainers and (in short: be nice, be respectful of volunteers' time, understand that maintainers and
contributors cannot respond to all DMs, and keep discussions in the public #flux channel as much as possible). contributors cannot respond to all DMs, and keep discussions in the public #flux channel as much as possible).
- Have feature proposals or want to contribute? - Have feature proposals or want to contribute?
- Propose features on our [GH Discussions page](https://github.com/fluxcd/flux2/discussions) - Propose features on our [GitHub Discussions page](https://github.com/fluxcd/flux2/discussions).
- Join our upcoming dev meetings ([meeting access and agenda](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)) - Join our upcoming dev meetings ([meeting access and agenda](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)).
- [Join the flux-dev mailing list](https://lists.cncf.io/g/cncf-flux-dev). - [Join the flux-dev mailing list](https://lists.cncf.io/g/cncf-flux-dev).
- Check out [how to contribute](CONTRIBUTING.md) to the project - Check out [how to contribute](CONTRIBUTING.md) to the project.
- Check out the [project roadmap](https://fluxcd.io/roadmap/).
### Events ### Events

@ -1,104 +1,22 @@
# Flux GitHub Action # Flux GitHub Action
Usage: To install the latest Flux CLI on Linux, macOS or Windows GitHub runners:
```yaml ```yaml
steps: steps:
- name: Setup Flux CLI - name: Setup Flux CLI
uses: fluxcd/flux2/action@main uses: fluxcd/flux2/action@main
- name: Run Flux commands with:
run: flux -v version: 'latest'
- name: Run Flux CLI
run: flux version --client
``` ```
The latest stable version of the `flux` binary is downloaded from The Flux GitHub Action can be used to automate various tasks in CI, such as:
GitHub [releases](https://github.com/fluxcd/flux2/releases)
and placed at `/usr/local/bin/flux`.
Note that this action can only be used on GitHub **Linux** runners. - [Automate Flux upgrades on clusters via Pull Requests](https://fluxcd.io/flux/flux-gh-action/#automate-flux-updates)
You can change the arch (defaults to `amd64`) with: - [Push Kubernetes manifests to container registries](https://fluxcd.io/flux/flux-gh-action/#push-kubernetes-manifests-to-container-registries)
- [Run end-to-end testing with Flux and Kubernetes Kind](https://fluxcd.io/flux/flux-gh-action/#end-to-end-testing)
```yaml For more information, please see the [Flux GitHub Action documentation](https://fluxcd.io/flux/flux-gh-action/).
steps:
- name: Setup Flux CLI
uses: fluxcd/flux2/action@main
with:
arch: arm64 # can be amd64, arm64 or arm
```
You can download a specific version with:
```yaml
steps:
- name: Setup Flux CLI
uses: fluxcd/flux2/action@main
with:
version: 0.8.0
```
### Automate Flux updates
Example workflow for updating Flux's components generated with `flux bootstrap --path=clusters/production`:
```yaml
name: update-flux
on:
workflow_dispatch:
schedule:
- cron: "0 * * * *"
jobs:
components:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Setup Flux CLI
uses: fluxcd/flux2/action@main
- name: Check for updates
id: update
run: |
flux install \
--export > ./clusters/production/flux-system/gotk-components.yaml
VERSION="$(flux -v)"
echo "::set-output name=flux_version::$VERSION"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: update-flux
commit-message: Update to ${{ steps.update.outputs.flux_version }}
title: Update to ${{ steps.update.outputs.flux_version }}
body: |
${{ steps.update.outputs.flux_version }}
```
### End-to-end testing
Example workflow for running Flux in Kubernetes Kind:
```yaml
name: e2e
on:
push:
branches:
- '*'
jobs:
kubernetes:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Flux CLI
uses: fluxcd/flux2/action@main
- name: Setup Kubernetes Kind
uses: engineerd/setup-kind@v0.5.0
- name: Install Flux in Kubernetes Kind
run: flux install
```
A complete e2e testing workflow is available here
[flux2-kustomize-helm-example](https://github.com/fluxcd/flux2-kustomize-helm-example/blob/main/.github/workflows/e2e.yaml)

@ -1,52 +1,120 @@
name: Setup Flux CLI name: Setup Flux CLI
description: A GitHub Action for running Flux commands description: A GitHub Action for installing the Flux CLI
author: Stefan Prodan author: Flux project
branding: branding:
color: blue color: blue
icon: command icon: command
inputs: inputs:
version: version:
description: "Flux version e.g. 0.8.0 (defaults to latest stable release)" description: "Flux version e.g. 2.0.0 (defaults to latest stable release)"
required: false required: false
arch: arch:
description: "arch can be amd64, arm64 or arm" description: "arch can be amd64, arm64 or arm"
required: true required: false
default: "amd64" deprecationMessage: "No longer required, action will now detect runner arch."
bindir: bindir:
description: "Optional location of the Flux binary. Will not use sudo if set. Updates System Path." description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE."
required: false
token:
description: "Token used to authentication against the GitHub.com API. Defaults to the token from the GitHub context of the workflow."
required: false required: false
runs: runs:
using: composite using: composite
steps: steps:
- name: "Download flux binary to tmp" - name: "Download the binary to the runner's cache dir"
shell: bash shell: bash
run: | run: |
ARCH=${{ inputs.arch }}
VERSION=${{ inputs.version }} VERSION=${{ inputs.version }}
if [ -z $VERSION ]; then TOKEN=${{ inputs.token }}
VERSION=$(curl https://api.github.com/repos/fluxcd/flux2/releases/latest -sL | grep tag_name | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) if [[ -z "$TOKEN" ]]; then
TOKEN=${{ github.token }}
fi fi
BIN_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_${ARCH}.tar.gz" if [[ -z "$VERSION" ]] || [[ "$VERSION" = "latest" ]]; then
curl -sL ${BIN_URL} -o /tmp/flux.tar.gz VERSION=$(curl -fsSL -H "Authorization: token ${TOKEN}" https://api.github.com/repos/fluxcd/flux2/releases/latest | grep tag_name | cut -d '"' -f 4)
mkdir -p /tmp/flux fi
tar -C /tmp/flux/ -zxvf /tmp/flux.tar.gz if [[ -z "$VERSION" ]]; then
- name: "Copy Flux binary to execute location" echo "Unable to determine Flux CLI version"
shell: bash exit 1
run: | fi
BINDIR=${{ inputs.bindir }} if [[ $VERSION = v* ]]; then
if [ -z $BINDIR ]; then VERSION="${VERSION:1}"
sudo cp /tmp/flux/flux /usr/local/bin fi
else
cp /tmp/flux/flux "${BINDIR}" OS=$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')
echo "${BINDIR}" >> $GITHUB_PATH if [[ "$OS" == "macos" ]]; then
fi OS="darwin"
- name: "Cleanup tmp" fi
shell: bash
run: | ARCH=$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')
rm -rf /tmp/flux/ /tmp/flux.tar.gz if [[ "$ARCH" == "x64" ]]; then
- name: "Verify correct installation of binary" ARCH="amd64"
elif [[ "$ARCH" == "x86" ]]; then
ARCH="386"
fi
FLUX_EXEC_FILE="flux"
if [[ "$OS" == "windows" ]]; then
FLUX_EXEC_FILE="${FLUX_EXEC_FILE}.exe"
fi
FLUX_TOOL_DIR=${{ inputs.bindir }}
if [[ -z "$FLUX_TOOL_DIR" ]]; then
FLUX_TOOL_DIR="${RUNNER_TOOL_CACHE}/flux2/${VERSION}/${OS}/${ARCH}"
fi
if [[ ! -x "$FLUX_TOOL_DIR/FLUX_EXEC_FILE" ]]; then
DL_DIR="$(mktemp -dt flux2-XXXXXX)"
trap 'rm -rf $DL_DIR' EXIT
echo "Downloading flux ${VERSION} for ${OS}/${ARCH}"
FLUX_TARGET_FILE="flux_${VERSION}_${OS}_${ARCH}.tar.gz"
if [[ "$OS" == "windows" ]]; then
FLUX_TARGET_FILE="flux_${VERSION}_${OS}_${ARCH}.zip"
fi
FLUX_CHECKSUMS_FILE="flux_${VERSION}_checksums.txt"
FLUX_DOWNLOAD_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/"
curl -fsSL -o "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_TARGET_FILE"
curl -fsSL -o "$DL_DIR/$FLUX_CHECKSUMS_FILE" "$FLUX_DOWNLOAD_URL/$FLUX_CHECKSUMS_FILE"
echo "Verifying checksum"
sum=""
if command -v openssl > /dev/null; then
sum=$(openssl sha256 "$DL_DIR/$FLUX_TARGET_FILE" | awk '{print $2}')
elif command -v sha256sum > /dev/null; then
sum=$(sha256sum "$DL_DIR/$FLUX_TARGET_FILE" | awk '{print $1}')
fi
if [[ -z "$sum" ]]; then
echo "Neither openssl nor sha256sum found. Cannot calculate checksum."
exit 1
fi
expected_sum=$(grep " $FLUX_TARGET_FILE\$" "$DL_DIR/$FLUX_CHECKSUMS_FILE" | awk '{print $1}')
if [ "$sum" != "$expected_sum" ]; then
echo "SHA sum of ${FLUX_TARGET_FILE} does not match. Aborting."
exit 1
fi
echo "Installing flux to ${FLUX_TOOL_DIR}"
mkdir -p "$FLUX_TOOL_DIR"
if [[ "$OS" == "windows" ]]; then
unzip "$DL_DIR/$FLUX_TARGET_FILE" "$FLUX_EXEC_FILE" -d "$FLUX_TOOL_DIR"
else
tar xzf "$DL_DIR/$FLUX_TARGET_FILE" -C "$FLUX_TOOL_DIR" $FLUX_EXEC_FILE
fi
chmod +x "$FLUX_TOOL_DIR/$FLUX_EXEC_FILE"
fi
echo "Adding flux to path"
echo "$FLUX_TOOL_DIR" >> "$GITHUB_PATH"
- name: "Print installed flux version"
shell: bash shell: bash
run: | run: |
flux -v flux -v

@ -19,14 +19,15 @@ 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/v1beta1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
) )
// notificationv1.Alert // notificationv1.Alert
var alertType = apiType{ var alertType = apiType{
kind: notificationv1.AlertKind, kind: notificationv1.AlertKind,
humanKind: "alert", humanKind: "alert",
groupVersion: notificationv1.GroupVersion,
} }
type alertAdapter struct { type alertAdapter struct {

@ -19,14 +19,15 @@ 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/v1beta1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
) )
// notificationv1.Provider // notificationv1.Provider
var alertProviderType = apiType{ var alertProviderType = apiType{
kind: notificationv1.ProviderKind, kind: notificationv1.ProviderKind,
humanKind: "alert provider", humanKind: "alert provider",
groupVersion: notificationv1.GroupVersion,
} }
type alertProviderAdapter struct { type alertProviderAdapter struct {

@ -17,27 +17,32 @@ limitations under the License.
package main package main
import ( import (
"context"
"crypto/elliptic" "crypto/elliptic"
"fmt" "fmt"
"os"
"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/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
) )
var bootstrapCmd = &cobra.Command{ var bootstrapCmd = &cobra.Command{
Use: "bootstrap", Use: "bootstrap",
Short: "Bootstrap toolkit components", Short: "Deploy Flux on a cluster the GitOps way.",
Long: "The bootstrap sub-commands bootstrap the toolkit components on the targeted Git provider.", Long: `The bootstrap sub-commands push the Flux manifests to a Git repository
and deploy Flux on the cluster.`,
} }
type bootstrapFlags struct { type bootstrapFlags struct {
version string version string
arch flags.Arch
logLevel flags.LogLevel logLevel flags.LogLevel
branch string branch string
@ -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
} }
@ -88,12 +97,14 @@ func init() {
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.defaultComponents, "components", rootArgs.defaults.Components, bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.defaultComponents, "components", rootArgs.defaults.Components,
"list of components, accepts comma-separated values") "list of components, accepts comma-separated values")
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.extraComponents, "components-extra", nil, bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.extraComponents, "components-extra", nil,
"list of components in addition to those supplied or defaulted, accepts comma-separated values") "list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller'")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd", bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd",
"container registry where the toolkit 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 toolkit images from a private registry") "Kubernetes secret name used for pulling the controller images from a private registry")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false, bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false,
@ -102,19 +113,20 @@ func init() {
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.watchAllNamespaces, "watch-all-namespaces", true, bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.watchAllNamespaces, "watch-all-namespaces", true,
"watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed") "watch for custom resources in all namespaces, if set to false it will only watch the namespace where the Flux controllers are installed")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.networkPolicy, "network-policy", true, bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.networkPolicy, "network-policy", true,
"deny ingress access to the toolkit controllers from other namespaces using network policies") "setup Kubernetes network policies to deny ingress access to the Flux controllers from other namespaces")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.tokenAuth, "token-auth", false, bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.tokenAuth, "token-auth", false,
"when enabled, the personal access token will be used instead of SSH deploy key") "when enabled, the personal access token will be used instead of the SSH deploy key")
bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.logLevel, "log-level", bootstrapArgs.logLevel.Description()) bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.logLevel, "log-level", bootstrapArgs.logLevel.Description())
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain")
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.tolerationKeys, "toleration-keys", nil, bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.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 controller pods onto nodes with matching taints")
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,8 +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().Var(&bootstrapArgs.arch, "arch", bootstrapArgs.arch.Description()) bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
bootstrapCmd.PersistentFlags().MarkDeprecated("arch", "multi-arch container image is now available for AMD64, ARMv7 and ARM64")
bootstrapCmd.PersistentFlags().MarkHidden("manifests") bootstrapCmd.PersistentFlags().MarkHidden("manifests")
rootCmd.AddCommand(bootstrapCmd) rootCmd.AddCommand(bootstrapCmd)
@ -154,7 +165,7 @@ func buildEmbeddedManifestBase() (string, error) {
if !isEmbeddedVersion(bootstrapArgs.version) { if !isEmbeddedVersion(bootstrapArgs.version) {
return "", nil return "", nil
} }
tmpBaseDir, err := os.MkdirTemp("", "flux-manifests-") tmpBaseDir, err := manifestgen.MkdirTempAbs("", "flux-manifests-")
if err != nil { if err != nil {
return "", err return "", err
} }
@ -176,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
} }
@ -190,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
}

@ -22,44 +22,42 @@ import (
"os" "os"
"time" "time"
"github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/internal/bootstrap" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/internal/bootstrap/git/gogit" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/internal/bootstrap/provider" "github.com/fluxcd/flux2/v2/pkg/bootstrap"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/pkg/manifestgen/sync" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
) )
var bootstrapBServerCmd = &cobra.Command{ var bootstrapBServerCmd = &cobra.Command{
Use: "bitbucket-server", Use: "bitbucket-server",
Short: "Bootstrap toolkit components in a Bitbucket Server repository", Short: "Deploy Flux on a cluster connected to a Bitbucket Server repository",
Long: `The bootstrap bitbucket-server command creates the Bitbucket Server repository if it doesn't exists and Long: `The bootstrap bitbucket-server command creates the Bitbucket Server repository if it doesn't exists and
commits the toolkit components manifests to the master branch. commits the Flux manifests to the master branch.
Then it configures the target cluster to synchronize with the repository. Then it configures the target cluster to synchronize with the repository.
If the toolkit components are present on the cluster, If the Flux components are present on the cluster,
the bootstrap command will perform an upgrade if needed.`, the bootstrap command will perform an upgrade if needed.`,
Example: ` # Create a Bitbucket Server API token and export it as an env var Example: ` # Create a Bitbucket Server API token and export it as an env var
export BITBUCKET_TOKEN=<my-token> export BITBUCKET_TOKEN=<my-token>
# Run bootstrap for a private repository using HTTPS token authentication # Run bootstrap for a private repository using HTTPS token authentication
flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --hostname=<domain> --token-auth flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --hostname=<domain> --token-auth --path=clusters/my-cluster
# Run bootstrap for a private repository using SSH authentication # Run bootstrap for a private repository using SSH authentication
flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --hostname=<domain> flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --hostname=<domain> --path=clusters/my-cluster
# Run bootstrap for a repository path
flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --path=dev-cluster --hostname=<domain>
# 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 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`, 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,
} }
@ -121,13 +119,22 @@ func bootstrapBServerCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
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
} else {
bootstrapArgs.version = ver bootstrapArgs.version = ver
} }
manifestsBase, err := buildEmbeddedManifestBase() manifestsBase, err := buildEmbeddedManifestBase()
@ -165,15 +172,22 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
} }
// Lazy go-git repository // Lazy go-git repository
tmpDir, err := os.MkdirTemp("", "flux-bootstrap-") tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temporary working dir: %w", err) return fmt.Errorf("failed to create temporary working dir: %w", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
gitClient := gogit.New(tmpDir, &http.BasicAuth{
Username: user, clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()}
Password: bitbucketToken, gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{
}) Transport: git.HTTPS,
Username: user,
Password: bitbucketToken,
CAFile: caBundle,
}, clientOpts...)
if err != nil {
return fmt.Errorf("failed to create a Git client: %w", err)
}
// Install manifest config // Install manifest config
installOptions := install.Options{ installOptions := install.Options{
@ -182,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,
@ -211,19 +226,18 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
secretOpts.Username = bServerArgs.username secretOpts.Username = bServerArgs.username
} }
secretOpts.Password = bitbucketToken secretOpts.Password = bitbucketToken
secretOpts.CAFile = caBundle
if bootstrapArgs.caFile != "" {
secretOpts.CAFilePath = bootstrapArgs.caFile
}
} else { } else {
keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password)
if err != nil {
return err
}
secretOpts.Keypair = keypair
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
secretOpts.SSHHostname = bServerArgs.hostname
if bootstrapArgs.privateKeyFile != "" { secretOpts.SSHHostname = bServerArgs.hostname
secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile
}
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
secretOpts.SSHHostname = bootstrapArgs.sshHostname secretOpts.SSHHostname = bootstrapArgs.sshHostname
} }
@ -238,22 +252,27 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
Secret: bootstrapArgs.secretName, Secret: bootstrapArgs.secretName,
TargetPath: bServerArgs.path.ToSlash(), TargetPath: bServerArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
GitImplementation: sourceGitArgs.gitImplementation.String(),
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
if err != nil {
return err
}
// Bootstrap config // Bootstrap config
bootstrapOpts := []bootstrap.GitProviderOption{ bootstrapOpts := []bootstrap.GitProviderOption{
bootstrap.WithProviderRepository(bServerArgs.owner, bServerArgs.repository, bServerArgs.personal), bootstrap.WithProviderRepository(bServerArgs.owner, bServerArgs.repository, bServerArgs.personal),
bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithBootstrapTransportType("https"), bootstrap.WithBootstrapTransportType("https"),
bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithProviderTeamPermissions(mapTeamSlice(bServerArgs.teams, bServerDefaultPermission)), bootstrap.WithProviderTeamPermissions(mapTeamSlice(bServerArgs.teams, bServerDefaultPermission)),
bootstrap.WithReadWriteKeyPermissions(bServerArgs.readWriteKey), bootstrap.WithReadWriteKeyPermissions(bServerArgs.readWriteKey),
bootstrap.WithKubeconfig(kubeconfigArgs), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions),
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithCABundle(caBundle), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))

@ -24,53 +24,71 @@ import (
"strings" "strings"
"time" "time"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/flux2/internal/bootstrap" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/flux2/internal/bootstrap/git/gogit" "github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/bootstrap"
"github.com/fluxcd/flux2/pkg/manifestgen/sync" "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 bootstrapGitCmd = &cobra.Command{ var bootstrapGitCmd = &cobra.Command{
Use: "git", Use: "git",
Short: "Bootstrap toolkit components in a Git repository", Short: "Deploy Flux on a cluster connected to a Git repository",
Long: `The bootstrap git command commits the toolkit components manifests to the Long: `The bootstrap git command commits the Flux manifests to the
branch of a Git repository. It then configures the target cluster to synchronize with branch of a Git repository. And then it configures the target cluster to synchronize with
the repository. If the toolkit components are present on the cluster, the bootstrap that repository. If the Flux components are present on the cluster, the bootstrap
command will perform an upgrade if needed.`, command will perform an upgrade if needed.`,
Example: ` # Run bootstrap for a Git repository and authenticate with your SSH agent Example: ` # Run bootstrap for a Git repository and authenticate with your SSH agent
flux bootstrap git --url=ssh://git@example.com/repository.git flux bootstrap git --url=ssh://git@example.com/repository.git --path=clusters/my-cluster
# Run bootstrap for a Git repository and authenticate using a password # Run bootstrap for a Git repository and authenticate using a password
flux bootstrap git --url=https://example.com/repository.git --password=<password> flux bootstrap git --url=https://example.com/repository.git --password=<password> --path=clusters/my-cluster
# Run bootstrap for a Git repository and authenticate using a password from environment variable
GIT_PASSWORD=<password> && flux bootstrap git --url=https://example.com/repository.git --path=clusters/my-cluster
# Run bootstrap for a Git repository with a passwordless private key # Run bootstrap for a Git repository with a passwordless private key
flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key> flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key> --path=clusters/my-cluster
# Run bootstrap for a Git repository with a private key and password # Run bootstrap for a Git repository with a private key and password
flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key> --password=<password> flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file=<path/to/private.key> --password=<password> --path=clusters/my-cluster
# Run bootstrap for a Git repository on AWS CodeCommit
flux bootstrap git --url=ssh://<SSH-Key-ID>@git-codecommit.<region>.amazonaws.com/v1/repos/<repository> --private-key-file=<path/to/private.key> --password=<SSH-passphrase> --path=clusters/my-cluster
# Run bootstrap for a Git repository on Azure Devops
flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/<org>/<project>/<repository> --private-key-file=<path/to/rsa-sha2-private.key> --ssh-hostkey-algos=rsa-sha2-512,rsa-sha2-256 --path=clusters/my-cluster
# Run bootstrap for a Git repository on Oracle VBS
flux bootstrap git --url=https://repository_url.git --with-bearer-token=true --password=<PAT> --path=clusters/my-cluster
`, `,
RunE: bootstrapGitCmdRun, RunE: bootstrapGitCmdRun,
} }
type gitFlags struct { type gitFlags struct {
url string url string
interval time.Duration interval time.Duration
path flags.SafeRelativePath path flags.SafeRelativePath
username string username string
password string password string
silent bool silent bool
insecureHttpAllowed bool
withBearerToken bool
} }
const (
gitPasswordEnvVar = "GIT_PASSWORD"
)
var gitArgs gitFlags var gitArgs gitFlags
func init() { func init() {
@ -80,11 +98,30 @@ func init() {
bootstrapGitCmd.Flags().StringVarP(&gitArgs.username, "username", "u", "git", "basic authentication username") bootstrapGitCmd.Flags().StringVarP(&gitArgs.username, "username", "u", "git", "basic authentication username")
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.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)
if gitPassword != "" && gitArgs.password == "" {
gitArgs.password = gitPassword
}
if bootstrapArgs.tokenAuth && gitArgs.password == "" {
var err error
gitPassword, err = readPasswordFromStdin("Please enter your Git repository password: ")
if err != nil {
return fmt.Errorf("could not read token: %w", err)
}
gitArgs.password = gitPassword
}
if err := bootstrapValidate(); err != nil { if err := bootstrapValidate(); err != nil {
return err return err
} }
@ -93,21 +130,43 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
if err != nil { if err != nil {
return err return err
} }
gitAuth, err := transportForURL(repositoryURL)
if err != nil { if strings.Contains(repositoryURL.Hostname(), "git-codecommit") && strings.Contains(repositoryURL.Hostname(), "amazonaws.com") {
return err if repositoryURL.Scheme == string(git.SSH) {
if repositoryURL.User == nil {
return fmt.Errorf("invalid AWS CodeCommit url: ssh username should be specified in the url")
}
if repositoryURL.User.Username() == git.DefaultPublicKeyAuthUser {
return fmt.Errorf("invalid AWS CodeCommit url: ssh username should be the SSH key ID for the provided private key")
}
if bootstrapArgs.privateKeyFile == "" {
return fmt.Errorf("private key file is required for bootstrapping against AWS CodeCommit using ssh")
}
}
if repositoryURL.Scheme == string(git.HTTPS) && !bootstrapArgs.tokenAuth {
return fmt.Errorf("--token-auth=true must be specified for using an HTTPS AWS CodeCommit url")
}
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
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
} else {
bootstrapArgs.version = ver bootstrapArgs.version = ver
} }
manifestsBase, err := buildEmbeddedManifestBase() manifestsBase, err := buildEmbeddedManifestBase()
@ -117,12 +176,33 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
defer os.RemoveAll(manifestsBase) defer os.RemoveAll(manifestsBase)
// Lazy go-git repository // Lazy go-git repository
tmpDir, err := os.MkdirTemp("", "flux-bootstrap-") tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temporary working dir: %w", err) return fmt.Errorf("failed to create temporary working dir: %w", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
gitClient := gogit.New(tmpDir, gitAuth)
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)
}
}
authOpts, err := getAuthOpts(repositoryURL, caBundle)
if err != nil {
return fmt.Errorf("failed to create authentication options for %s: %w", repositoryURL.String(), err)
}
clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()}
if gitArgs.insecureHttpAllowed {
clientOpts = append(clientOpts, gogit.WithInsecureCredentialsOverHTTP())
}
gitClient, err := gogit.NewClient(tmpDir, authOpts, clientOpts...)
if err != nil {
return fmt.Errorf("failed to create a Git client: %w", err)
}
// Install manifest config // Install manifest config
installOptions := install.Options{ installOptions := install.Options{
@ -131,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,
@ -153,14 +234,17 @@ 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 {
secretOpts.Username = gitArgs.username
secretOpts.Password = gitArgs.password
if bootstrapArgs.caFile != "" { if bootstrapArgs.tokenAuth {
secretOpts.CAFilePath = bootstrapArgs.caFile if gitArgs.withBearerToken {
secretOpts.BearerToken = gitArgs.password
} else {
secretOpts.Username = gitArgs.username
secretOpts.Password = gitArgs.password
} }
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
// This _might_ be overwritten later on by e.g. --ssh-hostname // This _might_ be overwritten later on by e.g. --ssh-hostname
if repositoryURL.Scheme != "https" && repositoryURL.Scheme != "http" { if repositoryURL.Scheme != "https" && repositoryURL.Scheme != "http" {
@ -169,7 +253,9 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
// Configure repository URL to match auth config for sync. // Configure repository URL to match auth config for sync.
repositoryURL.User = nil repositoryURL.User = nil
repositoryURL.Scheme = "https" if !gitArgs.insecureHttpAllowed {
repositoryURL.Scheme = "https"
}
} else { } else {
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.Password = gitArgs.password secretOpts.Password = gitArgs.password
@ -188,9 +274,12 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
repositoryURL.Host = bootstrapArgs.sshHostname repositoryURL.Host = bootstrapArgs.sshHostname
} }
if bootstrapArgs.privateKeyFile != "" {
secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password)
if err != nil {
return err
} }
secretOpts.Keypair = keypair
// Configure last as it depends on the config above. // Configure last as it depends on the config above.
secretOpts.SSHHostname = repositoryURL.Host secretOpts.SSHHostname = repositoryURL.Host
@ -206,30 +295,24 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
Secret: bootstrapArgs.secretName, Secret: bootstrapArgs.secretName,
TargetPath: gitArgs.path.ToSlash(), TargetPath: gitArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
GitImplementation: sourceGitArgs.gitImplementation.String(),
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
} }
var caBundle []byte entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
if bootstrapArgs.caFile != "" { if err != nil {
var err error return err
caBundle, err = os.ReadFile(bootstrapArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
} }
// Bootstrap config // Bootstrap config
bootstrapOpts := []bootstrap.GitOption{ bootstrapOpts := []bootstrap.GitOption{
bootstrap.WithRepositoryURL(gitArgs.url), bootstrap.WithRepositoryURL(gitArgs.url),
bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithKubeconfig(kubeconfigArgs), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions),
bootstrap.WithPostGenerateSecretFunc(promptPublicKey), bootstrap.WithPostGenerateSecretFunc(promptPublicKey),
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithCABundle(caBundle), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
// Setup bootstrapper with constructed configs // Setup bootstrapper with constructed configs
@ -242,22 +325,57 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout)
} }
// transportForURL constructs a transport.AuthMethod based on the scheme // getAuthOpts retruns a AuthOptions based on the scheme
// of the given URL and the configured flags. If the protocol equals // of the given URL and the configured flags. If the protocol equals
// "ssh" but no private key is configured, authentication using the local // "ssh" but no private key is configured, authentication using the local
// SSH-agent is attempted. // SSH-agent is attempted.
func transportForURL(u *url.URL) (transport.AuthMethod, error) { func getAuthOpts(u *url.URL, caBundle []byte) (*git.AuthOptions, error) {
switch u.Scheme { switch u.Scheme {
case "http":
if !gitArgs.insecureHttpAllowed {
return nil, fmt.Errorf("scheme http is insecure, pass --allow-insecure-http=true to allow it")
}
httpAuth := git.AuthOptions{
Transport: git.HTTP,
}
if gitArgs.withBearerToken {
httpAuth.BearerToken = gitArgs.password
} else {
httpAuth.Username = gitArgs.username
httpAuth.Password = gitArgs.password
}
return &httpAuth, nil
case "https": case "https":
return &http.BasicAuth{ httpsAuth := git.AuthOptions{
Username: gitArgs.username, Transport: git.HTTPS,
Password: gitArgs.password, 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{
Transport: git.SSH,
Username: u.User.Username(),
Password: gitArgs.password,
}
if bootstrapArgs.privateKeyFile != "" { if bootstrapArgs.privateKeyFile != "" {
return ssh.NewPublicKeysFromFile(u.User.Username(), bootstrapArgs.privateKeyFile, gitArgs.password) pk, err := os.ReadFile(bootstrapArgs.privateKeyFile)
if err != nil {
return nil, err
}
kh, err := sourcesecret.ScanHostKey(u.Host)
if err != nil {
return nil, err
}
authOpts.Identity = pk
authOpts.KnownHosts = kh
} }
return nil, nil return authOpts, nil
default: default:
return nil, fmt.Errorf("scheme %q is not supported", u.Scheme) return nil, fmt.Errorf("scheme %q is not supported", u.Scheme)
} }

@ -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)
}

@ -22,53 +22,51 @@ import (
"os" "os"
"time" "time"
"github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/internal/bootstrap" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/internal/bootstrap/git/gogit" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/internal/bootstrap/provider" "github.com/fluxcd/flux2/v2/pkg/bootstrap"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/pkg/manifestgen/sync" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
) )
var bootstrapGitHubCmd = &cobra.Command{ var bootstrapGitHubCmd = &cobra.Command{
Use: "github", Use: "github",
Short: "Bootstrap toolkit components in a GitHub repository", Short: "Deploy Flux on a cluster connected to a GitHub repository",
Long: `The bootstrap github command creates the GitHub repository if it doesn't exists and Long: `The bootstrap github command creates the GitHub repository if it doesn't exists and
commits the toolkit components manifests to the main branch. commits the Flux manifests to the specified branch.
Then it configures the target cluster to synchronize with the repository. Then it configures the target cluster to synchronize with that repository.
If the toolkit components are present on the cluster, If the Flux components are present on the cluster,
the bootstrap command will perform an upgrade if needed.`, the bootstrap command will perform an upgrade if needed.`,
Example: ` # Create a GitHub personal access token and export it as an env var Example: ` # Create a GitHub personal access token and export it as an env var
export GITHUB_TOKEN=<my-token> export GITHUB_TOKEN=<my-token>
# Run bootstrap for a private repository owned by a GitHub organization # Run bootstrap for a private repository owned by a GitHub organization
flux bootstrap github --owner=<organization> --repository=<repository name> flux bootstrap github --owner=<organization> --repository=<repository name> --path=clusters/my-cluster
# Run bootstrap for a private repository and assign organization teams to it # Run bootstrap for a private repository and assign organization teams to it
flux bootstrap github --owner=<organization> --repository=<repository name> --team=<team1 slug> --team=<team2 slug> flux bootstrap github --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 # Run bootstrap for a private repository and assign organization teams with their access level(e.g maintain, admin) to it
flux bootstrap github --owner=<organization> --repository=<repository name> --team=<team1 slug>:<access-level> flux bootstrap github --owner=<organization> --repository=<repository name> --team=<team1 slug>:<access-level> --path=clusters/my-cluster
# Run bootstrap for a repository path
flux bootstrap github --owner=<organization> --repository=<repository name> --path=dev-cluster
# Run bootstrap for a public repository on a personal account # Run bootstrap for a public repository on a personal account
flux bootstrap github --owner=<user> --repository=<repository name> --private=false --personal=true flux bootstrap github --owner=<user> --repository=<repository name> --private=false --personal=true --path=clusters/my-cluster
# Run bootstrap for a private repository hosted on GitHub Enterprise using SSH auth # Run bootstrap for a private repository hosted on GitHub Enterprise using SSH auth
flux bootstrap github --owner=<organization> --repository=<repository name> --hostname=<domain> --ssh-hostname=<domain> flux bootstrap github --owner=<organization> --repository=<repository name> --hostname=<domain> --ssh-hostname=<domain> --path=clusters/my-cluster
# Run bootstrap for a private repository hosted on GitHub Enterprise using HTTPS auth # Run bootstrap for a private repository hosted on GitHub Enterprise using HTTPS auth
flux bootstrap github --owner=<organization> --repository=<repository name> --hostname=<domain> --token-auth flux bootstrap github --owner=<organization> --repository=<repository name> --hostname=<domain> --token-auth --path=clusters/my-cluster
# Run bootstrap for an existing repository with a branch named main # Run bootstrap for an existing repository with a branch named main
flux bootstrap github --owner=<organization> --repository=<repository name> --branch=main`, flux bootstrap github --owner=<organization> --repository=<repository name> --branch=main --path=clusters/my-cluster`,
RunE: bootstrapGitHubCmdRun, RunE: bootstrapGitHubCmdRun,
} }
@ -125,13 +123,22 @@ func bootstrapGitHubCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
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
} else {
bootstrapArgs.version = ver bootstrapArgs.version = ver
} }
manifestsBase, err := buildEmbeddedManifestBase() manifestsBase, err := buildEmbeddedManifestBase()
@ -160,16 +167,22 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
// Lazy go-git repository tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-")
tmpDir, err := os.MkdirTemp("", "flux-bootstrap-")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temporary working dir: %w", err) return fmt.Errorf("failed to create temporary working dir: %w", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
gitClient := gogit.New(tmpDir, &http.BasicAuth{
Username: githubArgs.owner, clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()}
Password: ghToken, gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{
}) Transport: git.HTTPS,
Username: githubArgs.owner,
Password: ghToken,
CAFile: caBundle,
}, clientOpts...)
if err != nil {
return fmt.Errorf("failed to create a Git client: %w", err)
}
// Install manifest config // Install manifest config
installOptions := install.Options{ installOptions := install.Options{
@ -178,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,
@ -203,16 +217,13 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
if bootstrapArgs.tokenAuth { if bootstrapArgs.tokenAuth {
secretOpts.Username = "git" secretOpts.Username = "git"
secretOpts.Password = ghToken secretOpts.Password = ghToken
secretOpts.CAFile = caBundle
if bootstrapArgs.caFile != "" {
secretOpts.CAFilePath = bootstrapArgs.caFile
}
} else { } else {
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
secretOpts.SSHHostname = githubArgs.hostname
secretOpts.SSHHostname = githubArgs.hostname
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
secretOpts.SSHHostname = bootstrapArgs.sshHostname secretOpts.SSHHostname = bootstrapArgs.sshHostname
} }
@ -227,22 +238,26 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
Secret: bootstrapArgs.secretName, Secret: bootstrapArgs.secretName,
TargetPath: githubArgs.path.ToSlash(), TargetPath: githubArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
GitImplementation: sourceGitArgs.gitImplementation.String(),
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
if err != nil {
return err
}
// Bootstrap config // Bootstrap config
bootstrapOpts := []bootstrap.GitProviderOption{ bootstrapOpts := []bootstrap.GitProviderOption{
bootstrap.WithProviderRepository(githubArgs.owner, githubArgs.repository, githubArgs.personal), bootstrap.WithProviderRepository(githubArgs.owner, githubArgs.repository, githubArgs.personal),
bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithBootstrapTransportType("https"), bootstrap.WithBootstrapTransportType("https"),
bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithProviderTeamPermissions(mapTeamSlice(githubArgs.teams, ghDefaultPermission)), bootstrap.WithProviderTeamPermissions(mapTeamSlice(githubArgs.teams, ghDefaultPermission)),
bootstrap.WithReadWriteKeyPermissions(githubArgs.readWriteKey), bootstrap.WithReadWriteKeyPermissions(githubArgs.readWriteKey),
bootstrap.WithKubeconfig(kubeconfigArgs), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions),
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithCABundle(caBundle), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))

@ -24,26 +24,27 @@ import (
"strings" "strings"
"time" "time"
"github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/internal/bootstrap" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/internal/bootstrap/git/gogit" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/internal/bootstrap/provider" "github.com/fluxcd/flux2/v2/pkg/bootstrap"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/fluxcd/flux2/pkg/manifestgen/sync" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync"
) )
var bootstrapGitLabCmd = &cobra.Command{ var bootstrapGitLabCmd = &cobra.Command{
Use: "gitlab", Use: "gitlab",
Short: "Bootstrap toolkit components in a GitLab repository", Short: "Deploy Flux on a cluster connected to a GitLab repository",
Long: `The bootstrap gitlab command creates the GitLab repository if it doesn't exists and Long: `The bootstrap gitlab command creates the GitLab repository if it doesn't exists and
commits the toolkit components manifests to the master branch. commits the Flux manifests to the specified branch.
Then it configures the target cluster to synchronize with the repository. Then it configures the target cluster to synchronize with that repository.
If the toolkit components are present on the cluster, If the Flux components are present on the cluster,
the bootstrap command will perform an upgrade if needed.`, the bootstrap command will perform an upgrade if needed.`,
Example: ` # Create a GitLab API token and export it as an env var Example: ` # Create a GitLab API token and export it as an env var
export GITLAB_TOKEN=<my-token> export GITLAB_TOKEN=<my-token>
@ -63,8 +64,12 @@ 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
flux bootstrap gitlab --owner=<group> --repository=<repository name> --deploy-token-auth
`,
RunE: bootstrapGitLabCmdRun, RunE: bootstrapGitLabCmdRun,
} }
@ -76,16 +81,17 @@ const (
) )
type gitlabFlags struct { type gitlabFlags struct {
owner string owner string
repository string repository string
interval time.Duration interval time.Duration
personal bool personal bool
private bool private bool
hostname string hostname string
path flags.SafeRelativePath path flags.SafeRelativePath
teams []string teams []string
readWriteKey bool readWriteKey bool
reconcile bool reconcile bool
deployTokenAuth bool
} }
var gitlabArgs gitlabFlags var gitlabArgs gitlabFlags
@ -101,6 +107,7 @@ func init() {
bootstrapGitLabCmd.Flags().Var(&gitlabArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapGitLabCmd.Flags().Var(&gitlabArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions")
bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists")
bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.deployTokenAuth, "deploy-token-auth", false, "when enabled, a Project Deploy Token is generated and will be used instead of the SSH deploy token")
bootstrapCmd.AddCommand(bootstrapGitLabCmd) bootstrapCmd.AddCommand(bootstrapGitLabCmd)
} }
@ -122,6 +129,10 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if bootstrapArgs.tokenAuth && gitlabArgs.deployTokenAuth {
return fmt.Errorf("--token-auth and --deploy-token-auth cannot be set both.")
}
if err := bootstrapValidate(); err != nil { if err := bootstrapValidate(); err != nil {
return err return err
} }
@ -129,13 +140,22 @@ func bootstrapGitLabCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
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
} else {
bootstrapArgs.version = ver bootstrapArgs.version = ver
} }
manifestsBase, err := buildEmbeddedManifestBase() manifestsBase, err := buildEmbeddedManifestBase()
@ -172,15 +192,22 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
} }
// Lazy go-git repository // Lazy go-git repository
tmpDir, err := os.MkdirTemp("", "flux-bootstrap-") tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-")
if err != nil { if err != nil {
return fmt.Errorf("failed to create temporary working dir: %w", err) return fmt.Errorf("failed to create temporary working dir: %w", err)
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
gitClient := gogit.New(tmpDir, &http.BasicAuth{
Username: gitlabArgs.owner, clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()}
Password: glToken, gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{
}) Transport: git.HTTPS,
Username: gitlabArgs.owner,
Password: glToken,
CAFile: caBundle,
}, clientOpts...)
if err != nil {
return fmt.Errorf("failed to create a Git client: %w", err)
}
// Install manifest config // Install manifest config
installOptions := install.Options{ installOptions := install.Options{
@ -189,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,
@ -214,19 +242,21 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
if bootstrapArgs.tokenAuth { if bootstrapArgs.tokenAuth {
secretOpts.Username = "git" secretOpts.Username = "git"
secretOpts.Password = glToken secretOpts.Password = glToken
secretOpts.CAFile = caBundle
if bootstrapArgs.caFile != "" { } else if gitlabArgs.deployTokenAuth {
secretOpts.CAFilePath = bootstrapArgs.caFile // the actual deploy token will be reconciled later
} secretOpts.CAFile = caBundle
} else { } else {
keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password)
if err != nil {
return err
}
secretOpts.Keypair = keypair
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
secretOpts.SSHHostname = gitlabArgs.hostname
if bootstrapArgs.privateKeyFile != "" { secretOpts.SSHHostname = gitlabArgs.hostname
secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile
}
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
secretOpts.SSHHostname = bootstrapArgs.sshHostname secretOpts.SSHHostname = bootstrapArgs.sshHostname
} }
@ -241,29 +271,36 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
Secret: bootstrapArgs.secretName, Secret: bootstrapArgs.secretName,
TargetPath: gitlabArgs.path.ToSlash(), TargetPath: gitlabArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile, ManifestFile: sync.MakeDefaultOptions().ManifestFile,
GitImplementation: sourceGitArgs.gitImplementation.String(),
RecurseSubmodules: bootstrapArgs.recurseSubmodules, RecurseSubmodules: bootstrapArgs.recurseSubmodules,
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
if err != nil {
return err
}
// Bootstrap config // Bootstrap config
bootstrapOpts := []bootstrap.GitProviderOption{ bootstrapOpts := []bootstrap.GitProviderOption{
bootstrap.WithProviderRepository(gitlabArgs.owner, gitlabArgs.repository, gitlabArgs.personal), bootstrap.WithProviderRepository(gitlabArgs.owner, gitlabArgs.repository, gitlabArgs.personal),
bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithBootstrapTransportType("https"), bootstrap.WithBootstrapTransportType("https"),
bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithProviderTeamPermissions(mapTeamSlice(gitlabArgs.teams, glDefaultPermission)), bootstrap.WithProviderTeamPermissions(mapTeamSlice(gitlabArgs.teams, glDefaultPermission)),
bootstrap.WithReadWriteKeyPermissions(gitlabArgs.readWriteKey), bootstrap.WithReadWriteKeyPermissions(gitlabArgs.readWriteKey),
bootstrap.WithKubeconfig(kubeconfigArgs), bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions),
bootstrap.WithLogger(logger), bootstrap.WithLogger(logger),
bootstrap.WithCABundle(caBundle), bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
} }
if bootstrapArgs.sshHostname != "" { if bootstrapArgs.sshHostname != "" {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname))
} }
if bootstrapArgs.tokenAuth { if bootstrapArgs.tokenAuth || gitlabArgs.deployTokenAuth {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https"))
} }
if gitlabArgs.deployTokenAuth {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithDeployTokenAuth())
}
if !gitlabArgs.private { if !gitlabArgs.private {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public"))
} }

@ -23,7 +23,7 @@ import (
var buildCmd = &cobra.Command{ var buildCmd = &cobra.Command{
Use: "build", Use: "build",
Short: "Build a flux resource", Short: "Build a flux resource",
Long: "The build command is used to build flux resources.", Long: `The build command is used to build flux resources.`,
} }
func init() { func init() {

@ -0,0 +1,117 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"strings"
"github.com/spf13/cobra"
oci "github.com/fluxcd/pkg/oci/client"
"github.com/fluxcd/pkg/sourceignore"
)
var buildArtifactCmd = &cobra.Command{
Use: "artifact",
Short: "Build artifact",
Long: withPreviewNote(`The build artifact command creates a tgz file with the manifests
from the given directory or a single manifest file.`),
Example: ` # Build the given manifests directory into an artifact
flux build artifact --path ./path/to/local/manifests --output ./path/to/artifact.tgz
# Build the given single manifest file into an artifact
flux build artifact --path ./path/to/local/manifest.yaml --output ./path/to/artifact.tgz
# List the files bundled in the artifact
tar -ztvf ./path/to/artifact.tgz
`,
RunE: buildArtifactCmdRun,
}
type buildArtifactFlags struct {
output string
path string
ignorePaths []string
}
var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...)
var buildArtifactArgs buildArtifactFlags
func init() {
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.")
buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.")
buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format")
buildCmd.AddCommand(buildArtifactCmd)
}
func buildArtifactCmdRun(cmd *cobra.Command, args []string) error {
if buildArtifactArgs.path == "" {
return fmt.Errorf("invalid path %q", buildArtifactArgs.path)
}
path := buildArtifactArgs.path
var err error
if buildArtifactArgs.path == "-" {
path, err = saveReaderToFile(os.Stdin)
if err != nil {
return err
}
defer os.Remove(path)
}
if _, err := os.Stat(path); err != nil {
return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path)
}
logger.Actionf("building artifact from %s", path)
ociClient := oci.NewClient(oci.DefaultOptions())
if err := ociClient.Build(buildArtifactArgs.output, path, buildArtifactArgs.ignorePaths); err != nil {
return fmt.Errorf("building artifact failed, error: %w", err)
}
logger.Successf("artifact created at %s", buildArtifactArgs.output)
return nil
}
func saveReaderToFile(reader io.Reader) (string, error) {
b, err := io.ReadAll(bufio.NewReader(reader))
if err != nil {
return "", err
}
b = bytes.TrimRight(b, "\r\n")
f, err := os.CreateTemp("", "*.yaml")
if err != nil {
return "", fmt.Errorf("unable to create temp dir for stdin")
}
defer f.Close()
if _, err := f.Write(b); err != nil {
return "", fmt.Errorf("error writing stdin to file: %w", err)
}
return f.Name(), nil
}

@ -0,0 +1,70 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"os"
"strings"
"testing"
. "github.com/onsi/gomega"
)
func Test_saveReaderToFile(t *testing.T) {
g := NewWithT(t)
testString := `apiVersion: v1
kind: ConfigMap
metadata:
name: myapp
data:
foo: bar`
tests := []struct {
name string
string string
expectErr bool
}{
{
name: "yaml",
string: testString,
},
{
name: "yaml with carriage return",
string: testString + "\r\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpFile, err := saveReaderToFile(strings.NewReader(tt.string))
g.Expect(err).To(BeNil())
t.Cleanup(func() { _ = os.Remove(tmpFile) })
b, err := os.ReadFile(tmpFile)
if tt.expectErr {
g.Expect(err).To(Not(BeNil()))
return
}
g.Expect(err).To(BeNil())
g.Expect(string(b)).To(BeEquivalentTo(testString))
})
}
}

@ -23,8 +23,10 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/fluxcd/flux2/internal/build" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" ssautil "github.com/fluxcd/pkg/ssa/utils"
"github.com/fluxcd/flux2/v2/internal/build"
) )
var buildKsCmd = &cobra.Command{ var buildKsCmd = &cobra.Command{
@ -33,25 +35,50 @@ var buildKsCmd = &cobra.Command{
Short: "Build Kustomization", Short: "Build Kustomization",
Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization. Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization.
It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml
pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout.`, pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout.
It is possible to specify a Flux kustomization file using --kustomization-file.`,
Example: `# Build the local manifests as they were built on the cluster Example: `# Build the local manifests as they were built on the cluster
flux build kustomization my-app --path ./path/to/local/manifests`, flux build kustomization my-app --path ./path/to/local/manifests
# Build using a local flux kustomization file
flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml
# Build in dry-run mode without connecting to the cluster.
# Note that variable substitutions from Secrets and ConfigMaps are skipped in dry-run mode.
flux build kustomization my-app --path ./path/to/local/manifests \
--kustomization-file ./path/to/local/my-app.yaml \
--dry-run
# Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat.
flux build kustomization my-app --path ./path/to/local/manifests \
--kustomization-file ./path/to/local/my-app.yaml \
--ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: buildKsCmdRun, RunE: buildKsCmdRun,
} }
type buildKsFlags struct { type buildKsFlags struct {
path string kustomizationFile string
path string
ignorePaths []string
dryRun bool
strictSubst bool
} }
var buildKsArgs buildKsFlags var buildKsArgs buildKsFlags
func init() { func init() {
buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.)") buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.")
buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.")
buildKsCmd.Flags().StringSliceVar(&buildKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format")
buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.")
buildKsCmd.Flags().BoolVar(&buildKsArgs.strictSubst, "strict-substitute", false,
"When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.")
buildCmd.AddCommand(buildKsCmd) buildCmd.AddCommand(buildKsCmd)
} }
func buildKsCmdRun(cmd *cobra.Command, args []string) error { func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
if len(args) < 1 { if len(args) < 1 {
return fmt.Errorf("%s name is required", kustomizationType.humanKind) return fmt.Errorf("%s name is required", kustomizationType.humanKind)
} }
@ -65,7 +92,36 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid resource path %q", buildKsArgs.path) return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
} }
builder, err := build.NewBuilder(kubeconfigArgs, name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout)) if buildKsArgs.dryRun && buildKsArgs.kustomizationFile == "" {
return fmt.Errorf("dry-run mode requires a kustomization file")
}
if buildKsArgs.kustomizationFile != "" {
if fs, err := os.Stat(buildKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() {
return fmt.Errorf("invalid kustomization file %q", buildKsArgs.kustomizationFile)
}
}
var builder *build.Builder
if buildKsArgs.dryRun {
builder, err = build.NewBuilder(name, buildKsArgs.path,
build.WithTimeout(rootArgs.timeout),
build.WithKustomizationFile(buildKsArgs.kustomizationFile),
build.WithDryRun(buildKsArgs.dryRun),
build.WithNamespace(*kubeconfigArgs.Namespace),
build.WithIgnore(buildKsArgs.ignorePaths),
build.WithStrictSubstitute(buildKsArgs.strictSubst),
)
} else {
builder, err = build.NewBuilder(name, buildKsArgs.path,
build.WithClientConfig(kubeconfigArgs, kubeclientOptions),
build.WithTimeout(rootArgs.timeout),
build.WithKustomizationFile(buildKsArgs.kustomizationFile),
build.WithIgnore(buildKsArgs.ignorePaths),
build.WithStrictSubstitute(buildKsArgs.strictSubst),
)
}
if err != nil { if err != nil {
return err return err
} }
@ -76,12 +132,17 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error {
errChan := make(chan error) errChan := make(chan error)
go func() { go func() {
manifests, err := builder.Build() objects, err := builder.Build()
if err != nil {
errChan <- err
}
manifests, err := ssautil.ObjectsToYAML(objects)
if err != nil { if err != nil {
errChan <- err errChan <- err
} }
cmd.Print(string(manifests)) cmd.Print(manifests)
errChan <- nil errChan <- nil
}() }()

@ -20,7 +20,10 @@ limitations under the License.
package main package main
import ( import (
"bytes"
"os"
"testing" "testing"
"text/template"
) )
func setup(t *testing.T, tmpl map[string]string) { func setup(t *testing.T, tmpl map[string]string) {
@ -54,6 +57,18 @@ func TestBuildKustomization(t *testing.T) {
resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml",
assertFunc: "assertGoldenTemplateFile", assertFunc: "assertGoldenTemplateFile",
}, },
{
name: "build deployment and configmap with var substitution",
args: "build kustomization podinfo --path ./testdata/build-kustomization/var-substitution",
resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build ignore",
args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\"",
resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
} }
tmpl := map[string]string{ tmpl := map[string]string{
@ -81,3 +96,107 @@ func TestBuildKustomization(t *testing.T) {
}) })
} }
} }
func TestBuildLocalKustomization(t *testing.T) {
podinfo := `apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: podinfo
namespace: {{ .fluxns }}
spec:
interval: 5m0s
path: ./kustomize
force: true
prune: true
sourceRef:
kind: GitRepository
name: podinfo
targetNamespace: default
postBuild:
substitute:
cluster_env: "prod"
cluster_region: "eu-central-1"
`
tests := []struct {
name string
args string
resultFile string
assertFunc string
}{
{
name: "no args",
args: "build kustomization podinfo --kustomization-file ./wrongfile/ --path ./testdata/build-kustomization/podinfo",
resultFile: "invalid kustomization file \"./wrongfile/\"",
assertFunc: "assertError",
},
{
name: "build podinfo",
args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/podinfo",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build podinfo without service",
args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/delete-service",
resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build deployment and configmap with var substitution",
args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution",
resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build deployment and configmap with var substitution in dry-run mode",
args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution --dry-run",
resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
}
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setup(t, tmpl)
testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t)
temp, err := template.New("podinfo").Parse(podinfo)
if err != nil {
t.Fatal(err)
}
var b bytes.Buffer
err = temp.Execute(&b, tmpl)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile("./testdata/build-kustomization/podinfo.yaml", b.Bytes(), 0666)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = os.Remove("./testdata/build-kustomization/podinfo.yaml") })
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var assert assertFunc
switch tt.assertFunc {
case "assertGoldenTemplateFile":
assert = assertGoldenTemplateFile(tt.resultFile, tmpl)
case "assertError":
assert = assertError(tt.resultFile)
}
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: assert,
}
cmd.runTestCmd(t)
})
}
}

@ -18,28 +18,32 @@ package main
import ( import (
"context" "context"
"fmt"
"os" "os"
"time" "time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/spf13/cobra" "github.com/spf13/cobra"
v1 "k8s.io/api/apps/v1" v1 "k8s.io/api/apps/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"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen" "github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/pkg/manifestgen/install" "github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/pkg/status" "github.com/fluxcd/flux2/v2/pkg/status"
) )
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: `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.`),
Example: ` # Run pre-installation checks Example: ` # Run pre-installation checks
flux check --pre flux check --pre
@ -56,10 +60,7 @@ type checkFlags struct {
} }
var kubernetesConstraints = []string{ var kubernetesConstraints = []string{
">=1.19.0-0", ">=1.28.0-0",
">=1.16.11-0 <=1.16.15-0",
">=1.17.7-0 <=1.17.17-0",
">=1.18.4-0 <=1.18.20-0",
} }
var checkArgs checkFlags var checkArgs checkFlags
@ -82,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
} }
@ -94,13 +108,26 @@ 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
}
logger.Actionf("checking crds")
if !crdsCheck(ctx, kubeClient) {
checkFailed = true checkFailed = true
} }
if checkFailed { if checkFailed {
logger.Failuref("check failed")
os.Exit(1) os.Exit(1)
} }
logger.Successf("all checks passed") logger.Successf("all checks passed")
return nil return nil
} }
@ -123,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)
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())
@ -172,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)
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)
if err != nil { if err != nil {
return false return false
} }
@ -194,7 +202,14 @@ func componentsCheck() bool {
ok := true ok := true
selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue}
var list v1.DeploymentList var list v1.DeploymentList
if err := kubeClient.List(ctx, &list, client.InNamespace(*kubeconfigArgs.Namespace), selector); err == nil { ns := *kubeconfigArgs.Namespace
if err := kubeClient.List(ctx, &list, client.InNamespace(ns), selector); err == nil {
if len(list.Items) == 0 {
logger.Failuref("no controllers found in the '%s' namespace with the label selector '%s=%s'",
ns, manifestgen.PartOfLabelKey, manifestgen.PartOfLabelValue)
return false
}
for _, d := range list.Items { for _, d := range list.Items {
if ref, err := buildComponentObjectRefs(d.Name); err == nil { if ref, err := buildComponentObjectRefs(d.Name); err == nil {
if err := statusChecker.Assess(ref...); err != nil { if err := statusChecker.Assess(ref...); err != nil {
@ -208,3 +223,41 @@ func componentsCheck() bool {
} }
return ok return ok
} }
func crdsCheck(ctx context.Context, kubeClient client.Client) bool {
ok := true
selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue}
var list apiextensionsv1.CustomResourceDefinitionList
if err := kubeClient.List(ctx, &list, client.InNamespace(*kubeconfigArgs.Namespace), selector); err == nil {
if len(list.Items) == 0 {
logger.Failuref("no crds found with the label selector '%s=%s'",
manifestgen.PartOfLabelKey, manifestgen.PartOfLabelValue)
return false
}
for _, crd := range list.Items {
versions := crd.Status.StoredVersions
if len(versions) > 0 {
logger.Successf(crd.Name + "/" + versions[len(versions)-1])
} else {
ok = false
logger.Failuref("no stored versions for %s", crd.Name)
}
}
}
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
}

@ -25,8 +25,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"k8s.io/apimachinery/pkg/version"
) )
func TestCheckPre(t *testing.T) { func TestCheckPre(t *testing.T) {
@ -35,17 +34,19 @@ func TestCheckPre(t *testing.T) {
t.Fatalf("Error running utils.ExecKubectlCommand: %v", err.Error()) t.Fatalf("Error running utils.ExecKubectlCommand: %v", err.Error())
} }
var versions map[string]version.Info var versions map[string]interface{}
if err := json.Unmarshal([]byte(jsonOutput), &versions); err != nil { if err := json.Unmarshal([]byte(jsonOutput), &versions); err != nil {
t.Fatalf("Error unmarshalling: %v", err.Error()) t.Fatalf("Error unmarshalling '%s': %v", jsonOutput, err.Error())
} }
serverVersion := strings.TrimPrefix(versions["serverVersion"].GitVersion, "v") serverGitVersion := strings.TrimPrefix(
versions["serverVersion"].(map[string]interface{})["gitVersion"].(string),
"v")
cmd := cmdTestCase{ cmd := cmdTestCase{
args: "check --pre", args: "check --pre",
assert: assertGoldenTemplateFile("testdata/check/check_pre.golden", map[string]string{ assert: assertGoldenTemplateFile("testdata/check/check_pre.golden", map[string]string{
"serverVersion": serverVersion, "serverVersion": serverGitVersion,
}), }),
} }
cmd.runTestCmd(t) cmd.runTestCmd(t)

@ -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))
})
}
}

@ -20,7 +20,7 @@ import (
"context" "context"
"strings" "strings"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -31,7 +31,7 @@ import (
var completionCmd = &cobra.Command{ var completionCmd = &cobra.Command{
Use: "completion", Use: "completion",
Short: "Generates completion scripts for various shells", Short: "Generates completion scripts for various shells",
Long: "The completion sub-command generates completion scripts for various shells", Long: `The completion sub-command generates completion scripts for various shells.`,
} }
func init() { func init() {
@ -60,7 +60,7 @@ func resourceNamesCompletionFunc(gvk schema.GroupVersionKind) func(cmd *cobra.Co
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel() defer cancel()
cfg, err := utils.KubeConfig(kubeconfigArgs) cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return completionError(err) return completionError(err)
} }

@ -25,6 +25,7 @@ import (
var completionBashCmd = &cobra.Command{ var completionBashCmd = &cobra.Command{
Use: "bash", Use: "bash",
Short: "Generates bash completion scripts", Short: "Generates bash completion scripts",
Long: `The completion sub-command generates completion scripts for bash.`,
Example: `To load completion run Example: `To load completion run
. <(flux completion bash) . <(flux completion bash)

@ -25,6 +25,7 @@ import (
var completionFishCmd = &cobra.Command{ var completionFishCmd = &cobra.Command{
Use: "fish", Use: "fish",
Short: "Generates fish completion scripts", Short: "Generates fish completion scripts",
Long: `The completion sub-command generates completion scripts for fish.`,
Example: `To configure your fish shell to load completions for each session write this script to your completions dir: Example: `To configure your fish shell to load completions for each session write this script to your completions dir:
flux completion fish > ~/.config/fish/completions/flux.fish flux completion fish > ~/.config/fish/completions/flux.fish

@ -25,6 +25,7 @@ import (
var completionPowerShellCmd = &cobra.Command{ var completionPowerShellCmd = &cobra.Command{
Use: "powershell", Use: "powershell",
Short: "Generates powershell completion scripts", Short: "Generates powershell completion scripts",
Long: `The completion sub-command generates completion scripts for powershell.`,
Example: `To load completion run Example: `To load completion run
. <(flux completion powershell) . <(flux completion powershell)
@ -34,12 +35,12 @@ To configure your powershell shell to load completions for each session add to y
Windows: Windows:
cd "$env:USERPROFILE\Documents\WindowsPowerShell\Modules" cd "$env:USERPROFILE\Documents\WindowsPowerShell\Modules"
flux completion >> flux-completion.ps1 flux completion powershell >> flux-completion.ps1
Linux: Linux:
cd "${XDG_CONFIG_HOME:-"$HOME/.config/"}/powershell/modules" cd "${XDG_CONFIG_HOME:-"$HOME/.config/"}/powershell/modules"
flux completion >> flux-completions.ps1`, flux completion powershell >> flux-completions.ps1`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenPowerShellCompletion(os.Stdout) rootCmd.GenPowerShellCompletion(os.Stdout)
}, },

@ -26,6 +26,7 @@ import (
var completionZshCmd = &cobra.Command{ var completionZshCmd = &cobra.Command{
Use: "zsh", Use: "zsh",
Short: "Generates zsh completion scripts", Short: "Generates zsh completion scripts",
Long: `The completion sub-command generates completion scripts for zsh.`,
Example: `To load completion run Example: `To load completion run
. <(flux completion zsh) . <(flux completion zsh)

@ -19,6 +19,7 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings" "strings"
"time" "time"
@ -29,13 +30,13 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
var createCmd = &cobra.Command{ var createCmd = &cobra.Command{
Use: "create", Use: "create",
Short: "Create or update sources and resources", Short: "Create or update sources and resources",
Long: "The create sub-commands generate sources and resources.", Long: `The create sub-commands generate sources and resources.`,
} }
type createFlags struct { type createFlags struct {
@ -51,6 +52,18 @@ func init() {
createCmd.PersistentFlags().BoolVar(&createArgs.export, "export", false, "export in YAML format to stdout") createCmd.PersistentFlags().BoolVar(&createArgs.export, "export", false, "export in YAML format to stdout")
createCmd.PersistentFlags().StringSliceVar(&createArgs.labels, "label", nil, createCmd.PersistentFlags().StringSliceVar(&createArgs.labels, "label", nil,
"set labels on the resource (can specify multiple labels with commas: label1=value1,label2=value2)") "set labels on the resource (can specify multiple labels with commas: label1=value1,label2=value2)")
createCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("name is required")
}
name := args[0]
if !validateObjectName(name) {
return fmt.Errorf("name '%s' is invalid, it should adhere to standard defined in RFC 1123, the name can only contain alphanumeric characters or '-'", name)
}
return nil
}
rootCmd.AddCommand(createCmd) rootCmd.AddCommand(createCmd)
} }
@ -66,12 +79,12 @@ type upsertable interface {
// want to update. The mutate function is nullary -- you mutate a // want to update. The mutate function is nullary -- you mutate a
// value in the closure, e.g., by doing this: // value in the closure, e.g., by doing this:
// //
// var existing Value // var existing Value
// existing.Name = name // existing.Name = name
// existing.Namespace = ns // existing.Namespace = ns
// upsert(ctx, client, valueAdapter{&value}, func() error { // upsert(ctx, client, valueAdapter{&value}, func() error {
// value.Spec = onePreparedEarlier // value.Spec = onePreparedEarlier
// }) // })
func (names apiType) upsert(ctx context.Context, kubeClient client.Client, object upsertable, mutate func() error) (types.NamespacedName, error) { func (names apiType) upsert(ctx context.Context, kubeClient client.Client, object upsertable, mutate func() error) (types.NamespacedName, error) {
nsname := types.NamespacedName{ nsname := types.NamespacedName{
Namespace: object.GetNamespace(), Namespace: object.GetNamespace(),
@ -104,7 +117,7 @@ func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) e
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs) // NB globals kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) // NB globals
if err != nil { if err != nil {
return err return err
} }
@ -118,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)
@ -150,3 +163,8 @@ func parseLabels() (map[string]string, error) {
return result, nil return result, nil
} }
func validateObjectName(name string) bool {
r := regexp.MustCompile(`^[a-z0-9]([a-z0-9\-]){0,61}[a-z0-9]$`)
return r.MatchString(name)
}

@ -22,22 +22,22 @@ 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/v1beta1" notificationv1 "github.com/fluxcd/notification-controller/api/v1"
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
var createAlertCmd = &cobra.Command{ var createAlertCmd = &cobra.Command{
Use: "alert [name]", Use: "alert [name]",
Short: "Create or update a Alert resource", Short: "Create or update a Alert resource",
Long: "The create alert command generates a Alert resource.", Long: withPreviewNote(`The create alert command generates a Alert resource.`),
Example: ` # Create an Alert for kustomization events Example: ` # Create an Alert for kustomization events
flux create alert \ flux create alert \
--event-severity info \ --event-severity info \
@ -63,9 +63,6 @@ func init() {
} }
func createAlertCmdRun(cmd *cobra.Command, args []string) error { func createAlertCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("Alert name is required")
}
name := args[0] name := args[0]
if alertArgs.providerRef == "" { if alertArgs.providerRef == "" {
@ -99,13 +96,13 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error {
logger.Generatef("generating Alert") logger.Generatef("generating Alert")
} }
alert := notificationv1.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: notificationv1.AlertSpec{ Spec: notificationv1b3.AlertSpec{
ProviderRef: meta.LocalObjectReference{ ProviderRef: meta.LocalObjectReference{
Name: alertArgs.providerRef, Name: alertArgs.providerRef,
}, },
@ -122,7 +119,7 @@ func createAlertCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }
@ -134,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)
@ -143,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 *notificationv1.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 notificationv1.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) {
@ -172,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 *notificationv1.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,22 +22,21 @@ 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/v1beta1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
var createAlertProviderCmd = &cobra.Command{ var createAlertProviderCmd = &cobra.Command{
Use: "alert-provider [name]", Use: "alert-provider [name]",
Short: "Create or update a Provider resource", Short: "Create or update a Provider resource",
Long: "The create alert-provider command generates a Provider resource.", Long: withPreviewNote(`The create alert-provider command generates a Provider resource.`),
Example: ` # Create a Provider for a Slack channel Example: ` # Create a Provider for a Slack channel
flux create alert-provider slack \ flux create alert-provider slack \
--type slack \ --type slack \
@ -73,9 +72,6 @@ func init() {
} }
func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error { func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("Provider name is required")
}
name := args[0] name := args[0]
if alertProviderArgs.alertType == "" { if alertProviderArgs.alertType == "" {
@ -118,7 +114,7 @@ func createAlertProviderCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }
@ -130,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
} }
@ -170,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
}
}

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Flux authors Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -21,30 +21,33 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/fluxcd/flux2/internal/flags" "time"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/transform"
"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/v2"
"github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/transform"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
) )
var createHelmReleaseCmd = &cobra.Command{ var createHelmReleaseCmd = &cobra.Command{
Use: "helmrelease [name]", Use: "helmrelease [name]",
Aliases: []string{"hr"}, Aliases: []string{"hr"},
Short: "Create or update a HelmRelease resource", Short: "Create or update a HelmRelease resource",
Long: "The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.", Long: `The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.`,
Example: ` # Create a HelmRelease with a chart from a HelmRepository source Example: ` # Create a HelmRelease with a chart from a HelmRepository source
flux create hr podinfo \ flux create hr podinfo \
--interval=10m \ --interval=10m \
@ -81,9 +84,9 @@ var createHelmReleaseCmd = &cobra.Command{
# Create a HelmRelease with a custom release name # Create a HelmRelease with a custom release name
flux create hr podinfo \ flux create hr podinfo \
--release-name=podinfo-dev --release-name=podinfo-dev \
--source=HelmRepository/podinfo \ --source=HelmRepository/podinfo \
--chart=podinfo \ --chart=podinfo
# Create a HelmRelease targeting another namespace than the resource # Create a HelmRelease targeting another namespace than the resource
flux create hr podinfo \ flux create hr podinfo \
@ -103,26 +106,44 @@ var createHelmReleaseCmd = &cobra.Command{
--source=HelmRepository/podinfo \ --source=HelmRepository/podinfo \
--chart=podinfo \ --chart=podinfo \
--values=./values.yaml \ --values=./values.yaml \
--export > podinfo-release.yaml`, --export > podinfo-release.yaml
# Create a HelmRelease using a chart from a HelmChart resource
flux create hr podinfo \
--namespace=default \
--chart-ref=HelmChart/podinfo.flux-system \
# Create a HelmRelease using a chart from an OCIRepository resource
flux create hr podinfo \
--namespace=default \
--chart-ref=OCIRepository/podinfo.flux-system`,
RunE: createHelmReleaseCmdRun, RunE: createHelmReleaseCmdRun,
} }
type helmReleaseFlags struct { type helmReleaseFlags struct {
name string name string
source flags.HelmChartSource source flags.HelmChartSource
dependsOn []string dependsOn []string
chart string chart string
chartVersion string chartVersion string
targetNamespace string chartRef string
createNamespace bool targetNamespace string
valuesFiles []string createNamespace bool
valuesFrom flags.HelmReleaseValuesFrom valuesFiles []string
saName string valuesFrom []string
crds flags.CRDsPolicy saName string
crds flags.CRDsPolicy
reconcileStrategy string
chartInterval time.Duration
kubeConfigSecretRef string
} }
var helmReleaseArgs helmReleaseFlags var helmReleaseArgs helmReleaseFlags
var supportedHelmReleaseValuesFromKinds = []string{"Secret", "ConfigMap"}
var supportedHelmReleaseReferenceKinds = []string{sourcev1b2.OCIRepositoryKind, sourcev1.HelmChartKind}
func init() { func init() {
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.name, "release-name", "", "name used for the Helm release, defaults to a composition of '[<target-namespace>-]<HelmRelease-name>'") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.name, "release-name", "", "name used for the Helm release, defaults to a composition of '[<target-namespace>-]<HelmRelease-name>'")
createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.source, "source", helmReleaseArgs.source.Description()) createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.source, "source", helmReleaseArgs.source.Description())
@ -132,20 +153,21 @@ func init() {
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.targetNamespace, "target-namespace", "", "namespace to install this release, defaults to the HelmRelease namespace") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.targetNamespace, "target-namespace", "", "namespace to install this release, defaults to the HelmRelease namespace")
createHelmReleaseCmd.Flags().BoolVar(&helmReleaseArgs.createNamespace, "create-target-namespace", false, "create the target namespace if it does not exist") createHelmReleaseCmd.Flags().BoolVar(&helmReleaseArgs.createNamespace, "create-target-namespace", false, "create the target namespace if it does not exist")
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.saName, "service-account", "", "the name of the service account to impersonate when reconciling this HelmRelease") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.saName, "service-account", "", "the name of the service account to impersonate when reconciling this HelmRelease")
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.reconcileStrategy, "reconcile-strategy", "ChartVersion", "the reconcile strategy for helm chart created by the helm release(accepted values: Revision and ChartRevision)")
createHelmReleaseCmd.Flags().DurationVarP(&helmReleaseArgs.chartInterval, "chart-interval", "", 0, "the interval of which to check for new chart versions")
createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFiles, "values", nil, "local path to values.yaml files, also accepts comma-separated values") createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFiles, "values", nil, "local path to values.yaml files, also accepts comma-separated values")
createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.valuesFrom, "values-from", helmReleaseArgs.valuesFrom.Description()) createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFrom, "values-from", nil, "a Kubernetes object reference that contains the values.yaml data key in the format '<kind>/<name>', where kind must be one of: (Secret,ConfigMap)")
createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.crds, "crds", helmReleaseArgs.crds.Description()) createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.crds, "crds", helmReleaseArgs.crds.Description())
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.kubeConfigSecretRef, "kubeconfig-secret-ref", "", "the name of the Kubernetes Secret that contains a key with the kubeconfig file for connecting to a remote cluster")
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chartRef, "chart-ref", "", "the name of the HelmChart resource to use as source for the HelmRelease, in the format '<kind>/<name>.<namespace>', where kind must be one of: (OCIRepository,HelmChart)")
createCmd.AddCommand(createHelmReleaseCmd) createCmd.AddCommand(createHelmReleaseCmd)
} }
func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("HelmRelease name is required")
}
name := args[0] name := args[0]
if helmReleaseArgs.chart == "" { if helmReleaseArgs.chart == "" && helmReleaseArgs.chartRef == "" {
return fmt.Errorf("chart name or path is required") return fmt.Errorf("chart or chart-ref is required")
} }
sourceLabels, err := parseLabels() sourceLabels, err := parseLabels()
@ -157,6 +179,11 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
logger.Generatef("generating HelmRelease") logger.Generatef("generating HelmRelease")
} }
if !validateStrategy(helmReleaseArgs.reconcileStrategy) {
return fmt.Errorf("'%s' is an invalid reconcile strategy(valid: Revision, ChartVersion)",
helmReleaseArgs.reconcileStrategy)
}
helmRelease := helmv2.HelmRelease{ helmRelease := helmv2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
@ -170,20 +197,48 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
Duration: createArgs.interval, Duration: createArgs.interval,
}, },
TargetNamespace: helmReleaseArgs.targetNamespace, TargetNamespace: helmReleaseArgs.targetNamespace,
Suspend: false,
},
}
Chart: helmv2.HelmChartTemplate{ switch {
Spec: helmv2.HelmChartTemplateSpec{ case helmReleaseArgs.chart != "":
Chart: helmReleaseArgs.chart, helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{
Version: helmReleaseArgs.chartVersion, Spec: helmv2.HelmChartTemplateSpec{
SourceRef: helmv2.CrossNamespaceObjectReference{ Chart: helmReleaseArgs.chart,
Kind: helmReleaseArgs.source.Kind, Version: helmReleaseArgs.chartVersion,
Name: helmReleaseArgs.source.Name, SourceRef: helmv2.CrossNamespaceObjectReference{
Namespace: helmReleaseArgs.source.Namespace, Kind: helmReleaseArgs.source.Kind,
}, Name: helmReleaseArgs.source.Name,
Namespace: helmReleaseArgs.source.Namespace,
}, },
ReconcileStrategy: helmReleaseArgs.reconcileStrategy,
}, },
Suspend: false, }
}, if helmReleaseArgs.chartInterval != 0 {
helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{
Duration: helmReleaseArgs.chartInterval,
}
}
case helmReleaseArgs.chartRef != "":
kind, name, ns := utils.ParseObjectKindNameNamespace(helmReleaseArgs.chartRef)
if kind != sourcev1.HelmChartKind && kind != sourcev1b2.OCIRepositoryKind {
return fmt.Errorf("chart reference kind '%s' is not supported, must be one of: %s",
kind, strings.Join(supportedHelmReleaseReferenceKinds, ", "))
}
helmRelease.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{
Kind: kind,
Name: name,
Namespace: ns,
}
}
if helmReleaseArgs.kubeConfigSecretRef != "" {
helmRelease.Spec.KubeConfig = &meta.KubeConfigReference{
SecretRef: meta.SecretKeyReference{
Name: helmReleaseArgs.kubeConfigSecretRef,
},
}
} }
if helmReleaseArgs.createNamespace { if helmReleaseArgs.createNamespace {
@ -236,11 +291,25 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
helmRelease.Spec.Values = &apiextensionsv1.JSON{Raw: jsonRaw} helmRelease.Spec.Values = &apiextensionsv1.JSON{Raw: jsonRaw}
} }
if helmReleaseArgs.valuesFrom.String() != "" { if len(helmReleaseArgs.valuesFrom) != 0 {
helmRelease.Spec.ValuesFrom = []helmv2.ValuesReference{{ values := []helmv2.ValuesReference{}
Kind: helmReleaseArgs.valuesFrom.Kind, for _, value := range helmReleaseArgs.valuesFrom {
Name: helmReleaseArgs.valuesFrom.Name, sourceKind, sourceName := utils.ParseObjectKindName(value)
}} if sourceKind == "" {
return fmt.Errorf("invalid Kubernetes object reference '%s', must be in format <kind>/<name>", value)
}
cleanSourceKind, ok := utils.ContainsEqualFoldItemString(supportedHelmReleaseValuesFromKinds, sourceKind)
if !ok {
return fmt.Errorf("reference kind '%s' is not supported, must be one of: %s",
sourceKind, strings.Join(supportedHelmReleaseValuesFromKinds, ", "))
}
values = append(values, helmv2.ValuesReference{
Name: sourceName,
Kind: cleanSourceKind,
})
}
helmRelease.Spec.ValuesFrom = values
} }
if createArgs.export { if createArgs.export {
@ -250,7 +319,7 @@ func createHelmReleaseCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }
@ -262,13 +331,13 @@ 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)
logger.Successf("applied revision %s", helmRelease.Status.LastAppliedRevision) logger.Successf("applied revision %s", getHelmReleaseRevision(helmRelease))
return nil return nil
} }
@ -303,19 +372,14 @@ func upsertHelmRelease(ctx context.Context, kubeClient client.Client,
return namespacedName, nil return namespacedName, nil
} }
func isHelmReleaseReady(ctx context.Context, kubeClient client.Client, func validateStrategy(input string) bool {
namespacedName types.NamespacedName, helmRelease *helmv2.HelmRelease) wait.ConditionFunc { allowedStrategy := []string{"Revision", "ChartVersion"}
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 for _, strategy := range allowedStrategy {
if helmRelease.Generation != helmRelease.Status.ObservedGeneration { if strategy == input {
return false, nil return true
} }
return apimeta.IsStatusConditionTrue(helmRelease.Status.Conditions, meta.ReadyCondition), nil
} }
return false
} }

@ -0,0 +1,86 @@
//go:build unit
// +build unit
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import "testing"
func TestCreateHelmRelease(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupHRSource(t, tmpl)
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "missing name",
args: "create helmrelease --export",
assert: assertError("name is required"),
},
{
name: "missing chart template and chartRef",
args: "create helmrelease podinfo --export",
assert: assertError("chart or chart-ref is required"),
},
{
name: "unknown source kind",
args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export",
assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`),
},
{
name: "unknown chart reference kind",
args: "create helmrelease podinfo --chart-ref foobar/podinfo --export",
assert: assertError(`chart reference kind 'foobar' is not supported, must be one of: OCIRepository, HelmChart`),
},
{
name: "basic helmrelease",
args: "create helmrelease podinfo --source Helmrepository/podinfo --chart podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/basic.yaml", tmpl),
},
{
name: "chart with OCIRepository source",
args: "create helmrelease podinfo --chart-ref OCIRepository/podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/or_basic.yaml", tmpl),
},
{
name: "chart with HelmChart source",
args: "create helmrelease podinfo --chart-ref HelmChart/podinfo --interval=1m0s --export",
assert: assertGoldenTemplateFile("testdata/create_hr/hc_basic.yaml", tmpl),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
func setupHRSource(t *testing.T, tmpl map[string]string) {
t.Helper()
testEnv.CreateObjectFile("./testdata/create_hr/setup-source.yaml", tmpl, t)
}

@ -20,14 +20,12 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const createImageLong = `The create image sub-commands work with image automation objects; that is,
object controlling updates to git based on e.g., new container images
being available.`
var createImageCmd = &cobra.Command{ var createImageCmd = &cobra.Command{
Use: "image", Use: "image",
Short: "Create or update resources dealing with image automation", Short: "Create or update resources dealing with image automation",
Long: createImageLong, Long: `The create image sub-commands work with image automation objects;
that is, object controlling updates to git based on e.g., new container images
being available.`,
} }
func init() { func init() {

@ -28,18 +28,18 @@ import (
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
) )
var createImagePolicyCmd = &cobra.Command{ var createImagePolicyCmd = &cobra.Command{
Use: "policy [name]", Use: "policy [name]",
Short: "Create or update an ImagePolicy object", Short: "Create or update an ImagePolicy object",
Long: `The create image policy command generates an ImagePolicy resource. Long: withPreviewNote(`The create image policy command generates an ImagePolicy resource.
An ImagePolicy object calculates a "latest image" given an image An ImagePolicy object calculates a "latest image" given an image
repository and a policy, e.g., semver. repository and a policy, e.g., semver.
The image that sorts highest according to the policy is recorded in The image that sorts highest according to the policy is recorded in
the status of the object.`, the status of the object.`),
Example: ` # Create an ImagePolicy to select the latest stable release Example: ` # Create an ImagePolicy to select the latest stable release
flux create image policy podinfo \ flux create image policy podinfo \
--image-ref=podinfo \ --image-ref=podinfo \
@ -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{}
@ -84,9 +83,6 @@ func (obj imagePolicyAdapter) getObservedGeneration() int64 {
} }
func createImagePolicyRun(cmd *cobra.Command, args []string) error { func createImagePolicyRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("ImagePolicy name is required")
}
objectName := args[0] objectName := args[0]
if imagePolicyArgs.imageRef == "" { if imagePolicyArgs.imageRef == "" {
@ -186,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

@ -26,14 +26,14 @@ import (
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
) )
var createImageRepositoryCmd = &cobra.Command{ var createImageRepositoryCmd = &cobra.Command{
Use: "repository [name]", Use: "repository [name]",
Short: "Create or update an ImageRepository object", Short: "Create or update an ImageRepository object",
Long: `The create image repository command generates an ImageRepository resource. Long: withPreviewNote(`The create image repository command generates an ImageRepository resource.
An ImageRepository object specifies an image repository to scan.`, An ImageRepository object specifies an image repository to scan.`),
Example: ` # Create an ImageRepository object to scan the alpine image repository: Example: ` # Create an ImageRepository object to scan the alpine image repository:
flux create image repository alpine-repo --image alpine --interval 20m flux create image repository alpine-repo --image alpine --interval 20m
@ -83,9 +83,6 @@ func init() {
} }
func createImageRepositoryRun(cmd *cobra.Command, args []string) error { func createImageRepositoryRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("ImageRepository name is required")
}
objectName := args[0] objectName := args[0]
if imageRepoArgs.image == "" { if imageRepoArgs.image == "" {

@ -22,16 +22,16 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" sourcev1 "github.com/fluxcd/source-controller/api/v1"
) )
var createImageUpdateCmd = &cobra.Command{ var createImageUpdateCmd = &cobra.Command{
Use: "update [name]", Use: "update [name]",
Short: "Create or update an ImageUpdateAutomation object", Short: "Create or update an ImageUpdateAutomation object",
Long: `The create image update command generates an ImageUpdateAutomation resource. Long: withPreviewNote(`The create image update command generates an ImageUpdateAutomation resource.
An ImageUpdateAutomation object specifies an automated update to images An ImageUpdateAutomation object specifies an automated update to images
mentioned in YAMLs in a git repository.`, mentioned in YAMLs in a git repository.`),
Example: ` # Configure image updates for the main repository created by flux bootstrap Example: ` # Configure image updates for the main repository created by flux bootstrap
flux create image update flux-system \ flux create image update flux-system \
--git-repo-ref=flux-system \ --git-repo-ref=flux-system \
@ -49,25 +49,40 @@ mentioned in YAMLs in a git repository.`,
--push-branch=image-updates \ --push-branch=image-updates \
--author-name=flux \ --author-name=flux \
--author-email=flux@example.com \ --author-email=flux@example.com \
--commit-template="{{range .Updated.Images}}{{println .}}{{end}}"`, --commit-template="{{range .Updated.Images}}{{println .}}{{end}}"
# Configure image updates for a Git repository in a different namespace
flux create image update apps \
--namespace=apps \
--git-repo-ref=flux-system \
--git-repo-namespace=flux-system \
--git-repo-path="./clusters/my-cluster" \
--checkout-branch=main \
--push-branch=image-updates \
--author-name=flux \
--author-email=flux@example.com \
--commit-template="{{range .Updated.Images}}{{println .}}{{end}}"
`,
RunE: createImageUpdateRun, RunE: createImageUpdateRun,
} }
type imageUpdateFlags struct { type imageUpdateFlags struct {
gitRepoRef string gitRepoName string
gitRepoPath string gitRepoNamespace string
checkoutBranch string gitRepoPath string
pushBranch string checkoutBranch string
commitTemplate string pushBranch string
authorName string commitTemplate string
authorEmail string authorName string
authorEmail string
} }
var imageUpdateArgs = imageUpdateFlags{} var imageUpdateArgs = imageUpdateFlags{}
func init() { func init() {
flags := createImageUpdateCmd.Flags() flags := createImageUpdateCmd.Flags()
flags.StringVar(&imageUpdateArgs.gitRepoRef, "git-repo-ref", "", "the name of a GitRepository resource with details of the upstream Git repository") flags.StringVar(&imageUpdateArgs.gitRepoName, "git-repo-ref", "", "the name of a GitRepository resource with details of the upstream Git repository")
flags.StringVar(&imageUpdateArgs.gitRepoNamespace, "git-repo-namespace", "", "the namespace of the GitRepository resource, defaults to the ImageUpdateAutomation namespace")
flags.StringVar(&imageUpdateArgs.gitRepoPath, "git-repo-path", "", "path to the directory containing the manifests to be updated, defaults to the repository root") flags.StringVar(&imageUpdateArgs.gitRepoPath, "git-repo-path", "", "path to the directory containing the manifests to be updated, defaults to the repository root")
flags.StringVar(&imageUpdateArgs.checkoutBranch, "checkout-branch", "", "the branch to checkout") flags.StringVar(&imageUpdateArgs.checkoutBranch, "checkout-branch", "", "the branch to checkout")
flags.StringVar(&imageUpdateArgs.pushBranch, "push-branch", "", "the branch to push commits to, defaults to the checkout branch if not specified") flags.StringVar(&imageUpdateArgs.pushBranch, "push-branch", "", "the branch to push commits to, defaults to the checkout branch if not specified")
@ -79,12 +94,9 @@ func init() {
} }
func createImageUpdateRun(cmd *cobra.Command, args []string) error { func createImageUpdateRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("ImageUpdateAutomation name is required")
}
objectName := args[0] objectName := args[0]
if imageUpdateArgs.gitRepoRef == "" { if imageUpdateArgs.gitRepoName == "" {
return fmt.Errorf("a reference to a GitRepository is required (--git-repo-ref)") return fmt.Errorf("a reference to a GitRepository is required (--git-repo-ref)")
} }
@ -112,9 +124,10 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error {
Labels: labels, Labels: labels,
}, },
Spec: autov1.ImageUpdateAutomationSpec{ Spec: autov1.ImageUpdateAutomationSpec{
SourceRef: autov1.SourceReference{ SourceRef: autov1.CrossNamespaceSourceReference{
Kind: sourcev1.GitRepositoryKind, Kind: sourcev1.GitRepositoryKind,
Name: imageUpdateArgs.gitRepoRef, Name: imageUpdateArgs.gitRepoName,
Namespace: imageUpdateArgs.gitRepoNamespace,
}, },
GitSpec: &autov1.GitSpec{ GitSpec: &autov1.GitSpec{

@ -24,40 +24,38 @@ 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/v2"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
var createKsCmd = &cobra.Command{ var createKsCmd = &cobra.Command{
Use: "kustomization [name]", Use: "kustomization [name]",
Aliases: []string{"ks"}, Aliases: []string{"ks"},
Short: "Create or update a Kustomization resource", Short: "Create or update a Kustomization resource",
Long: "The kustomization source create command generates a Kustomize resource for a given source.", Long: `The create command generates a Kustomization resource for a given source.`,
Example: ` # Create a Kustomization resource from a source at a given path Example: ` # Create a Kustomization resource from a source at a given path
flux create kustomization contour \ flux create kustomization kyverno \
--source=GitRepository/contour \ --source=GitRepository/kyverno \
--path="./examples/contour/" \ --path="./config/release" \
--prune=true \ --prune=true \
--interval=10m \ --interval=60m \
--health-check="Deployment/contour.projectcontour" \ --wait=true \
--health-check="DaemonSet/envoy.projectcontour" \
--health-check-timeout=3m --health-check-timeout=3m
# Create a Kustomization resource that depends on the previous one # Create a Kustomization resource that depends on the previous one
flux create kustomization webapp \ flux create kustomization kyverno-policies \
--depends-on=contour \ --depends-on=kyverno \
--source=GitRepository/webapp \ --source=GitRepository/kyverno-policies \
--path="./deploy/overlays/dev" \ --path="./policies/flux" \
--prune=true \ --prune=true \
--interval=5m --interval=5m
@ -65,7 +63,14 @@ var createKsCmd = &cobra.Command{
flux create kustomization podinfo \ flux create kustomization podinfo \
--namespace=default \ --namespace=default \
--source=GitRepository/podinfo.flux-system \ --source=GitRepository/podinfo.flux-system \
--path="./deploy/overlays/dev" \ --path="./kustomize" \
--prune=true \
--interval=5m
# Create a Kustomization resource that references an OCIRepository
flux create kustomization podinfo \
--source=OCIRepository/podinfo \
--target-namespace=default \
--prune=true \ --prune=true \
--interval=5m --interval=5m
@ -78,18 +83,20 @@ var createKsCmd = &cobra.Command{
} }
type kustomizationFlags struct { type kustomizationFlags struct {
source flags.KustomizationSource source flags.KustomizationSource
path flags.SafeRelativePath path flags.SafeRelativePath
prune bool prune bool
dependsOn []string dependsOn []string
validation string validation string
healthCheck []string healthCheck []string
healthTimeout time.Duration healthTimeout time.Duration
saName string saName string
decryptionProvider flags.DecryptionProvider decryptionProvider flags.DecryptionProvider
decryptionSecret string decryptionSecret string
targetNamespace string targetNamespace string
wait bool wait bool
kubeConfigSecretRef string
retryInterval time.Duration
} }
var kustomizationArgs = NewKustomizationFlags() var kustomizationArgs = NewKustomizationFlags()
@ -107,7 +114,9 @@ func init() {
createKsCmd.Flags().Var(&kustomizationArgs.decryptionProvider, "decryption-provider", kustomizationArgs.decryptionProvider.Description()) createKsCmd.Flags().Var(&kustomizationArgs.decryptionProvider, "decryption-provider", kustomizationArgs.decryptionProvider.Description())
createKsCmd.Flags().StringVar(&kustomizationArgs.decryptionSecret, "decryption-secret", "", "set the Kubernetes secret name that contains the OpenPGP private keys used for sops decryption") createKsCmd.Flags().StringVar(&kustomizationArgs.decryptionSecret, "decryption-secret", "", "set the Kubernetes secret name that contains the OpenPGP private keys used for sops decryption")
createKsCmd.Flags().StringVar(&kustomizationArgs.targetNamespace, "target-namespace", "", "overrides the namespace of all Kustomization objects reconciled by this Kustomization") createKsCmd.Flags().StringVar(&kustomizationArgs.targetNamespace, "target-namespace", "", "overrides the namespace of all Kustomization objects reconciled by this Kustomization")
createKsCmd.Flags().StringVar(&kustomizationArgs.kubeConfigSecretRef, "kubeconfig-secret-ref", "", "the name of the Kubernetes Secret that contains a key with the kubeconfig file for connecting to a remote cluster")
createKsCmd.Flags().MarkDeprecated("validation", "this arg is no longer used, all resources are validated using server-side apply dry-run") createKsCmd.Flags().MarkDeprecated("validation", "this arg is no longer used, all resources are validated using server-side apply dry-run")
createKsCmd.Flags().DurationVar(&kustomizationArgs.retryInterval, "retry-interval", 0, "the interval at which to retry a previously failed reconciliation")
createCmd.AddCommand(createKsCmd) createCmd.AddCommand(createKsCmd)
} }
@ -119,9 +128,6 @@ func NewKustomizationFlags() kustomizationFlags {
} }
func createKsCmdRun(cmd *cobra.Command, args []string) error { func createKsCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("Kustomization name is required")
}
name := args[0] name := args[0]
if kustomizationArgs.path == "" { if kustomizationArgs.path == "" {
@ -163,6 +169,14 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
}, },
} }
if kustomizationArgs.kubeConfigSecretRef != "" {
kustomization.Spec.KubeConfig = &meta.KubeConfigReference{
SecretRef: meta.SecretKeyReference{
Name: kustomizationArgs.kubeConfigSecretRef,
},
}
}
if len(kustomizationArgs.healthCheck) > 0 && !kustomizationArgs.wait { if len(kustomizationArgs.healthCheck) > 0 && !kustomizationArgs.wait {
healthChecks := make([]meta.NamespacedObjectKindReference, 0) healthChecks := make([]meta.NamespacedObjectKindReference, 0)
for _, w := range kustomizationArgs.healthCheck { for _, w := range kustomizationArgs.healthCheck {
@ -225,6 +239,10 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
} }
} }
if kustomizationArgs.retryInterval > 0 {
kustomization.Spec.RetryInterval = &metav1.Duration{Duration: kustomizationArgs.retryInterval}
}
if createArgs.export { if createArgs.export {
return printExport(exportKs(&kustomization)) return printExport(exportKs(&kustomization))
} }
@ -232,7 +250,7 @@ func createKsCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }
@ -244,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)
@ -284,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,22 +22,21 @@ 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/v1beta1" notificationv1 "github.com/fluxcd/notification-controller/api/v1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
var createReceiverCmd = &cobra.Command{ var createReceiverCmd = &cobra.Command{
Use: "receiver [name]", Use: "receiver [name]",
Short: "Create or update a Receiver resource", Short: "Create or update a Receiver resource",
Long: "The create receiver command generates a Receiver resource.", Long: `The create receiver command generates a Receiver resource.`,
Example: ` # Create a Receiver Example: ` # Create a Receiver
flux create receiver github-receiver \ flux create receiver github-receiver \
--type github \ --type github \
@ -67,9 +66,6 @@ func init() {
} }
func createReceiverCmdRun(cmd *cobra.Command, args []string) error { func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("Receiver name is required")
}
name := args[0] name := args[0]
if receiverArgs.receiverType == "" { if receiverArgs.receiverType == "" {
@ -130,7 +126,7 @@ func createReceiverCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }
@ -142,13 +138,13 @@ 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)
logger.Successf("generated webhook URL %s", receiver.Status.URL) logger.Successf("generated webhook URL %s", receiver.Status.WebhookPath)
return nil return nil
} }
@ -182,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
}
}

@ -29,7 +29,7 @@ import (
var createSecretCmd = &cobra.Command{ var createSecretCmd = &cobra.Command{
Use: "secret", Use: "secret",
Short: "Create or update Kubernetes secrets", Short: "Create or update Kubernetes secrets",
Long: "The create source sub-commands generate Kubernetes secrets specific to Flux.", Long: `The create source sub-commands generate Kubernetes secrets specific to Flux.`,
} }
func init() { func init() {

@ -21,22 +21,25 @@ import (
"crypto/elliptic" "crypto/elliptic"
"fmt" "fmt"
"net/url" "net/url"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
) )
var createSecretGitCmd = &cobra.Command{ var createSecretGitCmd = &cobra.Command{
Use: "git [name]", Use: "git [name]",
Short: "Create or update a Kubernetes secret for Git authentication", Short: "Create or update a Kubernetes secret for Git authentication",
Long: `The create secret git command generates a Kubernetes secret with Git credentials. Long: `The create secret git command generates a Kubernetes secret with Git credentials.
For Git over SSH, the host and SSH keys are automatically generated and stored in the secret. For Git over SSH, the host and SSH keys are automatically generated and stored
For Git over HTTP/S, the provided basic authentication credentials are stored in the secret.`, in the secret.
For Git over HTTP/S, the provided basic authentication credentials or bearer
authentication token are stored in the secret.`,
Example: ` # Create a Git SSH authentication secret using an ECDSA P-521 curve public key Example: ` # Create a Git SSH authentication secret using an ECDSA P-521 curve public key
flux create secret git podinfo-auth \ flux create secret git podinfo-auth \
@ -85,7 +88,9 @@ type secretGitFlags struct {
rsaBits flags.RSAKeyBits rsaBits flags.RSAKeyBits
ecdsaCurve flags.ECDSACurve ecdsaCurve flags.ECDSACurve
caFile string caFile string
caCrtFile string
privateKeyFile string privateKeyFile string
bearerToken string
} }
var secretGitArgs = NewSecretGitFlags() var secretGitArgs = NewSecretGitFlags()
@ -98,7 +103,9 @@ func init() {
createSecretGitCmd.Flags().Var(&secretGitArgs.rsaBits, "ssh-rsa-bits", secretGitArgs.rsaBits.Description()) createSecretGitCmd.Flags().Var(&secretGitArgs.rsaBits, "ssh-rsa-bits", secretGitArgs.rsaBits.Description())
createSecretGitCmd.Flags().Var(&secretGitArgs.ecdsaCurve, "ssh-ecdsa-curve", secretGitArgs.ecdsaCurve.Description()) createSecretGitCmd.Flags().Var(&secretGitArgs.ecdsaCurve, "ssh-ecdsa-curve", secretGitArgs.ecdsaCurve.Description())
createSecretGitCmd.Flags().StringVar(&secretGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates") createSecretGitCmd.Flags().StringVar(&secretGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates")
createSecretGitCmd.Flags().StringVar(&secretGitArgs.caCrtFile, "ca-crt-file", "", "path to TLS CA certificate file used for validating self-signed certificates; takes precedence over --ca-file")
createSecretGitCmd.Flags().StringVar(&secretGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server") createSecretGitCmd.Flags().StringVar(&secretGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server")
createSecretGitCmd.Flags().StringVar(&secretGitArgs.bearerToken, "bearer-token", "", "bearer authentication token")
createSecretCmd.AddCommand(createSecretGitCmd) createSecretCmd.AddCommand(createSecretGitCmd)
} }
@ -112,9 +119,6 @@ func NewSecretGitFlags() secretGitFlags {
} }
func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { func createSecretGitCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("secret name is required")
}
name := args[0] name := args[0]
if secretGitArgs.url == "" { if secretGitArgs.url == "" {
return fmt.Errorf("url is required") return fmt.Errorf("url is required")
@ -138,19 +142,39 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error {
} }
switch u.Scheme { switch u.Scheme {
case "ssh": case "ssh":
keypair, err := sourcesecret.LoadKeyPairFromPath(secretGitArgs.privateKeyFile, secretGitArgs.password)
if err != nil {
return err
}
opts.Keypair = keypair
opts.SSHHostname = u.Host opts.SSHHostname = u.Host
opts.PrivateKeyPath = secretGitArgs.privateKeyFile
opts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(secretGitArgs.keyAlgorithm) opts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(secretGitArgs.keyAlgorithm)
opts.RSAKeyBits = int(secretGitArgs.rsaBits) opts.RSAKeyBits = int(secretGitArgs.rsaBits)
opts.ECDSACurve = secretGitArgs.ecdsaCurve.Curve opts.ECDSACurve = secretGitArgs.ecdsaCurve.Curve
opts.Password = secretGitArgs.password opts.Password = secretGitArgs.password
case "http", "https": case "http", "https":
if secretGitArgs.username == "" || secretGitArgs.password == "" { if (secretGitArgs.username == "" || secretGitArgs.password == "") && secretGitArgs.bearerToken == "" {
return fmt.Errorf("for Git over HTTP/S the username and password are required") return fmt.Errorf("for Git over HTTP/S the username and password, or a bearer token is required")
} }
opts.Username = secretGitArgs.username opts.Username = secretGitArgs.username
opts.Password = secretGitArgs.password opts.Password = secretGitArgs.password
opts.CAFilePath = secretGitArgs.caFile opts.BearerToken = secretGitArgs.bearerToken
if secretGitArgs.username != "" && secretGitArgs.password != "" && secretGitArgs.bearerToken != "" {
return fmt.Errorf("user credentials and bearer token cannot be used together")
}
// --ca-crt-file takes precedence over --ca-file.
if secretGitArgs.caCrtFile != "" {
opts.CACrt, err = os.ReadFile(secretGitArgs.caCrtFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
} else if secretGitArgs.caFile != "" {
opts.CAFile, err = os.ReadFile(secretGitArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
}
default: default:
return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme) return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme)
} }
@ -176,7 +200,7 @@ func createSecretGitCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }

@ -1,10 +1,21 @@
package main package main
import ( import (
"fmt"
"os"
"testing" "testing"
) )
func TestCreateGitSecret(t *testing.T) { func TestCreateGitSecret(t *testing.T) {
file, err := os.CreateTemp(t.TempDir(), "ca-crt")
if err != nil {
t.Fatal("could not create CA certificate file")
}
_, err = file.Write([]byte("ca-data"))
if err != nil {
t.Fatal("could not write to CA certificate file")
}
tests := []struct { tests := []struct {
name string name string
args string args string
@ -13,7 +24,7 @@ func TestCreateGitSecret(t *testing.T) {
{ {
name: "no args", name: "no args",
args: "create secret git", args: "create secret git",
assert: assertError("secret name is required"), assert: assertError("name is required"),
}, },
{ {
name: "basic secret", name: "basic secret",
@ -30,6 +41,21 @@ func TestCreateGitSecret(t *testing.T) {
args: "create secret git podinfo-auth --url=ssh://git@github.com/stefanprodan/podinfo --private-key-file=./testdata/create_secret/git/ecdsa-password.private --password=password --namespace=my-namespace --export", args: "create secret git podinfo-auth --url=ssh://git@github.com/stefanprodan/podinfo --private-key-file=./testdata/create_secret/git/ecdsa-password.private --password=password --namespace=my-namespace --export",
assert: assertGoldenFile("testdata/create_secret/git/git-ssh-secret-password.yaml"), assert: assertGoldenFile("testdata/create_secret/git/git-ssh-secret-password.yaml"),
}, },
{
name: "git authentication with bearer token",
args: "create secret git bearer-token-auth --url=https://github.com/stefanprodan/podinfo --bearer-token=ghp_baR2qnFF0O41WlucePL3udt2N9vVZS4R0hAS --namespace=my-namespace --export",
assert: assertGoldenFile("testdata/create_secret/git/git-bearer-token.yaml"),
},
{
name: "git authentication with CA certificate",
args: fmt.Sprintf("create secret git ca-crt --url=https://github.com/stefanprodan/podinfo --password=my-password --username=my-username --ca-crt-file=%s --namespace=my-namespace --export", file.Name()),
assert: assertGoldenFile("testdata/create_secret/git/secret-ca-crt.yaml"),
},
{
name: "git authentication with basic auth and bearer token",
args: "create secret git podinfo-auth --url=https://github.com/stefanprodan/podinfo --username=aaa --password=zzzz --bearer-token=aaaa --namespace=my-namespace --export",
assert: assertError("user credentials and bearer token cannot be used together"),
},
} }
for _, tt := range tests { for _, tt := range tests {

@ -19,13 +19,14 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
) )
var createSecretHelmCmd = &cobra.Command{ var createSecretHelmCmd = &cobra.Command{
@ -40,15 +41,8 @@ var createSecretHelmCmd = &cobra.Command{
--export > repo-auth.yaml --export > repo-auth.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \ sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place repo-auth.yaml --in-place repo-auth.yaml`,
# Create a Helm authentication secret using a custom TLS cert
flux create secret helm repo-auth \
--username=username \
--password=password \
--cert-file=./cert.crt \
--key-file=./key.crt \
--ca-file=./ca.crt`,
RunE: createSecretHelmCmdRun, RunE: createSecretHelmCmdRun,
} }
@ -61,16 +55,20 @@ type secretHelmFlags struct {
var secretHelmArgs secretHelmFlags var secretHelmArgs secretHelmFlags
func init() { func init() {
createSecretHelmCmd.Flags().StringVarP(&secretHelmArgs.username, "username", "u", "", "basic authentication username") flags := createSecretHelmCmd.Flags()
createSecretHelmCmd.Flags().StringVarP(&secretHelmArgs.password, "password", "p", "", "basic authentication password") flags.StringVarP(&secretHelmArgs.username, "username", "u", "", "basic authentication username")
initSecretTLSFlags(createSecretHelmCmd.Flags(), &secretHelmArgs.secretTLSFlags) flags.StringVarP(&secretHelmArgs.password, "password", "p", "", "basic authentication password")
initSecretDeprecatedTLSFlags(flags, &secretHelmArgs.secretTLSFlags)
deprecationMsg := "please use the command `flux create secret tls` to generate TLS secrets"
flags.MarkDeprecated("cert-file", deprecationMsg)
flags.MarkDeprecated("key-file", deprecationMsg)
flags.MarkDeprecated("ca-file", deprecationMsg)
createSecretCmd.AddCommand(createSecretHelmCmd) createSecretCmd.AddCommand(createSecretHelmCmd)
} }
func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error { func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("secret name is required")
}
name := args[0] name := args[0]
labels, err := parseLabels() labels, err := parseLabels()
@ -78,15 +76,34 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
caBundle := []byte{}
if secretHelmArgs.caFile != "" {
var err error
caBundle, err = os.ReadFile(secretHelmArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
}
var certFile, keyFile []byte
if secretHelmArgs.certFile != "" && secretHelmArgs.keyFile != "" {
if certFile, err = os.ReadFile(secretHelmArgs.certFile); err != nil {
return fmt.Errorf("failed to read cert file: %w", err)
}
if keyFile, err = os.ReadFile(secretHelmArgs.keyFile); err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
}
opts := sourcesecret.Options{ opts := sourcesecret.Options{
Name: name, Name: name,
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Labels: labels, Labels: labels,
Username: secretHelmArgs.username, Username: secretHelmArgs.username,
Password: secretHelmArgs.password, Password: secretHelmArgs.password,
CAFilePath: secretHelmArgs.caFile, CAFile: caBundle,
CertFilePath: secretHelmArgs.certFile, CertFile: certFile,
KeyFilePath: secretHelmArgs.keyFile, KeyFile: keyFile,
} }
secret, err := sourcesecret.Generate(opts) secret, err := sourcesecret.Generate(opts)
if err != nil { if err != nil {
@ -100,7 +117,7 @@ func createSecretHelmCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }

@ -1,3 +1,19 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main package main
import ( import (
@ -12,7 +28,7 @@ func TestCreateHelmSecret(t *testing.T) {
}{ }{
{ {
args: "create secret helm", args: "create secret helm",
assert: assertError("secret name is required"), assert: assertError("name is required"),
}, },
{ {
args: "create secret helm helm-secret --username=my-username --password=my-password --namespace=my-namespace --export", args: "create secret helm helm-secret --username=my-username --password=my-password --namespace=my-namespace --export",

@ -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)
})
}
}

@ -0,0 +1,121 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
var createSecretOCICmd = &cobra.Command{
Use: "oci [name]",
Short: "Create or update a Kubernetes image pull secret",
Long: withPreviewNote(`The create secret oci command generates a Kubernetes secret that can be used for OCIRepository authentication`),
Example: ` # Create an OCI authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret oci podinfo-auth \
--url=ghcr.io \
--username=username \
--password=password \
--export > repo-auth.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place repo-auth.yaml
`,
RunE: createSecretOCICmdRun,
}
type secretOCIFlags struct {
url string
password string
username string
}
var secretOCIArgs = secretOCIFlags{}
func init() {
createSecretOCICmd.Flags().StringVar(&secretOCIArgs.url, "url", "", "oci repository address e.g ghcr.io/stefanprodan/charts")
createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.username, "username", "u", "", "basic authentication username")
createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.password, "password", "p", "", "basic authentication password")
createSecretCmd.AddCommand(createSecretOCICmd)
}
func createSecretOCICmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("name is required")
}
secretName := args[0]
if secretOCIArgs.url == "" {
return fmt.Errorf("--url is required")
}
if secretOCIArgs.username == "" {
return fmt.Errorf("--username is required")
}
if secretOCIArgs.password == "" {
return fmt.Errorf("--password is required")
}
if _, err := name.ParseReference(secretOCIArgs.url); err != nil {
return fmt.Errorf("error parsing url: '%s'", err)
}
opts := sourcesecret.Options{
Name: secretName,
Namespace: *kubeconfigArgs.Namespace,
Registry: secretOCIArgs.url,
Password: secretOCIArgs.password,
Username: secretOCIArgs.username,
}
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("oci secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace)
return nil
}

@ -0,0 +1,51 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func TestCreateSecretOCI(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
args: "create secret oci",
assert: assertError("name is required"),
},
{
args: "create secret oci ghcr",
assert: assertError("--url is required"),
},
{
args: "create secret oci ghcr --namespace=my-namespace --url ghcr.io --username stefanprodan --password=password --export",
assert: assertGoldenFile("testdata/create_secret/oci/create-secret.yaml"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

@ -19,26 +19,28 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
) )
var createSecretTLSCmd = &cobra.Command{ var createSecretTLSCmd = &cobra.Command{
Use: "tls [name]", Use: "tls [name]",
Short: "Create or update a Kubernetes secret with TLS certificates", Short: "Create or update a Kubernetes secret with TLS certificates",
Long: `The create secret tls command generates a Kubernetes secret with certificates for use with TLS.`, Long: withPreviewNote(`The create secret tls command generates a Kubernetes secret with certificates for use with TLS.`),
Example: ` # Create a TLS secret on disk and encrypt it with Mozilla SOPS. Example: ` # Create a TLS secret on disk and encrypt it with Mozilla SOPS.
# Files are expected to be PEM-encoded. # Files are expected to be PEM-encoded.
flux create secret tls certs \ flux create secret tls certs \
--namespace=my-namespace \ --namespace=my-namespace \
--cert-file=./client.crt \ --tls-crt-file=./client.crt \
--key-file=./client.key \ --tls-key-file=./client.key \
--ca-crt-file=./ca.crt \
--export > certs.yaml --export > certs.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \ sops --encrypt --encrypted-regex '^(data|stringData)$' \
@ -47,29 +49,41 @@ var createSecretTLSCmd = &cobra.Command{
} }
type secretTLSFlags struct { type secretTLSFlags struct {
certFile string certFile string
keyFile string keyFile string
caFile string caFile string
caCrtFile string
tlsKeyFile string
tlsCrtFile string
} }
var secretTLSArgs secretTLSFlags var secretTLSArgs secretTLSFlags
func initSecretTLSFlags(flags *pflag.FlagSet, args *secretTLSFlags) { func initSecretDeprecatedTLSFlags(flags *pflag.FlagSet, args *secretTLSFlags) {
flags.StringVar(&args.certFile, "cert-file", "", "TLS authentication cert file path") flags.StringVar(&args.certFile, "cert-file", "", "TLS authentication cert file path")
flags.StringVar(&args.keyFile, "key-file", "", "TLS authentication key file path") flags.StringVar(&args.keyFile, "key-file", "", "TLS authentication key file path")
flags.StringVar(&args.caFile, "ca-file", "", "TLS authentication CA file path") flags.StringVar(&args.caFile, "ca-file", "", "TLS authentication CA file path")
} }
func initSecretTLSFlags(flags *pflag.FlagSet, args *secretTLSFlags) {
flags.StringVar(&args.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path")
flags.StringVar(&args.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path")
flags.StringVar(&args.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path")
}
func init() { func init() {
flags := createSecretTLSCmd.Flags() flags := createSecretTLSCmd.Flags()
initSecretDeprecatedTLSFlags(flags, &secretTLSArgs)
initSecretTLSFlags(flags, &secretTLSArgs) initSecretTLSFlags(flags, &secretTLSArgs)
flags.MarkDeprecated("cert-file", "please use --tls-crt-file instead")
flags.MarkDeprecated("key-file", "please use --tls-key-file instead")
flags.MarkDeprecated("ca-file", "please use --ca-crt-file instead")
createSecretCmd.AddCommand(createSecretTLSCmd) createSecretCmd.AddCommand(createSecretTLSCmd)
} }
func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error { func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("secret name is required")
}
name := args[0] name := args[0]
labels, err := parseLabels() labels, err := parseLabels()
@ -78,13 +92,39 @@ func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error {
} }
opts := sourcesecret.Options{ opts := sourcesecret.Options{
Name: name, Name: name,
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Labels: labels, Labels: labels,
CAFilePath: secretTLSArgs.caFile, }
CertFilePath: secretTLSArgs.certFile,
KeyFilePath: secretTLSArgs.keyFile, if secretTLSArgs.caCrtFile != "" {
opts.CACrt, err = os.ReadFile(secretTLSArgs.caCrtFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
} else if secretTLSArgs.caFile != "" {
opts.CAFile, err = os.ReadFile(secretTLSArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
} }
if secretTLSArgs.tlsCrtFile != "" && secretTLSArgs.tlsKeyFile != "" {
if opts.TLSCrt, err = os.ReadFile(secretTLSArgs.tlsCrtFile); err != nil {
return fmt.Errorf("failed to read cert file: %w", err)
}
if opts.TLSKey, err = os.ReadFile(secretTLSArgs.tlsKeyFile); err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
} else if secretTLSArgs.certFile != "" && secretTLSArgs.keyFile != "" {
if opts.CertFile, err = os.ReadFile(secretTLSArgs.certFile); err != nil {
return fmt.Errorf("failed to read cert file: %w", err)
}
if opts.KeyFile, err = os.ReadFile(secretTLSArgs.keyFile); err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
}
secret, err := sourcesecret.Generate(opts) secret, err := sourcesecret.Generate(opts)
if err != nil { if err != nil {
return err return err
@ -97,7 +137,7 @@ func createSecretTLSCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }

@ -4,7 +4,7 @@ import (
"testing" "testing"
) )
func TestCreateTlsSecretNoArgs(t *testing.T) { func TestCreateTlsSecret(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args string args string
@ -12,12 +12,16 @@ func TestCreateTlsSecretNoArgs(t *testing.T) {
}{ }{
{ {
args: "create secret tls", args: "create secret tls",
assert: assertError("secret name is required"), assert: assertError("name is required"),
}, },
{ {
args: "create secret tls certs --namespace=my-namespace --cert-file=./testdata/create_secret/tls/test-cert.pem --key-file=./testdata/create_secret/tls/test-key.pem --export", args: "create secret tls certs --namespace=my-namespace --tls-crt-file=./testdata/create_secret/tls/test-cert.pem --tls-key-file=./testdata/create_secret/tls/test-key.pem --ca-crt-file=./testdata/create_secret/tls/test-ca.pem --export",
assert: assertGoldenFile("testdata/create_secret/tls/secret-tls.yaml"), assert: assertGoldenFile("testdata/create_secret/tls/secret-tls.yaml"),
}, },
{
args: "create secret tls certs --namespace=my-namespace --cert-file=./testdata/create_secret/tls/test-cert.pem --key-file=./testdata/create_secret/tls/test-key.pem --ca-file=./testdata/create_secret/tls/test-ca.pem --export",
assert: assertGoldenFile("testdata/create_secret/tls/deprecated-secret-tls.yaml"),
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

@ -25,7 +25,7 @@ import (
var createSourceCmd = &cobra.Command{ var createSourceCmd = &cobra.Command{
Use: "source", Use: "source",
Short: "Create or update sources", Short: "Create or update sources",
Long: "The create source sub-commands generate sources.", Long: `The create source sub-commands generate sources.`,
} }
type createSourceFlags struct { type createSourceFlags struct {

@ -20,6 +20,7 @@ import (
"context" "context"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -30,17 +31,18 @@ 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"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/flux2/internal/flags" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
) )
var createSourceBucketCmd = &cobra.Command{ var createSourceBucketCmd = &cobra.Command{
Use: "bucket [name]", Use: "bucket [name]",
Short: "Create or update a Bucket source", Short: "Create or update a Bucket source",
Long: `The create source bucket command generates a Bucket resource and waits for it to be downloaded. Long: withPreviewNote(`The create source bucket command generates a Bucket resource and waits for it to be downloaded.
For Buckets with static authentication, the credentials are stored in a Kubernetes secret.`, For Buckets with static authentication, the credentials are stored in a Kubernetes secret.`),
Example: ` # Create a source for a Bucket using static authentication Example: ` # Create a source for a Bucket using static authentication
flux create source bucket podinfo \ flux create source bucket podinfo \
--bucket-name=podinfo \ --bucket-name=podinfo \
@ -61,17 +63,18 @@ For Buckets with static authentication, the credentials are stored in a Kubernet
} }
type sourceBucketFlags struct { type sourceBucketFlags struct {
name string name string
provider flags.SourceBucketProvider provider flags.SourceBucketProvider
endpoint string endpoint string
accessKey string accessKey string
secretKey string secretKey string
region string region string
insecure bool insecure bool
secretRef string secretRef string
ignorePaths []string
} }
var sourceBucketArgs = NewSourceBucketFlags() var sourceBucketArgs = newSourceBucketFlags()
func init() { func init() {
createSourceBucketCmd.Flags().Var(&sourceBucketArgs.provider, "provider", sourceBucketArgs.provider.Description()) createSourceBucketCmd.Flags().Var(&sourceBucketArgs.provider, "provider", sourceBucketArgs.provider.Description())
@ -82,20 +85,18 @@ func init() {
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.region, "region", "", "the bucket region") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.region, "region", "", "the bucket region")
createSourceBucketCmd.Flags().BoolVar(&sourceBucketArgs.insecure, "insecure", false, "for when connecting to a non-TLS S3 HTTP endpoint") createSourceBucketCmd.Flags().BoolVar(&sourceBucketArgs.insecure, "insecure", false, "for when connecting to a non-TLS S3 HTTP endpoint")
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.secretRef, "secret-ref", "", "the name of an existing secret containing credentials") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.secretRef, "secret-ref", "", "the name of an existing secret containing credentials")
createSourceBucketCmd.Flags().StringSliceVar(&sourceBucketArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in bucket resource (can specify multiple paths with commas: path1,path2)")
createSourceCmd.AddCommand(createSourceBucketCmd) createSourceCmd.AddCommand(createSourceBucketCmd)
} }
func NewSourceBucketFlags() sourceBucketFlags { func newSourceBucketFlags() sourceBucketFlags {
return sourceBucketFlags{ return sourceBucketFlags{
provider: flags.SourceBucketProvider(sourcev1.GenericBucketProvider), provider: flags.SourceBucketProvider(sourcev1.GenericBucketProvider),
} }
} }
func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error { func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("Bucket source name is required")
}
name := args[0] name := args[0]
if sourceBucketArgs.name == "" { if sourceBucketArgs.name == "" {
@ -117,6 +118,12 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
} }
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
var ignorePaths *string
if len(sourceBucketArgs.ignorePaths) > 0 {
ignorePathsStr := strings.Join(sourceBucketArgs.ignorePaths, "\n")
ignorePaths = &ignorePathsStr
}
bucket := &sourcev1.Bucket{ bucket := &sourcev1.Bucket{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
@ -132,6 +139,7 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
Interval: metav1.Duration{ Interval: metav1.Duration{
Duration: createArgs.interval, Duration: createArgs.interval,
}, },
Ignore: ignorePaths,
}, },
} }
@ -152,7 +160,7 @@ func createSourceBucketCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }
@ -195,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")

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

@ -0,0 +1,91 @@
//go:build unit
// +build unit
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import "testing"
func TestCreateSourceChart(t *testing.T) {
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setupSourceChart(t, tmpl)
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "missing name",
args: "create source chart --export",
assert: assertError("name is required"),
},
{
name: "missing source reference",
args: "create source chart podinfo --export ",
assert: assertError("chart source is required"),
},
{
name: "missing chart name",
args: "create source chart podinfo --source helmrepository/podinfo --export",
assert: assertError("chart name or path is required"),
},
{
name: "unknown source kind",
args: "create source chart podinfo --source foobar/podinfo --export",
assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`),
},
{
name: "basic chart",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/basic.yaml", tmpl),
},
{
name: "chart with basic signature verification",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_basic.yaml", tmpl),
},
{
name: "unknown signature verification provider",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider foobar --export",
assert: assertError(`invalid argument "foobar" for "--verify-provider" flag: source OCI verify provider 'foobar' is not supported, must be one of: cosign`),
},
{
name: "chart with complete signature verification",
args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --verify-issuer foo --verify-subject bar --export",
assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_complete.yaml", tmpl),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}
func setupSourceChart(t *testing.T, tmpl map[string]string) {
t.Helper()
testEnv.CreateObjectFile("./testdata/create_source_chart/setup-source.yaml", tmpl, t)
}

@ -22,23 +22,25 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"os" "os"
"strings"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"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"
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"
"github.com/fluxcd/flux2/internal/flags" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
) )
type sourceGitFlags struct { type sourceGitFlags struct {
@ -46,17 +48,19 @@ type sourceGitFlags struct {
branch string branch string
tag string tag string
semver string semver string
refName string
commit string
username string username string
password string password string
keyAlgorithm flags.PublicKeyAlgorithm keyAlgorithm flags.PublicKeyAlgorithm
keyRSABits flags.RSAKeyBits keyRSABits flags.RSAKeyBits
keyECDSACurve flags.ECDSACurve keyECDSACurve flags.ECDSACurve
secretRef string secretRef string
gitImplementation flags.GitImplementation
caFile string caFile string
privateKeyFile string privateKeyFile string
recurseSubmodules bool recurseSubmodules bool
silent bool silent bool
ignorePaths []string
} }
var createSourceGitCmd = &cobra.Command{ var createSourceGitCmd = &cobra.Command{
@ -113,6 +117,7 @@ For private Git repositories, the basic authentication credentials are stored in
# Create a source for a Git repository using basic authentication # Create a source for a Git repository using basic authentication
flux create source git podinfo \ flux create source git podinfo \
--url=https://github.com/stefanprodan/podinfo \ --url=https://github.com/stefanprodan/podinfo \
--branch=master \
--username=username \ --username=username \
--password=password`, --password=password`,
RunE: createSourceGitCmdRun, RunE: createSourceGitCmdRun,
@ -125,18 +130,20 @@ func init() {
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.branch, "branch", "", "git branch") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.branch, "branch", "", "git branch")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.tag, "tag", "", "git tag") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.tag, "tag", "", "git tag")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.semver, "tag-semver", "", "git tag semver range") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.semver, "tag-semver", "", "git tag semver range")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.refName, "ref-name", "", " git reference name")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.commit, "commit", "", "git commit")
createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.username, "username", "u", "", "basic authentication username") createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.username, "username", "u", "", "basic authentication username")
createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.password, "password", "p", "", "basic authentication password") createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.password, "password", "p", "", "basic authentication password")
createSourceGitCmd.Flags().Var(&sourceGitArgs.keyAlgorithm, "ssh-key-algorithm", sourceGitArgs.keyAlgorithm.Description()) createSourceGitCmd.Flags().Var(&sourceGitArgs.keyAlgorithm, "ssh-key-algorithm", sourceGitArgs.keyAlgorithm.Description())
createSourceGitCmd.Flags().Var(&sourceGitArgs.keyRSABits, "ssh-rsa-bits", sourceGitArgs.keyRSABits.Description()) createSourceGitCmd.Flags().Var(&sourceGitArgs.keyRSABits, "ssh-rsa-bits", sourceGitArgs.keyRSABits.Description())
createSourceGitCmd.Flags().Var(&sourceGitArgs.keyECDSACurve, "ssh-ecdsa-curve", sourceGitArgs.keyECDSACurve.Description()) createSourceGitCmd.Flags().Var(&sourceGitArgs.keyECDSACurve, "ssh-ecdsa-curve", sourceGitArgs.keyECDSACurve.Description())
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.secretRef, "secret-ref", "", "the name of an existing secret containing SSH or basic credentials") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.secretRef, "secret-ref", "", "the name of an existing secret containing SSH or basic credentials")
createSourceGitCmd.Flags().Var(&sourceGitArgs.gitImplementation, "git-implementation", sourceGitArgs.gitImplementation.Description())
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates")
createSourceGitCmd.Flags().StringVar(&sourceGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server")
createSourceGitCmd.Flags().BoolVar(&sourceGitArgs.recurseSubmodules, "recurse-submodules", false, createSourceGitCmd.Flags().BoolVar(&sourceGitArgs.recurseSubmodules, "recurse-submodules", false,
"when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces") "when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces")
createSourceGitCmd.Flags().BoolVarP(&sourceGitArgs.silent, "silent", "s", false, "assumes the deploy key is already setup, skips confirmation") createSourceGitCmd.Flags().BoolVarP(&sourceGitArgs.silent, "silent", "s", false, "assumes the deploy key is already setup, skips confirmation")
createSourceGitCmd.Flags().StringSliceVar(&sourceGitArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in git resource (can specify multiple paths with commas: path1,path2)")
createSourceCmd.AddCommand(createSourceGitCmd) createSourceCmd.AddCommand(createSourceGitCmd)
} }
@ -150,9 +157,6 @@ func newSourceGitFlags() sourceGitFlags {
} }
func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("GitRepository source name is required")
}
name := args[0] name := args[0]
if sourceGitArgs.url == "" { if sourceGitArgs.url == "" {
@ -167,16 +171,12 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme) return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme)
} }
if sourceGitArgs.branch == "" && sourceGitArgs.tag == "" && sourceGitArgs.semver == "" { if sourceGitArgs.branch == "" && sourceGitArgs.tag == "" && sourceGitArgs.semver == "" && sourceGitArgs.commit == "" && sourceGitArgs.refName == "" {
return fmt.Errorf("a Git ref is required, use one of the following: --branch, --tag or --tag-semver") return fmt.Errorf("a Git ref is required, use one of the following: --branch, --tag, --commit, --ref-name or --tag-semver")
} }
if sourceGitArgs.caFile != "" && u.Scheme == "ssh" { if sourceGitArgs.caFile != "" && u.Scheme == "ssh" {
return fmt.Errorf("specifing a CA file is not supported for Git over SSH") return fmt.Errorf("specifying a CA file is not supported for Git over SSH")
}
if sourceGitArgs.recurseSubmodules && sourceGitArgs.gitImplementation == sourcev1.LibGit2Implementation {
return fmt.Errorf("recurse submodules requires --git-implementation=%s", sourcev1.GoGitImplementation)
} }
tmpDir, err := os.MkdirTemp("", name) tmpDir, err := os.MkdirTemp("", name)
@ -190,6 +190,12 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
var ignorePaths *string
if len(sourceGitArgs.ignorePaths) > 0 {
ignorePathsStr := strings.Join(sourceGitArgs.ignorePaths, "\n")
ignorePaths = &ignorePathsStr
}
gitRepository := sourcev1.GitRepository{ gitRepository := sourcev1.GitRepository{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
@ -203,6 +209,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
}, },
RecurseSubmodules: sourceGitArgs.recurseSubmodules, RecurseSubmodules: sourceGitArgs.recurseSubmodules,
Reference: &sourcev1.GitRepositoryRef{}, Reference: &sourcev1.GitRepositoryRef{},
Ignore: ignorePaths,
}, },
} }
@ -210,11 +217,12 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
gitRepository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} gitRepository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout}
} }
if sourceGitArgs.gitImplementation != "" { if sourceGitArgs.commit != "" {
gitRepository.Spec.GitImplementation = sourceGitArgs.gitImplementation.String() gitRepository.Spec.Reference.Commit = sourceGitArgs.commit
} gitRepository.Spec.Reference.Branch = sourceGitArgs.branch
} else if sourceGitArgs.refName != "" {
if sourceGitArgs.semver != "" { gitRepository.Spec.Reference.Name = sourceGitArgs.refName
} else if sourceGitArgs.semver != "" {
gitRepository.Spec.Reference.SemVer = sourceGitArgs.semver gitRepository.Spec.Reference.SemVer = sourceGitArgs.semver
} else if sourceGitArgs.tag != "" { } else if sourceGitArgs.tag != "" {
gitRepository.Spec.Reference.Tag = sourceGitArgs.tag gitRepository.Spec.Reference.Tag = sourceGitArgs.tag
@ -235,7 +243,7 @@ func createSourceGitCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }
@ -249,16 +257,26 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error {
} }
switch u.Scheme { switch u.Scheme {
case "ssh": case "ssh":
keypair, err := sourcesecret.LoadKeyPairFromPath(sourceGitArgs.privateKeyFile, sourceGitArgs.password)
if err != nil {
return err
}
secretOpts.Keypair = keypair
secretOpts.SSHHostname = u.Host secretOpts.SSHHostname = u.Host
secretOpts.PrivateKeyPath = sourceGitArgs.privateKeyFile
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(sourceGitArgs.keyAlgorithm) secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(sourceGitArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(sourceGitArgs.keyRSABits) secretOpts.RSAKeyBits = int(sourceGitArgs.keyRSABits)
secretOpts.ECDSACurve = sourceGitArgs.keyECDSACurve.Curve secretOpts.ECDSACurve = sourceGitArgs.keyECDSACurve.Curve
secretOpts.Password = sourceGitArgs.password secretOpts.Password = sourceGitArgs.password
case "https": case "https":
if sourceGitArgs.caFile != "" {
caBundle, err := os.ReadFile(sourceGitArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
secretOpts.CAFile = caBundle
}
secretOpts.Username = sourceGitArgs.username secretOpts.Username = sourceGitArgs.username
secretOpts.Password = sourceGitArgs.password secretOpts.Password = sourceGitArgs.password
secretOpts.CAFilePath = sourceGitArgs.caFile
case "http": case "http":
logger.Warningf("insecure configuration: credentials configured for an HTTP URL") logger.Warningf("insecure configuration: credentials configured for an HTTP URL")
secretOpts.Username = sourceGitArgs.username secretOpts.Username = sourceGitArgs.username
@ -306,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")
@ -349,23 +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 := apimeta.FindStatusCondition(gitRepository.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
}
}

@ -21,15 +21,18 @@ package main
import ( import (
"context" "context"
"github.com/fluxcd/pkg/apis/meta" "testing"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" "time"
"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"
"testing"
"time" "github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
) )
var pollInterval = 50 * time.Millisecond var pollInterval = 50 * time.Millisecond
@ -83,6 +86,66 @@ func (r *reconciler) conditionFunc() (bool, error) {
return true, err return true, err
} }
func TestCreateSourceGitExport(t *testing.T) {
var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --ignore-paths .cosign,non-existent-dir/ -n default --interval 1m --export --timeout=" + testTimeout.String()
cases := []struct {
name string
args string
assert assertFunc
}{
{
"ExportSucceeded",
command,
assertGoldenFile("testdata/create_source_git/export.golden"),
},
{
name: "no args",
args: "create secret git",
assert: assertError("name is required"),
},
{
name: "source with commit",
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --commit=c88a2f41 --interval=1m0s --export",
assert: assertGoldenFile("./testdata/create_source_git/source-git-commit.yaml"),
},
{
name: "source with ref name",
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --ref-name=refs/heads/main --interval=1m0s --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-refname.yaml"),
},
{
name: "source with branch name and commit",
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --branch=main --commit=c88a2f41 --interval=1m0s --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-branch-commit.yaml"),
},
{
name: "source with semver",
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --tag-semver=v1.01 --interval=1m0s --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-semver.yaml"),
},
{
name: "source with git tag",
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --tag=test --interval=1m0s --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-tag.yaml"),
},
{
name: "source with git branch",
args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --branch=test --interval=1m0s --export",
assert: assertGoldenFile("testdata/create_source_git/source-git-branch.yaml"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tc.args,
assert: tc.assert,
}
cmd.runTestCmd(t)
})
}
}
func TestCreateSourceGit(t *testing.T) { func TestCreateSourceGit(t *testing.T) {
// Default command used for multiple tests // Default command used for multiple tests
var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --timeout=" + testTimeout.String() var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --timeout=" + testTimeout.String()
@ -96,25 +159,52 @@ func TestCreateSourceGit(t *testing.T) {
{ {
"NoArgs", "NoArgs",
"create source git", "create source git",
assertError("GitRepository source name is required"), assertError("name is required"),
nil, nil,
}, { }, {
"Succeeded", "Succeeded",
command, command,
assertGoldenFile("testdata/create_source_git/success.golden"), assertGoldenFile("testdata/create_source_git/success.golden"),
func(repo *sourcev1.GitRepository) { func(repo *sourcev1.GitRepository) {
meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionTrue, sourcev1.GitOperationSucceedReason, "succeeded message") newCondition := metav1.Condition{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
Reason: sourcev1.GitOperationSucceedReason,
Message: "succeeded message",
ObservedGeneration: repo.GetGeneration(),
}
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
repo.Status.Artifact = &sourcev1.Artifact{ repo.Status.Artifact = &sourcev1.Artifact{
Path: "some-path", Path: "some-path",
Revision: "v1", Revision: "v1",
LastUpdateTime: metav1.Time{
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) {
meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionFalse, sourcev1.URLInvalidReason, "failed message") stalledCondition := metav1.Condition{
Type: meta.StalledCondition,
Status: metav1.ConditionTrue,
Reason: sourcev1.URLInvalidReason,
Message: "failed message",
ObservedGeneration: repo.GetGeneration(),
}
apimeta.SetStatusCondition(&repo.Status.Conditions, stalledCondition)
newCondition := metav1.Condition{
Type: meta.ReadyCondition,
Status: metav1.ConditionFalse,
Reason: sourcev1.URLInvalidReason,
Message: "failed message",
ObservedGeneration: repo.GetGeneration(),
}
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
repo.Status.ObservedGeneration = repo.GetGeneration()
}, },
}, { }, {
"NoArtifact", "NoArtifact",
@ -122,7 +212,15 @@ func TestCreateSourceGit(t *testing.T) {
assertError("GitRepository source reconciliation completed but no artifact was found"), assertError("GitRepository source reconciliation completed but no artifact was found"),
func(repo *sourcev1.GitRepository) { func(repo *sourcev1.GitRepository) {
// Updated with no artifact // Updated with no artifact
meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionTrue, sourcev1.GitOperationSucceedReason, "succeeded message") newCondition := metav1.Condition{
Type: meta.ReadyCondition,
Status: metav1.ConditionTrue,
Reason: sourcev1.GitOperationSucceedReason,
Message: "succeeded message",
ObservedGeneration: repo.GetGeneration(),
}
apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition)
repo.Status.ObservedGeneration = repo.GetGeneration()
}, },
}, },
} }

@ -22,21 +22,20 @@ import (
"net/url" "net/url"
"os" "os"
"github.com/fluxcd/pkg/apis/meta"
"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"
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"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" "github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
) )
var createSourceHelmCmd = &cobra.Command{ var createSourceHelmCmd = &cobra.Command{
@ -44,23 +43,34 @@ var createSourceHelmCmd = &cobra.Command{
Short: "Create or update a HelmRepository source", Short: "Create or update a HelmRepository source",
Long: `The create source helm command generates a HelmRepository resource and waits for it to fetch the index. 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 a 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 \
--interval=10m --interval=10m
# Create a source for a Helm repository using basic authentication # Create a source for an HTTPS Helm repository using basic authentication
flux create source helm podinfo \ flux create source helm podinfo \
--url=https://stefanprodan.github.io/podinfo \ --url=https://stefanprodan.github.io/podinfo \
--username=username \ --username=username \
--password=password --password=password
# Create a source for a Helm repository using TLS authentication # Create a source for an HTTPS Helm repository using TLS authentication
flux create source helm podinfo \ flux create source helm podinfo \
--url=https://stefanprodan.github.io/podinfo \ --url=https://stefanprodan.github.io/podinfo \
--cert-file=./cert.crt \ --cert-file=./cert.crt \
--key-file=./key.crt \ --key-file=./key.crt \
--ca-file=./ca.crt`, --ca-file=./ca.crt
# Create a source for an OCI Helm repository
flux create source helm podinfo \
--url=oci://ghcr.io/stefanprodan/charts/podinfo \
--username=username \
--password=password
# Create a source for an OCI Helm repository using an existing secret with basic auth or dockerconfig credentials
flux create source helm podinfo \
--url=oci://ghcr.io/stefanprodan/charts/podinfo \
--secret-ref=docker-config`,
RunE: createSourceHelmCmdRun, RunE: createSourceHelmCmdRun,
} }
@ -72,6 +82,7 @@ type sourceHelmFlags struct {
keyFile string keyFile string
caFile string caFile string
secretRef string secretRef string
ociProvider string
passCredentials bool passCredentials bool
} }
@ -84,16 +95,14 @@ func init() {
createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.certFile, "cert-file", "", "TLS authentication cert file path") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.certFile, "cert-file", "", "TLS authentication cert file path")
createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.keyFile, "key-file", "", "TLS authentication key file path") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.keyFile, "key-file", "", "TLS authentication key file path")
createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.caFile, "ca-file", "", "TLS authentication CA file path") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.caFile, "ca-file", "", "TLS authentication CA file path")
createSourceHelmCmd.Flags().StringVarP(&sourceHelmArgs.secretRef, "secret-ref", "", "", "the name of an existing secret containing TLS or basic auth credentials") createSourceHelmCmd.Flags().StringVarP(&sourceHelmArgs.secretRef, "secret-ref", "", "", "the name of an existing secret containing TLS, basic auth or docker-config credentials")
createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.ociProvider, "oci-provider", "", "OCI provider for authentication")
createSourceHelmCmd.Flags().BoolVarP(&sourceHelmArgs.passCredentials, "pass-credentials", "", false, "pass credentials to all domains") createSourceHelmCmd.Flags().BoolVarP(&sourceHelmArgs.passCredentials, "pass-credentials", "", false, "pass credentials to all domains")
createSourceCmd.AddCommand(createSourceHelmCmd) createSourceCmd.AddCommand(createSourceHelmCmd)
} }
func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("HelmRepository source name is required")
}
name := args[0] name := args[0]
if sourceHelmArgs.url == "" { if sourceHelmArgs.url == "" {
@ -129,6 +138,15 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
}, },
} }
url, err := url.Parse(sourceHelmArgs.url)
if err != nil {
return fmt.Errorf("failed to parse URL: %w", err)
}
if url.Scheme == sourcev1.HelmRepositoryTypeOCI {
helmRepository.Spec.Type = sourcev1.HelmRepositoryTypeOCI
helmRepository.Spec.Provider = sourceHelmArgs.ociProvider
}
if createSourceArgs.fetchTimeout > 0 { if createSourceArgs.fetchTimeout > 0 {
helmRepository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} helmRepository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout}
} }
@ -147,11 +165,30 @@ func createSourceHelmCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }
caBundle := []byte{}
if sourceHelmArgs.caFile != "" {
var err error
caBundle, err = os.ReadFile(sourceHelmArgs.caFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
}
}
var certFile, keyFile []byte
if sourceHelmArgs.certFile != "" && sourceHelmArgs.keyFile != "" {
if certFile, err = os.ReadFile(sourceHelmArgs.certFile); err != nil {
return fmt.Errorf("failed to read cert file: %w", err)
}
if keyFile, err = os.ReadFile(sourceHelmArgs.keyFile); err != nil {
return fmt.Errorf("failed to read key file: %w", err)
}
}
logger.Generatef("generating HelmRepository source") logger.Generatef("generating HelmRepository source")
if sourceHelmArgs.secretRef == "" { if sourceHelmArgs.secretRef == "" {
secretName := fmt.Sprintf("helm-%s", name) secretName := fmt.Sprintf("helm-%s", name)
@ -160,9 +197,9 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error {
Namespace: *kubeconfigArgs.Namespace, Namespace: *kubeconfigArgs.Namespace,
Username: sourceHelmArgs.username, Username: sourceHelmArgs.username,
Password: sourceHelmArgs.password, Password: sourceHelmArgs.password,
CertFilePath: sourceHelmArgs.certFile, CAFile: caBundle,
KeyFilePath: sourceHelmArgs.keyFile, CertFile: certFile,
CAFilePath: sourceHelmArgs.caFile, KeyFile: keyFile,
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
} }
secret, err := sourcesecret.Generate(secretOpts) secret, err := sourcesecret.Generate(secretOpts)
@ -193,12 +230,21 @@ 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")
if helmRepository.Spec.Type == sourcev1.HelmRepositoryTypeOCI {
// OCI repos don't expose any artifact so we just return early here
return nil
}
if helmRepository.Status.Artifact == nil { if helmRepository.Status.Artifact == nil {
return fmt.Errorf("HelmRepository source reconciliation completed but no artifact was found") return fmt.Errorf("HelmRepository source reconciliation completed but no artifact was found")
} }
@ -236,28 +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
}
// Confirm the state we are observing is for the current generation
if helmRepository.Generation != helmRepository.Status.ObservedGeneration {
return false, nil
}
if c := apimeta.FindStatusCondition(helmRepository.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
}
}

@ -0,0 +1,81 @@
//go:build unit
// +build unit
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func TestCreateSourceHelm(t *testing.T) {
tests := []struct {
name string
args string
resultFile string
assertFunc string
}{
{
name: "no args",
args: "create source helm",
resultFile: "name is required",
assertFunc: "assertError",
},
{
name: "OCI repo",
args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --export",
resultFile: "./testdata/create_source_helm/oci.golden",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "OCI repo with Secret ref",
args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --secret-ref=creds --export",
resultFile: "./testdata/create_source_helm/oci-with-secret.golden",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "HTTPS repo",
args: "create source helm podinfo --url=https://stefanprodan.github.io/charts/podinfo --interval 5m --export",
resultFile: "./testdata/create_source_helm/https.golden",
assertFunc: "assertGoldenTemplateFile",
},
}
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setup(t, tmpl)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var assert assertFunc
switch tt.assertFunc {
case "assertGoldenTemplateFile":
assert = assertGoldenTemplateFile(tt.resultFile, tmpl)
case "assertError":
assert = assertError(tt.resultFile)
}
cmd := cmdTestCase{
args: tt.args + " -n " + tmpl["fluxns"],
assert: assert,
}
cmd.runTestCmd(t)
})
}
}

@ -0,0 +1,260 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"strings"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/pkg/apis/meta"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
)
var createSourceOCIRepositoryCmd = &cobra.Command{
Use: "oci [name]",
Short: "Create or update an OCIRepository",
Long: withPreviewNote(`The create source oci command generates an OCIRepository resource and waits for it to be ready.`),
Example: ` # Create an OCIRepository for a public container image
flux create source oci podinfo \
--url=oci://ghcr.io/stefanprodan/manifests/podinfo \
--tag=6.6.2 \
--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,
}
type sourceOCIRepositoryFlags struct {
url string
tag string
semver string
digest string
secretRef string
serviceAccount string
certSecretRef string
verifyProvider flags.SourceOCIVerifyProvider
verifySecretRef string
verifyOIDCIssuer string
verifySubject string
ignorePaths []string
provider flags.SourceOCIProvider
insecure bool
}
var sourceOCIRepositoryArgs = newSourceOCIFlags()
func newSourceOCIFlags() sourceOCIRepositoryFlags {
return sourceOCIRepositoryFlags{
provider: flags.SourceOCIProvider(sourcev1b2.GenericOCIProvider),
}
}
func init() {
createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description())
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.url, "url", "", "the OCI repository URL")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.tag, "tag", "", "the OCI artifact tag")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.semver, "tag-semver", "", "the OCI artifact tag semver range")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.digest, "digest", "", "the OCI artifact digest")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.serviceAccount, "service-account", "", "the name of the Kubernetes service account that refers to an image pull secret")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.certSecretRef, "cert-ref", "", "the name of a secret to use for TLS certificates")
createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description())
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification")
createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification")
createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)")
createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP")
createSourceCmd.AddCommand(createSourceOCIRepositoryCmd)
}
func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
if sourceOCIRepositoryArgs.url == "" {
return fmt.Errorf("url is required")
}
if sourceOCIRepositoryArgs.semver == "" && sourceOCIRepositoryArgs.tag == "" && sourceOCIRepositoryArgs.digest == "" {
return fmt.Errorf("--tag, --tag-semver or --digest is required")
}
sourceLabels, err := parseLabels()
if err != nil {
return err
}
var ignorePaths *string
if len(sourceOCIRepositoryArgs.ignorePaths) > 0 {
ignorePathsStr := strings.Join(sourceOCIRepositoryArgs.ignorePaths, "\n")
ignorePaths = &ignorePathsStr
}
repository := &sourcev1b2.OCIRepository{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: sourceLabels,
},
Spec: sourcev1b2.OCIRepositorySpec{
Provider: sourceOCIRepositoryArgs.provider.String(),
URL: sourceOCIRepositoryArgs.url,
Insecure: sourceOCIRepositoryArgs.insecure,
Interval: metav1.Duration{
Duration: createArgs.interval,
},
Reference: &sourcev1b2.OCIRepositoryRef{},
Ignore: ignorePaths,
},
}
if digest := sourceOCIRepositoryArgs.digest; digest != "" {
repository.Spec.Reference.Digest = digest
}
if semver := sourceOCIRepositoryArgs.semver; semver != "" {
repository.Spec.Reference.SemVer = semver
}
if tag := sourceOCIRepositoryArgs.tag; tag != "" {
repository.Spec.Reference.Tag = tag
}
if createSourceArgs.fetchTimeout > 0 {
repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout}
}
if saName := sourceOCIRepositoryArgs.serviceAccount; saName != "" {
repository.Spec.ServiceAccountName = saName
}
if secretName := sourceOCIRepositoryArgs.secretRef; secretName != "" {
repository.Spec.SecretRef = &meta.LocalObjectReference{
Name: secretName,
}
}
if secretName := sourceOCIRepositoryArgs.certSecretRef; secretName != "" {
repository.Spec.CertSecretRef = &meta.LocalObjectReference{
Name: secretName,
}
}
if provider := sourceOCIRepositoryArgs.verifyProvider.String(); provider != "" {
repository.Spec.Verify = &sourcev1.OCIRepositoryVerification{
Provider: provider,
}
if secretName := sourceOCIRepositoryArgs.verifySecretRef; secretName != "" {
repository.Spec.Verify.SecretRef = &meta.LocalObjectReference{
Name: secretName,
}
}
verifyIssuer := sourceOCIRepositoryArgs.verifyOIDCIssuer
verifySubject := sourceOCIRepositoryArgs.verifySubject
if verifyIssuer != "" || verifySubject != "" {
repository.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{
Issuer: verifyIssuer,
Subject: verifySubject,
}}
}
} else if sourceOCIRepositoryArgs.verifySecretRef != "" {
return fmt.Errorf("a verification provider must be specified when a secret is specified")
} else if sourceOCIRepositoryArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" {
return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified")
}
if createArgs.export {
return printExport(exportOCIRepository(repository))
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
logger.Actionf("applying OCIRepository")
namespacedName, err := upsertOCIRepository(ctx, kubeClient, repository)
if err != nil {
return err
}
logger.Waitingf("waiting for OCIRepository reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true,
isObjectReadyConditionFunc(kubeClient, namespacedName, repository)); err != nil {
return err
}
logger.Successf("OCIRepository reconciliation completed")
if repository.Status.Artifact == nil {
return fmt.Errorf("no artifact was found")
}
logger.Successf("fetched revision: %s", repository.Status.Artifact.Revision)
return nil
}
func upsertOCIRepository(ctx context.Context, kubeClient client.Client,
ociRepository *sourcev1b2.OCIRepository) (types.NamespacedName, error) {
namespacedName := types.NamespacedName{
Namespace: ociRepository.GetNamespace(),
Name: ociRepository.GetName(),
}
var existing sourcev1b2.OCIRepository
err := kubeClient.Get(ctx, namespacedName, &existing)
if err != nil {
if errors.IsNotFound(err) {
if err := kubeClient.Create(ctx, ociRepository); err != nil {
return namespacedName, err
} else {
logger.Successf("OCIRepository created")
return namespacedName, nil
}
}
return namespacedName, err
}
existing.Labels = ociRepository.Labels
existing.Spec = ociRepository.Spec
if err := kubeClient.Update(ctx, &existing); err != nil {
return namespacedName, err
}
ociRepository = &existing
logger.Successf("OCIRepository updated")
return namespacedName, nil
}

@ -0,0 +1,96 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func TestCreateSourceOCI(t *testing.T) {
tests := []struct {
name string
args string
assertFunc assertFunc
}{
{
name: "NoArgs",
args: "create source oci",
assertFunc: assertError("name is required"),
},
{
name: "NoURL",
args: "create source oci podinfo",
assertFunc: assertError("url is required"),
},
{
name: "verify secret specified but provider missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-secret-ref=cosign-pub",
assertFunc: assertError("a verification provider must be specified when a secret is specified"),
},
{
name: "verify issuer specified but provider missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github.com",
assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"),
},
{
name: "verify identity specified but provider missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=developer",
assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"),
},
{
name: "verify issuer specified but subject missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github --verify-provider=cosign --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"),
},
{
name: "all verify fields set",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github verify-subject=stefanprodan --verify-provider=cosign --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"),
},
{
name: "verify subject specified but issuer missing",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=stefanprodan --verify-provider=cosign --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_subject.golden"),
},
{
name: "export manifest",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --export",
assertFunc: assertGoldenFile("./testdata/oci/export.golden"),
},
{
name: "export manifest with secret",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --secret-ref=creds --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_secret.golden"),
},
{
name: "export manifest with verify secret",
args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export",
assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assertFunc,
}
cmd.runTestCmd(t)
})
}
}

@ -21,7 +21,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1" rbacv1 "k8s.io/api/rbac/v1"
@ -37,8 +37,8 @@ import (
var createTenantCmd = &cobra.Command{ var createTenantCmd = &cobra.Command{
Use: "tenant", Use: "tenant",
Short: "Create or update a tenant", Short: "Create or update a tenant",
Long: `The create tenant command generates namespaces, service accounts and role bindings to limit the Long: withPreviewNote(`The create tenant command generates namespaces, service accounts and role bindings to limit the
reconcilers scope to the tenant namespaces.`, reconcilers scope to the tenant namespaces.`),
Example: ` # Create a tenant with access to a namespace Example: ` # Create a tenant with access to a namespace
flux create tenant dev-team \ flux create tenant dev-team \
--with-namespace=frontend \ --with-namespace=frontend \
@ -70,9 +70,6 @@ func init() {
} }
func createTenantCmdRun(cmd *cobra.Command, args []string) error { func createTenantCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("tenant name is required")
}
tenant := args[0] tenant := args[0]
if err := validation.IsQualifiedName(tenant); len(err) > 0 { if err := validation.IsQualifiedName(tenant); len(err) > 0 {
return fmt.Errorf("invalid tenant name '%s': %v", tenant, err) return fmt.Errorf("invalid tenant name '%s': %v", tenant, err)
@ -159,7 +156,7 @@ func createTenantCmdRun(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }

@ -0,0 +1,55 @@
package main
import (
"testing"
"k8s.io/apimachinery/pkg/util/rand"
)
func Test_validateObjectName(t *testing.T) {
tests := []struct {
name string
valid bool
}{
{
name: "flux-system",
valid: true,
},
{
name: "-flux-system",
valid: false,
},
{
name: "-flux-system-",
valid: false,
},
{
name: "third.first",
valid: false,
},
{
name: "THirdfirst",
valid: false,
},
{
name: "THirdfirst",
valid: false,
},
{
name: rand.String(63),
valid: true,
},
{
name: rand.String(64),
valid: false,
},
}
for _, tt := range tests {
valid := validateObjectName(tt.name)
if valid != tt.valid {
t.Errorf("expected name %q to return %t for validateObjectName func but got %t",
tt.name, tt.valid, valid)
}
}
}

@ -24,13 +24,13 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
) )
var deleteCmd = &cobra.Command{ var deleteCmd = &cobra.Command{
Use: "delete", Use: "delete",
Short: "Delete sources and resources", Short: "Delete sources and resources",
Long: "The delete sub-commands delete sources and resources.", Long: `The delete sub-commands delete sources and resources.`,
} }
type deleteFlags struct { type deleteFlags struct {
@ -60,7 +60,7 @@ func (del deleteCommand) run(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()
kubeClient, err := utils.KubeClient(kubeconfigArgs) kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil { if err != nil {
return err return err
} }

@ -19,13 +19,13 @@ package main
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
) )
var deleteAlertCmd = &cobra.Command{ var deleteAlertCmd = &cobra.Command{
Use: "alert [name]", Use: "alert [name]",
Short: "Delete a Alert resource", Short: "Delete a Alert resource",
Long: "The delete alert command removes the given Alert from the cluster.", Long: withPreviewNote("The delete alert command removes the given Alert from the cluster."),
Example: ` # Delete an Alert and the Kubernetes resources created by it Example: ` # Delete an Alert and the Kubernetes resources created by it
flux delete alert main`, flux delete alert main`,
ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)),

@ -19,13 +19,13 @@ package main
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
) )
var deleteAlertProviderCmd = &cobra.Command{ var deleteAlertProviderCmd = &cobra.Command{
Use: "alert-provider [name]", Use: "alert-provider [name]",
Short: "Delete a Provider resource", Short: "Delete a Provider resource",
Long: "The delete alert-provider command removes the given Provider from the cluster.", Long: withPreviewNote("The delete alert-provider command removes the given Provider from the cluster."),
Example: ` # Delete a Provider and the Kubernetes resources created by it Example: ` # Delete a Provider and the Kubernetes resources created by it
flux delete alert-provider slack`, flux delete alert-provider slack`,
ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)),

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Flux authors Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -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/v2"
) )
var deleteHelmReleaseCmd = &cobra.Command{ var deleteHelmReleaseCmd = &cobra.Command{

@ -23,7 +23,7 @@ import (
var deleteImageCmd = &cobra.Command{ var deleteImageCmd = &cobra.Command{
Use: "image", Use: "image",
Short: "Delete image automation objects", Short: "Delete image automation objects",
Long: "The delete image sub-commands delete image automation objects.", Long: `The delete image sub-commands delete image automation objects.`,
} }
func init() { func init() {

@ -19,13 +19,13 @@ package main
import ( import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2"
) )
var deleteImagePolicyCmd = &cobra.Command{ var deleteImagePolicyCmd = &cobra.Command{
Use: "policy [name]", Use: "policy [name]",
Short: "Delete an ImagePolicy object", Short: "Delete an ImagePolicy object",
Long: "The delete image policy command deletes the given ImagePolicy from the cluster.", Long: withPreviewNote(`The delete image policy command deletes the given ImagePolicy from the cluster.`),
Example: ` # Delete an image policy Example: ` # Delete an image policy
flux delete image policy alpine3.x`, flux delete image policy alpine3.x`,
ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)),

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

Loading…
Cancel
Save