Compare commits

..

No commits in common. 'main' and 'v0.6.1' have entirely different histories.
main ... v0.6.1

@ -1,84 +0,0 @@
---
name: Bug report
description: Create a report to help us improve Flux
body:
- type: markdown
attributes:
value: |
## Support
Find out more about your support options and getting help at: https://fluxcd.io/support/
- type: textarea
validations:
required: true
attributes:
label: Describe the bug
description: A clear description of what the bug is.
- type: textarea
validations:
required: true
attributes:
label: Steps to reproduce
description: |
Steps to reproduce the problem.
placeholder: |
For example:
1. Install Flux with the additional image automation controllers
2. Run command '...'
3. See error
- type: textarea
validations:
required: true
attributes:
label: Expected behavior
description: A brief description of what you expected to happen.
- type: textarea
attributes:
label: Screenshots and recordings
description: |
If applicable, add screenshots to help explain your problem. You can also record an asciinema session: https://asciinema.org/
- type: input
validations:
required: true
attributes:
label: OS / Distro
description: The OS / distro you are executing `flux` on. If not applicable, write `N/A`.
placeholder: e.g. Windows 10, Ubuntu 20.04, Arch Linux, macOS 10.15...
- type: input
validations:
required: true
attributes:
label: Flux version
description: Run `flux version --client`. If not applicable, write `N/A`.
placeholder: e.g. v0.20.1
- type: textarea
validations:
required: true
attributes:
label: Flux check
description: Run `flux check`. If not applicable, write `N/A`.
placeholder: |
For example:
► checking prerequisites
✔ Kubernetes 1.21.1 >=1.19.0-0
► checking controllers
✔ all checks passed
- type: input
attributes:
label: Git provider
description: If applicable, add the Git provider you are having problems with, e.g. GitHub (Enterprise), GitLab, etc.
- type: input
attributes:
label: Container Registry provider
description: If applicable, add the Container Registry provider you are having problems with, e.g. DockerHub, GitHub Packages, Quay.io, etc.
- type: textarea
attributes:
label: Additional context
description: Add any other context about the problem here. This can be logs (e.g. output from `flux logs`), environment specific caveats, etc.
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/fluxcd/.github/blob/main/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

@ -1,5 +0,0 @@
blank_issues_enabled: true
contact_links:
- name: Ask a question
url: https://github.com/fluxcd/flux2/discussions
about: Please ask and answer questions here.

@ -4,16 +4,14 @@ pkgbase = flux-bin
pkgrel = ${PKGREL} pkgrel = ${PKGREL}
url = https://fluxcd.io/ url = https://fluxcd.io/
arch = x86_64 arch = x86_64
arch = armv6h
arch = armv7h arch = armv7h
arch = aarch64 arch = aarch64
license = APACHE license = APACHE
optdepends = bash-completion: auto-completion for flux in Bash optdepends = kubectl
optdepends = zsh-completions: auto-completion for flux in ZSH source_x86_64 = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_linux_amd64.tar.gz
source_x86_64 = flux-bin-${PKGVER}_linux_amd64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_amd64.tar.gz source_armv6h = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_linux_arm.tar.gz
sha256sums_x86_64 = ${SHA256SUM_AMD64} source_armv7h = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_linux_arm.tar.gz
source_armv7h = flux-bin-${PKGVER}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm.tar.gz source_aarch64 = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_linux_arm64.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,26 +4,29 @@
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" "armv7h" "aarch64") arch=("x86_64" "armv6h" "armv7h" "aarch64")
license=("APACHE") license=("APACHE")
optdepends=('bash-completion: auto-completion for flux in Bash' optdepends=("kubectl")
'zsh-completions: auto-completion for flux in ZSH')
source_x86_64=( source_x86_64=(
"${pkgname}-${pkgver}_linux_amd64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_amd64.tar.gz" "$pkgname-$pkgver.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${pkgver}/flux_${pkgver}_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}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm.tar.gz" "$pkgname-$pkgver.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${pkgver}/flux_${pkgver}_linux_arm.tar.gz"
) )
source_aarch64=( source_aarch64=(
"${pkgname}-${pkgver}_linux_arm64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm64.tar.gz" "$pkgname-$pkgver.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${pkgver}/flux_${pkgver}_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}
) )
@ -32,9 +35,5 @@ sha256sums_aarch64=(
) )
package() { package() {
install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" install -Dm755 flux "$pkgdir/usr/bin/flux"
"${pkgdir}/usr/bin/${_srcname}" completion bash | install -Dm644 /dev/stdin "${pkgdir}/usr/share/bash-completion/completions/${_srcname}"
"${pkgdir}/usr/bin/${_srcname}" completion fish | install -Dm644 /dev/stdin "${pkgdir}/usr/share/fish/vendor_completions.d/${_srcname}.fish"
"${pkgdir}/usr/bin/${_srcname}" completion zsh | install -Dm644 /dev/stdin "${pkgdir}/usr/share/zsh/site-functions/_${_srcname}"
} }

@ -28,7 +28,6 @@ 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
@ -37,12 +36,12 @@ else
export PKGREL=1 export PKGREL=1
fi fi
export SHA256SUM_ARM=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_arm.tar.gz | awk '{ print $1 }') export SHA256SUM_ARM=$(sha256sum ${ROOT}/dist/flux_${PKGVER}_linux_arm.tar.gz | awk '{ print $1 }')
export SHA256SUM_ARM64=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_arm64.tar.gz | awk '{ print $1 }') export SHA256SUM_ARM64=$(sha256sum ${ROOT}/dist/flux_${PKGVER}_linux_arm64.tar.gz | awk '{ print $1 }')
export SHA256SUM_AMD64=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_amd64.tar.gz | awk '{ print $1 }') export SHA256SUM_AMD64=$(sha256sum ${ROOT}/dist/flux_${PKGVER}_linux_amd64.tar.gz | awk '{ print $1 }')
envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < .SRCINFO.template > $GITDIR/.SRCINFO envsubst '$PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < .SRCINFO.template > $GITDIR/.SRCINFO
envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < PKGBUILD.template > $GITDIR/PKGBUILD envsubst '$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,14 +4,16 @@ 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
makedepends = go makedepends = go
depends = glibc depends = glibc
optdepends = kubectl
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${VERSION}.tar.gz source = flux-go-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/archive/v${PKGVER}.tar.gz
pkgname = flux-go pkgname = flux-go

@ -4,56 +4,40 @@
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" "armv7h" "aarch64") arch=("x86_64" "armv6h" "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.20', 'kustomize>=5.0') makedepends=("go")
optdepends=('bash-completion: auto-completion for flux in Bash', optdepends=("kubectl")
'zsh-completions: auto-completion for flux in ZSH')
source=( source=(
"${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/archive/v${_srcver}.tar.gz" "$pkgname-$pkgver.tar.gz::https://github.com/fluxcd/flux2/archive/v$pkgver.tar.gz"
) )
sha256sums=( sha256sums=(
${SHA256SUM} ${SHA256SUM}
) )
build() { build() {
cd "flux2-${_srcver}" cd "flux2-$pkgver"
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 -ldflags=-linkmode=external -mod=readonly -modcacherw"
make cmd/flux/.manifests.done go build -ldflags "-X main.VERSION=$pkgver" -o flux-bin ./cmd/flux
go build -ldflags "-linkmode=external -X main.VERSION=${_srcver}" -o ${_srcname} ./cmd/flux
} }
check() { check() {
cd "flux2-${_srcver}" cd "flux2-$pkgver"
case $CARCH in
aarch64)
export ENVTEST_ARCH=arm64
;;
armv7h)
export ENVTEST_ARCH=arm
;;
esac
make test make test
} }
package() { package() {
cd "flux2-${_srcver}" cd "flux2-$pkgver"
install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" install -Dm755 flux-bin "$pkgdir/usr/bin/flux"
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
"${pkgdir}/usr/bin/${_srcname}" completion bash | install -Dm644 /dev/stdin "${pkgdir}/usr/share/bash-completion/completions/${_srcname}"
"${pkgdir}/usr/bin/${_srcname}" completion fish | install -Dm644 /dev/stdin "${pkgdir}/usr/share/fish/vendor_completions.d/${_srcname}.fish"
"${pkgdir}/usr/bin/${_srcname}" completion zsh | install -Dm644 /dev/stdin "${pkgdir}/usr/share/zsh/site-functions/_${_srcname}"
} }

@ -28,7 +28,6 @@ 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
@ -37,10 +36,10 @@ else
export PKGREL=1 export PKGREL=1
fi fi
export SHA256SUM=$(curl -sL https://github.com/fluxcd/flux2/archive/v${VERSION}.tar.gz | sha256sum | awk '{ print $1 }') export SHA256SUM=$(curl -sL https://github.com/fluxcd/flux2/archive/v$PKGVER.tar.gz | sha256sum | awk '{ print $1 }')
envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM' < .SRCINFO.template > $GITDIR/.SRCINFO envsubst '$PKGVER $PKGREL $SHA256SUM' < .SRCINFO.template > $GITDIR/.SRCINFO
envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM' < PKGBUILD.template > $GITDIR/PKGBUILD envsubst '$PKGVER $PKGREL $SHA256SUM' < PKGBUILD.template > $GITDIR/PKGBUILD
cd $GITDIR cd $GITDIR
git config user.name "fluxcdbot" git config user.name "fluxcdbot"

@ -4,11 +4,13 @@ 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
makedepends = go makedepends = go
depends = glibc depends = glibc
optdepends = kubectl
provides = flux-bin provides = flux-bin
conflicts = flux-bin conflicts = flux-bin
source = git+https://github.com/fluxcd/flux2.git source = git+https://github.com/fluxcd/flux2.git

@ -4,17 +4,15 @@
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" "armv7h" "aarch64") arch=("x86_64" "armv6h" "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.20', 'kustomize>=5.0', 'git') makedepends=("go")
optdepends=('bash-completion: auto-completion for flux in Bash', optdepends=("kubectl")
'zsh-completions: auto-completion for flux in ZSH')
source=( source=(
"git+https://github.com/fluxcd/flux2.git" "git+https://github.com/fluxcd/flux2.git"
) )
@ -31,30 +29,17 @@ build() {
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 -ldflags=-linkmode=external -mod=readonly -modcacherw"
make cmd/flux/.manifests.done go build -ldflags "-X main.VERSION=$pkgver" -o flux-bin ./cmd/flux
go build -ldflags "-linkmode=external -X main.VERSION=${pkgver}" -o ${_srcname} ./cmd/flux
} }
check() { check() {
cd "flux2" cd "flux2"
case $CARCH in
aarch64)
export ENVTEST_ARCH=arm64
;;
armv7h)
export ENVTEST_ARCH=arm
;;
esac
make test make test
} }
package() { package() {
cd "flux2" cd "flux2"
install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" install -Dm755 flux-bin "$pkgdir/usr/bin/flux"
install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
"${pkgdir}/usr/bin/${_srcname}" completion bash | install -Dm644 /dev/stdin "${pkgdir}/usr/share/bash-completion/completions/${_srcname}"
"${pkgdir}/usr/bin/${_srcname}" completion fish | install -Dm644 /dev/stdin "${pkgdir}/usr/share/fish/vendor_completions.d/${_srcname}.fish"
"${pkgdir}/usr/bin/${_srcname}" completion zsh | install -Dm644 /dev/stdin "${pkgdir}/usr/share/zsh/site-functions/_${_srcname}"
} }

@ -28,7 +28,6 @@ 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

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

@ -1,55 +0,0 @@
# 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.3.x
description: To be backported to release/v2.3.x
color: '#ffd700'
- name: backport:release/v2.4.x
description: To be backported to release/v2.4.x
color: '#ffd700'
- name: backport:release/v2.5.x
description: To be backported to release/v2.5.x
color: '#ffd700'

@ -1,82 +0,0 @@
# Flux ARM64 GitHub runners
The Flux ARM64 end-to-end tests run on Equinix Metal instances provisioned with Docker and GitHub self-hosted runners.
## Current instances
| Repository | Runner | Instance | Location |
|-----------------------------|------------------|----------------|---------------|
| flux2 | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| flux2 | equinix-arm-dc-2 | flux-arm-dc-01 | Washington DC |
| flux2 | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| flux2 | equinix-arm-da-2 | flux-arm-da-01 | Dallas |
| flux-benchmark | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| flux-benchmark | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| source-controller | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| source-controller | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
| image-automation-controller | equinix-arm-dc-1 | flux-arm-dc-01 | Washington DC |
| image-automation-controller | equinix-arm-da-1 | flux-arm-da-01 | Dallas |
Instance spec:
- Ampere Altra Q80-30 80-core processor @ 2.8GHz
- 2 x 960GB NVME
- 256GB RAM
- 2 x 25Gbps
## Instance setup
In order to add a new runner to the GitHub Actions pool,
first create a server on Equinix with the following configuration:
- Type: `c3.large.arm64`
- OS: `Ubuntu 22.04 LTS`
### Install prerequisites
- SSH into a newly created instance
```shell
ssh root@<instance-public-IP>
```
- Create the ubuntu user
```shell
adduser ubuntu
usermod -aG sudo ubuntu
su - ubuntu
```
- Create the prerequisites dir
```shell
mkdir -p prereq && cd prereq
```
- Download the prerequisites script
```shell
curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/prereq.sh > prereq.sh \
&& chmod +x ./prereq.sh
```
- Install the prerequisites
```shell
sudo ./prereq.sh
```
### Install runners
- Retrieve the GitHub runner token from the repository [settings page](https://github.com/fluxcd/flux2/settings/actions/runners/new?arch=arm64&os=linux)
- Create two directories `flux2-01`, `flux2-02`
- In each dir run:
```shell
curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/runner-setup.sh > runner-setup.sh \
&& chmod +x ./runner-setup.sh
./runner-setup.sh equinix-arm-<NUMBER> <TOKEN> <REPO>
```
- Reboot the instance
```shell
sudo reboot
```
- Navigate to the GitHub repository [runners page](https://github.com/fluxcd/flux2/settings/actions/runners) and check the runner status

@ -1,72 +0,0 @@
#!/usr/bin/env bash
# Copyright 2021 The Flux authors. All rights reserved.
#
# 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.
# This script installs the prerequisites for running Flux end-to-end tests with Docker and GitHub self-hosted runners.
set -eu
KIND_VERSION=0.22.0
KUBECTL_VERSION=1.29.0
KUSTOMIZE_VERSION=5.3.0
HELM_VERSION=3.14.1
GITHUB_RUNNER_VERSION=2.313.0
PACKAGES="apt-transport-https ca-certificates software-properties-common build-essential libssl-dev gnupg lsb-release jq pkg-config"
# install prerequisites
apt-get update \
&& apt-get install -y -q ${PACKAGES} \
&& apt-get clean \
&& 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
curl -fsSL https://get.docker.com -o get-docker.sh \
&& chmod +x get-docker.sh
./get-docker.sh
systemctl enable docker.service
systemctl enable containerd.service
usermod -aG docker ubuntu
# install kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v${KIND_VERSION}/kind-linux-arm64
install -o root -g root -m 0755 kind /usr/local/bin/kind
# install kubectl
curl -LO "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/arm64/kubectl"
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
# install kustomize
curl -Lo ./kustomize.tar.gz https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2Fv${KUSTOMIZE_VERSION}/kustomize_v${KUSTOMIZE_VERSION}_linux_arm64.tar.gz \
&& tar -zxvf kustomize.tar.gz \
&& rm kustomize.tar.gz
install -o root -g root -m 0755 kustomize /usr/local/bin/kustomize
# install helm
curl -Lo ./helm.tar.gz https://get.helm.sh/helm-v${HELM_VERSION}-linux-arm64.tar.gz \
&& tar -zxvf helm.tar.gz \
&& rm helm.tar.gz
install -o root -g root -m 0755 linux-arm64/helm /usr/local/bin/helm
# 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 \
&& tar xzf actions-runner-linux-arm64.tar.gz \
&& rm actions-runner-linux-arm64.tar.gz
# install runner dependencies
./bin/installdependencies.sh

@ -1,37 +0,0 @@
#!/usr/bin/env bash
# Copyright 2021 The Flux authors. All rights reserved.
#
# 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.
# This script installs a GitHub self-hosted ARM64 runner for running Flux end-to-end tests.
set -eu
RUNNER_NAME=$1
REPOSITORY_TOKEN=$2
REPOSITORY_URL=${3:-https://github.com/fluxcd/flux2}
GITHUB_RUNNER_VERSION=2.313.0
# 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 \
&& tar xzf actions-runner-linux-arm64.tar.gz \
&& rm actions-runner-linux-arm64.tar.gz
# register runner with GitHub
./config.sh --unattended --url ${REPOSITORY_URL} --token ${REPOSITORY_TOKEN} --name ${RUNNER_NAME}
# start runner
sudo ./svc.sh install
sudo ./svc.sh start

@ -1,50 +0,0 @@
# 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/> |

@ -1,29 +0,0 @@
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup flux
uses: ./action

@ -1,34 +0,0 @@
name: backport
on:
pull_request_target:
types: [closed, labeled]
permissions:
contents: read
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Create backport PRs
uses: korthout/backport-action@be567af183754f6a5d831ae90f648954763f17f5 # v3.1.0
# xref: https://github.com/korthout/backport-action#inputs
with:
# Use token to allow workflows to be triggered for the created PR
github_token: ${{ secrets.BOT_GITHUB_TOKEN }}
# Match labels with a pattern `backport:<target-branch>`
label_pattern: '^backport:([^ ]+)$'
# A bit shorter pull-request title than the default
pull_title: '[${target_branch}] ${pull_title}'
# Simpler PR description than default
pull_description: |-
Automated backport to `${target_branch}`, triggered by a label in #${pull_number}.

@ -0,0 +1,77 @@
name: bootstrap
on:
push:
branches:
- '*'
jobs:
github:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Restore Go cache
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.15.x
- name: Setup Kubernetes
uses: engineerd/setup-kind@v0.5.0
- name: Set outputs
id: vars
run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)"
- name: Build
run: sudo go build -o ./bin/flux ./cmd/flux
- name: bootstrap init
run: |
./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--repository=flux-test-${{ steps.vars.outputs.sha_short }} \
--branch=main \
--path=test-cluster
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: bootstrap no-op
run: |
./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--repository=flux-test-${{ steps.vars.outputs.sha_short }} \
--branch=main \
--path=test-cluster
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: uninstall
run: |
./bin/flux uninstall --resources --crds -s --timeout=10m
- name: bootstrap reinstall
run: |
./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--repository=flux-test-${{ steps.vars.outputs.sha_short }} \
--branch=main \
--path=test-cluster
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: delete repository
run: |
./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--repository=flux-test-${{ steps.vars.outputs.sha_short }} \
--branch=main \
--path=test-cluster \
--delete
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Debug failure
if: failure()
run: |
kubectl -n flux-system get all
kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller

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

@ -0,0 +1,65 @@
name: Publish docs via GitHub Pages
on:
push:
branches:
- docs*
- main
jobs:
build:
name: Deploy docs
runs-on: ubuntu-latest
steps:
- name: Checkout master
uses: actions/checkout@v1
- name: Copy assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
controller_version() {
sed -n "s/.*$1\/archive\/\(.*\).zip.*/\1/p;n" manifests/bases/$1/kustomization.yaml
}
{
# source-controller CRDs
SOURCE_VER=$(controller_version source-controller)
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/source-controller/$SOURCE_VER/docs/api/source.md" > docs/components/source/api.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/source-controller/$SOURCE_VER/docs/spec/v1beta1/gitrepositories.md" > docs/components/source/gitrepositories.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/source-controller/$SOURCE_VER/docs/spec/v1beta1/helmrepositories.md" > docs/components/source/helmrepositories.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/source-controller/$SOURCE_VER/docs/spec/v1beta1/helmcharts.md" > docs/components/source/helmcharts.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/source-controller/$SOURCE_VER/docs/spec/v1beta1/buckets.md" > docs/components/source/buckets.md
}
{
# kustomize-controller CRDs
KUSTOMIZE_VER=$(controller_version kustomize-controller)
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/kustomize-controller/$KUSTOMIZE_VER/docs/api/kustomize.md" > docs/components/kustomize/api.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/kustomize-controller/$KUSTOMIZE_VER/docs/spec/v1beta1/kustomization.md" > docs/components/kustomize/kustomization.md
}
{
# helm-controller CRDs
HELM_VER=$(controller_version helm-controller)
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/helm-controller/$HELM_VER/docs/api/helmrelease.md" > docs/components/helm/api.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/helm-controller/$HELM_VER/docs/spec/v2beta1/helmreleases.md" > docs/components/helm/helmreleases.md
}
{
# notification-controller CRDs
NOTIFICATION_VER=$(controller_version notification-controller)
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/notification-controller/$NOTIFICATION_VER/docs/api/notification.md" > docs/components/notification/api.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/notification-controller/$NOTIFICATION_VER/docs/spec/v1beta1/event.md" > docs/components/notification/event.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/notification-controller/$NOTIFICATION_VER/docs/spec/v1beta1/alert.md" > docs/components/notification/alert.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/notification-controller/$NOTIFICATION_VER/docs/spec/v1beta1/provider.md" > docs/components/notification/provider.md
curl -# -Lf "https://raw.githubusercontent.com/fluxcd/notification-controller/$NOTIFICATION_VER/docs/spec/v1beta1/receiver.md" > docs/components/notification/receiver.md
}
{
# install script
cp install/flux.sh docs/install.sh
}
- name: Deploy docs
uses: mhausenblas/mkdocs-deploy-gh-pages@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CUSTOM_DOMAIN: toolkit.fluxcd.io

@ -1,93 +0,0 @@
name: e2e-azure
on:
workflow_dispatch:
schedule:
- cron: '0 6 * * *'
push:
branches:
- main
paths:
- 'tests/**'
- '.github/workflows/e2e-azure.yaml'
pull_request:
branches:
- main
paths:
- 'tests/**'
- '.github/workflows/e2e-azure.yaml'
permissions:
contents: read
jobs:
e2e-aks:
runs-on: ubuntu-22.04
defaults:
run:
working-directory: ./tests/integration
# This job is currently disabled. Remove the false check when Azure subscription is enabled.
if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps:
- name: CheckoutD
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: 1.23.x
cache-dependency-path: tests/integration/go.sum
- name: Setup Terraform
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
- name: Setup Flux CLI
run: make build
working-directory: ./
- name: Setup SOPS
run: |
mkdir -p $HOME/.local/bin
wget -O $HOME/.local/bin/sops https://github.com/mozilla/sops/releases/download/v$SOPS_VER/sops-v$SOPS_VER.linux
chmod +x $HOME/.local/bin/sops
env:
SOPS_VER: 3.7.1
- name: Authenticate to Azure
uses: Azure/login@a65d910e8af852a8061c627c456678983e180302 # v1.4.6
with:
creds: '{"clientId":"${{ secrets.AZ_ARM_CLIENT_ID }}","clientSecret":"${{ secrets.AZ_ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZ_ARM_TENANT_ID }}"}'
- name: Set dynamic variables in .env
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 Azure e2e tests
env:
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
TF_VAR_location: ${{ vars.TF_VAR_azure_location }}
GITREPO_SSH_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_CONTENTS }}
GITREPO_SSH_PUB_CONTENTS: ${{ secrets.AZURE_GITREPO_SSH_PUB_CONTENTS }}
run: |
source .env
mkdir -p ./build/ssh
touch ./build/ssh/key
echo $GITREPO_SSH_CONTENTS | base64 -d > build/ssh/key
export GITREPO_SSH_PATH=build/ssh/key
touch ./build/ssh/key.pub
echo $GITREPO_SSH_PUB_CONTENTS | base64 -d > ./build/ssh/key.pub
export GITREPO_SSH_PUB_PATH=build/ssh/key.pub
make test-azure
- name: Ensure resource cleanup
if: ${{ always() }}
env:
ARM_CLIENT_ID: ${{ secrets.AZ_ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.AZ_ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZ_ARM_TENANT_ID }}
TF_VAR_azuredevops_org: ${{ secrets.TF_VAR_azuredevops_org }}
TF_VAR_azuredevops_pat: ${{ secrets.TF_VAR_azuredevops_pat }}
TF_VAR_location: ${{ vars.TF_VAR_azure_location }}
run: source .env && make destroy-azure

@ -1,128 +0,0 @@
name: e2e-bootstrap
on:
workflow_dispatch:
push:
branches: [ 'main', 'release/**' ]
pull_request:
branches: [ 'main', 'release/**' ]
paths-ignore: [ 'docs/**', 'rfcs/**' ]
permissions:
contents: read
jobs:
e2e-boostrap-github:
runs-on: ubuntu-latest
if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]'
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: 1.23.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Kubernetes
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0
with:
version: v0.24.0
cluster_name: kind
# The versions below should target the newest Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes
node_image: ghcr.io/fluxcd/kindest/node:v1.31.0-amd64
kubectl_version: v1.31.0
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
- name: Setup yq
uses: fluxcd/pkg/actions/yq@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
- name: Build
run: make build-dev
- name: Set outputs
id: vars
run: |
REPOSITORY_NAME=${{ github.event.repository.name }}
BRANCH_NAME=${GITHUB_REF##*/}
COMMIT_SHA=$(git rev-parse HEAD)
PSEUDO_RAND_SUFFIX=$(echo "${BRANCH_NAME}-${COMMIT_SHA}" | shasum | awk '{print $1}')
TEST_REPO_NAME="${REPOSITORY_NAME}-${PSEUDO_RAND_SUFFIX}"
echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT
- name: bootstrap init
run: |
./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--image-pull-secret=ghcr-auth \
--registry-creds=fluxcd:$GITHUB_TOKEN \
--repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \
--path=test-cluster \
--team=team-z
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: verify image pull secret
run: |
kubectl -n flux-system get secret ghcr-auth | grep dockerconfigjson
- name: bootstrap no-op
run: |
./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--image-pull-secret=ghcr-auth \
--repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \
--path=test-cluster \
--team=team-z
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: bootstrap customize
run: |
make setup-bootstrap-patch
./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \
--path=test-cluster \
--team=team-z
if [ $(kubectl get deployments.apps source-controller -o jsonpath='{.spec.template.spec.securityContext.runAsUser}') != "10000" ]; then
echo "Bootstrap not customized as controller is not running as user 10000" && exit 1
fi
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }}
GITHUB_ORG_NAME: fluxcd-testing
- name: uninstall
run: |
./bin/flux uninstall -s --keep-namespace
kubectl delete ns flux-system --timeout=10m --wait=true
- name: test image automation
run: |
make setup-image-automation
./bin/flux bootstrap github --manifests ./manifests/install/ \
--owner=fluxcd-testing \
--repository=${{ steps.vars.outputs.test_repo_name }} \
--branch=main \
--path=test-cluster \
--read-write-key
./bin/flux reconcile image repository podinfo
./bin/flux reconcile image update flux-system
./bin/flux get images all
kubectl -n flux-system get -o yaml ImageUpdateAutomation flux-system | \
yq '.status.lastPushCommit | length > 1' | grep 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }}
GITHUB_ORG_NAME: fluxcd-testing
- name: delete repository
if: ${{ always() }}
continue-on-error: true
run: |
gh repo delete fluxcd-testing/${{ steps.vars.outputs.test_repo_name }} --yes
env:
GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }}
- name: Debug failure
if: failure()
run: |
kubectl -n flux-system get all
kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller

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

@ -1,56 +1,34 @@
name: e2e name: e2e
on: on:
workflow_dispatch:
push:
branches: [ 'main', 'release/**' ]
pull_request: pull_request:
branches: [ 'main', 'release/**' ] push:
paths-ignore: [ 'docs/**', 'rfcs/**' ] branches:
- main
permissions:
contents: read
jobs: jobs:
e2e-amd64-kubernetes: kind:
runs-on: runs-on: ubuntu-latest
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@v2
- name: Restore Go cache
uses: actions/cache@v1
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Setup Go - name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 uses: actions/setup-go@v2
with: with:
go-version: 1.23.x go-version: 1.15.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Setup Kubernetes - name: Setup Kubernetes
uses: helm/kind-action@a1b0e391336a6ee6713a0583f8c6240d70863de3 # v1.12.0 uses: engineerd/setup-kind@v0.5.0
with: with:
version: v0.24.0 image: kindest/node:v1.16.9
cluster_name: kind - name: Run test
wait: 5s
config: .github/kind/config.yaml # disable KIND-net
# The versions below should target the oldest supported Kubernetes version
# Keep this up-to-date with https://endoflife.date/kubernetes
node_image: ghcr.io/fluxcd/kindest/node:v1.30.9-amd64
kubectl_version: v1.30.9
- name: Setup Calico for network policy
run: |
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
- name: Run tests
run: make test run: make test
- name: Run e2e tests
run: TEST_KUBECONFIG=$HOME/.kube/config make e2e
- name: Check if working tree is dirty - name: Check if working tree is dirty
run: | run: |
if [[ $(git diff --stat) != '' ]]; then if [[ $(git diff --stat) != '' ]]; then
@ -59,32 +37,23 @@ jobs:
exit 1 exit 1
fi fi
- name: Build - name: Build
run: make build-dev run: sudo go build -o ./bin/flux ./cmd/flux
- name: flux check --pre - name: flux check --pre
run: | run: |
./bin/flux check --pre ./bin/flux check --pre
- name: flux install --manifests - name: flux install --manifests
run: | run: |
./bin/flux install --manifests ./manifests/install/ ./bin/flux install --manifests ./manifests/install/
- name: flux create secret
run: |
./bin/flux create secret git git-ssh-test \
--url ssh://git@github.com/stefanprodan/podinfo
./bin/flux create secret git git-https-test \
--url https://github.com/stefanprodan/podinfo \
--username=test --password=test
./bin/flux create secret helm helm-test \
--username=test --password=test
- name: flux create source git - name: flux create source git
run: | run: |
./bin/flux create source git podinfo \ ./bin/flux create source git podinfo \
--url https://github.com/stefanprodan/podinfo \ --url https://github.com/stefanprodan/podinfo \
--tag-semver=">=6.3.5" --tag-semver=">=3.2.3"
- name: flux create source git export apply - name: flux create source git export apply
run: | run: |
./bin/flux create source git podinfo-export \ ./bin/flux create source git podinfo-export \
--url https://github.com/stefanprodan/podinfo \ --url https://github.com/stefanprodan/podinfo \
--tag-semver=">=6.3.5" \ --tag-semver=">=3.2.3" \
--export | kubectl apply -f - --export | kubectl apply -f -
./bin/flux delete source git podinfo-export --silent ./bin/flux delete source git podinfo-export --silent
- name: flux get sources git - name: flux get sources git
@ -100,15 +69,10 @@ jobs:
--path="./deploy/overlays/dev" \ --path="./deploy/overlays/dev" \
--prune=true \ --prune=true \
--interval=5m \ --interval=5m \
--validation=client \
--health-check="Deployment/frontend.dev" \ --health-check="Deployment/frontend.dev" \
--health-check="Deployment/backend.dev" \ --health-check="Deployment/backend.dev" \
--health-check-timeout=3m --health-check-timeout=3m
- name: flux trace
run: |
./bin/flux trace frontend \
--kind=deployment \
--api-version=apps/v1 \
--namespace=dev
- name: flux reconcile kustomization --with-source - name: flux reconcile kustomization --with-source
run: | run: |
./bin/flux reconcile kustomization podinfo --with-source ./bin/flux reconcile kustomization podinfo --with-source
@ -139,9 +103,9 @@ jobs:
run: | run: |
./bin/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 \
--chart=podinfo \ --chart=podinfo \
--chart-version=">6.0.0 <7.0.0" --chart-version=">4.0.0 <5.0.0"
- name: flux create helmrelease --source=GitRepository/podinfo - name: flux create helmrelease --source=GitRepository/podinfo
run: | run: |
./bin/flux create hr podinfo-git \ ./bin/flux create hr podinfo-git \
@ -172,36 +136,6 @@ jobs:
- name: flux delete source git - name: flux delete source git
run: | run: |
./bin/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: |
./bin/flux create tenant dev-team --with-namespace=apps ./bin/flux create tenant dev-team --with-namespace=apps
@ -210,46 +144,42 @@ jobs:
./bin/flux -n apps create hr podinfo-helm \ ./bin/flux -n apps create hr podinfo-helm \
--source=HelmRepository/podinfo \ --source=HelmRepository/podinfo \
--chart=podinfo \ --chart=podinfo \
--chart-version="6.3.x" \ --chart-version="5.0.x" \
--service-account=dev-team --service-account=dev-team
- name: flux create image repository
run: |
./bin/flux create image repository podinfo \
--image=ghcr.io/stefanprodan/podinfo \
--interval=1m
- name: flux create image policy
run: |
./bin/flux create image policy podinfo \
--image-ref=podinfo \
--interval=1m \
--semver=5.0.x
- name: flux get image policy
run: |
./bin/flux get image policy podinfo | grep '5.0.3'
- name: flux2-kustomize-helm-example - name: flux2-kustomize-helm-example
run: | run: |
./bin/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
./bin/flux create kustomization flux-system \ ./bin/flux create kustomization flux-system \
--source=flux-system \ --source=flux-system \
--path=./clusters/staging --path=./clusters/staging
kubectl -n flux-system wait kustomization/infra-controllers --for=condition=ready --timeout=5m kubectl -n flux-system wait kustomization/apps --for=condition=ready --timeout=2m
kubectl -n flux-system wait kustomization/apps --for=condition=ready --timeout=5m
kubectl -n podinfo wait helmrelease/podinfo --for=condition=ready --timeout=5m
- name: flux tree
run: |
./bin/flux tree kustomization flux-system | grep Service/podinfo
- name: flux events
run: |
./bin/flux -n flux-system events --for Kustomization/apps | grep 'HelmRelease/podinfo'
./bin/flux -n podinfo events --for HelmRelease/podinfo | grep 'podinfo.v1'
- name: flux stats
run: |
./bin/flux stats -A
- name: flux check - name: flux check
run: | run: |
./bin/flux check ./bin/flux check
- name: flux version
run: |
./bin/flux version
- name: flux uninstall - name: flux uninstall
run: | run: |
./bin/flux uninstall --silent ./bin/flux uninstall --crds --silent --timeout=10m
- name: Debug failure - name: Debug failure
if: failure() if: failure()
run: | run: |
kubectl version --client --short kubectl version --client --short
kubectl -n flux-system get all kubectl -n flux-system get all
kubectl -n flux-system describe pods
kubectl -n flux-system get kustomizations -oyaml kubectl -n flux-system get kustomizations -oyaml
kubectl -n flux-system logs deploy/source-controller kubectl -n flux-system logs deploy/source-controller
kubectl -n flux-system logs deploy/kustomize-controller kubectl -n flux-system logs deploy/kustomize-controller

@ -0,0 +1,25 @@
name: FOSSA
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: "^1.14.x"
- name: Add GOPATH to GITHUB_ENV
run: echo "GOPATH=$(go env GOPATH)" >>"$GITHUB_ENV"
- name: Add GOPATH to GITHUB_PATH
run: echo "$GOPATH/bin" >>"$GITHUB_PATH"
- name: Run FOSSA scan and upload build data
uses: fossa-contrib/fossa-action@v1
with:
# FOSSA Push-Only API Token
fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de
github-token: ${{ github.token }}

@ -1,39 +0,0 @@
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run analysis
uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with:
results_file: results.sarif
results_format: sarif
repo_token: ${{ secrets.GITHUB_TOKEN }}
publish_results: true
- name: Upload artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: SARIF file
path: results.sarif
retention-days: 5
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
with:
sarif_file: results.sarif

@ -0,0 +1,21 @@
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 }}

@ -2,199 +2,72 @@ name: release
on: on:
push: push:
tags: ["v*"] tags:
- '*'
permissions:
contents: read
jobs: jobs:
release-flux-cli: goreleaser:
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@v2
- name: Unshallow - name: Unshallow
run: git fetch --prune --unshallow run: git fetch --prune --unshallow
- name: Setup Go - name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 uses: actions/setup-go@v2
with:
go-version: 1.23.x
cache: false
- name: Setup QEMU
uses: docker/setup-qemu-action@4574d27a4764455b42196d70a065bc6853246a25 # v3.4.0
- name: Setup Docker Buildx
id: buildx
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
- name: Setup Syft
uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
- name: Setup Cosign
uses: sigstore/cosign-installer@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3.8.0
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
- name: Login to GitHub Container Registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to Docker Hub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
- name: Generate manifests
run: |
make cmd/flux/.manifests.done
./manifests/scripts/bundle.sh "" ./output manifests.tar.gz
kustomize build ./manifests/install > ./output/install.yaml
- name: Build CRDs
run: |
kustomize build manifests/crds > all-crds.yaml
- name: Generate OpenAPI JSON schemas from CRDs
uses: fluxcd/pkg/actions/crdjsonschema@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
with: with:
crd: all-crds.yaml go-version: 1.15.x
output: schemas - name: Download release notes utility
- name: Archive the OpenAPI JSON schemas env:
GH_REL_URL: https://github.com/buchanae/github-release-notes/releases/download/0.2.0/github-release-notes-linux-amd64-0.2.0.tar.gz
run: cd /tmp && curl -sSL ${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/
- name: Generate release notes
run: | run: |
tar -czvf ./output/crd-schemas.tar.gz -C schemas . echo 'CHANGELOG' > /tmp/release.txt
- name: Run GoReleaser github-release-notes -org fluxcd -repo toolkit -since-latest-release -include-author >> /tmp/release.txt
id: run-goreleaser
uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0
with:
version: latest
args: release --skip=validate
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Kustomize - name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main uses: fluxcd/pkg//actions/kustomize@main
- name: Setup Flux CLI - name: Generate manifests tarball
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@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: fluxcdbot
password: ${{ secrets.GITHUB_TOKEN }}
- name: Login to DockerHub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
username: fluxcdbot
password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }}
- name: Push manifests to GHCR
run: | run: |
mkdir -p ./ghcr.io/flux-system mkdir -p ./output
flux install --registry=ghcr.io/fluxcd \ files=""
--components-extra=image-reflector-controller,image-automation-controller \
--export > ./ghcr.io/flux-system/gotk-components.yaml
cd ./ghcr.io && flux push artifact \ # build controllers
oci://ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ for controller in ./manifests/bases/*/; do
--path="./flux-system" \ output_path="./output/$(basename $controller).yaml"
--source=${{ github.repositoryUrl }} \ echo "building $controller to $output_path"
--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@c56c2d3e59e4281cc41dea2217323ba5694b171e # v3.8.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 }} \ kustomize build $controller > $output_path
--tag latest files+=" $(basename $output_path)"
done
release-provenance: # build rbac
needs: [release-flux-cli] rbac_path="./manifests/rbac"
permissions: rbac_output_path="./output/rbac.yaml"
actions: read # for detecting the Github Actions environment. echo "building $rbac_path to $rbac_output_path"
id-token: write # for creating OIDC tokens for signing. kustomize build $rbac_path > $rbac_output_path
contents: write # for uploading attestations to GitHub releases. files+=" $(basename $rbac_output_path)"
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: # build policies
needs: [release-flux-cli] policies_path="./manifests/policies"
permissions: policies_output_path="./output/policies.yaml"
actions: read # for detecting the Github Actions environment. echo "building $policies_path to $policies_output_path"
id-token: write # for creating OIDC tokens for signing. kustomize build $policies_path > $policies_output_path
packages: write # for uploading attestations. files+=" $(basename $policies_output_path)"
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: # create tarball
needs: [release-flux-cli] cd ./output && tar -cvzf manifests.tar.gz $files
permissions: - name: Generate install manifest
actions: read # for detecting the Github Actions environment. run: |
id-token: write # for creating OIDC tokens for signing. kustomize build ./manifests/install > ./output/install.yaml
packages: write # for uploading attestations. - name: Run GoReleaser
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 uses: goreleaser/goreleaser-action@v1
with: with:
image: ghcr.io/${{ needs.release-flux-cli.outputs.image_url }} version: latest
digest: ${{ needs.release-flux-cli.outputs.image_digest }} args: release --release-notes=/tmp/release.txt --skip-validate
registry-username: fluxcdbot env:
secrets: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
registry-password: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }}
AUR_BOT_SSH_PRIVATE_KEY: ${{ secrets.AUR_BOT_SSH_PRIVATE_KEY }}

@ -1,86 +0,0 @@
name: scan
on:
workflow_dispatch:
push:
branches: [ 'main', 'release/**' ]
pull_request:
branches: [ 'main', 'release/**' ]
schedule:
- cron: '18 10 * * 3'
permissions:
contents: read
jobs:
scan-fossa:
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run FOSSA scan and upload build data
uses: fossa-contrib/fossa-action@cdc5065bcdee31a32e47d4585df72d66e8e941c2 # v3.0.0
with:
# FOSSA Push-Only API Token
fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de
github-token: ${{ github.token }}
scan-snyk:
runs-on: ubuntu-latest
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:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Kustomize
uses: fluxcd/pkg/actions/kustomize@c964ce7b91949ff4b5e3959db4f1d7bb2e029a49 # main
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version-file: 'go.mod'
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Download modules and build manifests
run: |
make tidy
make cmd/flux/.manifests.done
- uses: snyk/actions/setup@b98d498629f1c368650224d6d212bf7dfa89e4bf
- name: Run Snyk to check for vulnerabilities
continue-on-error: true
run: |
snyk test --all-projects --sarif-file-output=snyk.sarif
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Upload result to GitHub Code Scanning
continue-on-error: true
uses: github/codeql-action/upload-sarif@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
with:
sarif_file: snyk.sarif
scan-codeql:
runs-on: ubuntu-latest
permissions:
security-events: write
if: github.actor != 'dependabot[bot]'
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version-file: 'go.mod'
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Initialize CodeQL
uses: github/codeql-action/init@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
with:
languages: go
# xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# xref: https://codeql.github.com/codeql-query-help/go/
queries: security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e8d0789d4a0fa9ceb6b1738f7e269594bdd67f0 # v3.28.9

@ -1,28 +0,0 @@
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3
with:
# Configuration file
config-file: |
https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml
.github/labels.yaml
# Strictly declarative
delete-other-labels: true

@ -1,108 +0,0 @@
name: update
on:
workflow_dispatch:
schedule:
- cron: "0 * * * *"
push:
branches: [main]
permissions:
contents: read
jobs:
update-components:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Check out code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
with:
go-version: 1.23.x
cache-dependency-path: |
**/go.sum
**/go.mod
- name: Update component versions
id: update
run: |
PR_BODY=$(mktemp)
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 CTRL_VERSION=$(sed -n "s/.*$1\/releases\/download\/\(.*\)\/.*/\1/p;n" manifests/bases/$1/kustomization.yaml)
local CRD_VERSION=$(sed -n "s/.*$1\/releases\/download\/\(.*\)\/.*/\1/p" manifests/crds/kustomization.yaml)
local MOD_VERSION=$(go list -m -f '{{ .Version }}' "github.com/fluxcd/$1/api")
local changed=false
if [[ "${CTRL_VERSION}" != "${LATEST_VERSION}" ]]; then
sed -i "s/\($1\/releases\/download\/\)v.*\(\/.*\)/\1${LATEST_VERSION}\2/g" "manifests/bases/$1/kustomization.yaml"
changed=true
fi
if [[ "${CRD_VERSION}" != "${LATEST_VERSION}" ]]; then
sed -i "s/\($1\/releases\/download\/\)v.*\(\/.*\)/\1${LATEST_VERSION}\2/g" "manifests/crds/kustomization.yaml"
changed=true
fi
if [[ "${MOD_VERSION}" != "${LATEST_VERSION}" ]]; then
go mod edit -require="github.com/fluxcd/$1/api@${LATEST_VERSION}"
make tidy
changed=true
fi
if [[ "$changed" == true ]]; then
echo "- $1 to ${LATEST_VERSION}" >> $PR_BODY
echo " https://github.com/fluxcd/$1/blob/${LATEST_VERSION}/CHANGELOG.md" >> $PR_BODY
fi
}
{
# bump controller versions
bump_version helm-controller
bump_version kustomize-controller
bump_version source-controller
bump_version notification-controller
bump_version image-reflector-controller
bump_version image-automation-controller
# diff change
git diff
# export PR_BODY for PR and commit
# 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
id: cpr
uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7.0.6
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
commit-message: |
Update toolkit components
${{ steps.update.outputs.pr_body }}
committer: GitHub <noreply@github.com>
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
signoff: true
branch: update-components
title: Update toolkit components
body: |
${{ steps.update.outputs.pr_body }}
labels: |
dependencies
reviewers: ${{ secrets.ASSIGNEES }}
- name: Check output
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"

@ -0,0 +1,76 @@
name: Update Components
on:
workflow_dispatch:
schedule:
- cron: "0 * * * *"
jobs:
update-components:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Update component versions
id: update
run: |
PR_BODY=""
bump_version() {
local RELEASE_VERSION=$(curl -s https://api.github.com/repos/fluxcd/$1/releases | jq -r 'sort_by(.published_at) | .[-1] | .tag_name')
local CURRENT_VERSION=$(sed -n "s/.*$1\/archive\/\(.*\).zip.*/\1/p;n" manifests/bases/$1/kustomization.yaml)
if [[ "${RELEASE_VERSION}" != "${CURRENT_VERSION}" ]]; then
# bump kustomize
sed -i "s/\($1\/archive\/\)v.*\(.zip\/\/$1-\).*\(\/config.*\)/\1${RELEASE_VERSION}\2${RELEASE_VERSION/v}\3/g" "manifests/bases/$1/kustomization.yaml"
if [[ ! -z $(go list -m all | grep "github.com/fluxcd/$1/api" | awk '{print $2}') ]]; then
# bump go mod
go mod edit -require="github.com/fluxcd/$1/api@${RELEASE_VERSION}"
fi
PR_BODY="$PR_BODY- $1 to ${RELEASE_VERSION}%0A"
fi
}
{
# bump controller versions
bump_version helm-controller
bump_version kustomize-controller
bump_version source-controller
bump_version notification-controller
bump_version image-reflector-controller
bump_version image-automation-controller
# add missing and remove unused modules
go mod tidy
# diff change
git diff
# export PR_BODY for PR
echo "::set-output name=pr_body::$PR_BODY"
}
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.BOT_GITHUB_TOKEN }}
commit-message: Update toolkit components
committer: GitHub <noreply@github.com>
author: fluxcdbot <fluxcdbot@users.noreply.github.com>
title: Update toolkit components
body: |
${{ steps.update.outputs.pr_body }}
Auto-generated by [create-pull-request][1]
[1]: https://github.com/peter-evans/create-pull-request
branch: update-components
reviewers: ${{ secrets.ASSIGNEES }}
- name: Check output
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"

9
.gitignore vendored

@ -11,16 +11,7 @@
# Output of the go coverage tool, specifically when used with LiteIDE # Output of the go coverage tool, specifically when used with LiteIDE
*.out *.out
# Release
dist/
# Dependency directories (remove the comment below to include it) # Dependency directories (remove the comment below to include it)
# vendor/ # vendor/
bin/ bin/
output/ output/
cmd/flux/manifests/
cmd/flux/.manifests.done
testbin/
# Docs
site/

@ -1,6 +1,4 @@
project_name: flux project_name: flux
changelog:
use: github-native
builds: builds:
- <<: &build_defaults - <<: &build_defaults
binary: flux binary: flux
@ -17,14 +15,11 @@ builds:
- arm64 - arm64
- arm - arm
goarm: goarm:
- "7" - 7
- <<: *build_defaults - <<: *build_defaults
id: darwin id: darwin
goos: goos:
- darwin - darwin
goarch:
- amd64
- arm64
- <<: *build_defaults - <<: *build_defaults
id: windows id: windows
goos: goos:
@ -42,50 +37,18 @@ archives:
format: zip format: zip
files: files:
- none* - none*
source:
enabled: true
name_template: '{{ .ProjectName }}_{{ .Version }}_source_code'
sboms:
- id: source
artifacts: source
documents:
- "{{ .ProjectName }}_{{ .Version }}_sbom.spdx.json"
release:
extra_files:
- glob: output/crd-schemas.tar.gz
- glob: output/manifests.tar.gz
- glob: output/install.yaml
checksum:
extra_files:
- glob: output/crd-schemas.tar.gz
- glob: output/manifests.tar.gz
- glob: output/install.yaml
signs:
- cmd: cosign
env:
- COSIGN_EXPERIMENTAL=1
certificate: '${artifact}.pem'
args:
- sign-blob
- "--yes"
- '--output-certificate=${certificate}'
- '--output-signature=${signature}'
- '${artifact}'
artifacts: checksum
output: true
brews: brews:
- name: flux - name: flux
repository: tap:
owner: fluxcd owner: fluxcd
name: homebrew-tap name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
directory: Formula folder: Formula
homepage: "https://fluxcd.io/" homepage: "https://toolkit.fluxcd.io/"
description: "Flux CLI" description: "Flux CLI"
install: | dependencies:
bin.install "flux" - name: kubectl
type: optional
generate_completions_from_executable(bin/"flux", "completion")
test: | test: |
system "#{bin}/flux --version" system "#{bin}/flux --version"
publishers: publishers:
@ -104,74 +67,7 @@ publishers:
- AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }} - AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }}
cmd: | cmd: |
.github/aur/flux-go/publish.sh {{ .Version }} .github/aur/flux-go/publish.sh {{ .Version }}
dockers: release:
- image_templates: extra_files:
- 'fluxcd/flux-cli:{{ .Tag }}-amd64' - glob: ./output/manifests.tar.gz
- 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-amd64' - glob: ./output/install.yaml
dockerfile: Dockerfile
use: buildx
goos: linux
goarch: amd64
build_flag_templates:
- "--pull"
- "--build-arg=ARCH=linux/amd64"
- "--label=org.opencontainers.image.created={{ .Date }}"
- "--label=org.opencontainers.image.name={{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{ .FullCommit }}"
- "--label=org.opencontainers.image.version={{ .Version }}"
- "--label=org.opencontainers.image.source={{ .GitURL }}"
- "--platform=linux/amd64"
- image_templates:
- 'fluxcd/flux-cli:{{ .Tag }}-arm64'
- 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm64'
dockerfile: Dockerfile
use: buildx
goos: linux
goarch: arm64
build_flag_templates:
- "--pull"
- "--build-arg=ARCH=linux/arm64"
- "--label=org.opencontainers.image.created={{ .Date }}"
- "--label=org.opencontainers.image.name={{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{ .FullCommit }}"
- "--label=org.opencontainers.image.version={{ .Version }}"
- "--label=org.opencontainers.image.source={{ .GitURL }}"
- "--platform=linux/arm64"
- image_templates:
- 'fluxcd/flux-cli:{{ .Tag }}-arm'
- 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm'
dockerfile: Dockerfile
use: buildx
goos: linux
goarch: arm
goarm: 7
build_flag_templates:
- "--pull"
- "--build-arg=ARCH=linux/arm"
- "--label=org.opencontainers.image.created={{ .Date }}"
- "--label=org.opencontainers.image.name={{ .ProjectName }}"
- "--label=org.opencontainers.image.revision={{ .FullCommit }}"
- "--label=org.opencontainers.image.version={{ .Version }}"
- "--label=org.opencontainers.image.source={{ .GitURL }}"
- "--platform=linux/arm/v7"
docker_manifests:
- name_template: 'fluxcd/flux-cli:{{ .Tag }}'
image_templates:
- 'fluxcd/flux-cli:{{ .Tag }}-amd64'
- 'fluxcd/flux-cli:{{ .Tag }}-arm64'
- 'fluxcd/flux-cli:{{ .Tag }}-arm'
- name_template: 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}'
image_templates:
- 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-amd64'
- 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm64'
- 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm'
docker_signs:
- cmd: cosign
env:
- COSIGN_EXPERIMENTAL=1
args:
- sign
- "--yes"
- '${artifact}'
artifacts: all
output: true

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

@ -30,7 +30,7 @@ you can sign your commit automatically with `git commit -s`.
For realtime communications we use Slack: To join the conversation, simply For realtime communications we use Slack: To join the conversation, simply
join the [CNCF](https://slack.cncf.io/) Slack workspace and use the join the [CNCF](https://slack.cncf.io/) Slack workspace and use the
[#flux-contributors](https://cloud-native.slack.com/messages/flux-contributors/) channel. [#flux-dev](https://cloud-native.slack.com/messages/flux-dev/) channel.
To discuss ideas and specifications we use [Github To discuss ideas and specifications we use [Github
Discussions](https://github.com/fluxcd/flux2/discussions). Discussions](https://github.com/fluxcd/flux2/discussions).
@ -48,80 +48,27 @@ you might want to take a look at the [introductory talk and demo](https://www.yo
This project is composed of: This project is composed of:
- [flux2](https://github.com/fluxcd/flux2): The Flux CLI - [/f/flux2](https://github.com/fluxcd/flux2): The Flux CLI
- [source-manager](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources (Git and Helm repositories, S3-compatible Buckets) - [/f/source-manager](https://github.com/fluxcd/source-controller): Kubernetes operator for managing sources
- [kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize - [/f/kustomize-controller](https://github.com/fluxcd/kustomize-controller): Kubernetes operator for building GitOps pipelines with Kustomize
- [helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm - [/f/helm-controller](https://github.com/fluxcd/helm-controller): Kubernetes operator for building GitOps pipelines with Helm
- [notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events - [/f/notification-controller](https://github.com/fluxcd/notification-controller): Kubernetes operator for handling inbound and outbound events
- [image-reflector-controller](https://github.com/fluxcd/image-reflector-controller): Kubernetes operator for scanning container registries
- [image-automation-controller](https://github.com/fluxcd/image-automation-controller): Kubernetes operator for patches container image tags in Git
### Understanding the code ### 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/flux/gitops-toolkit/source-watcher/) which [our guide](https://toolkit.fluxcd.io/dev-guides/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.
## How to run the test suite ### How to run the test suite
Prerequisites: You can run the unit tests by simply doing
* go >= 1.20
* kubectl >= 1.24
* kustomize >= 5.0
* coreutils (on Mac OS)
Install the [controller-runtime/envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest) binaries with:
```bash
make install-envtest
```
Then you can run the unit tests with:
```bash ```bash
make test make test
``` ```
After [installing Kubernetes kind](https://kind.sigs.k8s.io/docs/user/quick-start#installation) on your machine,
create a cluster for testing with:
```bash
make setup-kind
```
Then you can run the end-to-end tests with:
```bash
make e2e
```
When the output of the Flux CLI changes, to automatically update the golden
files used in the test, pass `-update` flag to the test as:
```bash
make e2e TEST_ARGS="-update"
```
Since not all packages use golden files for testing, `-update` argument must be
passed only for the packages that use golden files. Use the variables
`TEST_PKG_PATH` for unit tests and `E2E_TEST_PKG_PATH` for e2e tests, to set the
path of the target test package:
```bash
# Unit test
make test TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
# e2e test
make e2e E2E_TEST_PKG_PATH="./cmd/flux" TEST_ARGS="-update"
```
Teardown the e2e environment with:
```bash
make cleanup-kind
```
## Acceptance policy ## Acceptance policy
These things will make a PR more likely to be accepted: These things will make a PR more likely to be accepted:

@ -1,21 +0,0 @@
FROM alpine:3.21 AS builder
RUN apk add --no-cache ca-certificates curl
ARG ARCH=linux/amd64
ARG KUBECTL_VER=1.32.2
RUN curl -sL https://dl.k8s.io/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \
-o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl
RUN kubectl version --client=true
FROM alpine:3.21 AS flux-cli
RUN apk add --no-cache ca-certificates
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/
COPY --chmod=755 flux /usr/local/bin/
USER 65534:65534
ENTRYPOINT [ "flux" ]

@ -2,7 +2,17 @@ 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/).
The Flux2 maintainers team is identical with the core maintainers of the project These maintainers are shared with other Flux v2-related git
as listed in repositories under https://github.com/fluxcd, as noted in their
respective MAINTAINERS files.
https://github.com/fluxcd/community/blob/main/CORE-MAINTAINERS For convenience, they are reflected in the GitHub team
@fluxcd/flux2-maintainers -- if the list here changes, that team also
should.
In alphabetical order:
Aurel Canciu, Sortlist <aurel@sortlist.com> (github: @relu, slack: relu)
Hidde Beydals, Weaveworks <hidde@weave.works> (github: @hiddeco, slack: hidde)
Philip Laine, Xenit <philip.laine@xenit.se> (github: @phillebaba, slack: phillebaba)
Stefan Prodan, Weaveworks <stefan@weave.works> (github: @stefanprodan, slack: stefanprodan)

@ -1,24 +1,9 @@
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 }' | 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
TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig
# Architecture to use envtest with
ENVTEST_ARCH ?= amd64
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2)) $(filter $(subst *,%,$(2)),$(d)))
all: test build all: test build
tidy: tidy:
go mod tidy -compat=1.23 go mod tidy
cd tests/integration && go mod tidy -compat=1.23
fmt: fmt:
go fmt ./... go fmt ./...
@ -26,73 +11,19 @@ fmt:
vet: vet:
go vet ./... go vet ./...
setup-kind: test: tidy fmt vet docs
kind create cluster --name=flux-e2e-test --kubeconfig=$(TEST_KUBECONFIG) --config=.github/kind/config.yaml go test ./... -coverprofile cover.out
kubectl --kubeconfig=$(TEST_KUBECONFIG) apply -f https://docs.projectcalico.org/v3.16/manifests/calico.yaml
kubectl --kubeconfig=$(TEST_KUBECONFIG) -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true
cleanup-kind:
kind delete cluster --name=flux-e2e-test
rm $(TEST_KUBECONFIG)
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
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
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
make setup-kind
make e2e
make cleanup-kind
$(EMBEDDED_MANIFESTS_TARGET): $(call rwildcard,manifests/,*.yaml *.json)
./manifests/scripts/bundle.sh
touch $@
build: $(EMBEDDED_MANIFESTS_TARGET) build:
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(VERSION)" -o ./bin/flux ./cmd/flux CGO_ENABLED=0 go build -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
install: install:
CGO_ENABLED=0 go install ./cmd/flux go install cmd/flux
.PHONY: docs
docs:
rm docs/cmd/*
mkdir -p ./docs/cmd && go run ./cmd/flux/ docgen
install-dev: install-dev:
CGO_ENABLED=0 go build -o /usr/local/bin ./cmd/flux CGO_ENABLED=0 go build -o /usr/local/bin ./cmd/flux
setup-bootstrap-patch:
go run ./tests/bootstrap/main.go
setup-image-automation:
cd tests/image-automation && go run main.go
ENVTEST_ASSETS_DIR=$(shell pwd)/testbin
ENVTEST_KUBERNETES_VERSION?=latest
install-envtest: setup-envtest
mkdir -p ${ENVTEST_ASSETS_DIR}
$(ENVTEST) use $(ENVTEST_KUBERNETES_VERSION) --arch=$(ENVTEST_ARCH) --bin-dir=$(ENVTEST_ASSETS_DIR)
ENVTEST = $(shell pwd)/bin/setup-envtest
.PHONY: envtest
setup-envtest: ## Download envtest-setup locally if necessary.
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest)
# go-install-tool will 'go install' any package $2 and install it to $1.
PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST))))
define go-install-tool
@[ -f $(1) ] || { \
set -e ;\
TMP_DIR=$$(mktemp -d) ;\
cd $$TMP_DIR ;\
go mod init tmp ;\
echo "Downloading $(2)" ;\
GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\
rm -rf $$TMP_DIR ;\
}
endef

@ -1,15 +1,13 @@
# Flux version 2 # Flux version 2
[![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://scorecard.dev/viewer/?uri=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 OCI artifacts), configuration (like Git repositories), and automating updates to
and automating updates to configuration when there is new code to deploy. 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
@ -21,21 +19,52 @@ 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/)) graduated project, used in ## Flux installation
production by various [organisations](https://fluxcd.io/adopters) and [cloud providers](https://fluxcd.io/ecosystem).
## Quickstart and documentation With Homebrew:
To get started check out this [guide](https://fluxcd.io/flux/get-started/) ```sh
on how to bootstrap Flux on Kubernetes and deploy a sample application in a GitOps manner. brew install fluxcd/tap/flux
```
For more comprehensive documentation, see the following guides: With Bash:
- [Ways of structuring your repositories](https://fluxcd.io/flux/guides/repository-structure/)
- [Manage Helm Releases](https://fluxcd.io/flux/guides/helmreleases/)
- [Automate image updates to Git](https://fluxcd.io/flux/guides/image-update/)
- [Manage Kubernetes secrets with Flux and SOPS](https://fluxcd.io/flux/guides/mozilla-sops/)
If you need help, please refer to our **[Support page](https://fluxcd.io/support/)**. ```sh
curl -s https://toolkit.fluxcd.io/install.sh | sudo bash
# enable completions in ~/.bash_profile
. <(flux completion bash)
```
Arch Linux (AUR) packages:
- [flux-bin](https://aur.archlinux.org/packages/flux-bin): install the latest
stable version using a pre-build binary (recommended)
- [flux-go](https://aur.archlinux.org/packages/flux-go): build the latest
stable version from source code
- [flux-scm](https://aur.archlinux.org/packages/flux-scm): build the latest
(unstable) version from source code from our git `main` branch
Binaries for macOS, Windows and Linux AMD64/ARM are available to download on the
[release page](https://github.com/fluxcd/flux2/releases).
Verify that your cluster satisfies the prerequisites with:
```sh
flux check --pre
```
## Get started
To get started with Flux, start [browsing the
documentation](https://toolkit.fluxcd.io) or get started with one of
the following guides:
- [Get started with Flux (deep dive)](https://toolkit.fluxcd.io/get-started/)
- [Installation](https://toolkit.fluxcd.io/guides/installation/)
- [Manage Helm Releases](https://toolkit.fluxcd.io/guides/helmreleases/)
- [Setup Notifications](https://toolkit.fluxcd.io/guides/notifications/)
- [Setup Webhook Receivers](https://toolkit.fluxcd.io/guides/webhook-receivers/)
## GitOps Toolkit ## GitOps Toolkit
@ -44,58 +73,53 @@ 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](https://raw.githubusercontent.com/fluxcd/flux2/main/docs/diagrams/fluxcd-controllers.png) ![overview](docs/diagrams/gitops-toolkit.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/flux/gitops-toolkit/source-watcher/). guides](https://toolkit.fluxcd.io/dev-guides/source-watcher/).
### Components ### Components
- [Source Controller](https://fluxcd.io/flux/components/source/) - [Source Controller](https://toolkit.fluxcd.io/components/source/controller/)
- [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/) - [GitRepository CRD](https://toolkit.fluxcd.io/components/source/gitrepositories/)
- [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/) - [HelmRepository CRD](https://toolkit.fluxcd.io/components/source/helmrepositories/)
- [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/) - [HelmChart CRD](https://toolkit.fluxcd.io/components/source/helmcharts/)
- [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/) - [Bucket CRD](https://toolkit.fluxcd.io/components/source/buckets/)
- [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/) - [Kustomize Controller](https://toolkit.fluxcd.io/components/kustomize/controller/)
- [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/) - [Kustomization CRD](https://toolkit.fluxcd.io/components/kustomize/kustomization/)
- [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/) - [Helm Controller](https://toolkit.fluxcd.io/components/helm/controller/)
- [Helm Controller](https://fluxcd.io/flux/components/helm/) - [HelmRelease CRD](https://toolkit.fluxcd.io/components/helm/helmreleases/)
- [HelmRelease CRD](https://fluxcd.io/flux/components/helm/helmreleases/) - [Notification Controller](https://toolkit.fluxcd.io/components/notification/controller/)
- [Notification Controller](https://fluxcd.io/flux/components/notification/) - [Provider CRD](https://toolkit.fluxcd.io/components/notification/provider/)
- [Provider CRD](https://fluxcd.io/flux/components/notification/providers/) - [Alert CRD](https://toolkit.fluxcd.io/components/notification/alert/)
- [Alert CRD](https://fluxcd.io/flux/components/notification/alerts/) - [Receiver CRD](https://toolkit.fluxcd.io/components/notification/receiver/)
- [Receiver CRD](https://fluxcd.io/flux/components/notification/receivers/)
- [Image Automation Controllers](https://fluxcd.io/flux/components/image/)
- [ImageRepository CRD](https://fluxcd.io/flux/components/image/imagerepositories/)
- [ImagePolicy CRD](https://fluxcd.io/flux/components/image/imagepolicies/)
- [ImageUpdateAutomation CRD](https://fluxcd.io/flux/components/image/imageupdateautomations/)
## Community ## Community
Need help or want to contribute? Please see the links below. The Flux project is always looking for The Flux project is always looking for new contributors and there are a multitude of ways to get involved.
new contributors and there are a multitude of ways to get involved. Depending on what you want to do, some of the following bits might be your first steps:
- Getting Started? - Join our upcoming dev meetings ([meeting access and agenda](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view))
- Look at our [Get Started guide](https://fluxcd.io/flux/get-started/) and give us feedback - Talk to us in the #flux channel on [CNCF Slack](https://slack.cncf.io/)
- Need help? - Join the [planning discussions](https://github.com/fluxcd/flux2/discussions)
- First: Ask questions on our [GH Discussions page](https://github.com/fluxcd/flux2/discussions). - And if you are completely new to Flux and the GitOps Toolkit, take a look at our [Get Started guide](https://toolkit.fluxcd.io/get-started/) and give us feedback
- Second: Talk to us in the #flux channel on [CNCF Slack](https://slack.cncf.io/). - To be part of the conversation about Flux's development, [join the flux-dev mailing list](https://lists.cncf.io/g/cncf-flux-dev).
- Please follow our [Support Guidelines](https://fluxcd.io/support/) - Check out [how to contribute](CONTRIBUTING.md) to the project
(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). ### Upcoming Events
- Have feature proposals or want to contribute? - 11 Jan 2021 - [Helm + GitOps = ⚡️⚡️⚡️ with Scott Rigby](https://www.meetup.com/GitOps-Community/events/275348736/)
- Propose features on our [GitHub Discussions page](https://github.com/fluxcd/flux2/discussions). - 25 Jan 2021 - [GitOps Core Concepts & How to Teach Your Teams with Leigh Capili](https://www.meetup.com/GitOps-Community/events/275625806/)
- 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). ### Featured Talks
- Check out [how to contribute](CONTRIBUTING.md) to the project. - 14 Dec 2020 - [The Power of GitOps with Flux and Flagger (GitOps Hands-On) with Leigh Capili](https://youtu.be/cB7iXeNLteE)
- Check out the [project roadmap](https://fluxcd.io/roadmap/). - 30 Nov 2020 - [The Power of GitOps with Flux 2 - Part 3 with Leigh Capili](https://youtu.be/N_K5g7o9JKg)
- 24 Nov 2020 - [Flux CD v2 with GitOps Toolkit - Kubernetes Deployment and Sync Mechanism](https://youtu.be/R6OeIgb7lUI)
### Events - 02 Nov 2020 - [The Power of GitOps with Flux & GitOps Toolkit - Part 2 with Leigh Capili](https://youtu.be/fC2YCxQRUwU)
- 28 Oct 2020 - [The Kubelist Podcast: Flux with Michael Bridgen](https://www.heavybit.com/library/podcasts/the-kubelist-podcast/ep-5-flux-with-michael-bridgen-of-weaveworks/)
Check out our **[events calendar](https://fluxcd.io/#calendar)**, - 19 Oct 2020 - [The Power of GitOps with Flux & GitOps Toolkit - Part 1 with Leigh Capili](https://youtu.be/0v5bjysXTL8)
both with upcoming talks, events and meetings you can attend. - 12 Oct 2020 - [Rawkode Live: Introduction to GitOps Toolkit with Stefan Prodan](https://youtu.be/HqTzuOBP0eY)
Or view the **[resources section](https://fluxcd.io/resources)** - 04 Sep 2020 - [KubeCon Europe: The road to Flux v2 and Progressive Delivery with Stefan Prodan & Hidde Beydals](https://youtu.be/8v94nUkXsxU)
with past events videos you can watch. - 25 Jun 2020 - [Cloud Native Nordics: Introduction to GitOps & GitOps Toolkit with Alexis Richardson & Stefan Prodan](https://youtu.be/qQBtSkgl7tI)
We look forward to seeing you with us! We look forward to seeing you with us!

@ -0,0 +1,6 @@
FROM stefanprodan/alpine-base:latest
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

@ -1,22 +1,89 @@
# Flux GitHub Action # Flux GitHub Action
To install the latest Flux CLI on Linux, macOS or Windows GitHub runners: Usage:
```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
run: flux -v
```
This action places the `flux` binary inside your repository root under `bin/flux`.
You should add `bin/flux` to your `.gitignore` file, as in the following example:
```gitignore
# ignore flux binary
bin/flux
```
Note that this action can only be used on GitHub **Linux AMD64** runners.
### Automate Flux updates
Example workflow for updating Flux's components generated with `flux bootstrap --arch=amd64 --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 --arch=amd64 \
--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: with:
version: 'latest' token: ${{ secrets.GITHUB_TOKEN }}
- name: Run Flux CLI branch: update-flux
run: flux version --client commit-message: Update to ${{ steps.update.outputs.flux_version }}
title: Update to ${{ steps.update.outputs.flux_version }}
body: |
${{ steps.update.outputs.flux_version }}
``` ```
The Flux GitHub Action can be used to automate various tasks in CI, such as: ### End-to-end testing
Example workflow for running Flux in Kubernetes Kind:
- [Automate Flux upgrades on clusters via Pull Requests](https://fluxcd.io/flux/flux-gh-action/#automate-flux-updates) ```yaml
- [Push Kubernetes manifests to container registries](https://fluxcd.io/flux/flux-gh-action/#push-kubernetes-manifests-to-container-registries) name: e2e
- [Run end-to-end testing with Flux and Kubernetes Kind](https://fluxcd.io/flux/flux-gh-action/#end-to-end-testing)
For more information, please see the [Flux GitHub Action documentation](https://fluxcd.io/flux/flux-gh-action/). 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,120 +1,15 @@
name: Setup Flux CLI name: 'kustomize'
description: A GitHub Action for installing the Flux CLI description: 'A GitHub Action for running Flux commands'
author: Flux project author: 'Flux project'
branding: branding:
color: blue icon: 'command'
icon: command color: 'blue'
inputs: inputs:
version: version:
description: "Flux version e.g. 2.0.0 (defaults to latest stable release)" description: 'strict semver'
required: false
arch:
description: "arch can be amd64, arm64 or arm"
required: false
deprecationMessage: "No longer required, action will now detect runner arch."
bindir:
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: 'docker'
steps: image: 'Dockerfile'
- name: "Download the binary to the runner's cache dir" args:
shell: bash - ${{ inputs.version }}
run: |
VERSION=${{ inputs.version }}
TOKEN=${{ inputs.token }}
if [[ -z "$TOKEN" ]]; then
TOKEN=${{ github.token }}
fi
if [[ -z "$VERSION" ]] || [[ "$VERSION" = "latest" ]]; then
VERSION=$(curl -fsSL -H "Authorization: token ${TOKEN}" https://api.github.com/repos/fluxcd/flux2/releases/latest | grep tag_name | cut -d '"' -f 4)
fi
if [[ -z "$VERSION" ]]; then
echo "Unable to determine Flux CLI version"
exit 1
fi
if [[ $VERSION = v* ]]; then
VERSION="${VERSION:1}"
fi
OS=$(echo "${RUNNER_OS}" | tr '[:upper:]' '[:lower:]')
if [[ "$OS" == "macos" ]]; then
OS="darwin"
fi
ARCH=$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')
if [[ "$ARCH" == "x64" ]]; then
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
run: |
flux -v

@ -0,0 +1,40 @@
#!/bin/bash
# Copyright 2020 The Flux authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# 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.
set -e
VERSION=$1
if [ -z $VERSION ]; then
# Find latest release if no version is specified
VERSION=$(curl https://api.github.com/repos/fluxcd/flux2/releases/latest -sL | grep tag_name | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-)
fi
# Download linux binary
BIN_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_amd64.tar.gz"
curl -sL $BIN_URL | tar xz
# Copy binary to GitHub runner
mkdir -p $GITHUB_WORKSPACE/bin
mv ./flux $GITHUB_WORKSPACE/bin
chmod +x $GITHUB_WORKSPACE/bin/flux
# Print version
$GITHUB_WORKSPACE/bin/flux -v
# Add binary to GitHub runner path
echo "$GITHUB_WORKSPACE/bin" >> $GITHUB_PATH
echo "$RUNNER_WORKSPACE/$(basename $GITHUB_REPOSITORY)/bin" >> $GITHUB_PATH

@ -1,57 +0,0 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"sigs.k8s.io/controller-runtime/pkg/client"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
)
// notificationv1.Alert
var alertType = apiType{
kind: notificationv1.AlertKind,
humanKind: "alert",
groupVersion: notificationv1.GroupVersion,
}
type alertAdapter struct {
*notificationv1.Alert
}
func (a alertAdapter) asClientObject() client.Object {
return a.Alert
}
func (a alertAdapter) deepCopyClientObject() client.Object {
return a.Alert.DeepCopy()
}
// notificationv1.Alert
type alertListAdapter struct {
*notificationv1.AlertList
}
func (a alertListAdapter) asClientList() client.ObjectList {
return a.AlertList
}
func (a alertListAdapter) len() int {
return len(a.AlertList.Items)
}

@ -1,57 +0,0 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"sigs.k8s.io/controller-runtime/pkg/client"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3"
)
// notificationv1.Provider
var alertProviderType = apiType{
kind: notificationv1.ProviderKind,
humanKind: "alert provider",
groupVersion: notificationv1.GroupVersion,
}
type alertProviderAdapter struct {
*notificationv1.Provider
}
func (a alertProviderAdapter) asClientObject() client.Object {
return a.Provider
}
func (a alertProviderAdapter) deepCopyClientObject() client.Object {
return a.Provider.DeepCopy()
}
// notificationv1.Provider
type alertProviderListAdapter struct {
*notificationv1.ProviderList
}
func (a alertProviderListAdapter) asClientList() client.ObjectList {
return a.ProviderList
}
func (a alertProviderListAdapter) len() int {
return len(a.ProviderList.Items)
}

@ -18,222 +18,268 @@ package main
import ( import (
"context" "context"
"crypto/elliptic"
"fmt" "fmt"
"strings" "net/url"
"path/filepath"
"time"
"github.com/fluxcd/pkg/git"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/fluxcd/flux2/v2/internal/flags" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
"github.com/fluxcd/flux2/v2/internal/utils" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/pkg/manifestgen/install"
"github.com/fluxcd/flux2/pkg/manifestgen/sync"
) )
var bootstrapCmd = &cobra.Command{ var bootstrapCmd = &cobra.Command{
Use: "bootstrap", Use: "bootstrap",
Short: "Deploy Flux on a cluster the GitOps way.", Short: "Bootstrap toolkit components",
Long: `The bootstrap sub-commands push the Flux manifests to a Git repository Long: "The bootstrap sub-commands bootstrap the toolkit components on the targeted Git provider.",
and deploy Flux on the cluster.`,
} }
type bootstrapFlags struct { var (
version string bootstrapVersion string
logLevel flags.LogLevel bootstrapDefaultComponents []string
bootstrapExtraComponents []string
branch string bootstrapRegistry string
recurseSubmodules bool bootstrapImagePullSecret string
manifestsPath string bootstrapBranch string
bootstrapWatchAllNamespaces bool
defaultComponents []string bootstrapNetworkPolicy bool
extraComponents []string bootstrapManifestsPath string
requiredComponents []string bootstrapArch flags.Arch
bootstrapLogLevel = flags.LogLevel(defaults.LogLevel)
registry string bootstrapRequiredComponents = []string{"source-controller", "kustomize-controller"}
registryCredential string bootstrapTokenAuth bool
imagePullSecret string bootstrapClusterDomain string
)
secretName string const (
tokenAuth bool bootstrapDefaultBranch = "main"
keyAlgorithm flags.PublicKeyAlgorithm )
keyRSABits flags.RSAKeyBits
keyECDSACurve flags.ECDSACurve
sshHostname string
caFile string
privateKeyFile string
sshHostKeyAlgorithms []string
watchAllNamespaces bool func init() {
networkPolicy bool bootstrapCmd.PersistentFlags().StringVarP(&bootstrapVersion, "version", "v", defaults.Version,
clusterDomain string "toolkit version")
tolerationKeys []string bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapDefaultComponents, "components", defaults.Components,
"list of components, accepts comma-separated values")
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapExtraComponents, "components-extra", nil,
"list of components in addition to those supplied or defaulted, accepts comma-separated values")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapRegistry, "registry", "ghcr.io/fluxcd",
"container registry where the toolkit images are published")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapImagePullSecret, "image-pull-secret", "",
"Kubernetes secret name used for pulling the toolkit images from a private registry")
bootstrapCmd.PersistentFlags().Var(&bootstrapArch, "arch", bootstrapArch.Description())
bootstrapCmd.PersistentFlags().StringVar(&bootstrapBranch, "branch", bootstrapDefaultBranch,
"default branch (for GitHub this must match the default branch setting for the organization)")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapWatchAllNamespaces, "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")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapNetworkPolicy, "network-policy", true,
"deny ingress access to the toolkit controllers from other namespaces using network policies")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapTokenAuth, "token-auth", false,
"when enabled, the personal access token will be used instead of SSH deploy key")
bootstrapCmd.PersistentFlags().Var(&bootstrapLogLevel, "log-level", bootstrapLogLevel.Description())
bootstrapCmd.PersistentFlags().StringVar(&bootstrapManifestsPath, "manifests", "", "path to the manifest directory")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapClusterDomain, "cluster-domain", defaults.ClusterDomain, "internal cluster domain")
bootstrapCmd.PersistentFlags().MarkHidden("manifests")
bootstrapCmd.PersistentFlags().MarkDeprecated("arch", "multi-arch container image is now available for AMD64, ARMv7 and ARM64")
rootCmd.AddCommand(bootstrapCmd)
}
authorName string func bootstrapComponents() []string {
authorEmail string return append(bootstrapDefaultComponents, bootstrapExtraComponents...)
}
gpgKeyRingPath string func bootstrapValidate() error {
gpgPassphrase string components := bootstrapComponents()
gpgKeyID string for _, component := range bootstrapRequiredComponents {
if !utils.ContainsItemString(components, component) {
return fmt.Errorf("component %s is required", component)
}
}
force bool if err := utils.ValidateComponents(components); err != nil {
return err
}
commitMessageAppendix string return nil
} }
const ( func generateInstallManifests(targetPath, namespace, tmpDir string, localManifests string) (string, error) {
bootstrapDefaultBranch = "main" opts := install.Options{
) BaseURL: localManifests,
Version: bootstrapVersion,
Namespace: namespace,
Components: bootstrapComponents(),
Registry: bootstrapRegistry,
ImagePullSecret: bootstrapImagePullSecret,
WatchAllNamespaces: bootstrapWatchAllNamespaces,
NetworkPolicy: bootstrapNetworkPolicy,
LogLevel: bootstrapLogLevel.String(),
NotificationController: defaults.NotificationController,
ManifestFile: defaults.ManifestFile,
Timeout: timeout,
TargetPath: targetPath,
ClusterDomain: bootstrapClusterDomain,
}
var bootstrapArgs = NewBootstrapFlags() if localManifests == "" {
opts.BaseURL = defaults.BaseURL
}
func init() { output, err := install.Generate(opts)
bootstrapCmd.PersistentFlags().StringVarP(&bootstrapArgs.version, "version", "v", "", if err != nil {
"toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases") return "", fmt.Errorf("generating install manifests failed: %w", err)
}
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.defaultComponents, "components", rootArgs.defaults.Components, filePath, err := output.WriteFile(tmpDir)
"list of components, accepts comma-separated values") if err != nil {
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.extraComponents, "components-extra", nil, return "", fmt.Errorf("generating install manifests failed: %w", err)
"list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller'") }
return filePath, nil
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd", }
"container registry where the Flux controller images are published")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registryCredential, "registry-creds", "",
"container registry credentials in the format 'user:password', requires --image-pull-secret to be set")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.imagePullSecret, "image-pull-secret", "",
"Kubernetes secret name used for pulling the controller images from a private registry")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false,
"when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory")
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 Flux controllers are installed")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.networkPolicy, "network-policy", true,
"setup Kubernetes network policies to deny ingress access to the Flux controllers from other namespaces")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.tokenAuth, "token-auth", false,
"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().StringVar(&bootstrapArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain")
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.tolerationKeys, "toleration-keys", nil,
"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().Var(&bootstrapArgs.keyAlgorithm, "ssh-key-algorithm", bootstrapArgs.keyAlgorithm.Description())
bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyRSABits, "ssh-rsa-bits", bootstrapArgs.keyRSABits.Description())
bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.sshHostKeyAlgorithms, "ssh-hostkey-algos", nil, "list of host key algorithms to be used by the CLI for SSH connections")
bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.keyECDSACurve, "ssh-ecdsa-curve", bootstrapArgs.keyECDSACurve.Description())
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.sshHostname, "ssh-hostname", "", "SSH hostname, to be used when the SSH host differs from the HTTPS one")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.privateKeyFile, "private-key-file", "", "path to a private key file used for authenticating to the Git SSH server")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorName, "author-name", "Flux", "author name for Git commits")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.authorEmail, "author-email", "", "author email for Git commits")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyRingPath, "gpg-key-ring", "", "path to GPG key ring for signing commits")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgPassphrase, "gpg-passphrase", "", "passphrase for decrypting GPG private key")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.gpgKeyID, "gpg-key-id", "", "key id for selecting a particular key")
bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'")
bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm")
bootstrapCmd.PersistentFlags().MarkHidden("manifests")
rootCmd.AddCommand(bootstrapCmd) func applyInstallManifests(ctx context.Context, manifestPath string, components []string) error {
kubectlArgs := []string{"apply", "-f", manifestPath}
if _, err := utils.ExecKubectlCommand(ctx, utils.ModeOS, kubeconfig, kubecontext, kubectlArgs...); err != nil {
return fmt.Errorf("install failed")
} }
func NewBootstrapFlags() bootstrapFlags { for _, deployment := range components {
return bootstrapFlags{ kubectlArgs = []string{"-n", namespace, "rollout", "status", "deployment", deployment, "--timeout", timeout.String()}
logLevel: flags.LogLevel(rootArgs.defaults.LogLevel), if _, err := utils.ExecKubectlCommand(ctx, utils.ModeOS, kubeconfig, kubecontext, kubectlArgs...); err != nil {
requiredComponents: []string{"source-controller", "kustomize-controller"}, return fmt.Errorf("install failed")
keyAlgorithm: flags.PublicKeyAlgorithm(sourcesecret.ECDSAPrivateKeyAlgorithm),
keyRSABits: 2048,
keyECDSACurve: flags.ECDSACurve{Curve: elliptic.P384()},
} }
} }
return nil
}
func bootstrapComponents() []string { func generateSyncManifests(url, branch, name, namespace, targetPath, tmpDir string, interval time.Duration) (string, error) {
return append(bootstrapArgs.defaultComponents, bootstrapArgs.extraComponents...) opts := sync.Options{
Name: name,
Namespace: namespace,
URL: url,
Branch: branch,
Interval: interval,
TargetPath: targetPath,
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
} }
func buildEmbeddedManifestBase() (string, error) { manifest, err := sync.Generate(opts)
if !isEmbeddedVersion(bootstrapArgs.version) { if err != nil {
return "", nil return "", fmt.Errorf("generating install manifests failed: %w", err)
} }
tmpBaseDir, err := manifestgen.MkdirTempAbs("", "flux-manifests-")
output, err := manifest.WriteFile(tmpDir)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := writeEmbeddedManifests(tmpBaseDir); err != nil { outputDir := filepath.Dir(output)
if err := utils.GenerateKustomizationYaml(outputDir); err != nil {
return "", err return "", err
} }
return tmpBaseDir, nil return outputDir, nil
} }
func bootstrapValidate() error { func applySyncManifests(ctx context.Context, kubeClient client.Client, name, namespace, manifestsPath string) error {
components := bootstrapComponents() kubectlArgs := []string{"apply", "-k", manifestsPath}
for _, component := range bootstrapArgs.requiredComponents { if _, err := utils.ExecKubectlCommand(ctx, utils.ModeStderrOS, kubeconfig, kubecontext, kubectlArgs...); err != nil {
if !utils.ContainsItemString(components, component) { return err
return fmt.Errorf("component %s is required", component)
} }
logger.Waitingf("waiting for cluster sync")
var gitRepository sourcev1.GitRepository
if err := wait.PollImmediate(pollInterval, timeout,
isGitRepositoryReady(ctx, kubeClient, types.NamespacedName{Name: name, Namespace: namespace}, &gitRepository)); err != nil {
return err
} }
if err := utils.ValidateComponents(components); err != nil { var kustomization kustomizev1.Kustomization
if err := wait.PollImmediate(pollInterval, timeout,
isKustomizationReady(ctx, kubeClient, types.NamespacedName{Name: name, Namespace: namespace}, &kustomization)); err != nil {
return err return err
} }
if bootstrapArgs.registryCredential != "" && bootstrapArgs.imagePullSecret == "" { return nil
return fmt.Errorf("--registry-creds requires --image-pull-secret to be set")
} }
if bootstrapArgs.registryCredential != "" && len(strings.Split(bootstrapArgs.registryCredential, ":")) != 2 { func shouldInstallManifests(ctx context.Context, kubeClient client.Client, namespace string) bool {
return fmt.Errorf("invalid --registry-creds format, expected 'user:password'") namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
}
var kustomization kustomizev1.Kustomization
if err := kubeClient.Get(ctx, namespacedName, &kustomization); err != nil {
return true
} }
if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 { return kustomization.Status.LastAppliedRevision == ""
git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms
} }
return nil func shouldCreateDeployKey(ctx context.Context, kubeClient client.Client, namespace string) bool {
namespacedName := types.NamespacedName{
Namespace: namespace,
Name: namespace,
} }
func mapTeamSlice(s []string, defaultPermission string) map[string]string { var existing corev1.Secret
m := make(map[string]string, len(s)) if err := kubeClient.Get(ctx, namespacedName, &existing); err != nil {
for _, v := range s { return true
m[v] = defaultPermission
if s := strings.Split(v, ":"); len(s) == 2 {
m[s[0]] = s[1]
} }
return false
} }
return m func generateDeployKey(ctx context.Context, kubeClient client.Client, url *url.URL, namespace string) (string, error) {
pair, err := generateKeyPair(ctx, sourceGitKeyAlgorithm, sourceGitRSABits, sourceGitECDSACurve)
if err != nil {
return "", err
} }
// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation. hostKey, err := scanHostKey(ctx, url)
// 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 err != nil {
if !errors.IsNotFound(err) { return "", err
return fmt.Errorf("cluster info unavailable: %w", err)
} }
installed = false
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Namespace: namespace,
},
StringData: map[string]string{
"identity": string(pair.PrivateKey),
"identity.pub": string(pair.PublicKey),
"known_hosts": string(hostKey),
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return "", err
} }
if installed { return string(pair.PublicKey), nil
err = confirmFluxInstallOverride(info)
if err != nil {
if err == promptui.ErrAbort {
return fmt.Errorf("bootstrap cancelled")
} }
return err
func checkIfBootstrapPathDiffers(ctx context.Context, kubeClient client.Client, namespace string, path string) (string, bool) {
namespacedName := types.NamespacedName{
Name: namespace,
Namespace: namespace,
} }
var fluxSystemKustomization kustomizev1.Kustomization
err := kubeClient.Get(ctx, namespacedName, &fluxSystemKustomization)
if err != nil {
return "", false
} }
return nil if fluxSystemKustomization.Spec.Path == path {
return "", false
}
return fluxSystemKustomization.Spec.Path, true
} }

@ -1,298 +0,0 @@
/*
Copyright 2021 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 bootstrapBServerCmd = &cobra.Command{
Use: "bitbucket-server",
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
commits the Flux manifests to the master branch.
Then it configures the target cluster to synchronize with the repository.
If the Flux components are present on the cluster,
the bootstrap command will perform an upgrade if needed.`,
Example: ` # Create a Bitbucket Server API token and export it as an env var
export BITBUCKET_TOKEN=<my-token>
# 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 --path=clusters/my-cluster
# Run bootstrap for a private repository using SSH authentication
flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --hostname=<domain> --path=clusters/my-cluster
# Run bootstrap for a public repository on a personal account
flux bootstrap bitbucket-server --owner=<user> --repository=<repository name> --private=false --personal --hostname=<domain> --token-auth --path=clusters/my-cluster
# Run bootstrap for an existing repository with a branch named main
flux bootstrap bitbucket-server --owner=<project> --username=<user> --repository=<repository name> --branch=main --hostname=<domain> --token-auth --path=clusters/my-cluster`,
RunE: bootstrapBServerCmdRun,
}
const (
bServerDefaultPermission = "push"
bServerTokenEnvVar = "BITBUCKET_TOKEN"
)
type bServerFlags struct {
owner string
repository string
interval time.Duration
personal bool
username string
private bool
hostname string
path flags.SafeRelativePath
teams []string
readWriteKey bool
reconcile bool
}
var bServerArgs bServerFlags
func init() {
bootstrapBServerCmd.Flags().StringVar(&bServerArgs.owner, "owner", "", "Bitbucket Server user or project name")
bootstrapBServerCmd.Flags().StringVar(&bServerArgs.repository, "repository", "", "Bitbucket Server repository name")
bootstrapBServerCmd.Flags().StringSliceVar(&bServerArgs.teams, "group", []string{}, "Bitbucket Server groups to be given write access (also accepts comma-separated values)")
bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.personal, "personal", false, "if true, the owner is assumed to be a Bitbucket Server user; otherwise a group")
bootstrapBServerCmd.Flags().StringVarP(&bServerArgs.username, "username", "u", "git", "authentication username")
bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.private, "private", true, "if true, the repository is setup or configured as private")
bootstrapBServerCmd.Flags().DurationVar(&bServerArgs.interval, "interval", time.Minute, "sync interval")
bootstrapBServerCmd.Flags().StringVar(&bServerArgs.hostname, "hostname", "", "Bitbucket Server hostname")
bootstrapBServerCmd.Flags().Var(&bServerArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions")
bootstrapBServerCmd.Flags().BoolVar(&bServerArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists")
bootstrapCmd.AddCommand(bootstrapBServerCmd)
}
func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
bitbucketToken := os.Getenv(bServerTokenEnvVar)
if bitbucketToken == "" {
var err error
bitbucketToken, err = readPasswordFromStdin("Please enter your Bitbucket personal access token (PAT): ")
if err != nil {
return fmt.Errorf("could not read token: %w", err)
}
}
if bServerArgs.hostname == "" {
return fmt.Errorf("invalid hostname %q", bServerArgs.hostname)
}
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
}
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
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)
user := bServerArgs.username
if bServerArgs.personal {
user = bServerArgs.owner
}
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 Bitbucket Server provider
providerCfg := provider.Config{
Provider: provider.GitProviderStash,
Hostname: bServerArgs.hostname,
Username: user,
Token: bitbucketToken,
CaBundle: caBundle,
}
providerClient, err := provider.BuildGitProvider(providerCfg)
if err != nil {
return err
}
// Lazy go-git repository
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: user,
Password: bitbucketToken,
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: bServerArgs.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: bServerArgs.path.String(),
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
}
if bootstrapArgs.tokenAuth {
if bServerArgs.personal {
secretOpts.Username = bServerArgs.owner
} else {
secretOpts.Username = bServerArgs.username
}
secretOpts.Password = bitbucketToken
secretOpts.CACrt = caBundle
} else {
keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password)
if err != nil {
return err
}
secretOpts.Keypair = keypair
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
secretOpts.SSHHostname = bServerArgs.hostname
if bootstrapArgs.sshHostname != "" {
secretOpts.SSHHostname = bootstrapArgs.sshHostname
}
}
// Sync manifest config
syncOpts := sync.Options{
Interval: bServerArgs.interval,
Name: *kubeconfigArgs.Namespace,
Namespace: *kubeconfigArgs.Namespace,
Branch: bootstrapArgs.branch,
Secret: bootstrapArgs.secretName,
TargetPath: bServerArgs.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(bServerArgs.owner, bServerArgs.repository, bServerArgs.personal),
bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithBootstrapTransportType("https"),
bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithProviderTeamPermissions(mapTeamSlice(bServerArgs.teams, bServerDefaultPermission)),
bootstrap.WithReadWriteKeyPermissions(bServerArgs.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 !bServerArgs.private {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public"))
}
if bServerArgs.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)
}

@ -1,403 +0,0 @@
/*
Copyright 2021 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"
"net/url"
"os"
"strings"
"time"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/bootstrap"
"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{
Use: "git",
Short: "Deploy Flux on a cluster connected to a Git repository",
Long: `The bootstrap git command commits the Flux manifests to the
branch of a Git repository. And 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: ` # Run bootstrap for a Git repository and authenticate with your SSH agent
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
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
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
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,
}
type gitFlags struct {
url string
interval time.Duration
path flags.SafeRelativePath
username string
password string
silent bool
insecureHttpAllowed bool
withBearerToken bool
}
const (
gitPasswordEnvVar = "GIT_PASSWORD"
)
var gitArgs gitFlags
func init() {
bootstrapGitCmd.Flags().StringVar(&gitArgs.url, "url", "", "Git repository URL")
bootstrapGitCmd.Flags().DurationVar(&gitArgs.interval, "interval", time.Minute, "sync interval")
bootstrapGitCmd.Flags().Var(&gitArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
bootstrapGitCmd.Flags().StringVarP(&gitArgs.username, "username", "u", "git", "basic authentication username")
bootstrapGitCmd.Flags().StringVarP(&gitArgs.password, "password", "p", "", "basic authentication password")
bootstrapGitCmd.Flags().BoolVarP(&gitArgs.silent, "silent", "s", false, "assumes the deploy key is already setup, skips confirmation")
bootstrapGitCmd.Flags().BoolVar(&gitArgs.insecureHttpAllowed, "allow-insecure-http", false, "allows insecure HTTP connections")
bootstrapGitCmd.Flags().BoolVar(&gitArgs.withBearerToken, "with-bearer-token", false, "use password as bearer token for Authorization header")
bootstrapCmd.AddCommand(bootstrapGitCmd)
}
func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
if gitArgs.withBearerToken {
bootstrapArgs.tokenAuth = true
}
gitPassword := os.Getenv(gitPasswordEnvVar)
if gitPassword != "" && gitArgs.password == "" {
gitArgs.password = gitPassword
}
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 {
return err
}
repositoryURL, err := url.Parse(gitArgs.url)
if err != nil {
return err
}
if strings.Contains(repositoryURL.Hostname(), "git-codecommit") && strings.Contains(repositoryURL.Hostname(), "amazonaws.com") {
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)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err
} else {
bootstrapArgs.version = ver
}
manifestsBase, err := buildEmbeddedManifestBase()
if err != nil {
return err
}
defer os.RemoveAll(manifestsBase)
// Lazy go-git repository
tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-")
if err != nil {
return fmt.Errorf("failed to create temporary working dir: %w", err)
}
defer os.RemoveAll(tmpDir)
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
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: gitArgs.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: gitArgs.path.String(),
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
}
if bootstrapArgs.tokenAuth {
if gitArgs.withBearerToken {
secretOpts.BearerToken = gitArgs.password
} else {
secretOpts.Username = gitArgs.username
secretOpts.Password = gitArgs.password
}
secretOpts.CACrt = caBundle
// 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
if repositoryURL.Scheme != "https" && repositoryURL.Scheme != "http" {
repositoryURL.Host = repositoryURL.Hostname()
}
// Configure repository URL to match auth config for sync.
repositoryURL.User = nil
if !gitArgs.insecureHttpAllowed {
repositoryURL.Scheme = "https"
}
} else {
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.Password = gitArgs.password
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
// Configure repository URL to match auth config for sync
// Override existing user when user is not already set
// or when a username was passed in
if repositoryURL.User == nil || gitArgs.username != "git" {
repositoryURL.User = url.User(gitArgs.username)
}
repositoryURL.Scheme = "ssh"
if bootstrapArgs.sshHostname != "" {
repositoryURL.Host = bootstrapArgs.sshHostname
}
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.
secretOpts.SSHHostname = repositoryURL.Host
}
// Sync manifest config
syncOpts := sync.Options{
Interval: gitArgs.interval,
Name: *kubeconfigArgs.Namespace,
Namespace: *kubeconfigArgs.Namespace,
URL: repositoryURL.String(),
Branch: bootstrapArgs.branch,
Secret: bootstrapArgs.secretName,
TargetPath: gitArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
}
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath)
if err != nil {
return err
}
// Bootstrap config
bootstrapOpts := []bootstrap.GitOption{
bootstrap.WithRepositoryURL(gitArgs.url),
bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions),
bootstrap.WithPostGenerateSecretFunc(promptPublicKey),
bootstrap.WithLogger(logger),
bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID),
}
// Setup bootstrapper with constructed configs
b, err := bootstrap.NewPlainGitProvider(gitClient, kubeClient, bootstrapOpts...)
if err != nil {
return err
}
// Run
return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout)
}
// getAuthOpts retruns a AuthOptions based on the scheme
// of the given URL and the configured flags. If the protocol equals
// "ssh" but no private key is configured, authentication using the local
// SSH-agent is attempted.
func getAuthOpts(u *url.URL, caBundle []byte) (*git.AuthOptions, error) {
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":
httpsAuth := git.AuthOptions{
Transport: git.HTTPS,
CAFile: caBundle,
}
if gitArgs.withBearerToken {
httpsAuth.BearerToken = gitArgs.password
} else {
httpsAuth.Username = gitArgs.username
httpsAuth.Password = gitArgs.password
}
return &httpsAuth, nil
case "ssh":
authOpts := &git.AuthOptions{
Transport: git.SSH,
Username: u.User.Username(),
Password: gitArgs.password,
}
if bootstrapArgs.privateKeyFile != "" {
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 authOpts, nil
default:
return nil, fmt.Errorf("scheme %q is not supported", u.Scheme)
}
}
func promptPublicKey(ctx context.Context, secret corev1.Secret, _ sourcesecret.Options) error {
ppk, ok := secret.StringData[sourcesecret.PublicKeySecretKey]
if !ok {
return nil
}
logger.Successf("public key: %s", strings.TrimSpace(ppk))
if !gitArgs.silent {
prompt := promptui.Prompt{
Label: "Please give the key access to your repository",
IsConfirm: true,
}
_, err := prompt.Run()
if err != nil {
return fmt.Errorf("aborting")
}
}
return nil
}

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

@ -19,265 +19,280 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"net/url"
"os" "os"
"path"
"path/filepath"
"time" "time"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/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 bootstrapGitHubCmd = &cobra.Command{ var bootstrapGitHubCmd = &cobra.Command{
Use: "github", Use: "github",
Short: "Deploy Flux on a cluster connected to a GitHub repository", Short: "Bootstrap toolkit components in 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 Flux manifests to the specified branch. commits the toolkit components manifests to the main branch.
Then it configures the target cluster to synchronize with that repository. Then it configures the target cluster to synchronize with the repository.
If the Flux components are present on the cluster, If the toolkit 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 repo owned by a GitHub organization
flux bootstrap github --owner=<organization> --repository=<repository name> --path=clusters/my-cluster flux bootstrap github --owner=<organization> --repository=<repo name>
# Run bootstrap for a private repository and assign organization teams to it # Run bootstrap for a private repo and assign organization teams to it
flux bootstrap github --owner=<organization> --repository=<repository name> --team=<team1 slug> --team=<team2 slug> --path=clusters/my-cluster flux bootstrap github --owner=<organization> --repository=<repo name> --team=<team1 slug> --team=<team2 slug>
# Run bootstrap for a private repository and assign organization teams with their access level(e.g maintain, admin) to it # Run bootstrap for a repository path
flux bootstrap github --owner=<organization> --repository=<repository name> --team=<team1 slug>:<access-level> --path=clusters/my-cluster flux bootstrap github --owner=<organization> --repository=<repo 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 --path=clusters/my-cluster flux bootstrap github --owner=<user> --repository=<repo name> --private=false --personal=true
# Run bootstrap for a private repository hosted on GitHub Enterprise using SSH auth # Run bootstrap for a private repo hosted on GitHub Enterprise using SSH auth
flux bootstrap github --owner=<organization> --repository=<repository name> --hostname=<domain> --ssh-hostname=<domain> --path=clusters/my-cluster flux bootstrap github --owner=<organization> --repository=<repo name> --hostname=<domain> --ssh-hostname=<domain>
# Run bootstrap for a private repository hosted on GitHub Enterprise using HTTPS auth # Run bootstrap for a private repo hosted on GitHub Enterprise using HTTPS auth
flux bootstrap github --owner=<organization> --repository=<repository name> --hostname=<domain> --token-auth --path=clusters/my-cluster flux bootstrap github --owner=<organization> --repository=<repo name> --hostname=<domain> --token-auth
# Run bootstrap for an existing repository with a branch named main # Run bootstrap for a an existing repository with a branch named main
flux bootstrap github --owner=<organization> --repository=<repository name> --branch=main --path=clusters/my-cluster`, flux bootstrap github --owner=<organization> --repository=<repo name> --branch=main
`,
RunE: bootstrapGitHubCmdRun, RunE: bootstrapGitHubCmdRun,
} }
type githubFlags struct { var (
owner string ghOwner string
repository string ghRepository string
interval time.Duration ghInterval time.Duration
personal bool ghPersonal bool
private bool ghPrivate bool
hostname string ghHostname string
path flags.SafeRelativePath ghPath flags.SafeRelativePath
teams []string ghTeams []string
readWriteKey bool ghDelete bool
reconcile bool ghSSHHostname string
} )
const ( const (
ghDefaultPermission = "maintain" ghDefaultPermission = "maintain"
ghDefaultDomain = "github.com"
ghTokenEnvVar = "GITHUB_TOKEN"
) )
var githubArgs githubFlags
func init() { func init() {
bootstrapGitHubCmd.Flags().StringVar(&githubArgs.owner, "owner", "", "GitHub user or organization name") bootstrapGitHubCmd.Flags().StringVar(&ghOwner, "owner", "", "GitHub user or organization name")
bootstrapGitHubCmd.Flags().StringVar(&githubArgs.repository, "repository", "", "GitHub repository name") bootstrapGitHubCmd.Flags().StringVar(&ghRepository, "repository", "", "GitHub repository name")
bootstrapGitHubCmd.Flags().StringSliceVar(&githubArgs.teams, "team", []string{}, "GitHub 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)") bootstrapGitHubCmd.Flags().StringArrayVar(&ghTeams, "team", []string{}, "GitHub team to be given maintainer access")
bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.personal, "personal", false, "if true, the owner is assumed to be a GitHub user; otherwise an org") bootstrapGitHubCmd.Flags().BoolVar(&ghPersonal, "personal", false, "is personal repository")
bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.private, "private", true, "if true, the repository is setup or configured as private") bootstrapGitHubCmd.Flags().BoolVar(&ghPrivate, "private", true, "is private repository")
bootstrapGitHubCmd.Flags().DurationVar(&githubArgs.interval, "interval", time.Minute, "sync interval") bootstrapGitHubCmd.Flags().DurationVar(&ghInterval, "interval", time.Minute, "sync interval")
bootstrapGitHubCmd.Flags().StringVar(&githubArgs.hostname, "hostname", ghDefaultDomain, "GitHub hostname") bootstrapGitHubCmd.Flags().StringVar(&ghHostname, "hostname", git.GitHubDefaultHostname, "GitHub hostname")
bootstrapGitHubCmd.Flags().Var(&githubArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapGitHubCmd.Flags().StringVar(&ghSSHHostname, "ssh-hostname", "", "GitHub SSH hostname, to be used when the SSH host differs from the HTTPS one")
bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") bootstrapGitHubCmd.Flags().Var(&ghPath, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
bootstrapGitHubCmd.Flags().BoolVar(&githubArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists")
bootstrapGitHubCmd.Flags().BoolVar(&ghDelete, "delete", false, "delete repository (used for testing only)")
bootstrapGitHubCmd.Flags().MarkHidden("delete")
bootstrapCmd.AddCommand(bootstrapGitHubCmd) bootstrapCmd.AddCommand(bootstrapGitHubCmd)
} }
func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
ghToken := os.Getenv(ghTokenEnvVar) ghToken := os.Getenv(git.GitHubTokenName)
if ghToken == "" { if ghToken == "" {
var err error return fmt.Errorf("%s environment variable not found", git.GitHubTokenName)
ghToken, err = readPasswordFromStdin("Please enter your GitHub personal access token (PAT): ")
if err != nil {
return fmt.Errorf("could not read token: %w", err)
}
} }
if err := bootstrapValidate(); err != nil { if err := bootstrapValidate(); err != nil {
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil { if err != nil {
return err return err
} }
if !bootstrapArgs.force { usedPath, bootstrapPathDiffers := checkIfBootstrapPathDiffers(ctx, kubeClient, namespace, filepath.ToSlash(ghPath.String()))
err = confirmBootstrap(ctx, kubeClient)
if bootstrapPathDiffers {
return fmt.Errorf("cluster already bootstrapped to %v path", usedPath)
}
repository, err := git.NewRepository(ghRepository, ghOwner, ghHostname, ghToken, "flux", ghOwner+"@users.noreply.github.com")
if err != nil { if err != nil {
return err return err
} }
if ghSSHHostname != "" {
repository.SSHHost = ghSSHHostname
} }
// Manifest base provider := &git.GithubProvider{
if ver, err := getVersion(bootstrapArgs.version); err != nil { IsPrivate: ghPrivate,
return err IsPersonal: ghPersonal,
} else {
bootstrapArgs.version = ver
} }
manifestsBase, err := buildEmbeddedManifestBase()
tmpDir, err := ioutil.TempDir("", namespace)
if err != nil { if err != nil {
return err return err
} }
defer os.RemoveAll(manifestsBase) defer os.RemoveAll(tmpDir)
var caBundle []byte if ghDelete {
if bootstrapArgs.caFile != "" { if err := provider.DeleteRepository(ctx, repository); err != nil {
var err error return err
caBundle, err = os.ReadFile(bootstrapArgs.caFile) }
logger.Successf("repository deleted")
return nil
}
// create GitHub repository if doesn't exists
logger.Actionf("connecting to %s", ghHostname)
changed, err := provider.CreateRepository(ctx, repository)
if err != nil { if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err) return err
} }
if changed {
logger.Successf("repository created")
} }
// Build GitHub provider
providerCfg := provider.Config{ withErrors := false
Provider: provider.GitProviderGitHub, // add teams to org repository
Hostname: githubArgs.hostname, if !ghPersonal {
Token: ghToken, for _, team := range ghTeams {
CaBundle: caBundle, if changed, err := provider.AddTeam(ctx, repository, team, ghDefaultPermission); err != nil {
logger.Failuref(err.Error())
withErrors = true
} else if changed {
logger.Successf("%s team access granted", team)
} }
providerClient, err := provider.BuildGitProvider(providerCfg) }
if err != nil { }
// clone repository and checkout the main branch
if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
return err return err
} }
logger.Successf("repository cloned")
tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") // generate install manifests
logger.Generatef("generating manifests")
installManifest, err := generateInstallManifests(ghPath.String(), namespace, tmpDir, bootstrapManifestsPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create temporary working dir: %w", err) return err
} }
defer os.RemoveAll(tmpDir)
clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} // stage install manifests
gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ changed, err = repository.Commit(ctx, path.Join(ghPath.String(), namespace), "Add manifests")
Transport: git.HTTPS,
Username: githubArgs.owner,
Password: ghToken,
CAFile: caBundle,
}, clientOpts...)
if err != nil { if err != nil {
return fmt.Errorf("failed to create a Git client: %w", err) return 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: githubArgs.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: githubArgs.path.ToSlash(),
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
}
if bootstrapArgs.tokenAuth {
secretOpts.Username = "git"
secretOpts.Password = ghToken
secretOpts.CACrt = caBundle
} else {
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
secretOpts.SSHHostname = githubArgs.hostname // push install manifests
if bootstrapArgs.sshHostname != "" { if changed {
secretOpts.SSHHostname = bootstrapArgs.sshHostname if err := repository.Push(ctx); err != nil {
return err
} }
logger.Successf("components manifests pushed")
} else {
logger.Successf("components are up to date")
} }
// Sync manifest config // determine if repo synchronization is working
syncOpts := sync.Options{ isInstall := shouldInstallManifests(ctx, kubeClient, namespace)
Interval: githubArgs.interval,
Name: *kubeconfigArgs.Namespace, if isInstall {
Namespace: *kubeconfigArgs.Namespace, // apply install manifests
Branch: bootstrapArgs.branch, logger.Actionf("installing components in %s namespace", namespace)
Secret: bootstrapArgs.secretName, if err := applyInstallManifests(ctx, installManifest, bootstrapComponents()); err != nil {
TargetPath: githubArgs.path.ToSlash(), return err
ManifestFile: sync.MakeDefaultOptions().ManifestFile, }
RecurseSubmodules: bootstrapArgs.recurseSubmodules, logger.Successf("install completed")
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) repoURL := repository.GetURL()
if err != nil {
if bootstrapTokenAuth {
// setup HTTPS token auth
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: namespace,
Namespace: namespace,
},
StringData: map[string]string{
"username": "git",
"password": ghToken,
},
}
if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err return err
} }
} else {
// setup SSH deploy key
repoURL = repository.GetSSH()
if shouldCreateDeployKey(ctx, kubeClient, namespace) {
logger.Actionf("configuring deploy key")
u, err := url.Parse(repository.GetSSH())
if err != nil {
return fmt.Errorf("git URL parse failed: %w", err)
}
// Bootstrap config key, err := generateDeployKey(ctx, kubeClient, u, namespace)
bootstrapOpts := []bootstrap.GitProviderOption{ if err != nil {
bootstrap.WithProviderRepository(githubArgs.owner, githubArgs.repository, githubArgs.personal), return fmt.Errorf("generating deploy key failed: %w", err)
bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithBootstrapTransportType("https"),
bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithProviderTeamPermissions(mapTeamSlice(githubArgs.teams, ghDefaultPermission)),
bootstrap.WithReadWriteKeyPermissions(githubArgs.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)) keyName := "flux"
if ghPath != "" {
keyName = fmt.Sprintf("flux-%s", ghPath)
} }
if bootstrapArgs.tokenAuth {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
return err
} else if changed {
logger.Successf("deploy key configured")
} }
if !githubArgs.private {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public"))
} }
if githubArgs.reconcile {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
} }
// Setup bootstrapper with constructed configs // configure repo synchronization
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) logger.Actionf("generating sync manifests")
syncManifests, err := generateSyncManifests(repoURL, bootstrapBranch, namespace, namespace, filepath.ToSlash(ghPath.String()), tmpDir, ghInterval)
if err != nil { if err != nil {
return err return err
} }
// Run // commit and push manifests
return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) if changed, err = repository.Commit(ctx, path.Join(ghPath.String(), namespace), "Add manifests"); err != nil {
return err
} else if changed {
if err := repository.Push(ctx); err != nil {
return err
}
logger.Successf("sync manifests pushed")
}
// apply manifests and waiting for sync
logger.Actionf("applying sync manifests")
if err := applySyncManifests(ctx, kubeClient, namespace, namespace, syncManifests); err != nil {
return err
}
if withErrors {
return fmt.Errorf("bootstrap completed with errors")
}
logger.Successf("bootstrap finished")
return nil
} }

@ -19,314 +19,255 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"net/url"
"os" "os"
"path"
"path/filepath"
"regexp" "regexp"
"strings"
"time" "time"
"github.com/fluxcd/go-git-providers/gitprovider"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/pkg/git/gogit"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/git"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/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 bootstrapGitLabCmd = &cobra.Command{ var bootstrapGitLabCmd = &cobra.Command{
Use: "gitlab", Use: "gitlab",
Short: "Deploy Flux on a cluster connected to a GitLab repository", Short: "Bootstrap toolkit components in 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 Flux manifests to the specified branch. commits the toolkit components manifests to the master branch.
Then it configures the target cluster to synchronize with that repository. Then it configures the target cluster to synchronize with the repository.
If the Flux components are present on the cluster, If the toolkit 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>
# Run bootstrap for a private repository using HTTPS token authentication # Run bootstrap for a private repo using HTTPS token authentication
flux bootstrap gitlab --owner=<group> --repository=<repository name> --token-auth flux bootstrap gitlab --owner=<group> --repository=<repo name> --token-auth
# Run bootstrap for a private repository using SSH authentication # Run bootstrap for a private repo using SSH authentication
flux bootstrap gitlab --owner=<group> --repository=<repository name> flux bootstrap gitlab --owner=<group> --repository=<repo name>
# Run bootstrap for a repository path # Run bootstrap for a repository path
flux bootstrap gitlab --owner=<group> --repository=<repository name> --path=dev-cluster flux bootstrap gitlab --owner=<group> --repository=<repo name> --path=dev-cluster
# Run bootstrap for a public repository
flux bootstrap gitlab --owner=<group> --repository=<repository name> --visibility=public --token-auth
# Run bootstrap for a private repository hosted on a GitLab server # Run bootstrap for a public repository on a personal account
flux bootstrap gitlab --owner=<group> --repository=<repository name> --hostname=<gitlab_url> --token-auth flux bootstrap gitlab --owner=<user> --repository=<repo name> --private=false --personal --token-auth
# Run bootstrap for an existing repository with a branch named main # Run bootstrap for a private repo hosted on a GitLab server
flux bootstrap gitlab --owner=<group> --repository=<repository name> --branch=main --token-auth flux bootstrap gitlab --owner=<group> --repository=<repo name> --hostname=<domain> --token-auth
# Run bootstrap for a private repository using Deploy Token authentication # Run bootstrap for a an existing repository with a branch named main
flux bootstrap gitlab --owner=<group> --repository=<repository name> --deploy-token-auth flux bootstrap gitlab --owner=<organization> --repository=<repo name> --branch=main --token-auth
`, `,
RunE: bootstrapGitLabCmdRun, RunE: bootstrapGitLabCmdRun,
} }
const ( const (
glDefaultPermission = "maintain"
glDefaultDomain = "gitlab.com"
glTokenEnvVar = "GITLAB_TOKEN"
gitlabProjectRegex = `\A[[:alnum:]\x{00A9}-\x{1f9ff}_][[:alnum:]\p{Pd}\x{00A9}-\x{1f9ff}_\.]*\z` gitlabProjectRegex = `\A[[:alnum:]\x{00A9}-\x{1f9ff}_][[:alnum:]\p{Pd}\x{00A9}-\x{1f9ff}_\.]*\z`
) )
type gitlabFlags struct { var (
owner string glOwner string
repository string glRepository string
interval time.Duration glInterval time.Duration
personal bool glPersonal bool
visibility flags.GitLabVisibility glPrivate bool
private bool glHostname string
hostname string glSSHHostname string
path flags.SafeRelativePath glPath flags.SafeRelativePath
teams []string )
readWriteKey bool
reconcile bool
deployTokenAuth bool
}
func NewGitlabFlags() gitlabFlags {
return gitlabFlags{
visibility: flags.GitLabVisibility(gitprovider.RepositoryVisibilityPrivate),
}
}
var gitlabArgs = NewGitlabFlags()
func init() { func init() {
bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.owner, "owner", "", "GitLab user or group name") bootstrapGitLabCmd.Flags().StringVar(&glOwner, "owner", "", "GitLab user or group name")
bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.repository, "repository", "", "GitLab repository name") bootstrapGitLabCmd.Flags().StringVar(&glRepository, "repository", "", "GitLab repository name")
bootstrapGitLabCmd.Flags().StringSliceVar(&gitlabArgs.teams, "team", []string{}, "GitLab teams to be given maintainer access (also accepts comma-separated values)") bootstrapGitLabCmd.Flags().BoolVar(&glPersonal, "personal", false, "is personal repository")
bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.personal, "personal", false, "if true, the owner is assumed to be a GitLab user; otherwise a group") bootstrapGitLabCmd.Flags().BoolVar(&glPrivate, "private", true, "is private repository")
bootstrapGitLabCmd.Flags().BoolVar(&gitlabArgs.private, "private", true, "if true, the repository is setup or configured as private") bootstrapGitLabCmd.Flags().DurationVar(&glInterval, "interval", time.Minute, "sync interval")
bootstrapGitLabCmd.Flags().MarkDeprecated("private", "use --visibility instead") bootstrapGitLabCmd.Flags().StringVar(&glHostname, "hostname", git.GitLabDefaultHostname, "GitLab hostname")
bootstrapGitLabCmd.Flags().Var(&gitlabArgs.visibility, "visibility", gitlabArgs.visibility.Description()) bootstrapGitLabCmd.Flags().StringVar(&glSSHHostname, "ssh-hostname", "", "GitLab SSH hostname, to be used when the SSH host differs from the HTTPS one")
bootstrapGitLabCmd.Flags().DurationVar(&gitlabArgs.interval, "interval", time.Minute, "sync interval") bootstrapGitLabCmd.Flags().Var(&glPath, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
bootstrapGitLabCmd.Flags().StringVar(&gitlabArgs.hostname, "hostname", glDefaultDomain, "GitLab hostname")
bootstrapGitLabCmd.Flags().Var(&gitlabArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path")
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.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)
} }
func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
glToken := os.Getenv(glTokenEnvVar) glToken := os.Getenv(git.GitLabTokenName)
if glToken == "" { if glToken == "" {
var err error return fmt.Errorf("%s environment variable not found", git.GitLabTokenName)
glToken, err = readPasswordFromStdin("Please enter your GitLab personal access token (PAT): ") }
projectNameIsValid, err := regexp.MatchString(gitlabProjectRegex, glRepository)
if err != nil { if err != nil {
return fmt.Errorf("could not read token: %w", err) return err
} }
if !projectNameIsValid {
return fmt.Errorf("%s is an invalid project name for gitlab.\nIt can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'.", glRepository)
} }
if projectNameIsValid, err := regexp.MatchString(gitlabProjectRegex, gitlabArgs.repository); err != nil || !projectNameIsValid { if err := bootstrapValidate(); err != nil {
if err == nil {
err = fmt.Errorf("%s is an invalid project name for gitlab.\nIt can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'.", gitlabArgs.repository)
}
return err return err
} }
if bootstrapArgs.tokenAuth && gitlabArgs.deployTokenAuth { ctx, cancel := context.WithTimeout(context.Background(), timeout)
return fmt.Errorf("--token-auth and --deploy-token-auth cannot be set both.") defer cancel()
kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil {
return err
} }
if !gitlabArgs.private { usedPath, bootstrapPathDiffers := checkIfBootstrapPathDiffers(ctx, kubeClient, namespace, filepath.ToSlash(glPath.String()))
gitlabArgs.visibility.Set(string(gitprovider.RepositoryVisibilityPublic))
cmd.Println("Using visibility public as --private=false") if bootstrapPathDiffers {
return fmt.Errorf("cluster already bootstrapped to %v path", usedPath)
} }
if err := bootstrapValidate(); err != nil { repository, err := git.NewRepository(glRepository, glOwner, glHostname, glToken, "flux", glOwner+"@users.noreply.gitlab.com")
if err != nil {
return err return err
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) if glSSHHostname != "" {
defer cancel() repository.SSHHost = glSSHHostname
}
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) tmpDir, err := ioutil.TempDir("", namespace)
if err != nil { if err != nil {
return err return err
} }
defer os.RemoveAll(tmpDir)
if !bootstrapArgs.force { provider := &git.GitLabProvider{
err = confirmBootstrap(ctx, kubeClient) IsPrivate: glPrivate,
IsPersonal: glPersonal,
}
// create GitLab project if doesn't exists
logger.Actionf("connecting to %s", glHostname)
changed, err := provider.CreateRepository(ctx, repository)
if err != nil { if err != nil {
return err return err
} }
if changed {
logger.Successf("repository created")
} }
// Manifest base // clone repository and checkout the master branch
if ver, err := getVersion(bootstrapArgs.version); err != nil { if err := repository.Checkout(ctx, bootstrapBranch, tmpDir); err != nil {
return err return err
} else {
bootstrapArgs.version = ver
} }
manifestsBase, err := buildEmbeddedManifestBase() logger.Successf("repository cloned")
// generate install manifests
logger.Generatef("generating manifests")
installManifest, err := generateInstallManifests(glPath.String(), namespace, tmpDir, bootstrapManifestsPath)
if err != nil { if err != nil {
return err return err
} }
defer os.RemoveAll(manifestsBase)
var caBundle []byte // stage install manifests
if bootstrapArgs.caFile != "" { changed, err = repository.Commit(ctx, path.Join(glPath.String(), namespace), "Add manifests")
var err error
caBundle, err = os.ReadFile(bootstrapArgs.caFile)
if err != nil { if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err) return err
}
} }
// Build GitLab provider // push install manifests
providerCfg := provider.Config{ if changed {
Provider: provider.GitProviderGitLab, if err := repository.Push(ctx); err != nil {
Hostname: gitlabArgs.hostname, return err
Token: glToken,
CaBundle: caBundle,
} }
// Workaround for: https://github.com/fluxcd/go-git-providers/issues/55 logger.Successf("components manifests pushed")
if hostname := providerCfg.Hostname; hostname != glDefaultDomain && } else {
!strings.HasPrefix(hostname, "https://") && logger.Successf("components are up to date")
!strings.HasPrefix(hostname, "http://") {
providerCfg.Hostname = "https://" + providerCfg.Hostname
} }
providerClient, err := provider.BuildGitProvider(providerCfg)
if err != nil { // determine if repo synchronization is working
isInstall := shouldInstallManifests(ctx, kubeClient, namespace)
if isInstall {
// apply install manifests
logger.Actionf("installing components in %s namespace", namespace)
if err := applyInstallManifests(ctx, installManifest, bootstrapComponents()); err != nil {
return err return err
} }
logger.Successf("install completed")
// Lazy go-git repository
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()} repoURL := repository.GetURL()
gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{
Transport: git.HTTPS, if bootstrapTokenAuth {
Username: gitlabArgs.owner, // setup HTTPS token auth
Password: glToken, secret := corev1.Secret{
CAFile: caBundle, ObjectMeta: metav1.ObjectMeta{
}, clientOpts...) Name: namespace,
if err != nil { Namespace: namespace,
return fmt.Errorf("failed to create a Git client: %w", err) },
} StringData: map[string]string{
"username": "git",
// Install manifest config "password": glToken,
installOptions := install.Options{ },
BaseURL: rootArgs.defaults.BaseURL, }
Version: bootstrapArgs.version, if err := upsertSecret(ctx, kubeClient, secret); err != nil {
Namespace: *kubeconfigArgs.Namespace, return err
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: gitlabArgs.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: gitlabArgs.path.String(),
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile,
}
if bootstrapArgs.tokenAuth {
secretOpts.Username = "git"
secretOpts.Password = glToken
secretOpts.CACrt = caBundle
} else if gitlabArgs.deployTokenAuth {
// the actual deploy token will be reconciled later
secretOpts.CACrt = caBundle
} else { } else {
keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) // setup SSH deploy key
repoURL = repository.GetSSH()
if shouldCreateDeployKey(ctx, kubeClient, namespace) {
logger.Actionf("configuring deploy key")
u, err := url.Parse(repoURL)
if err != nil { if err != nil {
return err return fmt.Errorf("git URL parse failed: %w", err)
} }
secretOpts.Keypair = keypair
secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm)
secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits)
secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve
secretOpts.SSHHostname = gitlabArgs.hostname key, err := generateDeployKey(ctx, kubeClient, u, namespace)
if bootstrapArgs.sshHostname != "" { if err != nil {
secretOpts.SSHHostname = bootstrapArgs.sshHostname return fmt.Errorf("generating deploy key failed: %w", err)
}
} }
// Sync manifest config keyName := "flux"
syncOpts := sync.Options{ if glPath != "" {
Interval: gitlabArgs.interval, keyName = fmt.Sprintf("flux-%s", glPath)
Name: *kubeconfigArgs.Namespace,
Namespace: *kubeconfigArgs.Namespace,
Branch: bootstrapArgs.branch,
Secret: bootstrapArgs.secretName,
TargetPath: gitlabArgs.path.ToSlash(),
ManifestFile: sync.MakeDefaultOptions().ManifestFile,
RecurseSubmodules: bootstrapArgs.recurseSubmodules,
} }
entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) if changed, err := provider.AddDeployKey(ctx, repository, key, keyName); err != nil {
if err != nil {
return err return err
} else if changed {
logger.Successf("deploy key configured")
} }
// Bootstrap config
bootstrapOpts := []bootstrap.GitProviderOption{
bootstrap.WithProviderRepository(gitlabArgs.owner, gitlabArgs.repository, gitlabArgs.personal),
bootstrap.WithProviderVisibility(gitlabArgs.visibility.String()),
bootstrap.WithBranch(bootstrapArgs.branch),
bootstrap.WithBootstrapTransportType("https"),
bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail),
bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix),
bootstrap.WithProviderTeamPermissions(mapTeamSlice(gitlabArgs.teams, glDefaultPermission)),
bootstrap.WithReadWriteKeyPermissions(gitlabArgs.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 || gitlabArgs.deployTokenAuth {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) // configure repo synchronization
logger.Actionf("generating sync manifests")
syncManifests, err := generateSyncManifests(repoURL, bootstrapBranch, namespace, namespace, filepath.ToSlash(glPath.String()), tmpDir, glInterval)
if err != nil {
return err
} }
if gitlabArgs.deployTokenAuth {
bootstrapOpts = append(bootstrapOpts, bootstrap.WithDeployTokenAuth()) // commit and push manifests
if changed, err = repository.Commit(ctx, path.Join(glPath.String(), namespace), "Add manifests"); err != nil {
return err
} else if changed {
if err := repository.Push(ctx); err != nil {
return err
} }
if gitlabArgs.reconcile { logger.Successf("sync manifests pushed")
bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile())
} }
// Setup bootstrapper with constructed configs // apply manifests and waiting for sync
b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) logger.Actionf("applying sync manifests")
if err != nil { if err := applySyncManifests(ctx, kubeClient, namespace, namespace, syncManifests); err != nil {
return err return err
} }
// Run logger.Successf("bootstrap finished")
return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) return nil
} }

@ -1,31 +0,0 @@
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"github.com/spf13/cobra"
)
var buildCmd = &cobra.Command{
Use: "build",
Short: "Build a flux resource",
Long: `The build command is used to build flux resources.`,
}
func init() {
rootCmd.AddCommand(buildCmd)
}

@ -1,117 +0,0 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"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
}

@ -1,70 +0,0 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"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))
})
}
}

@ -1,174 +0,0 @@
/*
Copyright 2021 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"
"os/signal"
"github.com/spf13/cobra"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
ssautil "github.com/fluxcd/pkg/ssa/utils"
"github.com/fluxcd/flux2/v2/internal/build"
)
var buildKsCmd = &cobra.Command{
Use: "kustomization",
Aliases: []string{"ks"},
Short: "Build 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
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
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"
# Run recursively on all encountered Kustomizations
flux build kustomization my-app --path ./path/to/local/manifests \
--recursive \
--local-sources GitRepository/flux-system/my-repo=./path/to/local/git`,
ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)),
RunE: buildKsCmdRun,
}
type buildKsFlags struct {
kustomizationFile string
path string
ignorePaths []string
dryRun bool
strictSubst bool
recursive bool
localSources map[string]string
}
var buildKsArgs buildKsFlags
func init() {
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.")
buildKsCmd.Flags().BoolVarP(&buildKsArgs.recursive, "recursive", "r", false, "Recursively build Kustomizations")
buildKsCmd.Flags().StringToStringVar(&buildKsArgs.localSources, "local-sources", nil, "Comma-separated list of repositories in format: Kind/namespace/name=path")
buildCmd.AddCommand(buildKsCmd)
}
func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) {
if len(args) < 1 {
return fmt.Errorf("%s name is required", kustomizationType.humanKind)
}
name := args[0]
if buildKsArgs.path == "" {
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
}
if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() {
return fmt.Errorf("invalid resource path %q", buildKsArgs.path)
}
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),
build.WithRecursive(buildKsArgs.recursive),
build.WithLocalSources(buildKsArgs.localSources),
)
} 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),
build.WithRecursive(buildKsArgs.recursive),
build.WithLocalSources(buildKsArgs.localSources),
)
}
if err != nil {
return err
}
// create a signal channel
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt)
errChan := make(chan error)
go func() {
objects, err := builder.Build()
if err != nil {
errChan <- err
}
manifests, err := ssautil.ObjectsToYAML(objects)
if err != nil {
errChan <- err
}
cmd.Print(manifests)
errChan <- nil
}()
select {
case <-sigc:
fmt.Println("Build cancelled... exiting.")
return builder.Cancel()
case err := <-errChan:
if err != nil {
return err
}
}
return nil
}

@ -1,220 +0,0 @@
//go:build unit
// +build unit
/*
Copyright 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"os"
"path/filepath"
"testing"
"text/template"
)
func setup(t *testing.T, tmpl map[string]string) {
t.Helper()
testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t)
testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-kustomization.yaml", tmpl, t)
}
func TestBuildKustomization(t *testing.T) {
tests := []struct {
name string
args string
resultFile string
assertFunc string
}{
{
name: "no args",
args: "build kustomization podinfo",
resultFile: "invalid resource path \"\"",
assertFunc: "assertError",
},
{
name: "build podinfo",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build podinfo without service",
args: "build kustomization podinfo --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 --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",
},
{
name: "build with recursive",
args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
}
tmpl := map[string]string{
"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)
})
}
}
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"
`
tmpFile := filepath.Join(t.TempDir(), "podinfo.yaml")
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 " + tmpFile + " --path ./testdata/build-kustomization/podinfo",
resultFile: "./testdata/build-kustomization/podinfo-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build podinfo without service",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/delete-service",
resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build deployment and configmap with var substitution",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/var-substitution",
resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build deployment and configmap with var substitution in dry-run mode",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/var-substitution --dry-run",
resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with recursive",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
{
name: "build with recursive in dry-run mode",
args: "build kustomization podinfo --kustomization-file " + tmpFile + " --path ./testdata/build-kustomization/podinfo-with-my-app --recursive --local-sources GitRepository/default/podinfo=./testdata/build-kustomization --dry-run",
resultFile: "./testdata/build-kustomization/podinfo-with-my-app-result.yaml",
assertFunc: "assertGoldenTemplateFile",
},
}
tmpl := map[string]string{
"fluxns": allocateNamespace("flux-system"),
}
setup(t, tmpl)
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(tmpFile, b.Bytes(), 0666)
if err != nil {
t.Fatal(err)
}
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,89 +18,65 @@ package main
import ( import (
"context" "context"
"fmt" "encoding/json"
"os" "os"
"time" "os/exec"
"strings"
"github.com/Masterminds/semver/v3" "github.com/blang/semver/v4"
"github.com/fluxcd/flux2/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
v1 "k8s.io/api/apps/v1" apimachineryversion "k8s.io/apimachinery/pkg/version"
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"
"github.com/fluxcd/pkg/version"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/install"
"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: withPreviewNote(`The check command will perform a series of checks to validate that Long: `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
# Run installation checks # Run installation checks
flux check`, flux check
`,
RunE: runCheckCmd, RunE: runCheckCmd,
} }
type checkFlags struct { var (
pre bool checkPre bool
components []string checkComponents []string
extraComponents []string )
pollInterval time.Duration
}
var kubernetesConstraints = []string{ type kubectlVersion struct {
">=1.30.0-0", ClientVersion *apimachineryversion.Info `json:"clientVersion"`
} }
var checkArgs checkFlags
func init() { func init() {
checkCmd.Flags().BoolVarP(&checkArgs.pre, "pre", "", false, checkCmd.Flags().BoolVarP(&checkPre, "pre", "", false,
"only run pre-installation checks") "only run pre-installation checks")
checkCmd.Flags().StringSliceVar(&checkArgs.components, "components", rootArgs.defaults.Components, checkCmd.Flags().StringSliceVar(&checkComponents, "components", defaults.Components,
"list of components, accepts comma-separated values") "list of components, accepts comma-separated values")
checkCmd.Flags().StringSliceVar(&checkArgs.extraComponents, "components-extra", nil,
"list of components in addition to those supplied or defaulted, accepts comma-separated values")
checkCmd.Flags().DurationVar(&checkArgs.pollInterval, "poll-interval", 5*time.Second,
"how often the health checker should poll the cluster for the latest state of the resources.")
rootCmd.AddCommand(checkCmd) rootCmd.AddCommand(checkCmd)
} }
func runCheckCmd(cmd *cobra.Command, args []string) error { func runCheckCmd(cmd *cobra.Command, args []string) error {
logger.Actionf("checking prerequisites") ctx, cancel := context.WithTimeout(context.Background(), timeout)
checkFailed := false
fluxCheck()
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel() defer cancel()
cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) logger.Actionf("checking prerequisites")
if err != nil { checkFailed := false
return fmt.Errorf("Kubernetes client initialization failed: %s", err.Error())
}
kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()}) if !kubectlCheck(ctx, ">=1.18.0") {
if err != nil { checkFailed = true
return err
} }
if !kubernetesCheck(cfg, kubernetesConstraints) { if !kubernetesCheck(">=1.16.0") {
checkFailed = true checkFailed = true
} }
if checkArgs.pre { if checkPre {
if checkFailed { if checkFailed {
os.Exit(1) os.Exit(1)
} }
@ -108,156 +84,105 @@ 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(ctx, kubeClient) { if !componentsCheck() {
checkFailed = true checkFailed = true
} }
logger.Actionf("checking crds")
if !crdsCheck(ctx, kubeClient) {
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
} }
func fluxCheck() { func kubectlCheck(ctx context.Context, version string) bool {
curSv, err := version.ParseVersion(VERSION) _, err := exec.LookPath("kubectl")
if err != nil {
return
}
// Exclude development builds.
if curSv.Prerelease() != "" {
return
}
latest, err := install.GetLatestVersion()
if err != nil {
return
}
latestSv, err := version.ParseVersion(latest)
if err != nil { if err != nil {
return logger.Failuref("kubectl not found")
} return false
if latestSv.GreaterThan(curSv) {
logger.Failuref("flux %s <%s (new CLI version is available, please upgrade)", curSv, latestSv)
}
} }
func kubernetesCheck(cfg *rest.Config, constraints []string) bool { kubectlArgs := []string{"version", "--client", "--output", "json"}
clientSet, err := kubernetes.NewForConfig(cfg) output, err := utils.ExecKubectlCommand(ctx, utils.ModeCapture, kubeconfig, kubecontext, kubectlArgs...)
if err != nil { if err != nil {
logger.Failuref("Kubernetes client initialization failed: %s", err.Error()) logger.Failuref("kubectl version can't be determined")
return false return false
} }
kv, err := clientSet.Discovery().ServerVersion() kv := &kubectlVersion{}
if err != nil { if err = json.Unmarshal([]byte(output), kv); err != nil {
logger.Failuref("Kubernetes API call failed: %s", err.Error()) logger.Failuref("kubectl version output can't be unmarshaled")
return false return false
} }
v, err := version.ParseVersion(kv.String()) v, err := semver.ParseTolerant(kv.ClientVersion.GitVersion)
if err != nil { if err != nil {
logger.Failuref("Kubernetes version can't be determined") logger.Failuref("kubectl version can't be parsed")
return false return false
} }
var valid bool rng, _ := semver.ParseRange(version)
var vrange string if !rng(v) {
for _, constraint := range constraints { logger.Failuref("kubectl version must be %s", version)
c, _ := semver.NewConstraint(constraint)
if c.Check(v) {
valid = true
vrange = constraint
break
}
}
if !valid {
logger.Failuref("Kubernetes version %s does not match %s", v.Original(), constraints[0])
return false return false
} }
logger.Successf("Kubernetes %s %s", v.String(), vrange) logger.Successf("kubectl %s %s", v.String(), version)
return true return true
} }
func componentsCheck(ctx context.Context, kubeClient client.Client) bool { func kubernetesCheck(version string) bool {
statusChecker, err := status.NewStatusCheckerWithClient(kubeClient, checkArgs.pollInterval, rootArgs.timeout, logger) cfg, err := utils.KubeConfig(kubeconfig, kubecontext)
if err != nil { if err != nil {
logger.Failuref("Kubernetes client initialization failed: %s", err.Error())
return false return false
} }
ok := true client, err := kubernetes.NewForConfig(cfg)
selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} if err != nil {
var list v1.DeploymentList logger.Failuref("Kubernetes client initialization failed: %s", err.Error())
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 return false
} }
for _, d := range list.Items { ver, err := client.Discovery().ServerVersion()
if ref, err := buildComponentObjectRefs(d.Name); err == nil { if err != nil {
if err := statusChecker.Assess(ref...); err != nil { logger.Failuref("Kubernetes API call failed: %s", err.Error())
ok = false return false
}
}
for _, c := range d.Spec.Template.Spec.Containers {
logger.Actionf(c.Image)
}
}
} }
return ok
v, err := semver.ParseTolerant(ver.String())
if err != nil {
logger.Failuref("Kubernetes version can't be determined")
return false
} }
func crdsCheck(ctx context.Context, kubeClient client.Client) bool { rng, _ := semver.ParseRange(version)
ok := true if !rng(v) {
selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} logger.Failuref("Kubernetes version must be %s", version)
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 return false
} }
for _, crd := range list.Items { logger.Successf("Kubernetes %s %s", v.String(), version)
versions := crd.Status.StoredVersions return true
if len(versions) > 0 { }
logger.Successf(crd.Name + "/" + versions[len(versions)-1])
} else { func componentsCheck() bool {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
ok := true
for _, deployment := range checkComponents {
kubectlArgs := []string{"-n", namespace, "rollout", "status", "deployment", deployment, "--timeout", timeout.String()}
if output, err := utils.ExecKubectlCommand(ctx, utils.ModeCapture, kubeconfig, kubecontext, kubectlArgs...); err != nil {
logger.Failuref("%s: %s", deployment, strings.TrimSuffix(output, "\n"))
ok = false ok = false
logger.Failuref("no stored versions for %s", crd.Name) } else {
logger.Successf("%s is healthy", deployment)
} }
kubectlArgs = []string{"-n", namespace, "get", "deployment", deployment, "-o", "jsonpath=\"{..image}\""}
if output, err := utils.ExecKubectlCommand(ctx, utils.ModeCapture, kubeconfig, kubecontext, kubectlArgs...); err == nil {
logger.Actionf(strings.TrimPrefix(strings.TrimSuffix(output, "\""), "\""))
} }
} }
return ok return ok
} }
func fluxClusterVersionCheck(ctx context.Context, kubeClient client.Client) bool {
clusterInfo, err := getFluxClusterInfo(ctx, kubeClient)
if err != nil {
logger.Failuref("checking failed: %s", err.Error())
return false
}
if clusterInfo.distribution() != "" {
logger.Successf("distribution: %s", clusterInfo.distribution())
}
logger.Successf("bootstrapped: %t", clusterInfo.bootstrapped)
return true
}

@ -1,53 +0,0 @@
//go:build e2e
// +build e2e
/*
Copyright 2021 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"
"strings"
"testing"
"github.com/fluxcd/flux2/v2/internal/utils"
)
func TestCheckPre(t *testing.T) {
jsonOutput, err := utils.ExecKubectlCommand(context.TODO(), utils.ModeCapture, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, "version", "--output", "json")
if err != nil {
t.Fatalf("Error running utils.ExecKubectlCommand: %v", err.Error())
}
var versions map[string]interface{}
if err := json.Unmarshal([]byte(jsonOutput), &versions); err != nil {
t.Fatalf("Error unmarshalling '%s': %v", jsonOutput, err.Error())
}
serverGitVersion := strings.TrimPrefix(
versions["serverVersion"].(map[string]interface{})["gitVersion"].(string),
"v")
cmd := cmdTestCase{
args: "check --pre",
assert: assertGoldenTemplateFile("testdata/check/check_pre.golden", map[string]string{
"serverVersion": serverGitVersion,
}),
}
cmd.runTestCmd(t)
}

@ -1,126 +0,0 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"github.com/manifoldco/promptui"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/fluxcd/flux2/v2/pkg/manifestgen"
)
// bootstrapLabels are labels put on a resource by kustomize-controller. These labels on the CRD indicates
// that flux has been bootstrapped.
var bootstrapLabels = []string{
fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group),
fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group),
}
// fluxClusterInfo contains information about an existing flux installation on a cluster.
type fluxClusterInfo struct {
// bootstrapped indicates that Flux was installed using the `flux bootstrap` command.
bootstrapped bool
// managedBy is the name of the tool being used to manage the installation of Flux.
managedBy string
// partOf indicates which distribution the instance is a part of.
partOf string
// version is the Flux version number in semver format.
version string
}
// getFluxClusterInfo returns information on the Flux installation running on the cluster.
// If an error occurred, the returned error will be non-nil.
//
// This function retrieves the GitRepository CRD from the cluster and checks it
// for a set of labels used to determine the Flux version and how Flux was installed.
// It returns the NotFound error from the underlying library if it was unable to find
// the GitRepository CRD and this can be used to check if Flux is installed.
func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, error) {
var info fluxClusterInfo
crdMetadata := &metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{
APIVersion: apiextensionsv1.SchemeGroupVersion.String(),
Kind: "CustomResourceDefinition",
},
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("gitrepositories.%s", sourcev1.GroupVersion.Group),
},
}
if err := c.Get(ctx, client.ObjectKeyFromObject(crdMetadata), crdMetadata); err != nil {
return info, err
}
info.version = crdMetadata.Labels[manifestgen.VersionLabelKey]
var present bool
for _, l := range bootstrapLabels {
_, present = crdMetadata.Labels[l]
}
if present {
info.bootstrapped = true
}
// the `app.kubernetes.io/managed-by` label is not set by flux but might be set by other
// tools used to install Flux e.g Helm.
if manager, ok := crdMetadata.Labels["app.kubernetes.io/managed-by"]; ok {
info.managedBy = manager
}
if partOf, ok := crdMetadata.Labels[manifestgen.PartOfLabelKey]; ok {
info.partOf = partOf
}
return info, nil
}
// confirmFluxInstallOverride displays a prompt to the user so that they can confirm before overriding
// a Flux installation. It returns nil if the installation should continue,
// promptui.ErrAbort if the user doesn't confirm, or an error encountered.
func confirmFluxInstallOverride(info fluxClusterInfo) error {
// no need to display prompt if installation is managed by Flux
if installManagedByFlux(info.managedBy) {
return nil
}
display := fmt.Sprintf("Flux %s has been installed on this cluster with %s!", info.version, info.managedBy)
fmt.Fprintln(rootCmd.ErrOrStderr(), display)
prompt := promptui.Prompt{
Label: fmt.Sprintf("Are you sure you want to override the %s installation? Y/N", info.managedBy),
IsConfirm: true,
}
_, err := prompt.Run()
return err
}
func (info fluxClusterInfo) distribution() string {
distribution := info.version
if info.partOf != "" {
distribution = fmt.Sprintf("%s-%s", info.partOf, info.version)
}
return distribution
}
func installManagedByFlux(manager string) bool {
return manager == "" || manager == "flux"
}

@ -1,141 +0,0 @@
/*
Copyright 2023 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"os"
"testing"
. "github.com/onsi/gomega"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1"
ssautil "github.com/fluxcd/pkg/ssa/utils"
)
func Test_getFluxClusterInfo(t *testing.T) {
g := NewWithT(t)
f, err := os.Open("./testdata/cluster_info/gitrepositories.yaml")
g.Expect(err).To(BeNil())
objs, err := ssautil.ReadObjects(f)
g.Expect(err).To(Not(HaveOccurred()))
gitrepo := objs[0]
tests := []struct {
name string
labels map[string]string
wantErr bool
wantInfo fluxClusterInfo
}{
{
name: "no git repository CRD present",
wantErr: true,
},
{
name: "CRD with kustomize-controller labels",
labels: map[string]string{
fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): "flux-system",
fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system",
"app.kubernetes.io/version": "v2.1.0",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
bootstrapped: true,
},
},
{
name: "CRD with kustomize-controller labels and managed-by label",
labels: map[string]string{
fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): "flux-system",
fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system",
"app.kubernetes.io/version": "v2.1.0",
"app.kubernetes.io/managed-by": "flux",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
bootstrapped: true,
managedBy: "flux",
},
},
{
name: "CRD with only managed-by label",
labels: map[string]string{
"app.kubernetes.io/version": "v2.1.0",
"app.kubernetes.io/managed-by": "helm",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
managedBy: "helm",
},
},
{
name: "CRD with no labels",
labels: map[string]string{},
wantInfo: fluxClusterInfo{},
},
{
name: "CRD with only version label",
labels: map[string]string{
"app.kubernetes.io/version": "v2.1.0",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
},
},
{
name: "CRD with version and part-of labels",
labels: map[string]string{
"app.kubernetes.io/version": "v2.1.0",
"app.kubernetes.io/part-of": "flux",
},
wantInfo: fluxClusterInfo{
version: "v2.1.0",
partOf: "flux",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
g := NewWithT(t)
newscheme := runtime.NewScheme()
apiextensionsv1.AddToScheme(newscheme)
builder := fake.NewClientBuilder().WithScheme(newscheme)
if tt.labels != nil {
gitrepo.SetLabels(tt.labels)
builder = builder.WithRuntimeObjects(gitrepo)
}
client := builder.Build()
info, err := getFluxClusterInfo(context.Background(), client)
if tt.wantErr {
g.Expect(err).To(HaveOccurred())
g.Expect(errors.IsNotFound(err)).To(BeTrue())
} else {
g.Expect(err).To(Not(HaveOccurred()))
}
g.Expect(info).To(BeEquivalentTo(tt.wantInfo))
})
}
}

@ -17,96 +17,15 @@ limitations under the License.
package main package main
import ( import (
"context"
"strings"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
) )
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() {
rootCmd.AddCommand(completionCmd) rootCmd.AddCommand(completionCmd)
} }
func contextsCompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
rawConfig, err := kubeconfigArgs.ToRawKubeConfigLoader().RawConfig()
if err != nil {
return completionError(err)
}
var comps []string
for name := range rawConfig.Contexts {
if strings.HasPrefix(name, toComplete) {
comps = append(comps, name)
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
}
func resourceNamesCompletionFunc(gvk schema.GroupVersionKind) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions)
if err != nil {
return completionError(err)
}
mapper, err := kubeconfigArgs.ToRESTMapper()
if err != nil {
return completionError(err)
}
mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return completionError(err)
}
client, err := dynamic.NewForConfig(cfg)
if err != nil {
return completionError(err)
}
var dr dynamic.ResourceInterface
if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
dr = client.Resource(mapping.Resource).Namespace(*kubeconfigArgs.Namespace)
} else {
dr = client.Resource(mapping.Resource)
}
list, err := dr.List(ctx, metav1.ListOptions{})
if err != nil {
return completionError(err)
}
var comps []string
for _, item := range list.Items {
name := item.GetName()
if strings.HasPrefix(name, toComplete) {
comps = append(comps, name)
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
}
}
func completionError(err error) ([]string, cobra.ShellCompDirective) {
cobra.CompError(err.Error())
return nil, cobra.ShellCompDirectiveError
}

@ -25,7 +25,6 @@ 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)
@ -33,7 +32,8 @@ var completionBashCmd = &cobra.Command{
To configure your bash shell to load completions for each session add to your bashrc To configure your bash shell to load completions for each session add to your bashrc
# ~/.bashrc or ~/.profile # ~/.bashrc or ~/.profile
command -v flux >/dev/null && . <(flux completion bash)`, command -v flux >/dev/null && . <(flux completion bash)
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenBashCompletion(os.Stdout) rootCmd.GenBashCompletion(os.Stdout)
}, },

@ -25,12 +25,16 @@ 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 load completion run
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)
See http://fishshell.com/docs/current/index.html#completion-own for more details`, 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
See http://fishshell.com/docs/current/index.html#completion-own for more details
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenFishCompletion(os.Stdout, true) rootCmd.GenFishCompletion(os.Stdout, true)
}, },

@ -25,7 +25,6 @@ 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)
@ -35,12 +34,13 @@ 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 powershell >> flux-completion.ps1 flux completion >> flux-completion.ps1
Linux: Linux:
cd "${XDG_CONFIG_HOME:-"$HOME/.config/"}/powershell/modules" cd "${XDG_CONFIG_HOME:-"$HOME/.config/"}/powershell/modules"
flux completion powershell >> flux-completions.ps1`, flux completion >> 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)
}, },

@ -17,7 +17,6 @@ limitations under the License.
package main package main
import ( import (
"fmt"
"os" "os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -26,15 +25,14 @@ 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) && compdef _flux flux
To configure your zsh shell to load completions for each session add to your zshrc To configure your zsh shell to load completions for each session add to your zshrc
# ~/.zshrc or ~/.profile # ~/.zshrc or ~/.profile
command -v flux >/dev/null && . <(flux completion zsh) command -v flux >/dev/null && . <(flux completion zsh) && compdef _flux flux
or write a cached file in one of the completion directories in your ${fpath}: or write a cached file in one of the completion directories in your ${fpath}:
@ -42,11 +40,10 @@ echo "${fpath// /\n}" | grep -i completion
flux completion zsh > _flux flux completion zsh > _flux
mv _flux ~/.oh-my-zsh/completions # oh-my-zsh mv _flux ~/.oh-my-zsh/completions # oh-my-zsh
mv _flux ~/.zprezto/modules/completion/external/src/ # zprezto`, mv _flux ~/.zprezto/modules/completion/external/src/ # zprezto
`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
rootCmd.GenZshCompletion(os.Stdout) rootCmd.GenZshCompletion(os.Stdout)
// Cobra doesn't source zsh completion file, explicitly doing it here
fmt.Println("compdef _flux flux")
}, },
} }

@ -19,7 +19,6 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"regexp"
"strings" "strings"
"time" "time"
@ -30,40 +29,26 @@ 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/v2/internal/utils" "github.com/fluxcd/flux2/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 { var (
interval time.Duration interval time.Duration
export bool export bool
labels []string labels []string
} )
var createArgs createFlags
func init() { func init() {
createCmd.PersistentFlags().DurationVarP(&createArgs.interval, "interval", "", time.Minute, "source sync interval") createCmd.PersistentFlags().DurationVarP(&interval, "interval", "", time.Minute, "source sync interval")
createCmd.PersistentFlags().BoolVar(&createArgs.export, "export", false, "export in YAML format to stdout") createCmd.PersistentFlags().BoolVar(&export, "export", false, "export in YAML format to stdout")
createCmd.PersistentFlags().StringSliceVar(&createArgs.labels, "label", nil, createCmd.PersistentFlags().StringSliceVar(&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)
} }
@ -114,10 +99,10 @@ type upsertWaitable interface {
// resource, then waiting for it to reconcile. See the note on // resource, then waiting for it to reconcile. See the note on
// `upsert` for how to work with the `mutate` argument. // `upsert` for how to work with the `mutate` argument.
func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) error { func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) error {
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) // NB globals kubeClient, err := utils.KubeClient(kubeconfig, kubecontext) // NB globals
if err != nil { if err != nil {
return err return err
} }
@ -125,14 +110,14 @@ func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) e
logger.Generatef("generating %s", names.kind) logger.Generatef("generating %s", names.kind)
logger.Actionf("applying %s", names.kind) logger.Actionf("applying %s", names.kind)
namespacedName, err := names.upsert(ctx, kubeClient, object, mutate) namespacedName, err := imageRepositoryType.upsert(ctx, kubeClient, object, mutate)
if err != nil { if err != nil {
return err return err
} }
logger.Waitingf("waiting for %s reconciliation", names.kind) logger.Waitingf("waiting for %s reconciliation", names.kind)
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, if err := wait.PollImmediate(pollInterval, timeout,
isObjectReadyConditionFunc(kubeClient, namespacedName, object.asClientObject())); err != nil { isReady(ctx, kubeClient, namespacedName, object)); err != nil {
return err return err
} }
logger.Successf("%s reconciliation completed", names.kind) logger.Successf("%s reconciliation completed", names.kind)
@ -141,7 +126,7 @@ func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) e
func parseLabels() (map[string]string, error) { func parseLabels() (map[string]string, error) {
result := make(map[string]string) result := make(map[string]string)
for _, label := range createArgs.labels { for _, label := range labels {
// validate key value pair // validate key value pair
parts := strings.Split(label, "=") parts := strings.Split(label, "=")
if len(parts) != 2 { if len(parts) != 2 {
@ -163,8 +148,3 @@ 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)
}

@ -20,58 +20,61 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/pkg/apis/meta"
"github.com/spf13/cobra" "github.com/spf13/cobra"
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"
notificationv1 "github.com/fluxcd/notification-controller/api/v1" notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1"
notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3"
"github.com/fluxcd/pkg/apis/meta"
"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: withPreviewNote(`The create alert command generates a Alert resource.`), Long: "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 \
--event-source Kustomization/flux-system \ --event-source Kustomization/flux-system \
--provider-ref slack \ --provider-ref slack \
flux-system`, flux-system
`,
RunE: createAlertCmdRun, RunE: createAlertCmdRun,
} }
type alertFlags struct { var (
providerRef string aProviderRef string
eventSeverity string aEventSeverity string
eventSources []string aEventSources []string
} )
var alertArgs alertFlags
func init() { func init() {
createAlertCmd.Flags().StringVar(&alertArgs.providerRef, "provider-ref", "", "reference to provider") createAlertCmd.Flags().StringVar(&aProviderRef, "provider-ref", "", "reference to provider")
createAlertCmd.Flags().StringVar(&alertArgs.eventSeverity, "event-severity", "", "severity of events to send alerts for") createAlertCmd.Flags().StringVar(&aEventSeverity, "event-severity", "", "severity of events to send alerts for")
createAlertCmd.Flags().StringSliceVar(&alertArgs.eventSources, "event-source", []string{}, "sources that should generate alerts (<kind>/<name>), also accepts comma-separated values") createAlertCmd.Flags().StringArrayVar(&aEventSources, "event-source", []string{}, "sources that should generate alerts (<kind>/<name>)")
createCmd.AddCommand(createAlertCmd) createCmd.AddCommand(createAlertCmd)
} }
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 aProviderRef == "" {
return fmt.Errorf("provider ref is required") return fmt.Errorf("provider ref is required")
} }
eventSources := []notificationv1.CrossNamespaceObjectReference{} eventSources := []notificationv1.CrossNamespaceObjectReference{}
for _, eventSource := range alertArgs.eventSources { for _, eventSource := range aEventSources {
kind, name, namespace := utils.ParseObjectKindNameNamespace(eventSource) kind, name := utils.ParseObjectKindName(eventSource)
if kind == "" { if kind == "" {
return fmt.Errorf("invalid event source '%s', must be in format <kind>/<name>", eventSource) return fmt.Errorf("invalid event source '%s', must be in format <kind>/<name>", eventSource)
} }
@ -79,7 +82,6 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error {
eventSources = append(eventSources, notificationv1.CrossNamespaceObjectReference{ eventSources = append(eventSources, notificationv1.CrossNamespaceObjectReference{
Kind: kind, Kind: kind,
Name: name, Name: name,
Namespace: namespace,
}) })
} }
@ -92,34 +94,34 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !createArgs.export { if !export {
logger.Generatef("generating Alert") logger.Generatef("generating Alert")
} }
alert := notificationv1b3.Alert{ alert := notificationv1.Alert{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: sourceLabels, Labels: sourceLabels,
}, },
Spec: notificationv1b3.AlertSpec{ Spec: notificationv1.AlertSpec{
ProviderRef: meta.LocalObjectReference{ ProviderRef: corev1.LocalObjectReference{
Name: alertArgs.providerRef, Name: aProviderRef,
}, },
EventSeverity: alertArgs.eventSeverity, EventSeverity: aEventSeverity,
EventSources: eventSources, EventSources: eventSources,
Suspend: false, Suspend: false,
}, },
} }
if createArgs.export { if export {
return printExport(exportAlert(&alert)) return exportAlert(alert)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil { if err != nil {
return err return err
} }
@ -131,8 +133,8 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for Alert reconciliation") logger.Waitingf("waiting for Alert reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, if err := wait.PollImmediate(pollInterval, timeout,
isStaticObjectReadyConditionFunc(kubeClient, namespacedName, &alert)); err != nil { isAlertReady(ctx, kubeClient, namespacedName, &alert)); err != nil {
return err return err
} }
logger.Successf("Alert %s is ready", name) logger.Successf("Alert %s is ready", name)
@ -140,13 +142,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 *notificationv1b3.Alert) (types.NamespacedName, error) { alert *notificationv1.Alert) (types.NamespacedName, error) {
namespacedName := types.NamespacedName{ namespacedName := types.NamespacedName{
Namespace: alert.GetNamespace(), Namespace: alert.GetNamespace(),
Name: alert.GetName(), Name: alert.GetName(),
} }
var existing notificationv1b3.Alert var existing notificationv1.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) {
@ -169,3 +171,23 @@ 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
}
}

@ -21,22 +21,23 @@ import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
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"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/flux2/internal/utils"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"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: withPreviewNote(`The create alert-provider command generates a Provider resource.`), Long: "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 \
@ -48,33 +49,35 @@ var createAlertProviderCmd = &cobra.Command{
flux create alert-provider github-podinfo \ flux create alert-provider github-podinfo \
--type github \ --type github \
--address https://github.com/stefanprodan/podinfo \ --address https://github.com/stefanprodan/podinfo \
--secret-ref github-token`, --secret-ref github-token
`,
RunE: createAlertProviderCmdRun, RunE: createAlertProviderCmdRun,
} }
type alertProviderFlags struct { var (
alertType string apType string
channel string apChannel string
username string apUsername string
address string apAddress string
secretRef string apSecretRef string
} )
var alertProviderArgs alertProviderFlags
func init() { func init() {
createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.alertType, "type", "", "type of provider") createAlertProviderCmd.Flags().StringVar(&apType, "type", "", "type of provider")
createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.channel, "channel", "", "channel to send messages to in the case of a chat provider") createAlertProviderCmd.Flags().StringVar(&apChannel, "channel", "", "channel to send messages to in the case of a chat provider")
createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.username, "username", "", "bot username used by the provider") createAlertProviderCmd.Flags().StringVar(&apUsername, "username", "", "bot username used by the provider")
createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.address, "address", "", "path to either the git repository, chat provider or webhook") createAlertProviderCmd.Flags().StringVar(&apAddress, "address", "", "path to either the git repository, chat provider or webhook")
createAlertProviderCmd.Flags().StringVar(&alertProviderArgs.secretRef, "secret-ref", "", "name of secret containing authentication token") createAlertProviderCmd.Flags().StringVar(&apSecretRef, "secret-ref", "", "name of secret containing authentication token")
createCmd.AddCommand(createAlertProviderCmd) createCmd.AddCommand(createAlertProviderCmd)
} }
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 apType == "" {
return fmt.Errorf("Provider type is required") return fmt.Errorf("Provider type is required")
} }
@ -83,38 +86,38 @@ func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !createArgs.export { if !export {
logger.Generatef("generating Provider") logger.Generatef("generating Provider")
} }
provider := notificationv1.Provider{ provider := notificationv1.Provider{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: sourceLabels, Labels: sourceLabels,
}, },
Spec: notificationv1.ProviderSpec{ Spec: notificationv1.ProviderSpec{
Type: alertProviderArgs.alertType, Type: apType,
Channel: alertProviderArgs.channel, Channel: apChannel,
Username: alertProviderArgs.username, Username: apUsername,
Address: alertProviderArgs.address, Address: apAddress,
}, },
} }
if alertProviderArgs.secretRef != "" { if apSecretRef != "" {
provider.Spec.SecretRef = &meta.LocalObjectReference{ provider.Spec.SecretRef = &corev1.LocalObjectReference{
Name: alertProviderArgs.secretRef, Name: apSecretRef,
} }
} }
if createArgs.export { if export {
return printExport(exportAlertProvider(&provider)) return exportAlertProvider(provider)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil { if err != nil {
return err return err
} }
@ -126,8 +129,8 @@ func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for Provider reconciliation") logger.Waitingf("waiting for Provider reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, if err := wait.PollImmediate(pollInterval, timeout,
isStaticObjectReadyConditionFunc(kubeClient, namespacedName, &provider)); err != nil { isAlertProviderReady(ctx, kubeClient, namespacedName, &provider)); err != nil {
return err return err
} }
@ -166,3 +169,23 @@ 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 2024 The Flux authors Copyright 2020 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.
@ -18,36 +18,31 @@ package main
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os" "io/ioutil"
"strings"
"time" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/pkg/apis/meta"
"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/v2" helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
"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 \
@ -67,12 +62,11 @@ var createHelmReleaseCmd = &cobra.Command{
--source=Bucket/podinfo \ --source=Bucket/podinfo \
--chart=./charts/podinfo --chart=./charts/podinfo
# Create a HelmRelease with values from local YAML files # Create a HelmRelease with values from a local YAML file
flux create hr podinfo \ flux create hr podinfo \
--source=HelmRepository/podinfo \ --source=HelmRepository/podinfo \
--chart=podinfo \ --chart=podinfo \
--values=./my-values1.yaml \ --values=./my-values.yaml
--values=./my-values2.yaml
# Create a HelmRelease with values from a Kubernetes secret # Create a HelmRelease with values from a Kubernetes secret
kubectl -n app create secret generic my-secret-values \ kubectl -n app create secret generic my-secret-values \
@ -84,90 +78,59 @@ 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 \
--target-namespace=test \ --target-namespace=default \
--create-target-namespace=true \
--source=HelmRepository/podinfo \ --source=HelmRepository/podinfo \
--chart=podinfo --chart=podinfo
# Create a HelmRelease using a source from a different namespace
flux create hr podinfo \
--namespace=default \
--source=HelmRepository/podinfo.flux-system \
--chart=podinfo
# Create a HelmRelease definition on disk without applying it on the cluster # Create a HelmRelease definition on disk without applying it on the cluster
flux create hr podinfo \ flux create hr podinfo \
--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 { var (
name string hrName string
source flags.HelmChartSource hrSource flags.HelmChartSource
dependsOn []string hrDependsOn []string
chart string hrChart string
chartVersion string hrChartVersion string
chartRef string hrTargetNamespace string
targetNamespace string hrValuesFile string
createNamespace bool hrValuesFrom flags.HelmReleaseValuesFrom
valuesFiles []string hrSAName string
valuesFrom []string )
saName string
crds flags.CRDsPolicy
reconcileStrategy string
chartInterval time.Duration
kubeConfigSecretRef string
}
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(&hrName, "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(&hrSource, "source", hrSource.Description())
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chart, "chart", "", "Helm chart name or path") createHelmReleaseCmd.Flags().StringVar(&hrChart, "chart", "", "Helm chart name or path")
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)") createHelmReleaseCmd.Flags().StringVar(&hrChartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)")
createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.dependsOn, "depends-on", nil, "HelmReleases that must be ready before this release can be installed, supported formats '<name>' and '<namespace>/<name>'") createHelmReleaseCmd.Flags().StringArrayVar(&hrDependsOn, "depends-on", nil, "HelmReleases that must be ready before this release can be installed, supported formats '<name>' and '<namespace>/<name>'")
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.targetNamespace, "target-namespace", "", "namespace to install this release, defaults to the HelmRelease namespace") createHelmReleaseCmd.Flags().StringVar(&hrTargetNamespace, "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().StringVar(&hrSAName, "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(&hrValuesFile, "values", "", "local path to the values.yaml file")
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().Var(&hrValuesFrom, "values-from", hrValuesFrom.Description())
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.valuesFrom, "values-from", nil, "a Kubernetes object reference that contains the values.yaml data key in the format '<kind>/<name>', where kind must be one of: (Secret,ConfigMap)")
createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.crds, "crds", helmReleaseArgs.crds.Description())
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.kubeConfigSecretRef, "kubeconfig-secret-ref", "", "the name of the Kubernetes Secret that contains a key with the kubeconfig file for connecting to a remote cluster")
createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.chartRef, "chart-ref", "", "the name of the HelmChart resource to use as source for the HelmRelease, in the format '<kind>/<name>.<namespace>', where kind must be one of: (OCIRepository,HelmChart)")
createCmd.AddCommand(createHelmReleaseCmd) 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 == "" && helmReleaseArgs.chartRef == "" { if hrChart == "" {
return fmt.Errorf("chart or chart-ref is required") return fmt.Errorf("chart name or path is required")
} }
sourceLabels, err := parseLabels() sourceLabels, err := parseLabels()
@ -175,151 +138,70 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !createArgs.export { if !export {
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,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: sourceLabels, Labels: sourceLabels,
}, },
Spec: helmv2.HelmReleaseSpec{ Spec: helmv2.HelmReleaseSpec{
ReleaseName: helmReleaseArgs.name, ReleaseName: hrName,
DependsOn: utils.MakeDependsOn(helmReleaseArgs.dependsOn), DependsOn: utils.MakeDependsOn(hrDependsOn),
Interval: metav1.Duration{ Interval: metav1.Duration{
Duration: createArgs.interval, Duration: interval,
},
TargetNamespace: helmReleaseArgs.targetNamespace,
Suspend: false,
}, },
} TargetNamespace: hrTargetNamespace,
Chart: helmv2.HelmChartTemplate{
switch {
case helmReleaseArgs.chart != "":
helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{
Spec: helmv2.HelmChartTemplateSpec{ Spec: helmv2.HelmChartTemplateSpec{
Chart: helmReleaseArgs.chart, Chart: hrChart,
Version: helmReleaseArgs.chartVersion, Version: hrChartVersion,
SourceRef: helmv2.CrossNamespaceObjectReference{ SourceRef: helmv2.CrossNamespaceObjectReference{
Kind: helmReleaseArgs.source.Kind, Kind: hrSource.Kind,
Name: helmReleaseArgs.source.Name, Name: hrSource.Name,
Namespace: helmReleaseArgs.source.Namespace,
}, },
ReconcileStrategy: helmReleaseArgs.reconcileStrategy,
}, },
}
if helmReleaseArgs.chartInterval != 0 {
helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{
Duration: helmReleaseArgs.chartInterval,
}
}
case helmReleaseArgs.chartRef != "":
kind, name, ns := utils.ParseObjectKindNameNamespace(helmReleaseArgs.chartRef)
if kind != sourcev1.HelmChartKind && kind != sourcev1b2.OCIRepositoryKind {
return fmt.Errorf("chart reference kind '%s' is not supported, must be one of: %s",
kind, strings.Join(supportedHelmReleaseReferenceKinds, ", "))
}
helmRelease.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{
Kind: kind,
Name: name,
Namespace: ns,
}
}
if helmReleaseArgs.kubeConfigSecretRef != "" {
helmRelease.Spec.KubeConfig = &meta.KubeConfigReference{
SecretRef: meta.SecretKeyReference{
Name: helmReleaseArgs.kubeConfigSecretRef,
}, },
} Suspend: false,
} },
if helmReleaseArgs.createNamespace {
if helmRelease.Spec.Install == nil {
helmRelease.Spec.Install = &helmv2.Install{}
}
helmRelease.Spec.Install.CreateNamespace = helmReleaseArgs.createNamespace
}
if helmReleaseArgs.saName != "" {
helmRelease.Spec.ServiceAccountName = helmReleaseArgs.saName
}
if helmReleaseArgs.crds != "" {
if helmRelease.Spec.Install == nil {
helmRelease.Spec.Install = &helmv2.Install{}
}
helmRelease.Spec.Install.CRDs = helmv2.Create
helmRelease.Spec.Upgrade = &helmv2.Upgrade{CRDs: helmv2.CRDsPolicy(helmReleaseArgs.crds.String())}
} }
if len(helmReleaseArgs.valuesFiles) > 0 { if hrSAName != "" {
valuesMap := make(map[string]interface{}) helmRelease.Spec.ServiceAccountName = hrSAName
for _, v := range helmReleaseArgs.valuesFiles {
data, err := os.ReadFile(v)
if err != nil {
return fmt.Errorf("reading values from %s failed: %w", v, err)
} }
jsonBytes, err := yaml.YAMLToJSON(data) if hrValuesFile != "" {
data, err := ioutil.ReadFile(hrValuesFile)
if err != nil { if err != nil {
return fmt.Errorf("converting values to JSON from %s failed: %w", v, err) return fmt.Errorf("reading values from %s failed: %w", hrValuesFile, err)
}
jsonMap := make(map[string]interface{})
if err := json.Unmarshal(jsonBytes, &jsonMap); err != nil {
return fmt.Errorf("unmarshaling values from %s failed: %w", v, err)
} }
valuesMap = transform.MergeMaps(valuesMap, jsonMap) json, err := yaml.YAMLToJSON(data)
}
jsonRaw, err := json.Marshal(valuesMap)
if err != nil { if err != nil {
return fmt.Errorf("marshaling values failed: %w", err) return fmt.Errorf("converting values to JSON from %s failed: %w", hrValuesFile, err)
}
helmRelease.Spec.Values = &apiextensionsv1.JSON{Raw: jsonRaw}
} }
if len(helmReleaseArgs.valuesFrom) != 0 { helmRelease.Spec.Values = &apiextensionsv1.JSON{Raw: json}
values := []helmv2.ValuesReference{}
for _, value := range helmReleaseArgs.valuesFrom {
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{ if hrValuesFrom.String() != "" {
Name: sourceName, helmRelease.Spec.ValuesFrom = []helmv2.ValuesReference{{
Kind: cleanSourceKind, Kind: hrValuesFrom.Kind,
}) Name: hrValuesFrom.Name,
} }}
helmRelease.Spec.ValuesFrom = values
} }
if createArgs.export { if export {
return printExport(exportHelmRelease(&helmRelease)) return exportHelmRelease(helmRelease)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil { if err != nil {
return err return err
} }
@ -331,13 +213,13 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for HelmRelease reconciliation") logger.Waitingf("waiting for HelmRelease reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, if err := wait.PollImmediate(pollInterval, timeout,
isObjectReadyConditionFunc(kubeClient, namespacedName, &helmRelease)); err != nil { isHelmReleaseReady(ctx, 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", getHelmReleaseRevision(helmRelease)) logger.Successf("applied revision %s", helmRelease.Status.LastAppliedRevision)
return nil return nil
} }
@ -372,14 +254,19 @@ func upsertHelmRelease(ctx context.Context, kubeClient client.Client,
return namespacedName, nil return namespacedName, nil
} }
func validateStrategy(input string) bool { func isHelmReleaseReady(ctx context.Context, kubeClient client.Client,
allowedStrategy := []string{"Revision", "ChartVersion"} namespacedName types.NamespacedName, helmRelease *helmv2.HelmRelease) wait.ConditionFunc {
return func() (bool, error) {
for _, strategy := range allowedStrategy { err := kubeClient.Get(ctx, namespacedName, helmRelease)
if strategy == input { if err != nil {
return true return false, err
} }
// Confirm the state we are observing is for the current generation
if helmRelease.Generation != helmRelease.Status.ObservedGeneration {
return false, nil
} }
return false return apimeta.IsStatusConditionTrue(helmRelease.Status.Conditions, meta.ReadyCondition), nil
}
} }

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

@ -17,15 +17,20 @@ limitations under the License.
package main package main
import ( import (
"strings"
"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: `The create image sub-commands work with image automation objects; Long: strings.TrimSpace(createImageLong),
that is, object controlling updates to git based on e.g., new container images
being available.`,
} }
func init() { func init() {

@ -18,48 +18,29 @@ package main
import ( import (
"fmt" "fmt"
"regexp/syntax"
"strings"
"unicode"
"unicode/utf8"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/apis/meta" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
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: withPreviewNote(`The create image policy command generates an ImagePolicy resource. Long: `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
flux create image policy podinfo \
--image-ref=podinfo \
--select-semver=">=1.0.0"
# Create an ImagePolicy to select the latest main branch build tagged as "${GIT_BRANCH}-${GIT_SHA:0:7}-$(date +%s)"
flux create image policy podinfo \
--image-ref=podinfo \
--select-numeric=asc \
--filter-regex='^main-[a-f0-9]+-(?P<ts>[0-9]+)' \
--filter-extract='$ts'`,
RunE: createImagePolicyRun} RunE: createImagePolicyRun}
type imagePolicyFlags struct { type imagePolicyFlags struct {
imageRef string imageRef string
semver string semver string
alpha string
numeric string
filterRegex string filterRegex string
filterExtract string
} }
var imagePolicyArgs = imagePolicyFlags{} var imagePolicyArgs = imagePolicyFlags{}
@ -67,11 +48,8 @@ var imagePolicyArgs = imagePolicyFlags{}
func init() { func init() {
flags := createImagePolicyCmd.Flags() flags := createImagePolicyCmd.Flags()
flags.StringVar(&imagePolicyArgs.imageRef, "image-ref", "", "the name of an image repository object") flags.StringVar(&imagePolicyArgs.imageRef, "image-ref", "", "the name of an image repository object")
flags.StringVar(&imagePolicyArgs.semver, "select-semver", "", "a semver range to apply to tags; e.g., '1.x'") flags.StringVar(&imagePolicyArgs.semver, "semver", "", "a semver range to apply to tags; e.g., '1.x'")
flags.StringVar(&imagePolicyArgs.alpha, "select-alpha", "", "use alphabetical sorting to select image; either \"asc\" meaning select the last, or \"desc\" meaning select the first")
flags.StringVar(&imagePolicyArgs.numeric, "select-numeric", "", "use numeric sorting to select image; either \"asc\" meaning select the last, or \"desc\" meaning select the first")
flags.StringVar(&imagePolicyArgs.filterRegex, "filter-regex", "", " regular expression pattern used to filter the image tags") flags.StringVar(&imagePolicyArgs.filterRegex, "filter-regex", "", " regular expression pattern used to filter the image tags")
flags.StringVar(&imagePolicyArgs.filterExtract, "filter-extract", "", "replacement pattern (using capture groups from --filter-regex) to use for sorting")
createImageCmd.AddCommand(createImagePolicyCmd) createImageCmd.AddCommand(createImagePolicyCmd)
} }
@ -83,6 +61,9 @@ 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 == "" {
@ -97,63 +78,32 @@ func createImagePolicyRun(cmd *cobra.Command, args []string) error {
var policy = imagev1.ImagePolicy{ var policy = imagev1.ImagePolicy{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: objectName, Name: objectName,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: labels, Labels: labels,
}, },
Spec: imagev1.ImagePolicySpec{ Spec: imagev1.ImagePolicySpec{
ImageRepositoryRef: meta.NamespacedObjectReference{ ImageRepositoryRef: corev1.LocalObjectReference{
Name: imagePolicyArgs.imageRef, Name: imagePolicyArgs.imageRef,
}, },
}, },
} }
switch { switch {
case imagePolicyArgs.semver != "" && imagePolicyArgs.alpha != "":
case imagePolicyArgs.semver != "" && imagePolicyArgs.numeric != "":
case imagePolicyArgs.alpha != "" && imagePolicyArgs.numeric != "":
return fmt.Errorf("only one of --select-semver, --select-alpha or --select-numeric can be specified")
case imagePolicyArgs.semver != "": case imagePolicyArgs.semver != "":
policy.Spec.Policy.SemVer = &imagev1.SemVerPolicy{ policy.Spec.Policy.SemVer = &imagev1.SemVerPolicy{
Range: imagePolicyArgs.semver, Range: imagePolicyArgs.semver,
} }
case imagePolicyArgs.alpha != "":
if imagePolicyArgs.alpha != "desc" && imagePolicyArgs.alpha != "asc" {
return fmt.Errorf("--select-alpha must be one of [\"asc\", \"desc\"]")
}
policy.Spec.Policy.Alphabetical = &imagev1.AlphabeticalPolicy{
Order: imagePolicyArgs.alpha,
}
case imagePolicyArgs.numeric != "":
if imagePolicyArgs.numeric != "desc" && imagePolicyArgs.numeric != "asc" {
return fmt.Errorf("--select-numeric must be one of [\"asc\", \"desc\"]")
}
policy.Spec.Policy.Numerical = &imagev1.NumericalPolicy{
Order: imagePolicyArgs.numeric,
}
default: default:
return fmt.Errorf("a policy must be provided with either --select-semver or --select-alpha") return fmt.Errorf("a policy must be provided with --semver")
} }
if imagePolicyArgs.filterRegex != "" { if imagePolicyArgs.filterRegex != "" {
exp, err := syntax.Parse(imagePolicyArgs.filterRegex, syntax.Perl)
if err != nil {
return fmt.Errorf("--filter-regex is an invalid regex pattern")
}
policy.Spec.FilterTags = &imagev1.TagFilter{ policy.Spec.FilterTags = &imagev1.TagFilter{
Pattern: imagePolicyArgs.filterRegex, Pattern: imagePolicyArgs.filterRegex,
} }
if imagePolicyArgs.filterExtract != "" {
if err := validateExtractStr(imagePolicyArgs.filterExtract, exp.CapNames()); err != nil {
return err
}
policy.Spec.FilterTags.Extract = imagePolicyArgs.filterExtract
}
} else if imagePolicyArgs.filterExtract != "" {
return fmt.Errorf("cannot specify --filter-extract without specifying --filter-regex")
} }
if createArgs.export { if export {
return printExport(exportImagePolicy(&policy)) return printExport(exportImagePolicy(&policy))
} }
@ -166,93 +116,3 @@ func createImagePolicyRun(cmd *cobra.Command, args []string) error {
}) })
return err return err
} }
// Performs a dry-run of the extract function in Regexp to validate the template
func validateExtractStr(template string, capNames []string) error {
for len(template) > 0 {
i := strings.Index(template, "$")
if i < 0 {
return nil
}
template = template[i:]
if len(template) > 1 && template[1] == '$' {
template = template[2:]
continue
}
name, num, rest, ok := extract(template)
if !ok {
// Malformed extract string, assume user didn't want this
return fmt.Errorf("--filter-extract is malformed")
}
template = rest
if num >= 0 {
// we won't worry about numbers as we can't validate these
continue
} else {
found := false
for _, capName := range capNames {
if name == capName {
found = true
}
}
if !found {
return fmt.Errorf("capture group $%s used in --filter-extract not found in --filter-regex", name)
}
}
}
return nil
}
// extract method from the regexp package
// returns the name or number of the value prepended by $
func extract(str string) (name string, num int, rest string, ok bool) {
if len(str) < 2 || str[0] != '$' {
return
}
brace := false
if str[1] == '{' {
brace = true
str = str[2:]
} else {
str = str[1:]
}
i := 0
for i < len(str) {
rune, size := utf8.DecodeRuneInString(str[i:])
if !unicode.IsLetter(rune) && !unicode.IsDigit(rune) && rune != '_' {
break
}
i += size
}
if i == 0 {
// empty name is not okay
return
}
name = str[:i]
if brace {
if i >= len(str) || str[i] != '}' {
// missing closing brace
return
}
i++
}
// Parse number.
num = 0
for i := 0; i < len(name); i++ {
if name[i] < '0' || '9' < name[i] || num >= 1e8 {
num = -1
break
}
num = num*10 + int(name[i]) - '0'
}
// Disallow leading zeros.
if name[0] == '0' && len(name) > 1 {
num = -1
}
rest = str[i:]
ok = true
return
}

@ -22,49 +22,23 @@ import (
"github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/name"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/pkg/apis/meta" imagev1 "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
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: withPreviewNote(`The create image repository command generates an ImageRepository resource. Long: `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:
flux create image repository alpine-repo --image alpine --interval 20m
# Create an image repository that uses an image pull secret (assumed to
# have been created already):
flux create image repository myapp-repo \
--secret-ref image-pull \
--image ghcr.io/example.com/myapp --interval 5m
# Create a TLS secret for a local image registry using a self-signed
# host certificate, and use it to scan an image. ca.pem is a file
# containing the CA certificate used to sign the host certificate.
flux create secret tls local-registry-cert --ca-file ./ca.pem
flux create image repository app-repo \
--cert-secret-ref local-registry-cert \
--image local-registry:5000/app --interval 5m
# Create a TLS secret with a client certificate and key, and use it
# to scan a private image registry.
flux create secret tls client-cert \
--cert-file client.crt --key-file client.key
flux create image repository app-repo \
--cert-secret-ref client-cert \
--image registry.example.com/private/app --interval 5m`,
RunE: createImageRepositoryRun, RunE: createImageRepositoryRun,
} }
type imageRepoFlags struct { type imageRepoFlags struct {
image string image string
secretRef string secretRef string
certSecretRef string
timeout time.Duration timeout time.Duration
} }
@ -74,7 +48,6 @@ func init() {
flags := createImageRepositoryCmd.Flags() flags := createImageRepositoryCmd.Flags()
flags.StringVar(&imageRepoArgs.image, "image", "", "the image repository to scan; e.g., library/alpine") flags.StringVar(&imageRepoArgs.image, "image", "", "the image repository to scan; e.g., library/alpine")
flags.StringVar(&imageRepoArgs.secretRef, "secret-ref", "", "the name of a docker-registry secret to use for credentials") flags.StringVar(&imageRepoArgs.secretRef, "secret-ref", "", "the name of a docker-registry secret to use for credentials")
flags.StringVar(&imageRepoArgs.certSecretRef, "cert-ref", "", "the name of a secret to use for TLS certificates")
// NB there is already a --timeout in the global flags, for // NB there is already a --timeout in the global flags, for
// controlling timeout on operations while e.g., creating objects. // controlling timeout on operations while e.g., creating objects.
flags.DurationVar(&imageRepoArgs.timeout, "scan-timeout", 0, "a timeout for scanning; this defaults to the interval if not set") flags.DurationVar(&imageRepoArgs.timeout, "scan-timeout", 0, "a timeout for scanning; this defaults to the interval if not set")
@ -83,6 +56,9 @@ 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 == "" {
@ -101,29 +77,24 @@ func createImageRepositoryRun(cmd *cobra.Command, args []string) error {
var repo = imagev1.ImageRepository{ var repo = imagev1.ImageRepository{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: objectName, Name: objectName,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: labels, Labels: labels,
}, },
Spec: imagev1.ImageRepositorySpec{ Spec: imagev1.ImageRepositorySpec{
Image: imageRepoArgs.image, Image: imageRepoArgs.image,
Interval: metav1.Duration{Duration: createArgs.interval}, Interval: metav1.Duration{Duration: interval},
}, },
} }
if imageRepoArgs.timeout != 0 { if imageRepoArgs.timeout != 0 {
repo.Spec.Timeout = &metav1.Duration{Duration: imageRepoArgs.timeout} repo.Spec.Timeout = &metav1.Duration{Duration: imageRepoArgs.timeout}
} }
if imageRepoArgs.secretRef != "" { if imageRepoArgs.secretRef != "" {
repo.Spec.SecretRef = &meta.LocalObjectReference{ repo.Spec.SecretRef = &corev1.LocalObjectReference{
Name: imageRepoArgs.secretRef, Name: imageRepoArgs.secretRef,
} }
} }
if imageRepoArgs.certSecretRef != "" {
repo.Spec.CertSecretRef = &meta.LocalObjectReference{
Name: imageRepoArgs.certSecretRef,
}
}
if createArgs.export { if export {
return printExport(exportImageRepository(&repo)) return printExport(exportImageRepository(&repo))
} }

@ -1,178 +0,0 @@
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
)
var createImageUpdateCmd = &cobra.Command{
Use: "update [name]",
Short: "Create or update an ImageUpdateAutomation object",
Long: withPreviewNote(`The create image update command generates an ImageUpdateAutomation resource.
An ImageUpdateAutomation object specifies an automated update to images
mentioned in YAMLs in a git repository.`),
Example: ` # Configure image updates for the main repository created by flux bootstrap
flux create image update flux-system \
--git-repo-ref=flux-system \
--git-repo-path="./clusters/my-cluster" \
--checkout-branch=main \
--author-name=flux \
--author-email=flux@example.com \
--commit-template="{{range .Updated.Images}}{{println .}}{{end}}"
# Configure image updates to push changes to a different branch, if the branch doesn't exists it will be created
flux create image update flux-system \
--git-repo-ref=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}}"
# 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,
}
type imageUpdateFlags struct {
gitRepoName string
gitRepoNamespace string
gitRepoPath string
checkoutBranch string
pushBranch string
commitTemplate string
authorName string
authorEmail string
}
var imageUpdateArgs = imageUpdateFlags{}
func init() {
flags := createImageUpdateCmd.Flags()
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.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.commitTemplate, "commit-template", "", "a template for commit messages")
flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author")
flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author")
createImageCmd.AddCommand(createImageUpdateCmd)
}
func createImageUpdateRun(cmd *cobra.Command, args []string) error {
objectName := args[0]
if imageUpdateArgs.gitRepoName == "" {
return fmt.Errorf("a reference to a GitRepository is required (--git-repo-ref)")
}
if imageUpdateArgs.checkoutBranch == "" {
return fmt.Errorf("the Git repository branch is required (--checkout-branch)")
}
if imageUpdateArgs.authorName == "" {
return fmt.Errorf("the author name is required (--author-name)")
}
if imageUpdateArgs.authorEmail == "" {
return fmt.Errorf("the author email is required (--author-email)")
}
labels, err := parseLabels()
if err != nil {
return err
}
var update = autov1.ImageUpdateAutomation{
ObjectMeta: metav1.ObjectMeta{
Name: objectName,
Namespace: *kubeconfigArgs.Namespace,
Labels: labels,
},
Spec: autov1.ImageUpdateAutomationSpec{
SourceRef: autov1.CrossNamespaceSourceReference{
Kind: sourcev1.GitRepositoryKind,
Name: imageUpdateArgs.gitRepoName,
Namespace: imageUpdateArgs.gitRepoNamespace,
},
GitSpec: &autov1.GitSpec{
Checkout: &autov1.GitCheckoutSpec{
Reference: sourcev1.GitRepositoryRef{
Branch: imageUpdateArgs.checkoutBranch,
},
},
Commit: autov1.CommitSpec{
Author: autov1.CommitUser{
Name: imageUpdateArgs.authorName,
Email: imageUpdateArgs.authorEmail,
},
MessageTemplate: imageUpdateArgs.commitTemplate,
},
},
Interval: metav1.Duration{
Duration: createArgs.interval,
},
},
}
if imageUpdateArgs.pushBranch != "" {
update.Spec.GitSpec.Push = &autov1.PushSpec{
Branch: imageUpdateArgs.pushBranch,
}
}
if imageUpdateArgs.gitRepoPath != "" {
update.Spec.Update = &autov1.UpdateStrategy{
Path: imageUpdateArgs.gitRepoPath,
Strategy: autov1.UpdateStrategySetters,
}
}
if createArgs.export {
return printExport(exportImageUpdate(&update))
}
var existing autov1.ImageUpdateAutomation
copyName(&existing, &update)
err = imageUpdateAutomationType.upsertAndWait(imageUpdateAutomationAdapter{&existing}, func() error {
existing.Spec = update.Spec
existing.Labels = update.Labels
return nil
})
return err
}

@ -0,0 +1,117 @@
/*
Copyright 2020 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
autov1 "github.com/fluxcd/image-automation-controller/api/v1alpha1"
)
var createImageUpdateCmd = &cobra.Command{
Use: "update <name>",
Short: "Create or update an ImageUpdateAutomation object",
Long: `The create image update command generates an ImageUpdateAutomation resource.
An ImageUpdateAutomation object specifies an automated update to images
mentioned in YAMLs in a git repository.`,
RunE: createImageUpdateRun,
}
type imageUpdateFlags struct {
// git checkout spec
gitRepoRef string
branch string
// commit spec
commitTemplate string
authorName string
authorEmail string
}
var imageUpdateArgs = imageUpdateFlags{}
func init() {
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.branch, "branch", "", "the branch to checkout and push commits to")
flags.StringVar(&imageUpdateArgs.commitTemplate, "commit-template", "", "a template for commit messages")
flags.StringVar(&imageUpdateArgs.authorName, "author-name", "", "the name to use for commit author")
flags.StringVar(&imageUpdateArgs.authorEmail, "author-email", "", "the email to use for commit author")
createImageCmd.AddCommand(createImageUpdateCmd)
}
func createImageUpdateRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("ImageUpdateAutomation name is required")
}
objectName := args[0]
if imageUpdateArgs.gitRepoRef == "" {
return fmt.Errorf("a reference to a GitRepository is required (--git-repo-ref)")
}
if imageUpdateArgs.branch == "" {
return fmt.Errorf("the Git repoistory branch is required (--branch)")
}
labels, err := parseLabels()
if err != nil {
return err
}
var update = autov1.ImageUpdateAutomation{
ObjectMeta: metav1.ObjectMeta{
Name: objectName,
Namespace: namespace,
Labels: labels,
},
Spec: autov1.ImageUpdateAutomationSpec{
Checkout: autov1.GitCheckoutSpec{
GitRepositoryRef: corev1.LocalObjectReference{
Name: imageUpdateArgs.gitRepoRef,
},
Branch: imageUpdateArgs.branch,
},
Interval: metav1.Duration{Duration: interval},
Update: autov1.UpdateStrategy{
Setters: &autov1.SettersStrategy{},
},
Commit: autov1.CommitSpec{
AuthorName: imageUpdateArgs.authorName,
AuthorEmail: imageUpdateArgs.authorEmail,
MessageTemplate: imageUpdateArgs.commitTemplate,
},
},
}
if export {
return printExport(exportImageUpdate(&update))
}
var existing autov1.ImageUpdateAutomation
copyName(&existing, &update)
err = imageUpdateAutomationType.upsertAndWait(imageUpdateAutomationAdapter{&existing}, func() error {
existing.Spec = update.Spec
existing.Labels = update.Labels
return nil
})
return err
}

@ -23,125 +23,102 @@ import (
"time" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
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"
helmv2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/flux2/internal/flags"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/flux2/internal/utils"
helmv2 "github.com/fluxcd/helm-controller/api/v2beta1"
kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils"
) )
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 create command generates a Kustomization resource for a given source.`, Long: "The kustomization source create command generates a Kustomize 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 kyverno \ flux create kustomization contour \
--source=GitRepository/kyverno \ --source=contour \
--path="./config/release" \ --path="./examples/contour/" \
--prune=true \ --prune=true \
--interval=60m \ --interval=10m \
--wait=true \ --validation=client \
--health-check="Deployment/contour.projectcontour" \
--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 kyverno-policies \ flux create kustomization webapp \
--depends-on=kyverno \ --depends-on=contour \
--source=GitRepository/kyverno-policies \ --source=webapp \
--path="./policies/flux" \ --path="./deploy/overlays/dev" \
--prune=true \
--interval=5m
# Create a Kustomization using a source from a different namespace
flux create kustomization podinfo \
--namespace=default \
--source=GitRepository/podinfo.flux-system \
--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 \
--validation=client
# Create a Kustomization resource that references a Bucket # Create a Kustomization resource that references a Bucket
flux create kustomization secrets \ flux create kustomization secrets \
--source=Bucket/secrets \ --source=Bucket/secrets \
--prune=true \ --prune=true \
--interval=5m`, --interval=5m
`,
RunE: createKsCmdRun, RunE: createKsCmdRun,
} }
type kustomizationFlags struct { var (
source flags.KustomizationSource ksSource flags.KustomizationSource
path flags.SafeRelativePath ksPath flags.SafeRelativePath = "./"
prune bool ksPrune bool
dependsOn []string ksDependsOn []string
validation string ksValidation string
healthCheck []string ksHealthCheck []string
healthTimeout time.Duration ksHealthTimeout time.Duration
saName string ksSAName string
decryptionProvider flags.DecryptionProvider ksDecryptionProvider flags.DecryptionProvider
decryptionSecret string ksDecryptionSecret string
targetNamespace string ksTargetNamespace string
wait bool )
kubeConfigSecretRef string
retryInterval time.Duration
}
var kustomizationArgs = NewKustomizationFlags()
func init() { func init() {
createKsCmd.Flags().Var(&kustomizationArgs.source, "source", kustomizationArgs.source.Description()) createKsCmd.Flags().Var(&ksSource, "source", ksSource.Description())
createKsCmd.Flags().Var(&kustomizationArgs.path, "path", "path to the directory containing a kustomization.yaml file") createKsCmd.Flags().Var(&ksPath, "path", "path to the directory containing a kustomization.yaml file")
createKsCmd.Flags().BoolVar(&kustomizationArgs.prune, "prune", false, "enable garbage collection") createKsCmd.Flags().BoolVar(&ksPrune, "prune", false, "enable garbage collection")
createKsCmd.Flags().BoolVar(&kustomizationArgs.wait, "wait", false, "enable health checking of all the applied resources") createKsCmd.Flags().StringArrayVar(&ksHealthCheck, "health-check", nil, "workload to be included in the health assessment, in the format '<kind>/<name>.<namespace>'")
createKsCmd.Flags().StringSliceVar(&kustomizationArgs.healthCheck, "health-check", nil, "workload to be included in the health assessment, in the format '<kind>/<name>.<namespace>'") createKsCmd.Flags().DurationVar(&ksHealthTimeout, "health-check-timeout", 2*time.Minute, "timeout of health checking operations")
createKsCmd.Flags().DurationVar(&kustomizationArgs.healthTimeout, "health-check-timeout", 2*time.Minute, "timeout of health checking operations") createKsCmd.Flags().StringVar(&ksValidation, "validation", "", "validate the manifests before applying them on the cluster, can be 'client' or 'server'")
createKsCmd.Flags().StringVar(&kustomizationArgs.validation, "validation", "", "validate the manifests before applying them on the cluster, can be 'client' or 'server'") createKsCmd.Flags().StringArrayVar(&ksDependsOn, "depends-on", nil, "Kustomization that must be ready before this Kustomization can be applied, supported formats '<name>' and '<namespace>/<name>'")
createKsCmd.Flags().StringSliceVar(&kustomizationArgs.dependsOn, "depends-on", nil, "Kustomization that must be ready before this Kustomization can be applied, supported formats '<name>' and '<namespace>/<name>', also accepts comma-separated values") createKsCmd.Flags().StringVar(&ksSAName, "service-account", "", "the name of the service account to impersonate when reconciling this Kustomization")
createKsCmd.Flags().StringVar(&kustomizationArgs.saName, "service-account", "", "the name of the service account to impersonate when reconciling this Kustomization") createKsCmd.Flags().Var(&ksDecryptionProvider, "decryption-provider", ksDecryptionProvider.Description())
createKsCmd.Flags().Var(&kustomizationArgs.decryptionProvider, "decryption-provider", kustomizationArgs.decryptionProvider.Description()) createKsCmd.Flags().StringVar(&ksDecryptionSecret, "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(&ksTargetNamespace, "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().DurationVar(&kustomizationArgs.retryInterval, "retry-interval", 0, "the interval at which to retry a previously failed reconciliation")
createCmd.AddCommand(createKsCmd) createCmd.AddCommand(createKsCmd)
} }
func NewKustomizationFlags() kustomizationFlags {
return kustomizationFlags{
path: "./",
}
}
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 ksPath == "" {
return fmt.Errorf("path is required") return fmt.Errorf("path is required")
} }
if !strings.HasPrefix(kustomizationArgs.path.String(), "./") { if !strings.HasPrefix(ksPath.String(), "./") {
return fmt.Errorf("path must begin with ./") return fmt.Errorf("path must begin with ./")
} }
if !createArgs.export { if !export {
logger.Generatef("generating Kustomization") logger.Generatef("generating Kustomization")
} }
kslabels, err := parseLabels() ksLabels, err := parseLabels()
if err != nil { if err != nil {
return err return err
} }
@ -149,37 +126,29 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
kustomization := kustomizev1.Kustomization{ kustomization := kustomizev1.Kustomization{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: kslabels, Labels: ksLabels,
}, },
Spec: kustomizev1.KustomizationSpec{ Spec: kustomizev1.KustomizationSpec{
DependsOn: utils.MakeDependsOn(kustomizationArgs.dependsOn), DependsOn: utils.MakeDependsOn(ksDependsOn),
Interval: metav1.Duration{ Interval: metav1.Duration{
Duration: createArgs.interval, Duration: interval,
}, },
Path: kustomizationArgs.path.ToSlash(), Path: ksPath.String(),
Prune: kustomizationArgs.prune, Prune: ksPrune,
SourceRef: kustomizev1.CrossNamespaceSourceReference{ SourceRef: kustomizev1.CrossNamespaceSourceReference{
Kind: kustomizationArgs.source.Kind, Kind: ksSource.Kind,
Name: kustomizationArgs.source.Name, Name: ksSource.Name,
Namespace: kustomizationArgs.source.Namespace,
}, },
Suspend: false, Suspend: false,
TargetNamespace: kustomizationArgs.targetNamespace, Validation: ksValidation,
TargetNamespace: ksTargetNamespace,
}, },
} }
if kustomizationArgs.kubeConfigSecretRef != "" { if len(ksHealthCheck) > 0 {
kustomization.Spec.KubeConfig = &meta.KubeConfigReference{ healthChecks := make([]kustomizev1.CrossNamespaceObjectReference, 0)
SecretRef: meta.SecretKeyReference{ for _, w := range ksHealthCheck {
Name: kustomizationArgs.kubeConfigSecretRef,
},
}
}
if len(kustomizationArgs.healthCheck) > 0 && !kustomizationArgs.wait {
healthChecks := make([]meta.NamespacedObjectKindReference, 0)
for _, w := range kustomizationArgs.healthCheck {
kindObj := strings.Split(w, "/") kindObj := strings.Split(w, "/")
if len(kindObj) != 2 { if len(kindObj) != 2 {
return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace' %v", w, kindObj) return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace' %v", w, kindObj)
@ -201,7 +170,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace'", w) return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace'", w)
} }
check := meta.NamespacedObjectKindReference{ check := kustomizev1.CrossNamespaceObjectReference{
Kind: kind, Kind: kind,
Name: nameNs[0], Name: nameNs[0],
Namespace: nameNs[1], Namespace: nameNs[1],
@ -214,43 +183,32 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
} }
kustomization.Spec.HealthChecks = healthChecks kustomization.Spec.HealthChecks = healthChecks
kustomization.Spec.Timeout = &metav1.Duration{ kustomization.Spec.Timeout = &metav1.Duration{
Duration: kustomizationArgs.healthTimeout, Duration: ksHealthTimeout,
} }
} }
if kustomizationArgs.wait { if ksSAName != "" {
kustomization.Spec.Wait = true kustomization.Spec.ServiceAccountName = ksSAName
kustomization.Spec.Timeout = &metav1.Duration{
Duration: kustomizationArgs.healthTimeout,
}
} }
if kustomizationArgs.saName != "" { if ksDecryptionProvider != "" {
kustomization.Spec.ServiceAccountName = kustomizationArgs.saName
}
if kustomizationArgs.decryptionProvider != "" {
kustomization.Spec.Decryption = &kustomizev1.Decryption{ kustomization.Spec.Decryption = &kustomizev1.Decryption{
Provider: kustomizationArgs.decryptionProvider.String(), Provider: ksDecryptionProvider.String(),
} }
if kustomizationArgs.decryptionSecret != "" { if ksDecryptionSecret != "" {
kustomization.Spec.Decryption.SecretRef = &meta.LocalObjectReference{Name: kustomizationArgs.decryptionSecret} kustomization.Spec.Decryption.SecretRef = &corev1.LocalObjectReference{Name: ksDecryptionSecret}
}
} }
if kustomizationArgs.retryInterval > 0 {
kustomization.Spec.RetryInterval = &metav1.Duration{Duration: kustomizationArgs.retryInterval}
} }
if createArgs.export { if export {
return printExport(exportKs(&kustomization)) return exportKs(kustomization)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil { if err != nil {
return err return err
} }
@ -262,8 +220,8 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for Kustomization reconciliation") logger.Waitingf("waiting for Kustomization reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, if err := wait.PollImmediate(pollInterval, timeout,
isObjectReadyConditionFunc(kubeClient, namespacedName, &kustomization)); err != nil { isKustomizationReady(ctx, kubeClient, namespacedName, &kustomization)); err != nil {
return err return err
} }
logger.Successf("Kustomization %s is ready", name) logger.Successf("Kustomization %s is ready", name)
@ -302,3 +260,28 @@ 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
}
}

@ -21,22 +21,23 @@ import (
"fmt" "fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
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"
notificationv1 "github.com/fluxcd/notification-controller/api/v1" "github.com/fluxcd/flux2/internal/utils"
notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"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 \
@ -44,40 +45,42 @@ var createReceiverCmd = &cobra.Command{
--event push \ --event push \
--secret-ref webhook-token \ --secret-ref webhook-token \
--resource GitRepository/webapp \ --resource GitRepository/webapp \
--resource HelmRepository/webapp`, --resource HelmRepository/webapp
`,
RunE: createReceiverCmdRun, RunE: createReceiverCmdRun,
} }
type receiverFlags struct { var (
receiverType string rcvType string
secretRef string rcvSecretRef string
events []string rcvEvents []string
resources []string rcvResources []string
} )
var receiverArgs receiverFlags
func init() { func init() {
createReceiverCmd.Flags().StringVar(&receiverArgs.receiverType, "type", "", "") createReceiverCmd.Flags().StringVar(&rcvType, "type", "", "")
createReceiverCmd.Flags().StringVar(&receiverArgs.secretRef, "secret-ref", "", "") createReceiverCmd.Flags().StringVar(&rcvSecretRef, "secret-ref", "", "")
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.events, "event", []string{}, "also accepts comma-separated values") createReceiverCmd.Flags().StringArrayVar(&rcvEvents, "event", []string{}, "")
createReceiverCmd.Flags().StringSliceVar(&receiverArgs.resources, "resource", []string{}, "also accepts comma-separated values") createReceiverCmd.Flags().StringArrayVar(&rcvResources, "resource", []string{}, "")
createCmd.AddCommand(createReceiverCmd) createCmd.AddCommand(createReceiverCmd)
} }
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 rcvType == "" {
return fmt.Errorf("Receiver type is required") return fmt.Errorf("Receiver type is required")
} }
if receiverArgs.secretRef == "" { if rcvSecretRef == "" {
return fmt.Errorf("secret ref is required") return fmt.Errorf("secret ref is required")
} }
resources := []notificationv1.CrossNamespaceObjectReference{} resources := []notificationv1.CrossNamespaceObjectReference{}
for _, resource := range receiverArgs.resources { for _, resource := range rcvResources {
kind, name := utils.ParseObjectKindName(resource) kind, name := utils.ParseObjectKindName(resource)
if kind == "" { if kind == "" {
return fmt.Errorf("invalid event source '%s', must be in format <kind>/<name>", resource) return fmt.Errorf("invalid event source '%s', must be in format <kind>/<name>", resource)
@ -98,35 +101,35 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !createArgs.export { if !export {
logger.Generatef("generating Receiver") logger.Generatef("generating Receiver")
} }
receiver := notificationv1.Receiver{ receiver := notificationv1.Receiver{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: sourceLabels, Labels: sourceLabels,
}, },
Spec: notificationv1.ReceiverSpec{ Spec: notificationv1.ReceiverSpec{
Type: receiverArgs.receiverType, Type: rcvType,
Events: receiverArgs.events, Events: rcvEvents,
Resources: resources, Resources: resources,
SecretRef: meta.LocalObjectReference{ SecretRef: corev1.LocalObjectReference{
Name: receiverArgs.secretRef, Name: rcvSecretRef,
}, },
Suspend: false, Suspend: false,
}, },
} }
if createArgs.export { if export {
return printExport(exportReceiver(&receiver)) return exportReceiver(receiver)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil { if err != nil {
return err return err
} }
@ -138,13 +141,13 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error {
} }
logger.Waitingf("waiting for Receiver reconciliation") logger.Waitingf("waiting for Receiver reconciliation")
if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, if err := wait.PollImmediate(pollInterval, timeout,
isObjectReadyConditionFunc(kubeClient, namespacedName, &receiver)); err != nil { isReceiverReady(ctx, 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.WebhookPath) logger.Successf("generated webhook URL %s", receiver.Status.URL)
return nil return nil
} }
@ -178,3 +181,23 @@ 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
}
}

@ -18,18 +18,21 @@ package main
import ( import (
"context" "context"
"fmt"
"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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
) )
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() {
@ -61,3 +64,19 @@ func upsertSecret(ctx context.Context, kubeClient client.Client, secret corev1.S
} }
return nil return nil
} }
func exportSecret(secret corev1.Secret) error {
secret.TypeMeta = metav1.TypeMeta{
APIVersion: "v1",
Kind: "Secret",
}
data, err := yaml.Marshal(secret)
if err != nil {
return err
}
fmt.Println("---")
fmt.Println(resourceToString(data))
return nil
}

@ -21,25 +21,24 @@ import (
"crypto/elliptic" "crypto/elliptic"
"fmt" "fmt"
"net/url" "net/url"
"os" "time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" "github.com/fluxcd/pkg/ssh"
) )
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: `
For Git over SSH, the host and SSH keys are automatically generated and stored The create secret git command generates a Kubernetes secret with Git credentials.
in the secret. For Git over SSH, the host and SSH keys are automatically generated and stored in the secret.
For Git over HTTP/S, the provided basic authentication credentials or bearer For Git over HTTP/S, the provided basic authentication credentials are stored in the secret.`,
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 \
@ -47,160 +46,161 @@ authentication token are stored in the secret.`,
--ssh-key-algorithm=ecdsa \ --ssh-key-algorithm=ecdsa \
--ssh-ecdsa-curve=p521 --ssh-ecdsa-curve=p521
# Create a Git SSH authentication secret with a passwordless private key from file
# The public SSH host key will still be gathered from the host
flux create secret git podinfo-auth \
--url=ssh://git@github.com/stefanprodan/podinfo \
--private-key-file=./private.key
# Create a Git SSH authentication secret with a passworded private key from file
# The public SSH host key will still be gathered from the host
flux create secret git podinfo-auth \
--url=ssh://git@github.com/stefanprodan/podinfo \
--private-key-file=./private.key \
--password=<password>
# Create a secret for a Git repository using basic authentication # Create a secret for a Git repository using basic authentication
flux create secret git podinfo-auth \ flux create secret git podinfo-auth \
--url=https://github.com/stefanprodan/podinfo \ --url=https://github.com/stefanprodan/podinfo \
--username=username \ --username=username \
--password=password --password=password
# Create a Git SSH secret on disk # Create a Git SSH secret on disk and print the deploy key
flux create secret git podinfo-auth \ flux create secret git podinfo-auth \
--url=ssh://git@github.com/stefanprodan/podinfo \ --url=ssh://git@github.com/stefanprodan/podinfo \
--export > podinfo-auth.yaml --export > podinfo-auth.yaml
# Print the deploy key yq read podinfo-auth.yaml 'data."identity.pub"' | base64 --decode
yq eval '.stringData."identity.pub"' podinfo-auth.yaml
# Create a Git SSH secret on disk and encrypt it with Mozilla SOPS
flux create secret git podinfo-auth \
--namespace=apps \
--url=ssh://git@github.com/stefanprodan/podinfo \
--export > podinfo-auth.yaml
# Encrypt the secret on disk with Mozilla SOPS
sops --encrypt --encrypted-regex '^(data|stringData)$' \ sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place podinfo-auth.yaml`, --in-place podinfo-auth.yaml
`,
RunE: createSecretGitCmdRun, RunE: createSecretGitCmdRun,
} }
type secretGitFlags struct { var (
url string secretGitURL string
username string secretGitUsername string
password string secretGitPassword string
keyAlgorithm flags.PublicKeyAlgorithm secretGitKeyAlgorithm flags.PublicKeyAlgorithm = "rsa"
rsaBits flags.RSAKeyBits secretGitRSABits flags.RSAKeyBits = 2048
ecdsaCurve flags.ECDSACurve secretGitECDSACurve = flags.ECDSACurve{Curve: elliptic.P384()}
caCrtFile string )
privateKeyFile string
bearerToken string
}
var secretGitArgs = NewSecretGitFlags()
func init() { func init() {
createSecretGitCmd.Flags().StringVar(&secretGitArgs.url, "url", "", "git address, e.g. ssh://git@host/org/repository") createSecretGitCmd.Flags().StringVar(&secretGitURL, "url", "", "git address, e.g. ssh://git@host/org/repository")
createSecretGitCmd.Flags().StringVarP(&secretGitArgs.username, "username", "u", "", "basic authentication username") createSecretGitCmd.Flags().StringVarP(&secretGitUsername, "username", "u", "", "basic authentication username")
createSecretGitCmd.Flags().StringVarP(&secretGitArgs.password, "password", "p", "", "basic authentication password") createSecretGitCmd.Flags().StringVarP(&secretGitPassword, "password", "p", "", "basic authentication password")
createSecretGitCmd.Flags().Var(&secretGitArgs.keyAlgorithm, "ssh-key-algorithm", secretGitArgs.keyAlgorithm.Description()) createSecretGitCmd.Flags().Var(&secretGitKeyAlgorithm, "ssh-key-algorithm", secretGitKeyAlgorithm.Description())
createSecretGitCmd.Flags().Var(&secretGitArgs.rsaBits, "ssh-rsa-bits", secretGitArgs.rsaBits.Description()) createSecretGitCmd.Flags().Var(&secretGitRSABits, "ssh-rsa-bits", secretGitRSABits.Description())
createSecretGitCmd.Flags().Var(&secretGitArgs.ecdsaCurve, "ssh-ecdsa-curve", secretGitArgs.ecdsaCurve.Description()) createSecretGitCmd.Flags().Var(&secretGitECDSACurve, "ssh-ecdsa-curve", secretGitECDSACurve.Description())
createSecretGitCmd.Flags().StringVar(&secretGitArgs.caCrtFile, "ca-crt-file", "", "path to TLS CA certificate file used for validating self-signed certificates")
createSecretGitCmd.Flags().StringVar(&secretGitArgs.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)
} }
func NewSecretGitFlags() secretGitFlags {
return secretGitFlags{
keyAlgorithm: flags.PublicKeyAlgorithm(sourcesecret.ECDSAPrivateKeyAlgorithm),
rsaBits: 2048,
ecdsaCurve: flags.ECDSACurve{Curve: elliptic.P384()},
}
}
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 secretGitURL == "" {
return fmt.Errorf("url is required") return fmt.Errorf("url is required")
} }
u, err := url.Parse(secretGitArgs.url) u, err := url.Parse(secretGitURL)
if err != nil { if err != nil {
return fmt.Errorf("git URL parse failed: %w", err) return fmt.Errorf("git URL parse failed: %w", err)
} }
labels, err := parseLabels() secretLabels, err := parseLabels()
if err != nil { if err != nil {
return err return err
} }
opts := sourcesecret.Options{ ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: labels, Labels: secretLabels,
ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, },
} }
switch u.Scheme { switch u.Scheme {
case "ssh": case "ssh":
keypair, err := sourcesecret.LoadKeyPairFromPath(secretGitArgs.privateKeyFile, secretGitArgs.password) pair, err := generateKeyPair(ctx, secretGitKeyAlgorithm, secretGitRSABits, secretGitECDSACurve)
if err != nil { if err != nil {
return err return err
} }
opts.Keypair = keypair
opts.SSHHostname = u.Host hostKey, err := scanHostKey(ctx, u)
opts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(secretGitArgs.keyAlgorithm) if err != nil {
opts.RSAKeyBits = int(secretGitArgs.rsaBits) return err
opts.ECDSACurve = secretGitArgs.ecdsaCurve.Curve
opts.Password = secretGitArgs.password
case "http", "https":
if (secretGitArgs.username == "" || secretGitArgs.password == "") && secretGitArgs.bearerToken == "" {
return fmt.Errorf("for Git over HTTP/S the username and password, or a bearer token is required")
} }
opts.Username = secretGitArgs.username
opts.Password = secretGitArgs.password secret.Data = map[string][]byte{
opts.BearerToken = secretGitArgs.bearerToken "identity": pair.PrivateKey,
if secretGitArgs.username != "" && secretGitArgs.password != "" && secretGitArgs.bearerToken != "" { "identity.pub": pair.PublicKey,
return fmt.Errorf("user credentials and bearer token cannot be used together") "known_hosts": hostKey,
} }
// --ca-crt-file takes precedence over --ca-file. if !export {
if secretGitArgs.caCrtFile != "" { logger.Generatef("deploy key: %s", string(pair.PublicKey))
opts.CACrt, err = os.ReadFile(secretGitArgs.caCrtFile)
if err != nil {
return fmt.Errorf("unable to read TLS CA file: %w", err)
} }
case "http", "https":
if secretGitUsername == "" || secretGitPassword == "" {
return fmt.Errorf("for Git over HTTP/S the username and password are required")
}
// TODO: add cert data when it's implemented in source-controller
secret.Data = map[string][]byte{
"username": []byte(secretGitUsername),
"password": []byte(secretGitPassword),
} }
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)
} }
secret, err := sourcesecret.Generate(opts) if export {
if err != nil { return exportSecret(secret)
return err
} }
if createArgs.export { kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
rootCmd.Println(secret.Content) if err != nil {
return nil return err
} }
var s corev1.Secret if err := upsertSecret(ctx, kubeClient, secret); err != nil {
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err return err
} }
logger.Actionf("secret '%s' created in '%s' namespace", name, namespace)
if ppk, ok := s.StringData[sourcesecret.PublicKeySecretKey]; ok { return nil
logger.Generatef("deploy key: %s", ppk)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) func generateKeyPair(ctx context.Context, alg flags.PublicKeyAlgorithm, rsa flags.RSAKeyBits, ecdsa flags.ECDSACurve) (*ssh.KeyPair, error) {
defer cancel() var keyGen ssh.KeyPairGenerator
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) switch algorithm := alg.String(); algorithm {
case "rsa":
keyGen = ssh.NewRSAGenerator(int(rsa))
case "ecdsa":
keyGen = ssh.NewECDSAGenerator(ecdsa.Curve)
case "ed25519":
keyGen = ssh.NewEd25519Generator()
default:
return nil, fmt.Errorf("unsupported public key algorithm: %s", algorithm)
}
pair, err := keyGen.Generate()
if err != nil { if err != nil {
return err return nil, fmt.Errorf("key pair generation failed, error: %w", err)
} }
if err := upsertSecret(ctx, kubeClient, s); err != nil { return pair, nil
return err
} }
logger.Actionf("git secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
return nil func scanHostKey(ctx context.Context, url *url.URL) ([]byte, error) {
host := url.Host
if url.Port() == "" {
host = host + ":22"
}
hostKey, err := ssh.ScanHostKey(host, 30*time.Second)
if err != nil {
return nil, fmt.Errorf("SSH key scan for host %s failed, error: %w", host, err)
}
return hostKey, nil
} }

@ -1,70 +0,0 @@
package main
import (
"fmt"
"os"
"testing"
)
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 {
name string
args string
assert assertFunc
}{
{
name: "no args",
args: "create secret git",
assert: assertError("name is required"),
},
{
name: "basic secret",
args: "create secret git podinfo-auth --url=https://github.com/stefanprodan/podinfo --username=my-username --password=my-password --namespace=my-namespace --export",
assert: assertGoldenFile("./testdata/create_secret/git/secret-git-basic.yaml"),
},
{
name: "ssh key",
args: "create secret git podinfo-auth --url=ssh://git@github.com/stefanprodan/podinfo --private-key-file=./testdata/create_secret/git/ecdsa.private --namespace=my-namespace --export",
assert: assertGoldenFile("testdata/create_secret/git/git-ssh-secret.yaml"),
},
{
name: "ssh key with password",
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"),
},
{
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 {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

@ -1,128 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"os"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
var createSecretGitHubAppCmd = &cobra.Command{
Use: "githubapp [name]",
Short: "Create or update a github app secret",
Long: withPreviewNote(`The create secret githubapp command generates a Kubernetes secret that can be used for GitRepository authentication with github app`),
Example: ` # Create a githubapp authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret githubapp podinfo-auth \
--app-id="1" \
--app-installation-id="2" \
--app-private-key=./private-key-file.pem \
--export > githubapp-auth.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place githubapp-auth.yaml
`,
RunE: createSecretGitHubAppCmdRun,
}
type secretGitHubAppFlags struct {
appID string
appInstallationID string
privateKeyFile string
baseURL string
}
var secretGitHubAppArgs = secretGitHubAppFlags{}
func init() {
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appID, "app-id", "", "github app ID")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.appInstallationID, "app-installation-id", "", "github app installation ID")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.privateKeyFile, "app-private-key", "", "github app private key file path")
createSecretGitHubAppCmd.Flags().StringVar(&secretGitHubAppArgs.baseURL, "app-base-url", "", "github app base URL")
createSecretCmd.AddCommand(createSecretGitHubAppCmd)
}
func createSecretGitHubAppCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("name is required")
}
secretName := args[0]
if secretGitHubAppArgs.appID == "" {
return fmt.Errorf("--app-id is required")
}
if secretGitHubAppArgs.appInstallationID == "" {
return fmt.Errorf("--app-installation-id is required")
}
if secretGitHubAppArgs.privateKeyFile == "" {
return fmt.Errorf("--app-private-key is required")
}
privateKey, err := os.ReadFile(secretGitHubAppArgs.privateKeyFile)
if err != nil {
return fmt.Errorf("unable to read private key file: %w", err)
}
opts := sourcesecret.Options{
Name: secretName,
Namespace: *kubeconfigArgs.Namespace,
GitHubAppID: secretGitHubAppArgs.appID,
GitHubAppInstallationID: secretGitHubAppArgs.appInstallationID,
GitHubAppPrivateKey: string(privateKey),
}
if secretGitHubAppArgs.baseURL != "" {
opts.GitHubAppBaseURL = secretGitHubAppArgs.baseURL
}
secret, err := sourcesecret.Generate(opts)
if err != nil {
return err
}
if createArgs.export {
rootCmd.Println(secret.Content)
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
var s corev1.Secret
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err
}
logger.Actionf("githubapp secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace)
return nil
}

@ -1,74 +0,0 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func TestCreateSecretGitHubApp(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
name: "create githubapp secret with missing name",
args: "create secret githubapp",
assert: assertError("name is required"),
},
{
name: "create githubapp secret with missing app-id",
args: "create secret githubapp appinfo",
assert: assertError("--app-id is required"),
},
{
name: "create githubapp secret with missing appInstallationID",
args: "create secret githubapp appinfo --app-id 1",
assert: assertError("--app-installation-id is required"),
},
{
name: "create githubapp secret with missing private key file",
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2",
assert: assertError("--app-private-key is required"),
},
{
name: "create githubapp secret with private key file that does not exist",
args: "create secret githubapp appinfo --app-id 1 --app-installation-id 2 --app-private-key pk.pem",
assert: assertError("unable to read private key file: open pk.pem: no such file or directory"),
},
{
name: "create githubapp secret with app info",
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --export",
assert: assertGoldenFile("testdata/create_secret/githubapp/secret.yaml"),
},
{
name: "create githubapp secret with appinfo and base url",
args: "create secret githubapp appinfo --namespace my-namespace --app-id 1 --app-installation-id 2 --app-private-key ./testdata/create_secret/githubapp/test-private-key.pem --app-base-url www.example.com/api/v3 --export",
assert: assertGoldenFile("testdata/create_secret/githubapp/secret-with-baseurl.yaml"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

@ -19,21 +19,22 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"os" "io/ioutil"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
) )
var createSecretHelmCmd = &cobra.Command{ var createSecretHelmCmd = &cobra.Command{
Use: "helm [name]", Use: "helm [name]",
Short: "Create or update a Kubernetes secret for Helm repository authentication", Short: "Create or update a Kubernetes secret for Helm repository authentication",
Long: `The create secret helm command generates a Kubernetes secret with basic authentication credentials.`, Long: `
The create secret helm command generates a Kubernetes secret with basic authentication credentials.`,
Example: ` # Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS Example: ` # Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS
flux create secret helm repo-auth \ flux create secret helm repo-auth \
--namespace=my-namespace \ --namespace=my-namespace \
--username=my-username \ --username=my-username \
@ -41,91 +42,100 @@ 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 an 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,
} }
type secretHelmFlags struct { var (
username string secretHelmUsername string
password string secretHelmPassword string
secretTLSFlags secretHelmCertFile string
} secretHelmKeyFile string
secretHelmCAFile string
var secretHelmArgs secretHelmFlags )
func init() { func init() {
flags := createSecretHelmCmd.Flags() createSecretHelmCmd.Flags().StringVarP(&secretHelmUsername, "username", "u", "", "basic authentication username")
flags.StringVarP(&secretHelmArgs.username, "username", "u", "", "basic authentication username") createSecretHelmCmd.Flags().StringVarP(&secretHelmPassword, "password", "p", "", "basic authentication password")
flags.StringVarP(&secretHelmArgs.password, "password", "p", "", "basic authentication password") createSecretHelmCmd.Flags().StringVar(&secretHelmCertFile, "cert-file", "", "TLS authentication cert file path")
flags.StringVar(&secretHelmArgs.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path") createSecretHelmCmd.Flags().StringVar(&secretHelmKeyFile, "key-file", "", "TLS authentication key file path")
flags.StringVar(&secretHelmArgs.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path") createSecretHelmCmd.Flags().StringVar(&secretHelmCAFile, "ca-file", "", "TLS authentication CA file path")
flags.StringVar(&secretHelmArgs.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path")
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() secretLabels, err := parseLabels()
if err != nil { if err != nil {
return err return err
} }
caBundle := []byte{} secret := corev1.Secret{
if secretHelmArgs.caCrtFile != "" { ObjectMeta: metav1.ObjectMeta{
var err error Name: name,
caBundle, err = os.ReadFile(secretHelmArgs.caCrtFile) Namespace: namespace,
if err != nil { Labels: secretLabels,
return fmt.Errorf("unable to read TLS CA file: %w", err) },
StringData: map[string]string{},
} }
if secretHelmUsername != "" && secretHelmPassword != "" {
secret.StringData["username"] = secretHelmUsername
secret.StringData["password"] = secretHelmPassword
} }
var certFile, keyFile []byte if secretHelmCertFile != "" && secretHelmKeyFile != "" {
if secretHelmArgs.tlsCrtFile != "" && secretHelmArgs.tlsKeyFile != "" { cert, err := ioutil.ReadFile(secretHelmCertFile)
if certFile, err = os.ReadFile(secretHelmArgs.tlsCrtFile); err != nil { if err != nil {
return fmt.Errorf("failed to read cert file: %w", err) return fmt.Errorf("failed to read repository cert file '%s': %w", secretHelmCertFile, err)
} }
if keyFile, err = os.ReadFile(secretHelmArgs.tlsKeyFile); err != nil { secret.StringData["certFile"] = string(cert)
return fmt.Errorf("failed to read key file: %w", err)
key, err := ioutil.ReadFile(secretHelmKeyFile)
if err != nil {
return fmt.Errorf("failed to read repository key file '%s': %w", secretHelmKeyFile, err)
} }
secret.StringData["keyFile"] = string(key)
} }
opts := sourcesecret.Options{ if secretHelmCAFile != "" {
Name: name, ca, err := ioutil.ReadFile(secretHelmCAFile)
Namespace: *kubeconfigArgs.Namespace,
Labels: labels,
Username: secretHelmArgs.username,
Password: secretHelmArgs.password,
CACrt: caBundle,
TLSCrt: certFile,
TLSKey: keyFile,
}
secret, err := sourcesecret.Generate(opts)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to read repository CA file '%s': %w", secretHelmCAFile, err)
}
secret.StringData["caFile"] = string(ca)
} }
if createArgs.export { if export {
rootCmd.Println(secret.Content) return exportSecret(secret)
return nil
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil { if err != nil {
return err return err
} }
var s corev1.Secret
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err return err
} }
logger.Actionf("secret '%s' created in '%s' namespace", name, namespace)
logger.Actionf("helm secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
return nil return nil
} }

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

@ -1,161 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
)
var createSecretNotationCmd = &cobra.Command{
Use: "notation [name]",
Short: "Create or update a Kubernetes secret for verifications of artifacts signed by Notation",
Long: withPreviewNote(`The create secret notation command generates a Kubernetes secret with root ca certificates and trust policy.`),
Example: ` # Create a Notation configuration secret on disk and encrypt it with Mozilla SOPS
flux create secret notation my-notation-cert \
--namespace=my-namespace \
--trust-policy-file=./my-trust-policy.json \
--ca-cert-file=./my-cert.crt \
--export > my-notation-cert.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place my-notation-cert.yaml`,
RunE: createSecretNotationCmdRun,
}
type secretNotationFlags struct {
trustPolicyFile string
caCrtFile []string
}
var secretNotationArgs secretNotationFlags
func init() {
createSecretNotationCmd.Flags().StringVar(&secretNotationArgs.trustPolicyFile, "trust-policy-file", "", "notation trust policy file path")
createSecretNotationCmd.Flags().StringSliceVar(&secretNotationArgs.caCrtFile, "ca-cert-file", []string{}, "root ca cert file path")
createSecretCmd.AddCommand(createSecretNotationCmd)
}
func createSecretNotationCmdRun(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("name is required")
}
if secretNotationArgs.caCrtFile == nil || len(secretNotationArgs.caCrtFile) == 0 {
return fmt.Errorf("--ca-cert-file is required")
}
if secretNotationArgs.trustPolicyFile == "" {
return fmt.Errorf("--trust-policy-file is required")
}
name := args[0]
labels, err := parseLabels()
if err != nil {
return err
}
policy, err := os.ReadFile(secretNotationArgs.trustPolicyFile)
if err != nil {
return fmt.Errorf("unable to read trust policy file: %w", err)
}
var doc trustpolicy.Document
if err := json.Unmarshal(policy, &doc); err != nil {
return fmt.Errorf("failed to unmarshal trust policy %s: %w", secretNotationArgs.trustPolicyFile, err)
}
if err := doc.Validate(); err != nil {
return fmt.Errorf("invalid trust policy: %w", err)
}
var (
caCerts []sourcesecret.VerificationCrt
fileErr error
)
for _, caCrtFile := range secretNotationArgs.caCrtFile {
fileName := filepath.Base(caCrtFile)
if !strings.HasSuffix(fileName, ".crt") && !strings.HasSuffix(fileName, ".pem") {
fileErr = errors.Join(fileErr, fmt.Errorf("%s must end with either .crt or .pem", fileName))
continue
}
caBundle, err := os.ReadFile(caCrtFile)
if err != nil {
fileErr = errors.Join(fileErr, fmt.Errorf("unable to read TLS CA file: %w", err))
continue
}
caCerts = append(caCerts, sourcesecret.VerificationCrt{Name: fileName, CACrt: caBundle})
}
if fileErr != nil {
return fileErr
}
if len(caCerts) == 0 {
return fmt.Errorf("no CA certs found")
}
opts := sourcesecret.Options{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: labels,
VerificationCrts: caCerts,
TrustPolicy: policy,
}
secret, err := sourcesecret.Generate(opts)
if err != nil {
return err
}
if createArgs.export {
rootCmd.Println(secret.Content)
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
var s corev1.Secret
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err
}
logger.Actionf("notation configuration secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
return nil
}

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

@ -1,121 +0,0 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"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
}

@ -1,51 +0,0 @@
/*
Copyright 2022 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func 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)
})
}
}

@ -1,112 +0,0 @@
/*
Copyright 2024 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"errors"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
)
var createSecretProxyCmd = &cobra.Command{
Use: "proxy [name]",
Short: "Create or update a Kubernetes secret for proxy authentication",
Long: `The create secret proxy command generates a Kubernetes secret with the
proxy address and the basic authentication credentials.`,
Example: ` # Create a proxy secret on disk and encrypt it with SOPS
flux create secret proxy my-proxy \
--namespace=my-namespace \
--address=https://my-proxy.com \
--username=my-username \
--password=my-password \
--export > proxy.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place proxy.yaml`,
RunE: createSecretProxyCmdRun,
}
type secretProxyFlags struct {
address string
username string
password string
}
var secretProxyArgs secretProxyFlags
func init() {
createSecretProxyCmd.Flags().StringVar(&secretProxyArgs.address, "address", "", "proxy address")
createSecretProxyCmd.Flags().StringVarP(&secretProxyArgs.username, "username", "u", "", "basic authentication username")
createSecretProxyCmd.Flags().StringVarP(&secretProxyArgs.password, "password", "p", "", "basic authentication password")
createSecretCmd.AddCommand(createSecretProxyCmd)
}
func createSecretProxyCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
labels, err := parseLabels()
if err != nil {
return err
}
if secretProxyArgs.address == "" {
return errors.New("address is required")
}
opts := sourcesecret.Options{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: labels,
Address: secretProxyArgs.address,
Username: secretProxyArgs.username,
Password: secretProxyArgs.password,
}
secret, err := sourcesecret.Generate(opts)
if err != nil {
return err
}
if createArgs.export {
rootCmd.Println(secret.Content)
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
var s corev1.Secret
if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil {
return err
}
if err := upsertSecret(ctx, kubeClient, s); err != nil {
return err
}
logger.Actionf("proxy secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
return nil
}

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

@ -1,122 +0,0 @@
/*
Copyright 2020, 2021 The Flux authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"os"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/yaml"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret"
)
var createSecretTLSCmd = &cobra.Command{
Use: "tls [name]",
Short: "Create or update a Kubernetes secret with TLS certificates",
Long: `The create secret tls command generates a Kubernetes secret with certificates for use with TLS.`,
Example: ` # Create a TLS secret on disk and encrypt it with SOPS.
# Files are expected to be PEM-encoded.
flux create secret tls certs \
--namespace=my-namespace \
--tls-crt-file=./client.crt \
--tls-key-file=./client.key \
--ca-crt-file=./ca.crt \
--export > certs.yaml
sops --encrypt --encrypted-regex '^(data|stringData)$' \
--in-place certs.yaml`,
RunE: createSecretTLSCmdRun,
}
type secretTLSFlags struct {
caCrtFile string
tlsKeyFile string
tlsCrtFile string
}
var secretTLSArgs secretTLSFlags
func init() {
createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path")
createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path")
createSecretTLSCmd.Flags().StringVar(&secretTLSArgs.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path")
createSecretCmd.AddCommand(createSecretTLSCmd)
}
func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error {
name := args[0]
labels, err := parseLabels()
if err != nil {
return err
}
opts := sourcesecret.Options{
Name: name,
Namespace: *kubeconfigArgs.Namespace,
Labels: labels,
}
if secretTLSArgs.caCrtFile != "" {
opts.CACrt, err = os.ReadFile(secretTLSArgs.caCrtFile)
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)
}
}
secret, err := sourcesecret.Generate(opts)
if err != nil {
return err
}
if createArgs.export {
rootCmd.Print(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("tls secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace)
return nil
}

@ -1,31 +0,0 @@
package main
import (
"testing"
)
func TestCreateTlsSecret(t *testing.T) {
tests := []struct {
name string
args string
assert assertFunc
}{
{
args: "create secret tls",
assert: assertError("name is required"),
},
{
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"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := cmdTestCase{
args: tt.args,
assert: tt.assert,
}
cmd.runTestCmd(t)
})
}
}

@ -17,25 +17,15 @@ limitations under the License.
package main package main
import ( import (
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
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 {
fetchTimeout time.Duration
} }
var createSourceArgs createSourceFlags
func init() { func init() {
createSourceCmd.PersistentFlags().DurationVar(&createSourceArgs.fetchTimeout, "fetch-timeout", createSourceArgs.fetchTimeout,
"set a timeout for fetch operations performed by source-controller (e.g. 'git clone' or 'helm repo update')")
createCmd.AddCommand(createSourceCmd) createCmd.AddCommand(createSourceCmd)
} }

@ -19,8 +19,8 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -30,20 +30,18 @@ import (
"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"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/flux2/internal/flags"
"github.com/fluxcd/flux2/internal/utils"
sourcev1 "github.com/fluxcd/source-controller/api/v1" sourcev1 "github.com/fluxcd/source-controller/api/v1beta1"
"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: `
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 from a Buckets using static authentication
flux create source bucket podinfo \ flux create source bucket podinfo \
--bucket-name=podinfo \ --bucket-name=podinfo \
--endpoint=minio.minio.svc.cluster.local:9000 \ --endpoint=minio.minio.svc.cluster.local:9000 \
@ -52,60 +50,52 @@ For Buckets with static authentication, the credentials are stored in a Kubernet
--secret-key=mysecretkey \ --secret-key=mysecretkey \
--interval=10m --interval=10m
# Create a source for an Amazon S3 Bucket using IAM authentication # Create a source from an Amazon S3 Bucket using IAM authentication
flux create source bucket podinfo \ flux create source bucket podinfo \
--bucket-name=podinfo \ --bucket-name=podinfo \
--provider=aws \ --provider=aws \
--endpoint=s3.amazonaws.com \ --endpoint=s3.amazonaws.com \
--region=us-east-1 \ --region=us-east-1 \
--interval=10m`, --interval=10m
`,
RunE: createSourceBucketCmdRun, RunE: createSourceBucketCmdRun,
} }
type sourceBucketFlags struct { var (
name string sourceBucketName string
provider flags.SourceBucketProvider sourceBucketProvider = flags.SourceBucketProvider(sourcev1.GenericBucketProvider)
endpoint string sourceBucketEndpoint string
accessKey string sourceBucketAccessKey string
secretKey string sourceBucketSecretKey string
region string sourceBucketRegion string
insecure bool sourceBucketInsecure bool
secretRef string sourceBucketSecretRef string
proxySecretRef string )
ignorePaths []string
}
var sourceBucketArgs = newSourceBucketFlags()
func init() { func init() {
createSourceBucketCmd.Flags().Var(&sourceBucketArgs.provider, "provider", sourceBucketArgs.provider.Description()) createSourceBucketCmd.Flags().Var(&sourceBucketProvider, "provider", sourceBucketProvider.Description())
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.name, "bucket-name", "", "the bucket name") createSourceBucketCmd.Flags().StringVar(&sourceBucketName, "bucket-name", "", "the bucket name")
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.endpoint, "endpoint", "", "the bucket endpoint address") createSourceBucketCmd.Flags().StringVar(&sourceBucketEndpoint, "endpoint", "", "the bucket endpoint address")
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.accessKey, "access-key", "", "the bucket access key") createSourceBucketCmd.Flags().StringVar(&sourceBucketAccessKey, "access-key", "", "the bucket access key")
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.secretKey, "secret-key", "", "the bucket secret key") createSourceBucketCmd.Flags().StringVar(&sourceBucketSecretKey, "secret-key", "", "the bucket secret key")
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.region, "region", "", "the bucket region") createSourceBucketCmd.Flags().StringVar(&sourceBucketRegion, "region", "", "the bucket region")
createSourceBucketCmd.Flags().BoolVar(&sourceBucketArgs.insecure, "insecure", false, "for when connecting to a non-TLS S3 HTTP endpoint") createSourceBucketCmd.Flags().BoolVar(&sourceBucketInsecure, "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(&sourceBucketSecretRef, "secret-ref", "", "the name of an existing secret containing credentials")
createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.proxySecretRef, "proxy-secret-ref", "", "the name of an existing secret containing the proxy address and credentials")
createSourceBucketCmd.Flags().StringSliceVar(&sourceBucketArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in bucket resource (can specify multiple paths with commas: path1,path2)")
createSourceCmd.AddCommand(createSourceBucketCmd) createSourceCmd.AddCommand(createSourceBucketCmd)
} }
func newSourceBucketFlags() sourceBucketFlags {
return sourceBucketFlags{
provider: flags.SourceBucketProvider(sourcev1.BucketProviderGeneric),
}
}
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 sourceBucketName == "" {
return fmt.Errorf("bucket-name is required") return fmt.Errorf("bucket-name is required")
} }
if sourceBucketArgs.endpoint == "" { if sourceBucketEndpoint == "" {
return fmt.Errorf("endpoint is required") return fmt.Errorf("endpoint is required")
} }
@ -114,82 +104,64 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
tmpDir, err := os.MkdirTemp("", name) tmpDir, err := ioutil.TempDir("", name)
if err != nil { if err != nil {
return err return err
} }
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,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: sourceLabels, Labels: sourceLabels,
}, },
Spec: sourcev1.BucketSpec{ Spec: sourcev1.BucketSpec{
BucketName: sourceBucketArgs.name, BucketName: sourceBucketName,
Provider: sourceBucketArgs.provider.String(), Provider: sourceBucketProvider.String(),
Insecure: sourceBucketArgs.insecure, Insecure: sourceBucketInsecure,
Endpoint: sourceBucketArgs.endpoint, Endpoint: sourceBucketEndpoint,
Region: sourceBucketArgs.region, Region: sourceBucketRegion,
Interval: metav1.Duration{ Interval: metav1.Duration{
Duration: createArgs.interval, Duration: interval,
}, },
Ignore: ignorePaths,
}, },
} }
if sourceHelmSecretRef != "" {
if createSourceArgs.fetchTimeout > 0 { bucket.Spec.SecretRef = &corev1.LocalObjectReference{
bucket.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} Name: sourceBucketSecretRef,
}
if sourceBucketArgs.secretRef != "" {
bucket.Spec.SecretRef = &meta.LocalObjectReference{
Name: sourceBucketArgs.secretRef,
}
}
if sourceBucketArgs.proxySecretRef != "" {
bucket.Spec.ProxySecretRef = &meta.LocalObjectReference{
Name: sourceBucketArgs.proxySecretRef,
} }
} }
if createArgs.export { if export {
return printExport(exportBucket(bucket)) return exportBucket(*bucket)
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) kubeClient, err := utils.KubeClient(kubeconfig, kubecontext)
if err != nil { if err != nil {
return err return err
} }
logger.Generatef("generating Bucket source") logger.Generatef("generating Bucket source")
if sourceBucketArgs.secretRef == "" { if sourceBucketSecretRef == "" {
secretName := fmt.Sprintf("bucket-%s", name) secretName := fmt.Sprintf("bucket-%s", name)
secret := corev1.Secret{ secret := corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: secretName, Name: secretName,
Namespace: *kubeconfigArgs.Namespace, Namespace: namespace,
Labels: sourceLabels, Labels: sourceLabels,
}, },
StringData: map[string]string{}, StringData: map[string]string{},
} }
if sourceBucketArgs.accessKey != "" && sourceBucketArgs.secretKey != "" { if sourceBucketAccessKey != "" && sourceBucketSecretKey != "" {
secret.StringData["accesskey"] = sourceBucketArgs.accessKey secret.StringData["accesskey"] = sourceBucketAccessKey
secret.StringData["secretkey"] = sourceBucketArgs.secretKey secret.StringData["secretkey"] = sourceBucketSecretKey
} }
if len(secret.StringData) > 0 { if len(secret.StringData) > 0 {
@ -197,7 +169,7 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error {
if err := upsertSecret(ctx, kubeClient, secret); err != nil { if err := upsertSecret(ctx, kubeClient, secret); err != nil {
return err return err
} }
bucket.Spec.SecretRef = &meta.LocalObjectReference{ bucket.Spec.SecretRef = &corev1.LocalObjectReference{
Name: secretName, Name: secretName,
} }
logger.Successf("authentication configured") logger.Successf("authentication configured")
@ -211,8 +183,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.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, if err := wait.PollImmediate(pollInterval, timeout,
isObjectReadyConditionFunc(kubeClient, namespacedName, bucket)); err != nil { isBucketReady(ctx, kubeClient, namespacedName, bucket)); err != nil {
return err return err
} }
logger.Successf("Bucket source reconciliation completed") logger.Successf("Bucket source reconciliation completed")

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

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

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

Loading…
Cancel
Save