diff --git a/.github/aur/flux-bin/.SRCINFO.template b/.github/aur/flux-bin/.SRCINFO.template index 5a4e706e..d09772f6 100644 --- a/.github/aur/flux-bin/.SRCINFO.template +++ b/.github/aur/flux-bin/.SRCINFO.template @@ -4,13 +4,16 @@ pkgbase = flux-bin pkgrel = ${PKGREL} url = https://fluxcd.io/ arch = x86_64 - arch = armv6h arch = armv7h arch = aarch64 license = APACHE - source_x86_64 = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_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 - source_armv7h = flux-bin-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v1/flux_${PKGVER}_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 + optdepends = bash-completion: auto-completion for flux in Bash + optdepends = zsh-completions: auto-completion for flux in ZSH + source_x86_64 = flux-bin-${PKGVER}_linux_amd64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_amd64.tar.gz + sha256sums_x86_64 = ${SHA256SUM_AMD64} + source_armv7h = flux-bin-${PKGVER}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm.tar.gz + sha256sums_armv7h = ${SHA256SUM_ARM} + source_aarch64 = flux-bin-${PKGVER}_linux_arm64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_arm64.tar.gz + sha256sums_aarch64 = ${SHA256SUM_ARM64} pkgname = flux-bin diff --git a/.github/aur/flux-bin/PKGBUILD.template b/.github/aur/flux-bin/PKGBUILD.template index f3106d1c..f011e1f9 100644 --- a/.github/aur/flux-bin/PKGBUILD.template +++ b/.github/aur/flux-bin/PKGBUILD.template @@ -4,37 +4,32 @@ pkgname=flux-bin pkgver=${PKGVER} pkgrel=${PKGREL} +_srcname=flux +_srcver=${VERSION} pkgdesc="Open and extensible continuous delivery solution for Kubernetes" url="https://fluxcd.io/" -arch=("x86_64" "armv6h" "armv7h" "aarch64") +arch=("x86_64" "armv7h" "aarch64") license=("APACHE") -optdepends=('bash-completion: auto-completion for flux in Bash', -'zsh-completions: auto-completion for flux in ZSH') +optdepends=('bash-completion: auto-completion for flux in Bash' + 'zsh-completions: auto-completion for flux in ZSH') source_x86_64=( - "${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" + "${pkgname}-${pkgver}_linux_amd64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_amd64.tar.gz" ) source_armv7h=( - "${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${pkgver}/flux_${pkgver}_linux_arm.tar.gz" + "${pkgname}-${pkgver}_linux_arm.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm.tar.gz" ) source_aarch64=( - "${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${pkgver}/flux_${pkgver}_linux_arm64.tar.gz" + "${pkgname}-${pkgver}_linux_arm64.tar.gz::https://github.com/fluxcd/flux2/releases/download/v${_srcver}/flux_${_srcver}_linux_arm64.tar.gz" ) sha256sums_x86_64=( ${SHA256SUM_AMD64} ) -sha256sums_armv6h=( - ${SHA256SUM_ARM} -) sha256sums_armv7h=( ${SHA256SUM_ARM} ) sha256sums_aarch64=( ${SHA256SUM_ARM64} ) -_srcname=flux package() { install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" diff --git a/.github/aur/flux-bin/publish.sh b/.github/aur/flux-bin/publish.sh index 0e6097c1..00feb89a 100755 --- a/.github/aur/flux-bin/publish.sh +++ b/.github/aur/flux-bin/publish.sh @@ -28,6 +28,7 @@ git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1 CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') +# Transform pre-release to AUR compatible version format export PKGVER=${VERSION/-/} if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then @@ -36,12 +37,12 @@ else export PKGREL=1 fi -export SHA256SUM_ARM=$(sha256sum ${ROOT}/dist/flux_${PKGVER}_linux_arm.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_${PKGVER}_linux_amd64.tar.gz | awk '{ print $1 }') +export SHA256SUM_ARM=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_arm.tar.gz | awk '{ print $1 }') +export SHA256SUM_ARM64=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_arm64.tar.gz | awk '{ print $1 }') +export SHA256SUM_AMD64=$(sha256sum ${ROOT}/dist/flux_${VERSION}_linux_amd64.tar.gz | awk '{ print $1 }') -envsubst '$PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < .SRCINFO.template > $GITDIR/.SRCINFO -envsubst '$PKGVER $PKGREL $SHA256SUM_AMD64 $SHA256SUM_ARM $SHA256SUM_ARM64' < PKGBUILD.template > $GITDIR/PKGBUILD +envsubst '$VERSION $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 cd $GITDIR git config user.name "fluxcdbot" diff --git a/.github/aur/flux-go/.SRCINFO.template b/.github/aur/flux-go/.SRCINFO.template index 9f7aaf8b..134f969c 100644 --- a/.github/aur/flux-go/.SRCINFO.template +++ b/.github/aur/flux-go/.SRCINFO.template @@ -4,7 +4,6 @@ pkgbase = flux-go pkgrel = ${PKGREL} url = https://fluxcd.io/ arch = x86_64 - arch = armv6h arch = armv7h arch = aarch64 license = APACHE @@ -13,6 +12,6 @@ pkgbase = flux-go provides = flux-bin conflicts = flux-bin replaces = flux-cli - source = flux-go-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/archive/v${PKGVER}.tar.gz + source = flux-go-${PKGVER}.tar.gz::https://github.com/fluxcd/flux2/archive/v${VERSION}.tar.gz pkgname = flux-go diff --git a/.github/aur/flux-go/PKGBUILD.template b/.github/aur/flux-go/PKGBUILD.template index 225320da..6125498a 100644 --- a/.github/aur/flux-go/PKGBUILD.template +++ b/.github/aur/flux-go/PKGBUILD.template @@ -4,43 +4,44 @@ pkgname=flux-go pkgver=${PKGVER} pkgrel=${PKGREL} +_srcname=flux +_srcver=${VERSION} pkgdesc="Open and extensible continuous delivery solution for Kubernetes" url="https://fluxcd.io/" -arch=("x86_64" "armv6h" "armv7h" "aarch64") +arch=("x86_64" "armv7h" "aarch64") license=("APACHE") provides=("flux-bin") conflicts=("flux-bin") replaces=("flux-cli") depends=("glibc") -makedepends=('go>=1.17', 'kustomize>=3.0') +makedepends=('go>=1.20', 'kustomize>=5.0') optdepends=('bash-completion: auto-completion for flux in Bash', 'zsh-completions: auto-completion for flux in ZSH') source=( - "${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/archive/v${pkgver}.tar.gz" + "${pkgname}-${pkgver}.tar.gz::https://github.com/fluxcd/flux2/archive/v${_srcver}.tar.gz" ) sha256sums=( ${SHA256SUM} ) -_srcname=flux build() { - cd "flux2-${pkgver}" + cd "flux2-${_srcver}" export CGO_LDFLAGS="$LDFLAGS" export CGO_CFLAGS="$CFLAGS" export CGO_CXXFLAGS="$CXXFLAGS" export CGO_CPPFLAGS="$CPPFLAGS" export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw" make cmd/flux/.manifests.done - go build -ldflags "-linkmode=external -X main.VERSION=${pkgver}" -o ${_srcname} ./cmd/flux + go build -ldflags "-linkmode=external -X main.VERSION=${_srcver}" -o ${_srcname} ./cmd/flux } check() { - cd "flux2-${pkgver}" + cd "flux2-${_srcver}" case $CARCH in aarch64) export ENVTEST_ARCH=arm64 ;; - armv6h|armv7h) + armv7h) export ENVTEST_ARCH=arm ;; esac @@ -48,7 +49,7 @@ check() { } package() { - cd "flux2-${pkgver}" + cd "flux2-${_srcver}" install -Dm755 ${_srcname} "${pkgdir}/usr/bin/${_srcname}" install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" diff --git a/.github/aur/flux-go/publish.sh b/.github/aur/flux-go/publish.sh index bea591dd..fba19071 100755 --- a/.github/aur/flux-go/publish.sh +++ b/.github/aur/flux-go/publish.sh @@ -28,6 +28,7 @@ git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1 CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') +# Transform pre-release to AUR compatible version format export PKGVER=${VERSION/-/} if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then @@ -36,10 +37,10 @@ else export PKGREL=1 fi -export SHA256SUM=$(curl -sL https://github.com/fluxcd/flux2/archive/v$PKGVER.tar.gz | sha256sum | awk '{ print $1 }') +export SHA256SUM=$(curl -sL https://github.com/fluxcd/flux2/archive/v${VERSION}.tar.gz | sha256sum | awk '{ print $1 }') -envsubst '$PKGVER $PKGREL $SHA256SUM' < .SRCINFO.template > $GITDIR/.SRCINFO -envsubst '$PKGVER $PKGREL $SHA256SUM' < PKGBUILD.template > $GITDIR/PKGBUILD +envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM' < .SRCINFO.template > $GITDIR/.SRCINFO +envsubst '$VERSION $PKGVER $PKGREL $SHA256SUM' < PKGBUILD.template > $GITDIR/PKGBUILD cd $GITDIR git config user.name "fluxcdbot" diff --git a/.github/aur/flux-scm/.SRCINFO.template b/.github/aur/flux-scm/.SRCINFO.template index 343c7ce2..6237e672 100644 --- a/.github/aur/flux-scm/.SRCINFO.template +++ b/.github/aur/flux-scm/.SRCINFO.template @@ -4,7 +4,6 @@ pkgbase = flux-scm pkgrel = ${PKGREL} url = https://fluxcd.io/ arch = x86_64 - arch = armv6h arch = armv7h arch = aarch64 license = APACHE diff --git a/.github/aur/flux-scm/PKGBUILD.template b/.github/aur/flux-scm/PKGBUILD.template index 9593cf83..dc091714 100644 --- a/.github/aur/flux-scm/PKGBUILD.template +++ b/.github/aur/flux-scm/PKGBUILD.template @@ -4,21 +4,21 @@ pkgname=flux-scm pkgver=${PKGVER} pkgrel=${PKGREL} +_srcname=flux pkgdesc="Open and extensible continuous delivery solution for Kubernetes" url="https://fluxcd.io/" -arch=("x86_64" "armv6h" "armv7h" "aarch64") +arch=("x86_64" "armv7h" "aarch64") license=("APACHE") provides=("flux-bin") conflicts=("flux-bin") depends=("glibc") -makedepends=('go>=1.17', 'kustomize>=3.0', 'git') +makedepends=('go>=1.20', 'kustomize>=5.0', 'git') optdepends=('bash-completion: auto-completion for flux in Bash', 'zsh-completions: auto-completion for flux in ZSH') source=( "git+https://github.com/fluxcd/flux2.git" ) md5sums=('SKIP') -_srcname=flux pkgver() { cd "flux2" @@ -42,7 +42,7 @@ check() { aarch64) export ENVTEST_ARCH=arm64 ;; - armv6h|armv7h) + armv7h) export ENVTEST_ARCH=arm ;; esac diff --git a/.github/aur/flux-scm/publish.sh b/.github/aur/flux-scm/publish.sh index 0b743c29..ac704308 100755 --- a/.github/aur/flux-scm/publish.sh +++ b/.github/aur/flux-scm/publish.sh @@ -28,6 +28,7 @@ git clone aur@aur.archlinux.org:$PKGNAME $GITDIR 2>&1 CURRENT_PKGVER=$(cat $GITDIR/.SRCINFO | grep pkgver | awk '{ print $3 }') CURRENT_PKGREL=$(cat $GITDIR/.SRCINFO | grep pkgrel | awk '{ print $3 }') +# Transform pre-release to AUR compatible version format export PKGVER=${VERSION/-/} if [[ "${CURRENT_PKGVER}" == "${PKGVER}" ]]; then diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..e07776c5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + labels: ["area/ci", "dependencies"] + groups: + # Group all updates together, so that they are all applied in a single PR. + # Grouped updates are currently in beta and is subject to change. + # xref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups + ci: + patterns: + - "*" + schedule: + # By default, this will be on a monday. + interval: "weekly" diff --git a/.github/kind/config.yaml b/.github/kind/config.yaml index e2834d6c..12c4c2d9 100644 --- a/.github/kind/config.yaml +++ b/.github/kind/config.yaml @@ -1,5 +1,9 @@ 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 diff --git a/.github/labels.yaml b/.github/labels.yaml new file mode 100644 index 00000000..58c66d1e --- /dev/null +++ b/.github/labels.yaml @@ -0,0 +1,58 @@ +# Configuration file to declaratively configure labels +# Ref: https://github.com/EndBug/label-sync#Config-files + +- name: area/bootstrap + description: Bootstrap related issues and pull requests + color: '#86efc9' +- name: area/install + description: Install and uninstall related issues and pull requests + color: '#86efc9' +- name: area/diff + description: Diff related issues and pull requests + color: '#BA4192' +- name: area/bucket + description: Bucket related issues and pull requests + color: '#00b140' +- name: area/git + description: Git related issues and pull requests + color: '#863faf' +- name: area/oci + description: OCI related issues and pull requests + color: '#c739ff' +- name: area/kustomization + description: Kustomization related issues and pull requests + color: '#00e54d' +- name: area/helm + description: Helm related issues and pull requests + color: '#1673b6' +- name: area/image-automation + description: Automated image updates related issues and pull requests + color: '#c5def5' +- name: area/monitoring + description: Monitoring related issues and pull requests + color: '#dd75ae' +- name: area/multi-tenancy + description: Multi-tenancy related issues and pull requests + color: '#72CDBD' +- name: area/notification + description: Notification API related issues and pull requests + color: '#434ec1' +- name: area/source + description: Source API related issues and pull requests + color: '#863faf' +- name: area/rfc + description: Feature request proposals in the RFC format + color: '#D621C3' + aliases: ['area/RFC'] +- name: backport:release/v2.0.x + description: To be backported to release/v2.0.x + color: '#ffd700' +- name: backport:release/v2.1.x + description: To be backported to release/v2.1.x + color: '#ffd700' +- name: backport:release/v2.2.x + description: To be backported to release/v2.2.x + color: '#ffd700' +- name: backport:release/v2.3.x + description: To be backported to release/v2.3.x + color: '#ffd700' diff --git a/.github/runners/README.md b/.github/runners/README.md index a7964234..8dc738f1 100644 --- a/.github/runners/README.md +++ b/.github/runners/README.md @@ -1,24 +1,34 @@ # Flux ARM64 GitHub runners -The Flux ARM64 end-to-end tests run on Equinix instances provisioned with Docker and GitHub self-hosted runners. +The Flux ARM64 end-to-end tests run on Equinix Metal instances provisioned with Docker and GitHub self-hosted runners. ## Current instances -| Runner | Instance | Region | -|---------------|---------------------|--------| -| equinix-arm-1 | flux-equinix-arm-01 | AMS1 | -| equinix-arm-2 | flux-equinix-arm-01 | AMS1 | -| equinix-arm-3 | flux-equinix-arm-01 | AMS1 | -| equinix-arm-4 | flux-equinix-arm-02 | DFW2 | -| equinix-arm-5 | flux-equinix-arm-02 | DFW2 | -| equinix-arm-6 | flux-equinix-arm-02 | DFW2 | +| 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: c2.large.arm -- OS: Ubuntu 20.04 +- Type: `c3.large.arm64` +- OS: `Ubuntu 22.04 LTS` ### Install prerequisites @@ -54,14 +64,14 @@ sudo ./prereq.sh - Retrieve the GitHub runner token from the repository [settings page](https://github.com/fluxcd/flux2/settings/actions/runners/new?arch=arm64&os=linux) -- Create 3 directories `runner1`, `runner2`, `runner3` +- 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- +./runner-setup.sh equinix-arm- ``` - Reboot the instance diff --git a/.github/runners/prereq.sh b/.github/runners/prereq.sh index 013ced4a..e1e497f4 100755 --- a/.github/runners/prereq.sh +++ b/.github/runners/prereq.sh @@ -18,12 +18,12 @@ set -eu -KIND_VERSION=0.11.1 -KUBECTL_VERSION=1.21.2 -KUSTOMIZE_VERSION=4.1.3 -HELM_VERSION=3.7.2 -GITHUB_RUNNER_VERSION=2.285.1 -PACKAGES="apt-transport-https ca-certificates software-properties-common build-essential libssl-dev gnupg lsb-release jq" +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 \ @@ -31,6 +31,10 @@ apt-get update \ && 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 diff --git a/.github/runners/runner-setup.sh b/.github/runners/runner-setup.sh index cef53faf..e7e1d332 100755 --- a/.github/runners/runner-setup.sh +++ b/.github/runners/runner-setup.sh @@ -22,7 +22,7 @@ RUNNER_NAME=$1 REPOSITORY_TOKEN=$2 REPOSITORY_URL=${3:-https://github.com/fluxcd/flux2} -GITHUB_RUNNER_VERSION=2.285.1 +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 \ diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 00000000..79ca735a --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,50 @@ +# Flux GitHub Workflows + +## End-to-end Testing + +The e2e workflows run a series of tests to ensure that the Flux CLI and +the GitOps Toolkit controllers work well all together. +The tests are written in Go, Bash, Make and Terraform. + +| Workflow | Jobs | Runner | Role | +|--------------------|----------------------|----------------|-----------------------------------------------| +| e2e.yaml | e2e-amd64-kubernetes | GitHub Ubuntu | integration testing with Kubernetes Kind
| +| e2e-arm64.yaml | e2e-arm64-kubernetes | Equinix Ubuntu | integration testing with Kubernetes Kind
| +| e2e-bootstrap.yaml | e2e-boostrap-github | GitHub Ubuntu | integration testing with GitHub API
| +| e2e-azure.yaml | e2e-amd64-aks | GitHub Ubuntu | integration testing with Azure API
| +| scan.yaml | scan-fossa | GitHub Ubuntu | license scanning
| +| scan.yaml | scan-snyk | GitHub Ubuntu | vulnerability scanning
| +| scan.yaml | scan-codeql | GitHub Ubuntu | vulnerability scanning
| + +## 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
| + +## 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
| +| release.yaml | release-flux-manifests | GitHub Ubuntu | build, push and sign the Flux install manifests
| diff --git a/.github/workflows/action.yaml b/.github/workflows/action.yaml new file mode 100644 index 00000000..9047fff3 --- /dev/null +++ b/.github/workflows/action.yaml @@ -0,0 +1,29 @@ +name: test-gh-action + +on: + pull_request: + paths: + - 'action/**' + push: + paths: + - 'action/**' + branches: + - 'main' + - 'release/**' + +permissions: read-all + +jobs: + actions: + strategy: + fail-fast: false + matrix: + version: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.version }} + name: action on ${{ matrix.version }} + steps: + - name: Checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup flux + uses: ./action diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml new file mode 100644 index 00000000..326293fd --- /dev/null +++ b/.github/workflows/backport.yaml @@ -0,0 +1,31 @@ +name: backport + +on: + pull_request_target: + types: [closed, labeled] + +jobs: + pull-request: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + if: github.event.pull_request.state == 'closed' && github.event.pull_request.merged && (github.event_name != 'labeled' || startsWith('backport:', github.event.label.name)) + steps: + - name: Checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Create backport PRs + uses: korthout/backport-action@bd410d37cdcae80be6d969823ff5a225fe5c833f # v3.0.2 + # xref: https://github.com/korthout/backport-action#inputs + with: + # Use token to allow workflows to be triggered for the created PR + github_token: ${{ secrets.BOT_GITHUB_TOKEN }} + # Match labels with a pattern `backport:` + 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}. diff --git a/.github/workflows/conformance.yaml b/.github/workflows/conformance.yaml new file mode 100644 index 00000000..c6f7a7b2 --- /dev/null +++ b/.github/workflows/conformance.yaml @@ -0,0 +1,267 @@ +name: conformance + +on: + workflow_dispatch: + push: + branches: [ 'main', 'update-components', 'release/**', 'conform*' ] + +permissions: + contents: read + +env: + GO_VERSION: 1.22.x + +jobs: + conform-kubernetes: + # Hosted on Equinix + # Docs: https://github.com/fluxcd/flux2/tree/main/.github/runners + runs-on: [self-hosted, Linux, ARM64, equinix] + strategy: + matrix: + # Keep this list up-to-date with https://endoflife.date/kubernetes + # Build images with https://github.com/fluxcd/flux-benchmark/actions/workflows/build-kind.yaml + KUBERNETES_VERSION: [ 1.28.9, 1.29.4, 1.30.0 ] + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup Go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: | + **/go.sum + **/go.mod + - name: Prepare + id: prep + run: | + ID=${GITHUB_SHA:0:7}-${{ matrix.KUBERNETES_VERSION }}-$(date +%s) + echo "CLUSTER=arm64-${ID}" >> $GITHUB_OUTPUT + - name: Build + run: | + make build + - name: Setup Kubernetes Kind + run: | + kind create cluster \ + --wait 5m \ + --name ${{ steps.prep.outputs.CLUSTER }} \ + --kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }} \ + --image=ghcr.io/fluxcd/kindest/node:v${{ matrix.KUBERNETES_VERSION }}-arm64 + - name: Run e2e tests + run: TEST_KUBECONFIG=/tmp/${{ steps.prep.outputs.CLUSTER }} make e2e + - name: Run multi-tenancy tests + env: + KUBECONFIG: /tmp/${{ steps.prep.outputs.CLUSTER }} + run: | + ./bin/flux install + ./bin/flux create source git flux-system \ + --interval=15m \ + --url=https://github.com/fluxcd/flux2-multi-tenancy \ + --branch=main \ + --ignore-paths="./clusters/**/flux-system/" + ./bin/flux create kustomization flux-system \ + --interval=15m \ + --source=flux-system \ + --path=./clusters/staging + kubectl -n flux-system wait kustomization/tenants --for=condition=ready --timeout=5m + kubectl -n apps wait kustomization/dev-team --for=condition=ready --timeout=1m + kubectl -n apps wait helmrelease/podinfo --for=condition=ready --timeout=1m + - name: Debug failure + if: failure() + env: + KUBECONFIG: /tmp/${{ steps.prep.outputs.CLUSTER }} + run: | + kubectl -n flux-system get all + kubectl -n flux-system describe po + kubectl -n flux-system logs deploy/source-controller + kubectl -n flux-system logs deploy/kustomize-controller + - name: Cleanup + if: always() + run: | + kind delete cluster --name ${{ steps.prep.outputs.CLUSTER }} + rm /tmp/${{ steps.prep.outputs.CLUSTER }} + + conform-k3s: + runs-on: ubuntu-latest + strategy: + matrix: + # Keep this list up-to-date with https://endoflife.date/kubernetes + # Available versions can be found with "replicated cluster versions" + K3S_VERSION: [ 1.28.7, 1.29.2 ] + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup Go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: | + **/go.sum + **/go.mod + - name: Prepare + id: prep + run: | + ID=${GITHUB_SHA:0:7}-${{ matrix.K3S_VERSION }}-$(date +%s) + PSEUDO_RAND_SUFFIX=$(echo "${ID}" | shasum | awk '{print $1}') + echo "cluster=flux2-k3s-${PSEUDO_RAND_SUFFIX}" >> $GITHUB_OUTPUT + KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml" + echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT + - name: Setup Kustomize + uses: fluxcd/pkg/actions/kustomize@main + - name: Build + run: make build-dev + - name: Create repository + run: | + gh repo create --private --add-readme fluxcd-testing/${{ steps.prep.outputs.cluster }} + env: + GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} + - name: Create cluster + id: create-cluster + uses: replicatedhq/compatibility-actions/create-cluster@v1 + with: + api-token: ${{ secrets.REPLICATED_API_TOKEN }} + kubernetes-distribution: "k3s" + kubernetes-version: ${{ matrix.K3S_VERSION }} + ttl: 20m + cluster-name: "${{ steps.prep.outputs.cluster }}" + kubeconfig-path: ${{ steps.prep.outputs.kubeconfig-path }} + export-kubeconfig: true + - name: Run e2e tests + run: TEST_KUBECONFIG=${{ steps.prep.outputs.kubeconfig-path }} make e2e + - name: Run flux bootstrap + run: | + ./bin/flux bootstrap git --manifests ./manifests/install/ \ + --components-extra=image-reflector-controller,image-automation-controller \ + --url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \ + --branch=main \ + --path=clusters/k3s \ + --token-auth + env: + GIT_PASSWORD: ${{ secrets.GITPROVIDER_BOT_TOKEN }} + - name: Run flux check + run: | + ./bin/flux check + - name: Run flux reconcile + run: | + ./bin/flux reconcile ks flux-system --with-source + ./bin/flux get all + ./bin/flux events + - name: Collect reconcile logs + if: ${{ always() }} + continue-on-error: true + run: | + kubectl -n flux-system get all + kubectl -n flux-system describe pods + kubectl -n flux-system logs deploy/source-controller + kubectl -n flux-system logs deploy/kustomize-controller + kubectl -n flux-system logs deploy/notification-controller + - name: Delete flux + run: | + ./bin/flux uninstall -s --keep-namespace + kubectl delete ns flux-system --wait + - name: Delete cluster + if: ${{ always() }} + uses: replicatedhq/replicated-actions/remove-cluster@v1 + continue-on-error: true + with: + api-token: ${{ secrets.REPLICATED_API_TOKEN }} + cluster-id: ${{ steps.create-cluster.outputs.cluster-id }} + - name: Delete repository + if: ${{ always() }} + continue-on-error: true + run: | + gh repo delete fluxcd-testing/${{ steps.prep.outputs.cluster }} --yes + env: + GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} + + conform-openshift: + runs-on: ubuntu-latest + strategy: + matrix: + # Keep this list up-to-date with https://endoflife.date/red-hat-openshift + OPENSHIFT_VERSION: [ 4.15.0-okd ] + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup Go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: | + **/go.sum + **/go.mod + - name: Prepare + id: prep + run: | + ID=${GITHUB_SHA:0:7}-${{ matrix.OPENSHIFT_VERSION }}-$(date +%s) + PSEUDO_RAND_SUFFIX=$(echo "${ID}" | shasum | awk '{print $1}') + echo "cluster=flux2-openshift-${PSEUDO_RAND_SUFFIX}" >> $GITHUB_OUTPUT + KUBECONFIG_PATH="$(git rev-parse --show-toplevel)/bin/kubeconfig.yaml" + echo "kubeconfig-path=${KUBECONFIG_PATH}" >> $GITHUB_OUTPUT + - name: Setup Kustomize + uses: fluxcd/pkg/actions/kustomize@main + - name: Build + run: make build-dev + - name: Create repository + run: | + gh repo create --private --add-readme fluxcd-testing/${{ steps.prep.outputs.cluster }} + env: + GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} + - name: Create cluster + id: create-cluster + uses: replicatedhq/compatibility-actions/create-cluster@v1 + with: + api-token: ${{ secrets.REPLICATED_API_TOKEN }} + kubernetes-distribution: "openshift" + kubernetes-version: ${{ matrix.OPENSHIFT_VERSION }} + ttl: 20m + cluster-name: "${{ steps.prep.outputs.cluster }}" + kubeconfig-path: ${{ steps.prep.outputs.kubeconfig-path }} + export-kubeconfig: true + - name: Run flux bootstrap + run: | + ./bin/flux bootstrap git --manifests ./manifests/openshift/ \ + --components-extra=image-reflector-controller,image-automation-controller \ + --url=https://github.com/fluxcd-testing/${{ steps.prep.outputs.cluster }} \ + --branch=main \ + --path=clusters/openshift \ + --token-auth + env: + GIT_PASSWORD: ${{ secrets.GITPROVIDER_BOT_TOKEN }} + - name: Run flux check + run: | + ./bin/flux check + - name: Run flux reconcile + run: | + ./bin/flux reconcile ks flux-system --with-source + ./bin/flux get all + ./bin/flux events + - name: Collect reconcile logs + if: ${{ always() }} + continue-on-error: true + run: | + kubectl -n flux-system get all + kubectl -n flux-system describe pods + kubectl -n flux-system logs deploy/source-controller + kubectl -n flux-system logs deploy/kustomize-controller + kubectl -n flux-system logs deploy/notification-controller + - name: Delete flux + run: | + ./bin/flux uninstall -s --keep-namespace + kubectl delete ns flux-system --wait + - name: Delete cluster + if: ${{ always() }} + uses: replicatedhq/replicated-actions/remove-cluster@v1 + continue-on-error: true + with: + api-token: ${{ secrets.REPLICATED_API_TOKEN }} + cluster-id: ${{ steps.create-cluster.outputs.cluster-id }} + - name: Delete repository + if: ${{ always() }} + continue-on-error: true + run: | + gh repo delete fluxcd-testing/${{ steps.prep.outputs.cluster }} --yes + env: + GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} diff --git a/.github/workflows/e2e-arm64.yaml b/.github/workflows/e2e-arm64.yaml deleted file mode 100644 index 5855e30b..00000000 --- a/.github/workflows/e2e-arm64.yaml +++ /dev/null @@ -1,37 +0,0 @@ -name: e2e-arm64 - -on: - workflow_dispatch: - push: - branches: [ main, update-components, equinix-runners ] - -jobs: - test: - # Hosted on Equinix - # Docs: https://github.com/fluxcd/flux2/tree/main/.github/runners - runs-on: [self-hosted, Linux, ARM64, equinix] - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup Go - uses: actions/setup-go@v2 - with: - go-version: 1.17.x - - name: Prepare - id: prep - run: | - echo ::set-output name=CLUSTER::arm64-${GITHUB_SHA:0:7}-$(date +%s) - echo ::set-output name=CONTEXT::kind-arm64-${GITHUB_SHA:0:7}-$(date +%s) - - name: Build - run: | - make build - - name: Setup Kubernetes Kind - run: | - kind create cluster --name ${{ steps.prep.outputs.CLUSTER }} --kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }} - - name: Run e2e tests - run: TEST_KUBECONFIG=/tmp/${{ steps.prep.outputs.CLUSTER }} make e2e - - name: Cleanup - if: always() - run: | - kind delete cluster --name ${{ steps.prep.outputs.CLUSTER }} - rm /tmp/${{ steps.prep.outputs.CLUSTER }} diff --git a/.github/workflows/e2e-azure.yaml b/.github/workflows/e2e-azure.yaml index 75abf7ef..d2764d64 100644 --- a/.github/workflows/e2e-azure.yaml +++ b/.github/workflows/e2e-azure.yaml @@ -3,64 +3,89 @@ name: e2e-azure on: workflow_dispatch: schedule: - - cron: '0 6 * * *' + - cron: '0 6 * * *' push: - branches: [ azure* ] + branches: + - main + paths: + - 'tests/**' + - '.github/workflows/e2e-azure.yaml' + pull_request: + branches: + - main + paths: + - 'tests/**' + - '.github/workflows/e2e-azure.yaml' + +permissions: + contents: read jobs: - e2e: - runs-on: ubuntu-latest + 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: Checkout - uses: actions/checkout@v2 - - name: Restore Go cache - uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go1.17-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go1.17- + - name: CheckoutD + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: - go-version: 1.17.x - - name: Install libgit2 - run: | - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 648ACFD622F3D138 - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 0E98404D386FA1D9 - echo "deb http://deb.debian.org/debian unstable main" | sudo tee -a /etc/apt/sources.list - echo "deb-src http://deb.debian.org/debian unstable main" | sudo tee -a /etc/apt/sources.list - sudo apt-get update - sudo apt-get install -y --allow-downgrades libgit2-dev/unstable zlib1g-dev/unstable libssh2-1-dev/unstable libpcre3-dev/unstable + go-version: 1.22.x + cache-dependency-path: tests/integration/go.sum - name: Setup Flux CLI - run: | - make build - mkdir -p $HOME/.local/bin - mv ./bin/flux $HOME/.local/bin + run: make build + working-directory: ./ - name: Setup SOPS run: | - wget https://github.com/mozilla/sops/releases/download/v3.7.1/sops-v3.7.1.linux - chmod +x sops-v3.7.1.linux mkdir -p $HOME/.local/bin - mv sops-v3.7.1.linux $HOME/.local/bin/sops - - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 + 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@6c251865b4e6290e7b78be643ea2d005bc51f69a # v1.4.6 with: - terraform_version: 1.0.7 - terraform_wrapper: false - - name: Setup Azure CLI + 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: | - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + cat > .env < 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 diff --git a/.github/workflows/bootstrap.yaml b/.github/workflows/e2e-bootstrap.yaml similarity index 54% rename from .github/workflows/bootstrap.yaml rename to .github/workflows/e2e-bootstrap.yaml index 90d8802d..052a552a 100644 --- a/.github/workflows/bootstrap.yaml +++ b/.github/workflows/e2e-bootstrap.yaml @@ -1,40 +1,45 @@ -name: bootstrap +name: e2e-bootstrap on: + workflow_dispatch: push: - branches: [ main ] + branches: [ 'main', 'release/**' ] pull_request: - branches: [ main ] + branches: [ 'main', 'release/**' ] + paths-ignore: [ 'docs/**', 'rfcs/**' ] + +permissions: + contents: read jobs: - github: + e2e-boostrap-github: runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' steps: - name: Checkout - uses: actions/checkout@v2 - - name: Restore Go cache - uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go1.17-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go1.17- + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: - go-version: 1.17.x + go-version: 1.22.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Setup Kubernetes - uses: engineerd/setup-kind@v0.5.0 + uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0 with: - version: v0.11.1 - image: kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6 + version: v0.22.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.30.0-amd64 + kubectl_version: v1.30.0 - name: Setup Kustomize - uses: fluxcd/pkg//actions/kustomize@main + uses: fluxcd/pkg/actions/kustomize@main + - name: Setup yq + uses: fluxcd/pkg/actions/yq@main - name: Build - run: | - make cmd/flux/.manifests.done - go build -o /tmp/flux ./cmd/flux + run: make build-dev - name: Set outputs id: vars run: | @@ -43,21 +48,27 @@ jobs: 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 "::set-output name=test_repo_name::$TEST_REPO_NAME" + echo "test_repo_name=$TEST_REPO_NAME" >> $GITHUB_OUTPUT - name: bootstrap init run: | - /tmp/flux bootstrap github --manifests ./manifests/install/ \ + ./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: | - /tmp/flux bootstrap github --manifests ./manifests/install/ \ + ./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 \ @@ -67,7 +78,7 @@ jobs: - name: bootstrap customize run: | make setup-bootstrap-patch - /tmp/flux bootstrap github --manifests ./manifests/install/ \ + ./bin/flux bootstrap github --manifests ./manifests/install/ \ --owner=fluxcd-testing \ --repository=${{ steps.vars.outputs.test_repo_name }} \ --branch=main \ @@ -80,56 +91,33 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }} GITHUB_ORG_NAME: fluxcd-testing - - name: libgit2 - run: | - /tmp/flux create source git test-libgit2 \ - --url=ssh://git@github.com/fluxcd-testing/${{ steps.vars.outputs.test_repo_name }} \ - --git-implementation=libgit2 \ - --secret-ref=flux-system \ - --branch=main - name: uninstall run: | - /tmp/flux uninstall -s --keep-namespace + ./bin/flux uninstall -s --keep-namespace kubectl delete ns flux-system --timeout=10m --wait=true - name: test image automation run: | make setup-image-automation - /tmp/flux bootstrap github --manifests ./manifests/install/ \ + ./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 - /tmp/flux reconcile image repository podinfo - /tmp/flux reconcile image update flux-system - /tmp/flux get images all - - retries=10 - count=0 - ok=false - until ${ok}; do - /tmp/flux get image update flux-system | grep 'commit' && ok=true || ok=false - count=$(($count + 1)) - if [[ ${count} -eq ${retries} ]]; then - echo "No more retries left" - exit 1 - fi - sleep 6 - /tmp/flux reconcile image update flux-system - done + ./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: | - curl \ - -X DELETE \ - -H "Accept: application/vnd.github.v3+json" \ - -H "Authorization: token ${GITHUB_TOKEN}" \ - --fail --silent \ - https://api.github.com/repos/fluxcd-testing/${{ steps.vars.outputs.test_repo_name }} + gh repo delete fluxcd-testing/${{ steps.vars.outputs.test_repo_name }} --yes env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} - name: Debug failure diff --git a/.github/workflows/e2e-gcp.yaml b/.github/workflows/e2e-gcp.yaml new file mode 100644 index 00000000..24a52b80 --- /dev/null +++ b/.github/workflows/e2e-gcp.yaml @@ -0,0 +1,102 @@ +name: e2e-gcp + +on: + workflow_dispatch: + schedule: + - cron: '0 6 * * *' + push: + branches: + - main + paths: + - 'tests/**' + - '.github/workflows/e2e-gcp.yaml' + pull_request: + branches: + - main + paths: + - 'tests/**' + - '.github/workflows/e2e-gcp.yaml' + +permissions: + contents: read + +jobs: + e2e-gcp: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: ./tests/integration + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' + steps: + - name: Checkout + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup Go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version: 1.22.x + cache-dependency-path: tests/integration/go.sum + - name: Setup Flux CLI + run: make build + working-directory: ./ + - name: Setup SOPS + run: | + mkdir -p $HOME/.local/bin + wget -O $HOME/.local/bin/sops https://github.com/mozilla/sops/releases/download/v$SOPS_VER/sops-v$SOPS_VER.linux + chmod +x $HOME/.local/bin/sops + env: + SOPS_VER: 3.7.1 + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@71fee32a0bb7e97b4d33d548e7d957010649d8fa # v2.1.3 + id: 'auth' + with: + credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}' + token_format: 'access_token' + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@98ddc00a17442e89a24bbf282954a3b65ce6d200 # v2.1.0 + - name: Setup QEMU + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 + - name: Log into us-central1-docker.pkg.dev + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + with: + registry: us-central1-docker.pkg.dev + username: oauth2accesstoken + password: ${{ steps.auth.outputs.access_token }} + - name: Set dynamic variables in .env + run: | + cat > .env < 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 diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 866af2c6..0905925f 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -1,40 +1,52 @@ name: e2e on: + workflow_dispatch: push: - branches: [ main, e2e* ] + branches: [ 'main', 'release/**' ] pull_request: - branches: [ main ] + branches: [ 'main', 'release/**' ] + paths-ignore: [ 'docs/**', 'rfcs/**' ] + +permissions: + contents: read jobs: - kind: - runs-on: ubuntu-latest + e2e-amd64-kubernetes: + runs-on: + group: "Default Larger Runners" + labels: ubuntu-latest-16-cores + services: + registry: + image: registry:2 + ports: + - 5000:5000 steps: - name: Checkout - uses: actions/checkout@v2 - - name: Restore Go cache - uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go1.17-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go1.17- + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: - go-version: 1.17.x + go-version: 1.22.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Setup Kubernetes - uses: engineerd/setup-kind@v0.5.0 + uses: helm/kind-action@0025e74a8c7512023d06dc019c617aa3cf561fde # v1.10.0 with: - version: v0.11.1 - image: kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729 + version: v0.22.0 + cluster_name: kind + 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.28.9-amd64 + kubectl_version: v1.28.9 - name: Setup Calico for network policy run: | - kubectl apply -f https://docs.projectcalico.org/v3.20/manifests/calico.yaml - kubectl -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true + kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/calico.yaml - name: Setup Kustomize - uses: fluxcd/pkg//actions/kustomize@main + uses: fluxcd/pkg/actions/kustomize@main - name: Run tests run: make test - name: Run e2e tests @@ -47,51 +59,43 @@ jobs: exit 1 fi - name: Build - run: | - go build -o /tmp/flux ./cmd/flux + run: make build-dev - name: flux check --pre run: | - /tmp/flux check --pre + ./bin/flux check --pre - name: flux install --manifests run: | - /tmp/flux install --manifests ./manifests/install/ + ./bin/flux install --manifests ./manifests/install/ - name: flux create secret run: | - /tmp/flux create secret git git-ssh-test \ + ./bin/flux create secret git git-ssh-test \ --url ssh://git@github.com/stefanprodan/podinfo - /tmp/flux create secret git git-https-test \ + ./bin/flux create secret git git-https-test \ --url https://github.com/stefanprodan/podinfo \ --username=test --password=test - /tmp/flux create secret helm helm-test \ + ./bin/flux create secret helm helm-test \ --username=test --password=test - name: flux create source git run: | - /tmp/flux create source git podinfo \ + ./bin/flux create source git podinfo \ --url https://github.com/stefanprodan/podinfo \ - --tag-semver=">=3.2.3" + --tag-semver=">=6.3.5" - name: flux create source git export apply run: | - /tmp/flux create source git podinfo-export \ + ./bin/flux create source git podinfo-export \ --url https://github.com/stefanprodan/podinfo \ - --tag-semver=">=3.2.3" \ + --tag-semver=">=6.3.5" \ --export | kubectl apply -f - - /tmp/flux delete source git podinfo-export --silent - - name: flux create source git libgit2 semver - run: | - /tmp/flux create source git podinfo-libgit2 \ - --url https://github.com/stefanprodan/podinfo \ - --tag-semver=">=3.2.3" \ - --git-implementation=libgit2 - /tmp/flux delete source git podinfo-libgit2 --silent + ./bin/flux delete source git podinfo-export --silent - name: flux get sources git run: | - /tmp/flux get sources git + ./bin/flux get sources git - name: flux get sources git --all-namespaces run: | - /tmp/flux get sources git --all-namespaces + ./bin/flux get sources git --all-namespaces - name: flux create kustomization run: | - /tmp/flux create kustomization podinfo \ + ./bin/flux create kustomization podinfo \ --source=podinfo \ --path="./deploy/overlays/dev" \ --prune=true \ @@ -101,106 +105,145 @@ jobs: --health-check-timeout=3m - name: flux trace run: | - /tmp/flux trace frontend \ + ./bin/flux trace frontend \ --kind=deployment \ --api-version=apps/v1 \ --namespace=dev - name: flux reconcile kustomization --with-source run: | - /tmp/flux reconcile kustomization podinfo --with-source + ./bin/flux reconcile kustomization podinfo --with-source - name: flux get kustomizations run: | - /tmp/flux get kustomizations + ./bin/flux get kustomizations - name: flux get kustomizations --all-namespaces run: | - /tmp/flux get kustomizations --all-namespaces + ./bin/flux get kustomizations --all-namespaces - name: flux suspend kustomization run: | - /tmp/flux suspend kustomization podinfo + ./bin/flux suspend kustomization podinfo - name: flux resume kustomization run: | - /tmp/flux resume kustomization podinfo + ./bin/flux resume kustomization podinfo - name: flux export run: | - /tmp/flux export source git --all - /tmp/flux export kustomization --all + ./bin/flux export source git --all + ./bin/flux export kustomization --all - name: flux delete kustomization run: | - /tmp/flux delete kustomization podinfo --silent + ./bin/flux delete kustomization podinfo --silent - name: flux create source helm run: | - /tmp/flux create source helm podinfo \ + ./bin/flux create source helm podinfo \ --url https://stefanprodan.github.io/podinfo - name: flux create helmrelease --source=HelmRepository/podinfo run: | - /tmp/flux create hr podinfo-helm \ + ./bin/flux create hr podinfo-helm \ --target-namespace=default \ --source=HelmRepository/podinfo.flux-system \ --chart=podinfo \ - --chart-version=">4.0.0 <5.0.0" + --chart-version=">6.0.0 <7.0.0" - name: flux create helmrelease --source=GitRepository/podinfo run: | - /tmp/flux create hr podinfo-git \ + ./bin/flux create hr podinfo-git \ --target-namespace=default \ --source=GitRepository/podinfo \ --chart=./charts/podinfo - name: flux reconcile helmrelease --with-source run: | - /tmp/flux reconcile helmrelease podinfo-git --with-source + ./bin/flux reconcile helmrelease podinfo-git --with-source - name: flux get helmreleases run: | - /tmp/flux get helmreleases + ./bin/flux get helmreleases - name: flux get helmreleases --all-namespaces run: | - /tmp/flux get helmreleases --all-namespaces + ./bin/flux get helmreleases --all-namespaces - name: flux export helmrelease run: | - /tmp/flux export hr --all + ./bin/flux export hr --all - name: flux delete helmrelease podinfo-helm run: | - /tmp/flux delete hr podinfo-helm --silent + ./bin/flux delete hr podinfo-helm --silent - name: flux delete helmrelease podinfo-git run: | - /tmp/flux delete hr podinfo-git --silent + ./bin/flux delete hr podinfo-git --silent - name: flux delete source helm run: | - /tmp/flux delete source helm podinfo --silent + ./bin/flux delete source helm podinfo --silent - name: flux delete source git run: | - /tmp/flux delete source git podinfo --silent + ./bin/flux delete source git podinfo --silent + - name: flux oci artifacts + run: | + ./bin/flux push artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ + --path="./manifests" \ + --source="${{ github.repositoryUrl }}" \ + --revision="${{ github.ref }}@sha1:${{ github.sha }}" + ./bin/flux tag artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ + --tag latest + ./bin/flux list artifacts oci://localhost:5000/fluxcd/flux + - name: flux oci repositories + run: | + ./bin/flux create source oci podinfo-oci \ + --url oci://ghcr.io/stefanprodan/manifests/podinfo \ + --tag-semver 6.3.x \ + --interval 10m + ./bin/flux create kustomization podinfo-oci \ + --source=OCIRepository/podinfo-oci \ + --path="./" \ + --prune=true \ + --interval=5m \ + --target-namespace=default \ + --wait=true \ + --health-check-timeout=3m + ./bin/flux reconcile source oci podinfo-oci + ./bin/flux suspend source oci podinfo-oci + ./bin/flux get sources oci + ./bin/flux resume source oci podinfo-oci + ./bin/flux export source oci podinfo-oci + ./bin/flux delete ks podinfo-oci --silent + ./bin/flux delete source oci podinfo-oci --silent - name: flux create tenant run: | - /tmp/flux create tenant dev-team --with-namespace=apps - /tmp/flux -n apps create source helm podinfo \ + ./bin/flux create tenant dev-team --with-namespace=apps + ./bin/flux -n apps create source helm podinfo \ --url https://stefanprodan.github.io/podinfo - /tmp/flux -n apps create hr podinfo-helm \ + ./bin/flux -n apps create hr podinfo-helm \ --source=HelmRepository/podinfo \ --chart=podinfo \ - --chart-version="5.0.x" \ + --chart-version="6.3.x" \ --service-account=dev-team - name: flux2-kustomize-helm-example run: | - /tmp/flux create source git flux-system \ + ./bin/flux create source git flux-system \ --url=https://github.com/fluxcd/flux2-kustomize-helm-example \ --branch=main \ + --ignore-paths="./clusters/**/flux-system/" \ --recurse-submodules - /tmp/flux create kustomization flux-system \ + ./bin/flux create kustomization flux-system \ --source=flux-system \ --path=./clusters/staging - kubectl -n flux-system wait kustomization/infrastructure --for=condition=ready --timeout=5m + kubectl -n flux-system wait kustomization/infra-controllers --for=condition=ready --timeout=5m kubectl -n flux-system wait kustomization/apps --for=condition=ready --timeout=5m - kubectl -n nginx wait helmrelease/nginx --for=condition=ready --timeout=5m - kubectl -n redis wait helmrelease/redis --for=condition=ready --timeout=5m kubectl -n podinfo wait helmrelease/podinfo --for=condition=ready --timeout=5m - name: flux tree run: | - /tmp/flux tree kustomization flux-system | grep Service/podinfo + ./bin/flux tree kustomization flux-system | grep Service/podinfo + - name: flux events + run: | + ./bin/flux -n flux-system events --for Kustomization/apps | grep 'HelmRelease/podinfo' + ./bin/flux -n podinfo events --for HelmRelease/podinfo | grep 'podinfo.v1' + - name: flux stats + run: | + ./bin/flux stats -A - name: flux check run: | - /tmp/flux check + ./bin/flux check + - name: flux version + run: | + ./bin/flux version - name: flux uninstall run: | - /tmp/flux uninstall --silent + ./bin/flux uninstall --silent - name: Debug failure if: failure() run: | diff --git a/.github/workflows/ossf.yaml b/.github/workflows/ossf.yaml new file mode 100644 index 00000000..348ef7eb --- /dev/null +++ b/.github/workflows/ossf.yaml @@ -0,0 +1,39 @@ +name: ossf +on: + workflow_dispatch: + push: + branches: + - main + schedule: + # Weekly on Saturdays. + - cron: '30 1 * * 6' + +permissions: read-all + +jobs: + scorecard: + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + actions: read + contents: read + steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Run analysis + uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3 + with: + results_file: results.sarif + results_format: sarif + repo_token: ${{ secrets.GITHUB_TOKEN }} + publish_results: true + - name: Upload artifact + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + - name: Upload SARIF results + uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 + with: + sarif_file: results.sarif diff --git a/.github/workflows/rebase.yaml b/.github/workflows/rebase.yaml deleted file mode 100644 index 623b0fbe..00000000 --- a/.github/workflows/rebase.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: rebase - -on: - pull_request: - types: [ opened ] - issue_comment: - types: [ created ] - -jobs: - rebase: - if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') && (github.event.comment.author_association == 'CONTRIBUTOR' || github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') - runs-on: ubuntu-latest - steps: - - name: Checkout the latest code - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Automatic Rebase - uses: cirrus-actions/rebase@1.3.1 - env: - GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index bd49f177..32bb188b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -5,41 +5,48 @@ on: tags: [ 'v*' ] permissions: - contents: write # needed to write releases - id-token: write # needed for keyless signing - packages: write # needed for ghcr access + contents: read jobs: - goreleaser: + release-flux-cli: + outputs: + hashes: ${{ steps.slsa.outputs.hashes }} + image_url: ${{ steps.slsa.outputs.image_url }} + image_digest: ${{ steps.slsa.outputs.image_digest }} runs-on: ubuntu-latest + permissions: + contents: write # needed to write releases + id-token: write # needed for keyless signing + packages: write # needed for ghcr access steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Unshallow run: git fetch --prune --unshallow - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: - go-version: 1.17.x + go-version: 1.22.x + cache: false - name: Setup QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Setup Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@d70bba72b1f3fd22344832f00baa16ece964efeb # v3.3.0 - name: Setup Syft - uses: anchore/sbom-action/download-syft@v0 + uses: anchore/sbom-action/download-syft@e8d2a6937ecead383dfe75190d104edd1f9c5751 # v0.16.0 - name: Setup Cosign - uses: sigstore/cosign-installer@main + uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 - name: Setup Kustomize - uses: fluxcd/pkg//actions/kustomize@main + uses: fluxcd/pkg/actions/kustomize@main - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 with: registry: ghcr.io username: fluxcdbot password: ${{ secrets.GHCR_TOKEN }} - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 with: username: fluxcdbot password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} @@ -51,10 +58,8 @@ jobs: - name: Build CRDs run: | kustomize build manifests/crds > all-crds.yaml - # Pinned to commit before https://github.com/fluxcd/pkg/pull/189 due to - # introduction faulty behavior. - name: Generate OpenAPI JSON schemas from CRDs - uses: fluxcd/pkg//actions/crdjsonschema@49e26aa2ee9e734c3233c560253fd9542afe18ae + uses: fluxcd/pkg/actions/crdjsonschema@main with: crd: all-crds.yaml output: schemas @@ -73,11 +78,134 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v1 + id: run-goreleaser + uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 with: version: latest - args: release --release-notes=output/notes.md --skip-validate + args: release --release-notes=output/notes.md --skip=validate env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} AUR_BOT_SSH_PRIVATE_KEY: ${{ secrets.AUR_BOT_SSH_PRIVATE_KEY }} + - name: Generate SLSA metadata + id: slsa + env: + ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}" + run: | + set -euo pipefail + + hashes=$(echo -E $ARTIFACTS | jq --raw-output '.[] | {name, "digest": (.extra.Digest // .extra.Checksum)} | select(.digest) | {digest} + {name} | join(" ") | sub("^sha256:";"")' | base64 -w0) + echo "hashes=$hashes" >> $GITHUB_OUTPUT + + image_url=fluxcd/flux-cli:$GITHUB_REF_NAME + echo "image_url=$image_url" >> $GITHUB_OUTPUT + + image_digest=$(docker buildx imagetools inspect ${image_url} --format '{{json .}}' | jq -r .manifest.digest) + echo "image_digest=$image_digest" >> $GITHUB_OUTPUT + + release-flux-manifests: + runs-on: ubuntu-latest + needs: release-flux-cli + permissions: + id-token: write + packages: write + steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup Kustomize + uses: fluxcd/pkg/actions/kustomize@main + - name: Setup Flux CLI + uses: ./action/ + - name: Prepare + id: prep + run: | + VERSION=$(flux version --client | awk '{ print $NF }') + echo "version=${VERSION}" >> $GITHUB_OUTPUT + - name: Login to GHCR + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + with: + registry: ghcr.io + username: fluxcdbot + password: ${{ secrets.GHCR_TOKEN }} + - name: Login to DockerHub + uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0 + with: + username: fluxcdbot + password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} + - name: Push manifests to GHCR + run: | + mkdir -p ./ghcr.io/flux-system + flux install --registry=ghcr.io/fluxcd \ + --components-extra=image-reflector-controller,image-automation-controller \ + --export > ./ghcr.io/flux-system/gotk-components.yaml + + cd ./ghcr.io && flux push artifact \ + oci://ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ + --path="./flux-system" \ + --source=${{ github.repositoryUrl }} \ + --revision="${{ github.ref_name }}@sha1:${{ github.sha }}" + - name: Push manifests to DockerHub + run: | + mkdir -p ./docker.io/flux-system + flux install --registry=docker.io/fluxcd \ + --components-extra=image-reflector-controller,image-automation-controller \ + --export > ./docker.io/flux-system/gotk-components.yaml + + cd ./docker.io && flux push artifact \ + oci://docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ + --path="./flux-system" \ + --source=${{ github.repositoryUrl }} \ + --revision="${{ github.ref_name }}@sha1:${{ github.sha }}" + - uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0 + - name: Sign manifests + env: + COSIGN_EXPERIMENTAL: 1 + run: | + cosign sign --yes ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} + cosign sign --yes docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} + - name: Tag manifests + run: | + flux tag artifact oci://ghcr.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ + --tag latest + + flux tag artifact oci://docker.io/fluxcd/flux-manifests:${{ steps.prep.outputs.version }} \ + --tag latest + + release-provenance: + needs: [release-flux-cli] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OIDC tokens for signing. + contents: write # for uploading attestations to GitHub releases. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0 + with: + provenance-name: "provenance.intoto.jsonl" + base64-subjects: "${{ needs.release-flux-cli.outputs.hashes }}" + upload-assets: true + + dockerhub-provenance: + needs: [release-flux-cli] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OIDC tokens for signing. + packages: write # for uploading attestations. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 + with: + image: ${{ needs.release-flux-cli.outputs.image_url }} + digest: ${{ needs.release-flux-cli.outputs.image_digest }} + registry-username: fluxcdbot + secrets: + registry-password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} + + ghcr-provenance: + needs: [release-flux-cli] + permissions: + actions: read # for detecting the Github Actions environment. + id-token: write # for creating OIDC tokens for signing. + packages: write # for uploading attestations. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0 + with: + image: ghcr.io/${{ needs.release-flux-cli.outputs.image_url }} + digest: ${{ needs.release-flux-cli.outputs.image_digest }} + registry-username: fluxcdbot + secrets: + registry-password: ${{ secrets.GHCR_TOKEN }} diff --git a/.github/workflows/scan.yaml b/.github/workflows/scan.yaml index 9e1cc45b..466883cd 100644 --- a/.github/workflows/scan.yaml +++ b/.github/workflows/scan.yaml @@ -1,60 +1,86 @@ -name: Scan +name: scan on: + workflow_dispatch: push: - branches: [ main ] + branches: [ 'main', 'release/**' ] pull_request: - branches: [ main ] + branches: [ 'main', 'release/**' ] schedule: - cron: '18 10 * * 3' +permissions: + contents: read + jobs: - fossa: - name: FOSSA + scan-fossa: runs-on: ubuntu-latest + if: github.actor != 'dependabot[bot]' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Run FOSSA scan and upload build data - uses: fossa-contrib/fossa-action@v1 + uses: fossa-contrib/fossa-action@cdc5065bcdee31a32e47d4585df72d66e8e941c2 # v3.0.0 with: # FOSSA Push-Only API Token fossa-api-key: 5ee8bf422db1471e0bcf2bcb289185de github-token: ${{ github.token }} - snyk: - name: Snyk + scan-snyk: runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + permissions: + security-events: write + if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Kustomize - uses: fluxcd/pkg//actions/kustomize@main - - name: Build manifests + uses: fluxcd/pkg/actions/kustomize@main + - name: Setup Go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: 'go.mod' + cache-dependency-path: | + **/go.sum + **/go.mod + - name: Download modules and build manifests run: | + make tidy make cmd/flux/.manifests.done - - name: Run Snyk to check for vulnerabilities - uses: snyk/actions/golang@master + - 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 }} - with: - args: --sarif-file-output=snyk.sarif - name: Upload result to GitHub Code Scanning - uses: github/codeql-action/upload-sarif@v1 + continue-on-error: true + uses: github/codeql-action/upload-sarif@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 with: sarif_file: snyk.sarif - codeql: - name: CodeQL + scan-codeql: runs-on: ubuntu-latest + permissions: + security-events: write + if: github.actor != 'dependabot[bot]' steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - name: Setup Go + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 + with: + go-version-file: 'go.mod' + cache-dependency-path: | + **/go.sum + **/go.mod - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 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@v1 + uses: github/codeql-action/autobuild@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@2e230e8fe0ad3a14a340ad0815ddb96d599d2aff # v3.25.8 diff --git a/.github/workflows/sync-labels.yaml b/.github/workflows/sync-labels.yaml new file mode 100644 index 00000000..8276a782 --- /dev/null +++ b/.github/workflows/sync-labels.yaml @@ -0,0 +1,28 @@ +name: sync-labels +on: + workflow_dispatch: + push: + branches: + - main + paths: + - .github/labels.yaml + +permissions: + contents: read + +jobs: + labels: + name: Run sync + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + - uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2.3.3 + with: + # Configuration file + config-file: | + https://raw.githubusercontent.com/fluxcd/community/main/.github/standard-labels.yaml + .github/labels.yaml + # Strictly declarative + delete-other-labels: true diff --git a/.github/workflows/update.yaml b/.github/workflows/update.yaml index bb815fdd..a0efca24 100644 --- a/.github/workflows/update.yaml +++ b/.github/workflows/update.yaml @@ -1,4 +1,4 @@ -name: Update Components +name: update on: workflow_dispatch: @@ -7,20 +7,29 @@ on: 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@v2 + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1 with: - go-version: 1.17.x + go-version: 1.22.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Update component versions id: update run: | - PR_BODY="" + 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') @@ -42,13 +51,13 @@ jobs: if [[ "${MOD_VERSION}" != "${LATEST_VERSION}" ]]; then go mod edit -require="github.com/fluxcd/$1/api@${LATEST_VERSION}" - rm go.sum - go mod tidy + make tidy changed=true fi if [[ "$changed" == true ]]; then - PR_BODY="$PR_BODY- $1 to ${LATEST_VERSION}%0A https://github.com/fluxcd/$1/blob/${LATEST_VERSION}/CHANGELOG.md%0A" + echo "- $1 to ${LATEST_VERSION}" >> $PR_BODY + echo " https://github.com/fluxcd/$1/blob/${LATEST_VERSION}/CHANGELOG.md" >> $PR_BODY fi } @@ -65,12 +74,17 @@ jobs: git diff # export PR_BODY for PR and commit - echo "::set-output name=pr_body::$PR_BODY" + # NB: this may look strange but it is the way it should be done to + # maintain our precious newlines + # Ref: https://github.com/github/docs/issues/21529 + echo 'pr_body<> $GITHUB_OUTPUT + cat $PR_BODY >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT } - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@v3 + uses: peter-evans/create-pull-request@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5 with: token: ${{ secrets.BOT_GITHUB_TOKEN }} commit-message: | @@ -85,7 +99,7 @@ jobs: body: | ${{ steps.update.outputs.pr_body }} labels: | - area/build + dependencies reviewers: ${{ secrets.ASSIGNEES }} - name: Check output diff --git a/.goreleaser.yml b/.goreleaser.yml index 59f4aff9..c2734b55 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -15,7 +15,7 @@ builds: - arm64 - arm goarm: - - 7 + - "7" - <<: *build_defaults id: darwin goos: @@ -65,6 +65,7 @@ signs: certificate: '${artifact}.pem' args: - sign-blob + - "--yes" - '--output-certificate=${certificate}' - '--output-signature=${signature}' - '${artifact}' @@ -72,24 +73,17 @@ signs: output: true brews: - name: flux - tap: + repository: owner: fluxcd name: homebrew-tap token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" - folder: Formula + directory: Formula homepage: "https://fluxcd.io/" description: "Flux CLI" install: | bin.install "flux" - bash_output = Utils.safe_popen_read(bin/"flux", "completion", "bash") - (bash_completion/"flux").write bash_output - - zsh_output = Utils.safe_popen_read(bin/"flux", "completion", "zsh") - (zsh_completion/"_flux").write zsh_output - - fish_output = Utils.safe_popen_read(bin/"flux", "completion", "fish") - (fish_completion/"flux.fish").write fish_output + generate_completions_from_executable(bin/"flux", "completion") test: | system "#{bin}/flux --version" publishers: @@ -175,6 +169,7 @@ docker_signs: - COSIGN_EXPERIMENTAL=1 args: - sign + - "--yes" - '${artifact}' artifacts: all output: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c492f631..84b54ef2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -59,7 +59,7 @@ This project is composed of: ### Understanding the code To get started with developing controllers, you might want to review -[our guide](https://fluxcd.io/docs/gitops-toolkit/source-watcher/) which +[our guide](https://fluxcd.io/flux/gitops-toolkit/source-watcher/) which walks you through writing a short and concise controller that watches out for source changes. @@ -67,9 +67,10 @@ for source changes. Prerequisites: -* go >= 1.16 -* kubectl >= 1.19 -* kustomize >= 4.0 +* 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: @@ -96,6 +97,25 @@ Then you can run the end-to-end tests with: 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 diff --git a/Dockerfile b/Dockerfile index ffc5c945..c918f4b0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,20 @@ -FROM alpine:3.15 as builder +FROM alpine:3.19 as builder RUN apk add --no-cache ca-certificates curl ARG ARCH=linux/amd64 -ARG KUBECTL_VER=1.23.1 +ARG KUBECTL_VER=1.30.0 RUN curl -sL https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VER}/bin/${ARCH}/kubectl \ -o /usr/local/bin/kubectl && chmod +x /usr/local/bin/kubectl && \ kubectl version --client=true -FROM alpine:3.15 as flux-cli - -# Create minimal nsswitch.conf file to prioritize the usage of /etc/hosts over DNS queries. -# https://github.com/gliderlabs/docker-alpine/issues/367#issuecomment-354316460 -RUN [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf +FROM alpine:3.19 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" ] diff --git a/MAINTAINERS b/MAINTAINERS index bd9d47ab..03d9f7ba 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2,19 +2,7 @@ The maintainers are generally available in Slack at https://cloud-native.slack.com in #flux (https://cloud-native.slack.com/messages/CLAJ40HV3) (obtain an invitation at https://slack.cncf.io/). -These maintainers are shared with other Flux v2-related git -repositories under https://github.com/fluxcd, as noted in their -respective MAINTAINERS files. +The Flux2 maintainers team is identical with the core maintainers of the project +as listed in -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, NexHealth (github: @relu, slack: relu) -Hidde Beydals, Weaveworks (github: @hiddeco, slack: hidde) -Max Jonas Werner, D2iQ (github: @makkes, slack: max) -Philip Laine, Xenit (github: @phillebaba, slack: phillebaba) -Stefan Prodan, Weaveworks (github: @stefanprodan, slack: stefanprodan) -Sunny, Weaveworks (github: @darkowlzz, slack: darkowlzz) + https://github.com/fluxcd/community/blob/main/CORE-MAINTAINERS diff --git a/Makefile b/Makefile index 29ca788a..787d369f 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ VERSION?=$(shell grep 'VERSION' cmd/flux/main.go | awk '{ print $$4 }' | head -n 1 | tr -d '"') +DEV_VERSION?=0.0.0-$(shell git rev-parse --abbrev-ref HEAD)-$(shell git rev-parse --short HEAD)-$(shell date +%s) EMBEDDED_MANIFESTS_TARGET=cmd/flux/.manifests.done TEST_KUBECONFIG?=/tmp/flux-e2e-test-kubeconfig # Architecture to use envtest with @@ -16,8 +17,8 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2 all: test build tidy: - go mod tidy - cd tests/azure && go mod tidy + go mod tidy -compat=1.22 + cd tests/integration && go mod tidy -compat=1.22 fmt: go fmt ./... @@ -35,11 +36,13 @@ cleanup-kind: 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 ./... -coverprofile cover.out --tags=unit + KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" go test $(TEST_PKG_PATH) -coverprofile cover.out --tags=unit $(TEST_ARGS) +E2E_TEST_PKG_PATH="./cmd/flux/..." e2e: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet - TEST_KUBECONFIG=$(TEST_KUBECONFIG) go test ./cmd/flux/... -coverprofile e2e.cover.out --tags=e2e -v -failfast + TEST_KUBECONFIG=$(TEST_KUBECONFIG) go test $(E2E_TEST_PKG_PATH) -coverprofile e2e.cover.out --tags=e2e -v -failfast $(TEST_ARGS) test-with-kind: install-envtest make setup-kind @@ -53,6 +56,9 @@ $(EMBEDDED_MANIFESTS_TARGET): $(call rwildcard,manifests/,*.yaml *.json) build: $(EMBEDDED_MANIFESTS_TARGET) CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(VERSION)" -o ./bin/flux ./cmd/flux +build-dev: $(EMBEDDED_MANIFESTS_TARGET) + CGO_ENABLED=0 go build -ldflags="-s -w -X main.VERSION=$(DEV_VERSION)" -o ./bin/flux ./cmd/flux + .PHONY: install install: CGO_ENABLED=0 go install ./cmd/flux diff --git a/README.md b/README.md index cc96ed4c..7eb5deea 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # Flux version 2 -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4782/badge)](https://bestpractices.coreinfrastructure.org/projects/4782) -[![e2e](https://github.com/fluxcd/flux2/workflows/e2e/badge.svg)](https://github.com/fluxcd/flux2/actions) -[![report](https://goreportcard.com/badge/github.com/fluxcd/flux2)](https://goreportcard.com/report/github.com/fluxcd/flux2) -[![license](https://img.shields.io/github/license/fluxcd/flux2.svg)](https://github.com/fluxcd/flux2/blob/main/LICENSE) [![release](https://img.shields.io/github/release/fluxcd/flux2/all.svg)](https://github.com/fluxcd/flux2/releases) +[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4782/badge)](https://bestpractices.coreinfrastructure.org/projects/4782) +[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2/badge)](https://api.securityscorecards.dev/projects/github.com/fluxcd/flux2) +[![FOSSA Status](https://app.fossa.com/api/projects/custom%2B162%2Fgithub.com%2Ffluxcd%2Fflux2.svg?type=shield)](https://app.fossa.com/projects/custom%2B162%2Fgithub.com%2Ffluxcd%2Fflux2?ref=badge_shield) +[![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/flux2)](https://artifacthub.io/packages/helm/fluxcd-community/flux2) +[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://fluxcd.io/flux/security/slsa-assessment) Flux is a tool for keeping Kubernetes clusters in sync with sources of -configuration (like Git repositories), and automating updates to -configuration when there is new code to deploy. +configuration (like Git repositories and OCI artifacts), +and automating updates to configuration when there is new code to deploy. Flux version 2 ("v2") is built from the ground up to use Kubernetes' API extension system, and to integrate with Prometheus and other core @@ -20,18 +21,19 @@ Flux v2 is constructed with the [GitOps Toolkit](#gitops-toolkit), a set of composable APIs and specialized tools for building Continuous Delivery on top of Kubernetes. -Flux is a Cloud Native Computing Foundation ([CNCF](https://www.cncf.io/)) project. +Flux is a Cloud Native Computing Foundation ([CNCF](https://www.cncf.io/)) graduated project, used in +production by various [organisations](https://fluxcd.io/adopters) and [cloud providers](https://fluxcd.io/ecosystem). ## Quickstart and documentation -To get started check out this [guide](https://fluxcd.io/docs/get-started/) +To get started check out this [guide](https://fluxcd.io/flux/get-started/) on how to bootstrap Flux on Kubernetes and deploy a sample application in a GitOps manner. For more comprehensive documentation, see the following guides: -- [Ways of structuring your repositories](https://fluxcd.io/docs/guides/repository-structure/) -- [Manage Helm Releases](https://fluxcd.io/docs/guides/helmreleases/) -- [Automate image updates to Git](https://fluxcd.io/docs/guides/image-update/) -- [Manage Kubernetes secrets with Mozilla SOPS](https://fluxcd.io/docs/guides/mozilla-sops/) +- [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/)**. @@ -42,50 +44,52 @@ runtime for Flux v2. The APIs comprise Kubernetes custom resources, which can be created and updated by a cluster user, or by other automation tooling. -![overview](docs/_files/gitops-toolkit.png) +![overview](https://raw.githubusercontent.com/fluxcd/flux2/main/docs/diagrams/fluxcd-controllers.png) You can use the toolkit to extend Flux, or to build your own systems for continuous delivery -- see [the developer -guides](https://fluxcd.io/docs/gitops-toolkit/source-watcher/). +guides](https://fluxcd.io/flux/gitops-toolkit/source-watcher/). ### Components -- [Source Controller](https://fluxcd.io/docs/components/source/) - - [GitRepository CRD](https://fluxcd.io/docs/components/source/gitrepositories/) - - [HelmRepository CRD](https://fluxcd.io/docs/components/source/helmrepositories/) - - [HelmChart CRD](https://fluxcd.io/docs/components/source/helmcharts/) - - [Bucket CRD](https://fluxcd.io/docs/components/source/buckets/) -- [Kustomize Controller](https://fluxcd.io/docs/components/kustomize/) - - [Kustomization CRD](https://fluxcd.io/docs/components/kustomize/kustomization/) -- [Helm Controller](https://fluxcd.io/docs/components/helm/) - - [HelmRelease CRD](https://fluxcd.io/docs/components/helm/helmreleases/) -- [Notification Controller](https://fluxcd.io/docs/components/notification/) - - [Provider CRD](https://fluxcd.io/docs/components/notification/provider/) - - [Alert CRD](https://fluxcd.io/docs/components/notification/alert/) - - [Receiver CRD](https://fluxcd.io/docs/components/notification/receiver/) -- [Image Automation Controllers](https://fluxcd.io/docs/components/image/) - - [ImageRepository CRD](https://fluxcd.io/docs/components/image/imagerepositories/) - - [ImagePolicy CRD](https://fluxcd.io/docs/components/image/imagepolicies/) - - [ImageUpdateAutomation CRD](https://fluxcd.io/docs/components/image/imageupdateautomations/) - +- [Source Controller](https://fluxcd.io/flux/components/source/) + - [GitRepository CRD](https://fluxcd.io/flux/components/source/gitrepositories/) + - [OCIRepository CRD](https://fluxcd.io/flux/components/source/ocirepositories/) + - [HelmRepository CRD](https://fluxcd.io/flux/components/source/helmrepositories/) + - [HelmChart CRD](https://fluxcd.io/flux/components/source/helmcharts/) + - [Bucket CRD](https://fluxcd.io/flux/components/source/buckets/) +- [Kustomize Controller](https://fluxcd.io/flux/components/kustomize/) + - [Kustomization CRD](https://fluxcd.io/flux/components/kustomize/kustomizations/) +- [Helm Controller](https://fluxcd.io/flux/components/helm/) + - [HelmRelease CRD](https://fluxcd.io/flux/components/helm/helmreleases/) +- [Notification Controller](https://fluxcd.io/flux/components/notification/) + - [Provider CRD](https://fluxcd.io/flux/components/notification/providers/) + - [Alert CRD](https://fluxcd.io/flux/components/notification/alerts/) + - [Receiver CRD](https://fluxcd.io/flux/components/notification/receivers/) +- [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 Need help or want to contribute? Please see the links below. The Flux project is always looking for new contributors and there are a multitude of ways to get involved. - Getting Started? - - Look at our [Get Started guide](https://fluxcd.io/docs/get-started/) and give us feedback + - Look at our [Get Started guide](https://fluxcd.io/flux/get-started/) and give us feedback - Need help? - - First: Ask questions on our [GH Discussions page](https://github.com/fluxcd/flux2/discussions) - - Second: Talk to us in the #flux channel on [CNCF Slack](https://slack.cncf.io/) + - First: Ask questions on our [GH Discussions page](https://github.com/fluxcd/flux2/discussions). + - Second: Talk to us in the #flux channel on [CNCF Slack](https://slack.cncf.io/). - Please follow our [Support Guidelines](https://fluxcd.io/support/) (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). - Have feature proposals or want to contribute? - - Propose features on our [GH Discussions page](https://github.com/fluxcd/flux2/discussions) - - Join our upcoming dev meetings ([meeting access and agenda](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)) + - Propose features on our [GitHub Discussions page](https://github.com/fluxcd/flux2/discussions). + - Join our upcoming dev meetings ([meeting access and agenda](https://docs.google.com/document/d/1l_M0om0qUEN_NNiGgpqJ2tvsF2iioHkaARDeh6b70B0/view)). - [Join the flux-dev mailing list](https://lists.cncf.io/g/cncf-flux-dev). - - Check out [how to contribute](CONTRIBUTING.md) to the project + - Check out [how to contribute](CONTRIBUTING.md) to the project. + - Check out the [project roadmap](https://fluxcd.io/roadmap/). ### Events diff --git a/action/README.md b/action/README.md index 7a432d6b..d511dc9c 100644 --- a/action/README.md +++ b/action/README.md @@ -1,104 +1,22 @@ # Flux GitHub Action -Usage: +To install the latest Flux CLI on Linux, macOS or Windows GitHub runners: ```yaml - steps: - - name: Setup Flux CLI - uses: fluxcd/flux2/action@main - - name: Run Flux commands - run: flux -v +steps: + - name: Setup Flux CLI + uses: fluxcd/flux2/action@main + with: + version: 'latest' + - name: Run Flux CLI + run: flux version --client ``` -The latest stable version of the `flux` binary is downloaded from -GitHub [releases](https://github.com/fluxcd/flux2/releases) -and placed at `/usr/local/bin/flux`. +The Flux GitHub Action can be used to automate various tasks in CI, such as: -Note that this action can only be used on GitHub **Linux** runners. -You can change the arch (defaults to `amd64`) with: +- [Automate Flux upgrades on clusters via Pull Requests](https://fluxcd.io/flux/flux-gh-action/#automate-flux-updates) +- [Push Kubernetes manifests to container registries](https://fluxcd.io/flux/flux-gh-action/#push-kubernetes-manifests-to-container-registries) +- [Run end-to-end testing with Flux and Kubernetes Kind](https://fluxcd.io/flux/flux-gh-action/#end-to-end-testing) -```yaml - steps: - - name: Setup Flux CLI - uses: fluxcd/flux2/action@main - with: - arch: arm64 # can be amd64, arm64 or arm -``` - -You can download a specific version with: - -```yaml - steps: - - name: Setup Flux CLI - uses: fluxcd/flux2/action@main - with: - version: 0.8.0 -``` - -### Automate Flux updates - -Example workflow for updating Flux's components generated with `flux bootstrap --path=clusters/production`: - -```yaml -name: update-flux - -on: - workflow_dispatch: - schedule: - - cron: "0 * * * *" - -jobs: - components: - runs-on: ubuntu-latest - steps: - - name: Check out code - uses: actions/checkout@v2 - - name: Setup Flux CLI - uses: fluxcd/flux2/action@main - - name: Check for updates - id: update - run: | - flux install \ - --export > ./clusters/production/flux-system/gotk-components.yaml - - VERSION="$(flux -v)" - echo "::set-output name=flux_version::$VERSION" - - name: Create Pull Request - uses: peter-evans/create-pull-request@v3 - with: - token: ${{ secrets.GITHUB_TOKEN }} - branch: update-flux - commit-message: Update to ${{ steps.update.outputs.flux_version }} - title: Update to ${{ steps.update.outputs.flux_version }} - body: | - ${{ steps.update.outputs.flux_version }} -``` - -### End-to-end testing - -Example workflow for running Flux in Kubernetes Kind: - -```yaml -name: e2e - -on: - push: - branches: - - '*' - -jobs: - kubernetes: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Setup Flux CLI - uses: fluxcd/flux2/action@main - - name: Setup Kubernetes Kind - uses: engineerd/setup-kind@v0.5.0 - - name: Install Flux in Kubernetes Kind - run: flux install -``` +For more information, please see the [Flux GitHub Action documentation](https://fluxcd.io/flux/flux-gh-action/). -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) diff --git a/action/action.yml b/action/action.yml index 7bb0be5c..d88786ee 100644 --- a/action/action.yml +++ b/action/action.yml @@ -1,52 +1,120 @@ name: Setup Flux CLI -description: A GitHub Action for running Flux commands -author: Stefan Prodan +description: A GitHub Action for installing the Flux CLI +author: Flux project branding: color: blue icon: command inputs: version: - description: "Flux version e.g. 0.8.0 (defaults to latest stable release)" + description: "Flux version e.g. 2.0.0 (defaults to latest stable release)" required: false arch: description: "arch can be amd64, arm64 or arm" - required: true - default: "amd64" + required: false + deprecationMessage: "No longer required, action will now detect runner arch." bindir: - description: "Optional location of the Flux binary. Will not use sudo if set. Updates System Path." + description: "Alternative location for the Flux binary, defaults to path relative to $RUNNER_TOOL_CACHE." + required: false + token: + description: "Token used to authentication against the GitHub.com API. Defaults to the token from the GitHub context of the workflow." required: false runs: using: composite steps: - - name: "Download flux binary to tmp" + - name: "Download the binary to the runner's cache dir" shell: bash run: | - ARCH=${{ inputs.arch }} VERSION=${{ inputs.version }} - if [ -z $VERSION ]; then - VERSION=$(curl https://api.github.com/repos/fluxcd/flux2/releases/latest -sL | grep tag_name | sed -E 's/.*"([^"]+)".*/\1/' | cut -c 2-) + TOKEN=${{ inputs.token }} + if [[ -z "$TOKEN" ]]; then + TOKEN=${{ github.token }} fi - BIN_URL="https://github.com/fluxcd/flux2/releases/download/v${VERSION}/flux_${VERSION}_linux_${ARCH}.tar.gz" - curl -sL ${BIN_URL} -o /tmp/flux.tar.gz - mkdir -p /tmp/flux - tar -C /tmp/flux/ -zxvf /tmp/flux.tar.gz - - name: "Copy Flux binary to execute location" - shell: bash - run: | - BINDIR=${{ inputs.bindir }} - if [ -z $BINDIR ]; then - sudo cp /tmp/flux/flux /usr/local/bin - else - cp /tmp/flux/flux "${BINDIR}" - echo "${BINDIR}" >> $GITHUB_PATH - fi - - name: "Cleanup tmp" - shell: bash - run: | - rm -rf /tmp/flux/ /tmp/flux.tar.gz - - name: "Verify correct installation of binary" + 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 diff --git a/cmd/flux/alert.go b/cmd/flux/alert.go index ea8012b9..16310e3a 100644 --- a/cmd/flux/alert.go +++ b/cmd/flux/alert.go @@ -19,14 +19,15 @@ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) // notificationv1.Alert var alertType = apiType{ - kind: notificationv1.AlertKind, - humanKind: "alert", + kind: notificationv1.AlertKind, + humanKind: "alert", + groupVersion: notificationv1.GroupVersion, } type alertAdapter struct { diff --git a/cmd/flux/alert_provider.go b/cmd/flux/alert_provider.go index fb7cb063..c370a5c0 100644 --- a/cmd/flux/alert_provider.go +++ b/cmd/flux/alert_provider.go @@ -19,14 +19,15 @@ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) // notificationv1.Provider var alertProviderType = apiType{ - kind: notificationv1.ProviderKind, - humanKind: "alert provider", + kind: notificationv1.ProviderKind, + humanKind: "alert provider", + groupVersion: notificationv1.GroupVersion, } type alertProviderAdapter struct { diff --git a/cmd/flux/bootstrap.go b/cmd/flux/bootstrap.go index 278da91a..d23540e9 100644 --- a/cmd/flux/bootstrap.go +++ b/cmd/flux/bootstrap.go @@ -17,27 +17,32 @@ limitations under the License. package main import ( + "context" "crypto/elliptic" "fmt" - "os" "strings" + "github.com/fluxcd/pkg/git" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var bootstrapCmd = &cobra.Command{ Use: "bootstrap", - Short: "Bootstrap toolkit components", - Long: "The bootstrap sub-commands bootstrap the toolkit components on the targeted Git provider.", + Short: "Deploy Flux on a cluster the GitOps way.", + Long: `The bootstrap sub-commands push the Flux manifests to a Git repository +and deploy Flux on the cluster.`, } type bootstrapFlags struct { version string - arch flags.Arch logLevel flags.LogLevel branch string @@ -48,17 +53,19 @@ type bootstrapFlags struct { extraComponents []string requiredComponents []string - registry string - imagePullSecret string + registry string + registryCredential string + imagePullSecret string - secretName string - tokenAuth bool - keyAlgorithm flags.PublicKeyAlgorithm - keyRSABits flags.RSAKeyBits - keyECDSACurve flags.ECDSACurve - sshHostname string - caFile string - privateKeyFile string + secretName string + tokenAuth bool + keyAlgorithm flags.PublicKeyAlgorithm + keyRSABits flags.RSAKeyBits + keyECDSACurve flags.ECDSACurve + sshHostname string + caFile string + privateKeyFile string + sshHostKeyAlgorithms []string watchAllNamespaces bool networkPolicy bool @@ -72,6 +79,8 @@ type bootstrapFlags struct { gpgPassphrase string gpgKeyID string + force bool + commitMessageAppendix string } @@ -88,12 +97,14 @@ func init() { bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.defaultComponents, "components", rootArgs.defaults.Components, "list of components, accepts comma-separated values") bootstrapCmd.PersistentFlags().StringSliceVar(&bootstrapArgs.extraComponents, "components-extra", nil, - "list of components in addition to those supplied or defaulted, accepts comma-separated values") + "list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller'") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registry, "registry", "ghcr.io/fluxcd", - "container registry where the toolkit images are published") + "container registry where the Flux controller images are published") + bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.registryCredential, "registry-creds", "", + "container registry credentials in the format 'user:password', requires --image-pull-secret to be set") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.imagePullSecret, "image-pull-secret", "", - "Kubernetes secret name used for pulling the toolkit images from a private registry") + "Kubernetes secret name used for pulling the controller images from a private registry") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.branch, "branch", bootstrapDefaultBranch, "Git branch") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.recurseSubmodules, "recurse-submodules", false, @@ -102,19 +113,20 @@ func init() { bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.manifestsPath, "manifests", "", "path to the manifest directory") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.watchAllNamespaces, "watch-all-namespaces", true, - "watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed") + "watch for custom resources in all namespaces, if set to false it will only watch the namespace where the Flux controllers are installed") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.networkPolicy, "network-policy", true, - "deny ingress access to the toolkit controllers from other namespaces using network policies") + "setup Kubernetes network policies to deny ingress access to the Flux controllers from other namespaces") bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.tokenAuth, "token-auth", false, - "when enabled, the personal access token will be used instead of SSH deploy key") + "when enabled, the personal access token will be used instead of the SSH deploy key") bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.logLevel, "log-level", bootstrapArgs.logLevel.Description()) bootstrapCmd.PersistentFlags().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 components pods onto nodes with matching taints") + "list of toleration keys used to schedule the controller pods onto nodes with matching taints") bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.secretName, "secret-name", rootArgs.defaults.Namespace, "name of the secret the sync credentials can be found in or stored to") bootstrapCmd.PersistentFlags().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") @@ -129,8 +141,7 @@ func init() { bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'") - bootstrapCmd.PersistentFlags().Var(&bootstrapArgs.arch, "arch", bootstrapArgs.arch.Description()) - bootstrapCmd.PersistentFlags().MarkDeprecated("arch", "multi-arch container image is now available for AMD64, ARMv7 and ARM64") + 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) @@ -154,7 +165,7 @@ func buildEmbeddedManifestBase() (string, error) { if !isEmbeddedVersion(bootstrapArgs.version) { return "", nil } - tmpBaseDir, err := os.MkdirTemp("", "flux-manifests-") + tmpBaseDir, err := manifestgen.MkdirTempAbs("", "flux-manifests-") if err != nil { return "", err } @@ -176,6 +187,18 @@ func bootstrapValidate() error { return err } + if bootstrapArgs.registryCredential != "" && bootstrapArgs.imagePullSecret == "" { + return fmt.Errorf("--registry-creds requires --image-pull-secret to be set") + } + + if bootstrapArgs.registryCredential != "" && len(strings.Split(bootstrapArgs.registryCredential, ":")) != 2 { + return fmt.Errorf("invalid --registry-creds format, expected 'user:password'") + } + + if len(bootstrapArgs.sshHostKeyAlgorithms) > 0 { + git.HostKeyAlgos = bootstrapArgs.sshHostKeyAlgorithms + } + return nil } @@ -190,3 +213,27 @@ func mapTeamSlice(s []string, defaultPermission string) map[string]string { return m } + +// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation. +// It returns a nil error if Flux is not installed or the user confirms overriding an existing installation +func confirmBootstrap(ctx context.Context, kubeClient client.Client) error { + installed := true + info, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("cluster info unavailable: %w", err) + } + installed = false + } + + if installed { + err = confirmFluxInstallOverride(info) + if err != nil { + if err == promptui.ErrAbort { + return fmt.Errorf("bootstrap cancelled") + } + return err + } + } + return nil +} diff --git a/cmd/flux/bootstrap_bitbucket_server.go b/cmd/flux/bootstrap_bitbucket_server.go index 56e1a906..c26515f6 100644 --- a/cmd/flux/bootstrap_bitbucket_server.go +++ b/cmd/flux/bootstrap_bitbucket_server.go @@ -22,44 +22,42 @@ import ( "os" "time" - "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/gogit" "github.com/spf13/cobra" - "github.com/fluxcd/flux2/internal/bootstrap" - "github.com/fluxcd/flux2/internal/bootstrap/git/gogit" - "github.com/fluxcd/flux2/internal/bootstrap/provider" - "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/sourcesecret" - "github.com/fluxcd/flux2/pkg/manifestgen/sync" + "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: "Bootstrap toolkit components in a Bitbucket Server repository", + Short: "Deploy Flux on a cluster connected to a Bitbucket Server repository", Long: `The bootstrap bitbucket-server command creates the Bitbucket Server repository if it doesn't exists and -commits the toolkit components manifests to the master branch. +commits the Flux manifests to the master branch. Then it configures the target cluster to synchronize with the repository. -If the toolkit components are present on the cluster, +If the Flux components are present on the cluster, the bootstrap command will perform an upgrade if needed.`, Example: ` # Create a Bitbucket Server API token and export it as an env var export BITBUCKET_TOKEN= # Run bootstrap for a private repository using HTTPS token authentication - flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= --token-auth + flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= --token-auth --path=clusters/my-cluster # Run bootstrap for a private repository using SSH authentication - flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= - - # Run bootstrap for a repository path - flux bootstrap bitbucket-server --owner= --username= --repository= --path=dev-cluster --hostname= + flux bootstrap bitbucket-server --owner= --username= --repository= --hostname= --path=clusters/my-cluster # Run bootstrap for a public repository on a personal account - flux bootstrap bitbucket-server --owner= --repository= --private=false --personal --hostname= --token-auth + flux bootstrap bitbucket-server --owner= --repository= --private=false --personal --hostname= --token-auth --path=clusters/my-cluster - # Run bootstrap for a an existing repository with a branch named main - flux bootstrap bitbucket-server --owner= --username= --repository= --branch=main --hostname= --token-auth`, + # Run bootstrap for an existing repository with a branch named main + flux bootstrap bitbucket-server --owner= --username= --repository= --branch=main --hostname= --token-auth --path=clusters/my-cluster`, RunE: bootstrapBServerCmdRun, } @@ -121,13 +119,22 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + 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 { + if ver, err := getVersion(bootstrapArgs.version); err != nil { + return err + } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() @@ -165,15 +172,22 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { } // Lazy go-git repository - tmpDir, err := os.MkdirTemp("", "flux-bootstrap-") + tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) - gitClient := gogit.New(tmpDir, &http.BasicAuth{ - Username: user, - Password: bitbucketToken, - }) + + 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{ @@ -182,6 +196,7 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, @@ -211,19 +226,18 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { secretOpts.Username = bServerArgs.username } secretOpts.Password = bitbucketToken - - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile - } + secretOpts.CAFile = 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.privateKeyFile != "" { - secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile - } + secretOpts.SSHHostname = bServerArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } @@ -238,22 +252,27 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { Secret: bootstrapArgs.secretName, TargetPath: bServerArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, - GitImplementation: sourceGitArgs.gitImplementation.String(), 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.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), + bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithProviderTeamPermissions(mapTeamSlice(bServerArgs.teams, bServerDefaultPermission)), bootstrap.WithReadWriteKeyPermissions(bServerArgs.readWriteKey), - bootstrap.WithKubeconfig(kubeconfigArgs), + bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), - bootstrap.WithCABundle(caBundle), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) diff --git a/cmd/flux/bootstrap_git.go b/cmd/flux/bootstrap_git.go index 91738ae5..6686dcc1 100644 --- a/cmd/flux/bootstrap_git.go +++ b/cmd/flux/bootstrap_git.go @@ -24,53 +24,71 @@ import ( "strings" "time" - "github.com/go-git/go-git/v5/plumbing/transport" - "github.com/go-git/go-git/v5/plumbing/transport/http" - "github.com/go-git/go-git/v5/plumbing/transport/ssh" "github.com/manifoldco/promptui" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" - "github.com/fluxcd/flux2/internal/bootstrap" - "github.com/fluxcd/flux2/internal/bootstrap/git/gogit" - "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/sourcesecret" - "github.com/fluxcd/flux2/pkg/manifestgen/sync" + "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: "Bootstrap toolkit components in a Git repository", - Long: `The bootstrap git command commits the toolkit components manifests to the -branch of a Git repository. It then configures the target cluster to synchronize with -the repository. If the toolkit components are present on the cluster, the bootstrap + 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 + 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= + flux bootstrap git --url=https://example.com/repository.git --password= --path=clusters/my-cluster + + # Run bootstrap for a Git repository and authenticate using a password from environment variable + GIT_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= + flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file= --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= --password= + flux bootstrap git --url=ssh://git@example.com/repository.git --private-key-file= --password= --path=clusters/my-cluster + + # Run bootstrap for a Git repository on AWS CodeCommit + flux bootstrap git --url=ssh://@git-codecommit..amazonaws.com/v1/repos/ --private-key-file= --password= --path=clusters/my-cluster + + # Run bootstrap for a Git repository on Azure Devops + flux bootstrap git --url=ssh://git@ssh.dev.azure.com/v3/// --private-key-file= --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= --path=clusters/my-cluster `, RunE: bootstrapGitCmdRun, } type gitFlags struct { - url string - interval time.Duration - path flags.SafeRelativePath - username string - password string - silent bool + 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() { @@ -80,11 +98,30 @@ func init() { 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 } @@ -93,21 +130,43 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } - gitAuth, err := transportForURL(repositoryURL) - 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) + 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 { + if ver, err := getVersion(bootstrapArgs.version); err != nil { + return err + } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() @@ -117,12 +176,33 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { defer os.RemoveAll(manifestsBase) // Lazy go-git repository - tmpDir, err := os.MkdirTemp("", "flux-bootstrap-") + tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) - gitClient := gogit.New(tmpDir, gitAuth) + + var caBundle []byte + if bootstrapArgs.caFile != "" { + var err error + caBundle, err = os.ReadFile(bootstrapArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } + authOpts, err := getAuthOpts(repositoryURL, caBundle) + if err != nil { + return fmt.Errorf("failed to create authentication options for %s: %w", repositoryURL.String(), err) + } + + clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} + if gitArgs.insecureHttpAllowed { + clientOpts = append(clientOpts, gogit.WithInsecureCredentialsOverHTTP()) + } + gitClient, err := gogit.NewClient(tmpDir, authOpts, clientOpts...) + if err != nil { + return fmt.Errorf("failed to create a Git client: %w", err) + } // Install manifest config installOptions := install.Options{ @@ -131,6 +211,7 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, @@ -153,14 +234,17 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { TargetPath: gitArgs.path.String(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } - if bootstrapArgs.tokenAuth { - secretOpts.Username = gitArgs.username - secretOpts.Password = gitArgs.password - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile + if bootstrapArgs.tokenAuth { + if gitArgs.withBearerToken { + secretOpts.BearerToken = gitArgs.password + } else { + secretOpts.Username = gitArgs.username + secretOpts.Password = gitArgs.password } + secretOpts.CAFile = caBundle + // Remove port of the given host when not syncing over HTTP/S to not assume port for protocol // This _might_ be overwritten later on by e.g. --ssh-hostname if repositoryURL.Scheme != "https" && repositoryURL.Scheme != "http" { @@ -169,7 +253,9 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { // Configure repository URL to match auth config for sync. repositoryURL.User = nil - repositoryURL.Scheme = "https" + if !gitArgs.insecureHttpAllowed { + repositoryURL.Scheme = "https" + } } else { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.Password = gitArgs.password @@ -188,9 +274,12 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { if bootstrapArgs.sshHostname != "" { repositoryURL.Host = bootstrapArgs.sshHostname } - if bootstrapArgs.privateKeyFile != "" { - secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile + + keypair, err := sourcesecret.LoadKeyPairFromPath(bootstrapArgs.privateKeyFile, gitArgs.password) + if err != nil { + return err } + secretOpts.Keypair = keypair // Configure last as it depends on the config above. secretOpts.SSHHostname = repositoryURL.Host @@ -206,30 +295,24 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { Secret: bootstrapArgs.secretName, TargetPath: gitArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, - GitImplementation: sourceGitArgs.gitImplementation.String(), RecurseSubmodules: bootstrapArgs.recurseSubmodules, } - 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) - } + 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.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), + bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), - bootstrap.WithKubeconfig(kubeconfigArgs), + bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithPostGenerateSecretFunc(promptPublicKey), bootstrap.WithLogger(logger), - bootstrap.WithCABundle(caBundle), - bootstrap.WithGitCommitSigning(bootstrapArgs.gpgKeyRingPath, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } // Setup bootstrapper with constructed configs @@ -242,22 +325,57 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) } -// transportForURL constructs a transport.AuthMethod based on the scheme +// getAuthOpts retruns a AuthOptions based on the scheme // of the given URL and the configured flags. If the protocol equals // "ssh" but no private key is configured, authentication using the local // SSH-agent is attempted. -func transportForURL(u *url.URL) (transport.AuthMethod, error) { +func getAuthOpts(u *url.URL, caBundle []byte) (*git.AuthOptions, error) { switch u.Scheme { + 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": - return &http.BasicAuth{ - Username: gitArgs.username, - Password: gitArgs.password, - }, nil + 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 != "" { - return ssh.NewPublicKeysFromFile(u.User.Username(), bootstrapArgs.privateKeyFile, gitArgs.password) + pk, err := os.ReadFile(bootstrapArgs.privateKeyFile) + if err != nil { + return nil, err + } + kh, err := sourcesecret.ScanHostKey(u.Host) + if err != nil { + return nil, err + } + authOpts.Identity = pk + authOpts.KnownHosts = kh } - return nil, nil + return authOpts, nil default: return nil, fmt.Errorf("scheme %q is not supported", u.Scheme) } diff --git a/cmd/flux/bootstrap_gitea.go b/cmd/flux/bootstrap_gitea.go new file mode 100644 index 00000000..48b18b0e --- /dev/null +++ b/cmd/flux/bootstrap_gitea.go @@ -0,0 +1,276 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/gogit" + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/bootstrap" + "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" + "github.com/fluxcd/flux2/v2/pkg/manifestgen" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" +) + +var bootstrapGiteaCmd = &cobra.Command{ + Use: "gitea", + Short: "Deploy Flux on a cluster connected to a Gitea repository", + Long: `The bootstrap gitea command creates the Gitea repository if it doesn't exists and +commits the Flux manifests to the specified branch. +Then it configures the target cluster to synchronize with that repository. +If the Flux components are present on the cluster, +the bootstrap command will perform an upgrade if needed.`, + Example: ` # Create a Gitea personal access token and export it as an env var + export GITEA_TOKEN= + + # Run bootstrap for a private repository owned by a Gitea organization + flux bootstrap gitea --owner= --repository= --path=clusters/my-cluster + + # Run bootstrap for a private repository and assign organization teams to it + flux bootstrap gitea --owner= --repository= --team= --team= --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= --repository= --team=: --path=clusters/my-cluster + + # Run bootstrap for a public repository on a personal account + flux bootstrap gitea --owner= --repository= --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= --repository= --hostname= --ssh-hostname= --path=clusters/my-cluster + + # Run bootstrap for a private repository hosted on Gitea Enterprise using HTTPS auth + flux bootstrap gitea --owner= --repository= --hostname= --token-auth --path=clusters/my-cluster + + # Run bootstrap for an existing repository with a branch named main + flux bootstrap gitea --owner= --repository= --branch=main --path=clusters/my-cluster`, + RunE: bootstrapGiteaCmdRun, +} + +type giteaFlags struct { + owner string + repository string + interval time.Duration + personal bool + private bool + hostname string + path flags.SafeRelativePath + teams []string + readWriteKey bool + reconcile bool +} + +const ( + gtDefaultPermission = "maintain" + gtDefaultDomain = "gitea.com" + gtTokenEnvVar = "GITEA_TOKEN" +) + +var giteaArgs giteaFlags + +func init() { + bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.owner, "owner", "", "Gitea user or organization name") + bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.repository, "repository", "", "Gitea repository name") + bootstrapGiteaCmd.Flags().StringSliceVar(&giteaArgs.teams, "team", []string{}, "Gitea team and the access to be given to it(team:maintain). Defaults to maintainer access if no access level is specified (also accepts comma-separated values)") + bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.personal, "personal", false, "if true, the owner is assumed to be a Gitea user; otherwise an org") + bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.private, "private", true, "if true, the repository is setup or configured as private") + bootstrapGiteaCmd.Flags().DurationVar(&giteaArgs.interval, "interval", time.Minute, "sync interval") + bootstrapGiteaCmd.Flags().StringVar(&giteaArgs.hostname, "hostname", gtDefaultDomain, "Gitea hostname") + bootstrapGiteaCmd.Flags().Var(&giteaArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") + bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.readWriteKey, "read-write-key", false, "if true, the deploy key is configured with read/write permissions") + bootstrapGiteaCmd.Flags().BoolVar(&giteaArgs.reconcile, "reconcile", false, "if true, the configured options are also reconciled if the repository already exists") + + bootstrapCmd.AddCommand(bootstrapGiteaCmd) +} + +func bootstrapGiteaCmdRun(cmd *cobra.Command, args []string) error { + gtToken := os.Getenv(gtTokenEnvVar) + if gtToken == "" { + var err error + gtToken, err = readPasswordFromStdin("Please enter your Gitea personal access token (PAT): ") + if err != nil { + return fmt.Errorf("could not read token: %w", err) + } + } + + if err := bootstrapValidate(); err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + + // Manifest base + if ver, err := getVersion(bootstrapArgs.version); err != nil { + return err + } else { + bootstrapArgs.version = ver + } + manifestsBase, err := buildEmbeddedManifestBase() + if err != nil { + return err + } + defer os.RemoveAll(manifestsBase) + + var caBundle []byte + if bootstrapArgs.caFile != "" { + var err error + caBundle, err = os.ReadFile(bootstrapArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } + // Build Gitea provider + providerCfg := provider.Config{ + Provider: provider.GitProviderGitea, + Hostname: giteaArgs.hostname, + Token: gtToken, + CaBundle: caBundle, + } + providerClient, err := provider.BuildGitProvider(providerCfg) + if err != nil { + return err + } + + tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") + if err != nil { + return fmt.Errorf("failed to create temporary working dir: %w", err) + } + defer os.RemoveAll(tmpDir) + + clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} + gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ + Transport: git.HTTPS, + Username: giteaArgs.owner, + Password: gtToken, + CAFile: caBundle, + }, clientOpts...) + if err != nil { + return fmt.Errorf("failed to create a Git client: %w", err) + } + + // Install manifest config + installOptions := install.Options{ + BaseURL: rootArgs.defaults.BaseURL, + Version: bootstrapArgs.version, + Namespace: *kubeconfigArgs.Namespace, + Components: bootstrapComponents(), + Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, + ImagePullSecret: bootstrapArgs.imagePullSecret, + WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, + NetworkPolicy: bootstrapArgs.networkPolicy, + LogLevel: bootstrapArgs.logLevel.String(), + NotificationController: rootArgs.defaults.NotificationController, + ManifestFile: rootArgs.defaults.ManifestFile, + Timeout: rootArgs.timeout, + TargetPath: giteaArgs.path.ToSlash(), + ClusterDomain: bootstrapArgs.clusterDomain, + TolerationKeys: bootstrapArgs.tolerationKeys, + } + if customBaseURL := bootstrapArgs.manifestsPath; customBaseURL != "" { + installOptions.BaseURL = customBaseURL + } + + // Source generation and secret config + secretOpts := sourcesecret.Options{ + Name: bootstrapArgs.secretName, + Namespace: *kubeconfigArgs.Namespace, + TargetPath: giteaArgs.path.ToSlash(), + ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, + } + if bootstrapArgs.tokenAuth { + secretOpts.Username = "git" + secretOpts.Password = gtToken + secretOpts.CAFile = caBundle + } else { + secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) + secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) + secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve + + secretOpts.SSHHostname = giteaArgs.hostname + if bootstrapArgs.sshHostname != "" { + secretOpts.SSHHostname = bootstrapArgs.sshHostname + } + } + + // Sync manifest config + syncOpts := sync.Options{ + Interval: giteaArgs.interval, + Name: *kubeconfigArgs.Namespace, + Namespace: *kubeconfigArgs.Namespace, + Branch: bootstrapArgs.branch, + Secret: bootstrapArgs.secretName, + TargetPath: giteaArgs.path.ToSlash(), + ManifestFile: sync.MakeDefaultOptions().ManifestFile, + RecurseSubmodules: bootstrapArgs.recurseSubmodules, + } + + entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) + if err != nil { + return err + } + + // Bootstrap config + bootstrapOpts := []bootstrap.GitProviderOption{ + bootstrap.WithProviderRepository(giteaArgs.owner, giteaArgs.repository, giteaArgs.personal), + bootstrap.WithBranch(bootstrapArgs.branch), + bootstrap.WithBootstrapTransportType("https"), + bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), + bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), + bootstrap.WithProviderTeamPermissions(mapTeamSlice(giteaArgs.teams, gtDefaultPermission)), + bootstrap.WithReadWriteKeyPermissions(giteaArgs.readWriteKey), + bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), + bootstrap.WithLogger(logger), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), + } + if bootstrapArgs.sshHostname != "" { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) + } + if bootstrapArgs.tokenAuth { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) + } + if !giteaArgs.private { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) + } + if giteaArgs.reconcile { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithReconcile()) + } + + // Setup bootstrapper with constructed configs + b, err := bootstrap.NewGitProviderBootstrapper(gitClient, providerClient, kubeClient, bootstrapOpts...) + if err != nil { + return err + } + + // Run + return bootstrap.Run(ctx, b, manifestsBase, installOptions, secretOpts, syncOpts, rootArgs.pollInterval, rootArgs.timeout) +} diff --git a/cmd/flux/bootstrap_github.go b/cmd/flux/bootstrap_github.go index fd04ac6f..a82fb1ce 100644 --- a/cmd/flux/bootstrap_github.go +++ b/cmd/flux/bootstrap_github.go @@ -22,53 +22,51 @@ import ( "os" "time" - "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/gogit" "github.com/spf13/cobra" - "github.com/fluxcd/flux2/internal/bootstrap" - "github.com/fluxcd/flux2/internal/bootstrap/git/gogit" - "github.com/fluxcd/flux2/internal/bootstrap/provider" - "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/sourcesecret" - "github.com/fluxcd/flux2/pkg/manifestgen/sync" + "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 bootstrapGitHubCmd = &cobra.Command{ Use: "github", - Short: "Bootstrap toolkit components in a GitHub repository", + Short: "Deploy Flux on a cluster connected to a GitHub repository", Long: `The bootstrap github command creates the GitHub repository if it doesn't exists and -commits the toolkit components manifests to the main branch. -Then it configures the target cluster to synchronize with the repository. -If the toolkit components are present on the cluster, +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 GitHub personal access token and export it as an env var export GITHUB_TOKEN= # Run bootstrap for a private repository owned by a GitHub organization - flux bootstrap github --owner= --repository= + flux bootstrap github --owner= --repository= --path=clusters/my-cluster # Run bootstrap for a private repository and assign organization teams to it - flux bootstrap github --owner= --repository= --team= --team= + flux bootstrap github --owner= --repository= --team= --team= --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 github --owner= --repository= --team=: - - # Run bootstrap for a repository path - flux bootstrap github --owner= --repository= --path=dev-cluster + flux bootstrap github --owner= --repository= --team=: --path=clusters/my-cluster # Run bootstrap for a public repository on a personal account - flux bootstrap github --owner= --repository= --private=false --personal=true + flux bootstrap github --owner= --repository= --private=false --personal=true --path=clusters/my-cluster # Run bootstrap for a private repository hosted on GitHub Enterprise using SSH auth - flux bootstrap github --owner= --repository= --hostname= --ssh-hostname= + flux bootstrap github --owner= --repository= --hostname= --ssh-hostname= --path=clusters/my-cluster # Run bootstrap for a private repository hosted on GitHub Enterprise using HTTPS auth - flux bootstrap github --owner= --repository= --hostname= --token-auth + flux bootstrap github --owner= --repository= --hostname= --token-auth --path=clusters/my-cluster # Run bootstrap for an existing repository with a branch named main - flux bootstrap github --owner= --repository= --branch=main`, + flux bootstrap github --owner= --repository= --branch=main --path=clusters/my-cluster`, RunE: bootstrapGitHubCmdRun, } @@ -125,13 +123,22 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + 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 { + if ver, err := getVersion(bootstrapArgs.version); err != nil { + return err + } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() @@ -160,16 +167,22 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { return err } - // Lazy go-git repository - tmpDir, err := os.MkdirTemp("", "flux-bootstrap-") + tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) - gitClient := gogit.New(tmpDir, &http.BasicAuth{ - Username: githubArgs.owner, - Password: ghToken, - }) + + clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} + gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ + Transport: git.HTTPS, + Username: githubArgs.owner, + Password: ghToken, + CAFile: caBundle, + }, clientOpts...) + if err != nil { + return fmt.Errorf("failed to create a Git client: %w", err) + } // Install manifest config installOptions := install.Options{ @@ -178,6 +191,7 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, @@ -203,16 +217,13 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { if bootstrapArgs.tokenAuth { secretOpts.Username = "git" secretOpts.Password = ghToken - - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile - } + secretOpts.CAFile = caBundle } else { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(bootstrapArgs.keyRSABits) secretOpts.ECDSACurve = bootstrapArgs.keyECDSACurve.Curve - secretOpts.SSHHostname = githubArgs.hostname + secretOpts.SSHHostname = githubArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } @@ -227,22 +238,26 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { Secret: bootstrapArgs.secretName, TargetPath: githubArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, - GitImplementation: sourceGitArgs.gitImplementation.String(), RecurseSubmodules: bootstrapArgs.recurseSubmodules, } + entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) + if err != nil { + return err + } + // Bootstrap config bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(githubArgs.owner, githubArgs.repository, githubArgs.personal), bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBootstrapTransportType("https"), - bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), + bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithProviderTeamPermissions(mapTeamSlice(githubArgs.teams, ghDefaultPermission)), bootstrap.WithReadWriteKeyPermissions(githubArgs.readWriteKey), - bootstrap.WithKubeconfig(kubeconfigArgs), + bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), - bootstrap.WithCABundle(caBundle), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) diff --git a/cmd/flux/bootstrap_gitlab.go b/cmd/flux/bootstrap_gitlab.go index 6906a2a7..15716623 100644 --- a/cmd/flux/bootstrap_gitlab.go +++ b/cmd/flux/bootstrap_gitlab.go @@ -24,26 +24,27 @@ import ( "strings" "time" - "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/gogit" "github.com/spf13/cobra" - "github.com/fluxcd/flux2/internal/bootstrap" - "github.com/fluxcd/flux2/internal/bootstrap/git/gogit" - "github.com/fluxcd/flux2/internal/bootstrap/provider" - "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/sourcesecret" - "github.com/fluxcd/flux2/pkg/manifestgen/sync" + "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 bootstrapGitLabCmd = &cobra.Command{ Use: "gitlab", - Short: "Bootstrap toolkit components in a GitLab repository", + Short: "Deploy Flux on a cluster connected to a GitLab repository", Long: `The bootstrap gitlab command creates the GitLab repository if it doesn't exists and -commits the toolkit components manifests to the master branch. -Then it configures the target cluster to synchronize with the repository. -If the toolkit components are present on the cluster, +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 GitLab API token and export it as an env var export GITLAB_TOKEN= @@ -63,8 +64,12 @@ the bootstrap command will perform an upgrade if needed.`, # Run bootstrap for a private repository hosted on a GitLab server flux bootstrap gitlab --owner= --repository= --hostname= --token-auth - # Run bootstrap for a an existing repository with a branch named main - flux bootstrap gitlab --owner= --repository= --branch=main --token-auth`, + # Run bootstrap for an existing repository with a branch named main + flux bootstrap gitlab --owner= --repository= --branch=main --token-auth + + # Run bootstrap for a private repository using Deploy Token authentication + flux bootstrap gitlab --owner= --repository= --deploy-token-auth + `, RunE: bootstrapGitLabCmdRun, } @@ -76,16 +81,17 @@ const ( ) type gitlabFlags struct { - owner string - repository string - interval time.Duration - personal bool - private bool - hostname string - path flags.SafeRelativePath - teams []string - readWriteKey bool - reconcile bool + owner string + repository string + interval time.Duration + personal bool + private bool + hostname string + path flags.SafeRelativePath + teams []string + readWriteKey bool + reconcile bool + deployTokenAuth bool } var gitlabArgs gitlabFlags @@ -101,6 +107,7 @@ func init() { bootstrapGitLabCmd.Flags().Var(&gitlabArgs.path, "path", "path relative to the repository root, when specified the cluster sync will be scoped to this path") bootstrapGitLabCmd.Flags().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) } @@ -122,6 +129,10 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { return err } + if bootstrapArgs.tokenAuth && gitlabArgs.deployTokenAuth { + return fmt.Errorf("--token-auth and --deploy-token-auth cannot be set both.") + } + if err := bootstrapValidate(); err != nil { return err } @@ -129,13 +140,22 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + 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 { + if ver, err := getVersion(bootstrapArgs.version); err != nil { + return err + } else { bootstrapArgs.version = ver } manifestsBase, err := buildEmbeddedManifestBase() @@ -172,15 +192,22 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { } // Lazy go-git repository - tmpDir, err := os.MkdirTemp("", "flux-bootstrap-") + tmpDir, err := manifestgen.MkdirTempAbs("", "flux-bootstrap-") if err != nil { return fmt.Errorf("failed to create temporary working dir: %w", err) } defer os.RemoveAll(tmpDir) - gitClient := gogit.New(tmpDir, &http.BasicAuth{ - Username: gitlabArgs.owner, - Password: glToken, - }) + + clientOpts := []gogit.ClientOption{gogit.WithDiskStorage(), gogit.WithFallbackToDefaultKnownHosts()} + gitClient, err := gogit.NewClient(tmpDir, &git.AuthOptions{ + Transport: git.HTTPS, + Username: gitlabArgs.owner, + Password: glToken, + CAFile: caBundle, + }, clientOpts...) + if err != nil { + return fmt.Errorf("failed to create a Git client: %w", err) + } // Install manifest config installOptions := install.Options{ @@ -189,6 +216,7 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, + RegistryCredential: bootstrapArgs.registryCredential, ImagePullSecret: bootstrapArgs.imagePullSecret, WatchAllNamespaces: bootstrapArgs.watchAllNamespaces, NetworkPolicy: bootstrapArgs.networkPolicy, @@ -214,19 +242,21 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { if bootstrapArgs.tokenAuth { secretOpts.Username = "git" secretOpts.Password = glToken - - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile - } + secretOpts.CAFile = caBundle + } else if gitlabArgs.deployTokenAuth { + // the actual deploy token will be reconciled later + secretOpts.CAFile = 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 = gitlabArgs.hostname - if bootstrapArgs.privateKeyFile != "" { - secretOpts.PrivateKeyPath = bootstrapArgs.privateKeyFile - } + secretOpts.SSHHostname = gitlabArgs.hostname if bootstrapArgs.sshHostname != "" { secretOpts.SSHHostname = bootstrapArgs.sshHostname } @@ -241,29 +271,36 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { Secret: bootstrapArgs.secretName, TargetPath: gitlabArgs.path.ToSlash(), ManifestFile: sync.MakeDefaultOptions().ManifestFile, - GitImplementation: sourceGitArgs.gitImplementation.String(), RecurseSubmodules: bootstrapArgs.recurseSubmodules, } + entityList, err := bootstrap.LoadEntityListFromPath(bootstrapArgs.gpgKeyRingPath) + if err != nil { + return err + } + // Bootstrap config bootstrapOpts := []bootstrap.GitProviderOption{ bootstrap.WithProviderRepository(gitlabArgs.owner, gitlabArgs.repository, gitlabArgs.personal), bootstrap.WithBranch(bootstrapArgs.branch), bootstrap.WithBootstrapTransportType("https"), - bootstrap.WithAuthor(bootstrapArgs.authorName, bootstrapArgs.authorEmail), + bootstrap.WithSignature(bootstrapArgs.authorName, bootstrapArgs.authorEmail), bootstrap.WithCommitMessageAppendix(bootstrapArgs.commitMessageAppendix), bootstrap.WithProviderTeamPermissions(mapTeamSlice(gitlabArgs.teams, glDefaultPermission)), bootstrap.WithReadWriteKeyPermissions(gitlabArgs.readWriteKey), - bootstrap.WithKubeconfig(kubeconfigArgs), + bootstrap.WithKubeconfig(kubeconfigArgs, kubeclientOptions), bootstrap.WithLogger(logger), - bootstrap.WithCABundle(caBundle), + bootstrap.WithGitCommitSigning(entityList, bootstrapArgs.gpgPassphrase, bootstrapArgs.gpgKeyID), } if bootstrapArgs.sshHostname != "" { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSSHHostname(bootstrapArgs.sshHostname)) } - if bootstrapArgs.tokenAuth { + if bootstrapArgs.tokenAuth || gitlabArgs.deployTokenAuth { bootstrapOpts = append(bootstrapOpts, bootstrap.WithSyncTransportType("https")) } + if gitlabArgs.deployTokenAuth { + bootstrapOpts = append(bootstrapOpts, bootstrap.WithDeployTokenAuth()) + } if !gitlabArgs.private { bootstrapOpts = append(bootstrapOpts, bootstrap.WithProviderRepositoryConfig("", "", "public")) } diff --git a/cmd/flux/build.go b/cmd/flux/build.go index 0c901036..1ff140be 100644 --- a/cmd/flux/build.go +++ b/cmd/flux/build.go @@ -23,7 +23,7 @@ import ( var buildCmd = &cobra.Command{ Use: "build", Short: "Build a flux resource", - Long: "The build command is used to build flux resources.", + Long: `The build command is used to build flux resources.`, } func init() { diff --git a/cmd/flux/build_artifact.go b/cmd/flux/build_artifact.go new file mode 100644 index 00000000..ce3caf09 --- /dev/null +++ b/cmd/flux/build_artifact.go @@ -0,0 +1,117 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "strings" + + "github.com/spf13/cobra" + + oci "github.com/fluxcd/pkg/oci/client" + "github.com/fluxcd/pkg/sourceignore" +) + +var buildArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Build artifact", + Long: withPreviewNote(`The build artifact command creates a tgz file with the manifests +from the given directory or a single manifest file.`), + Example: ` # Build the given manifests directory into an artifact + flux build artifact --path ./path/to/local/manifests --output ./path/to/artifact.tgz + + # Build the given single manifest file into an artifact + flux build artifact --path ./path/to/local/manifest.yaml --output ./path/to/artifact.tgz + + # List the files bundled in the artifact + tar -ztvf ./path/to/artifact.tgz +`, + RunE: buildArtifactCmdRun, +} + +type buildArtifactFlags struct { + output string + path string + ignorePaths []string +} + +var excludeOCI = append(strings.Split(sourceignore.ExcludeVCS, ","), strings.Split(sourceignore.ExcludeExt, ",")...) + +var buildArtifactArgs buildArtifactFlags + +func init() { + buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.path, "path", "p", "", "Path to the directory where the Kubernetes manifests are located.") + buildArtifactCmd.Flags().StringVarP(&buildArtifactArgs.output, "output", "o", "artifact.tgz", "Path to where the artifact tgz file should be written.") + buildArtifactCmd.Flags().StringSliceVar(&buildArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") + + buildCmd.AddCommand(buildArtifactCmd) +} + +func buildArtifactCmdRun(cmd *cobra.Command, args []string) error { + if buildArtifactArgs.path == "" { + return fmt.Errorf("invalid path %q", buildArtifactArgs.path) + } + + path := buildArtifactArgs.path + var err error + if buildArtifactArgs.path == "-" { + path, err = saveReaderToFile(os.Stdin) + if err != nil { + return err + } + + defer os.Remove(path) + } + + if _, err := os.Stat(path); err != nil { + return fmt.Errorf("invalid path '%s', must point to an existing directory or file", path) + } + + logger.Actionf("building artifact from %s", path) + + ociClient := oci.NewClient(oci.DefaultOptions()) + if err := ociClient.Build(buildArtifactArgs.output, path, buildArtifactArgs.ignorePaths); err != nil { + return fmt.Errorf("building artifact failed, error: %w", err) + } + + logger.Successf("artifact created at %s", buildArtifactArgs.output) + return nil +} + +func saveReaderToFile(reader io.Reader) (string, error) { + b, err := io.ReadAll(bufio.NewReader(reader)) + if err != nil { + return "", err + } + b = bytes.TrimRight(b, "\r\n") + f, err := os.CreateTemp("", "*.yaml") + if err != nil { + return "", fmt.Errorf("unable to create temp dir for stdin") + } + + defer f.Close() + + if _, err := f.Write(b); err != nil { + return "", fmt.Errorf("error writing stdin to file: %w", err) + } + + return f.Name(), nil +} diff --git a/cmd/flux/build_artifact_test.go b/cmd/flux/build_artifact_test.go new file mode 100644 index 00000000..ba84186c --- /dev/null +++ b/cmd/flux/build_artifact_test.go @@ -0,0 +1,70 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + "strings" + "testing" + + . "github.com/onsi/gomega" +) + +func Test_saveReaderToFile(t *testing.T) { + g := NewWithT(t) + + testString := `apiVersion: v1 +kind: ConfigMap +metadata: + name: myapp +data: + foo: bar` + + tests := []struct { + name string + string string + expectErr bool + }{ + { + name: "yaml", + string: testString, + }, + { + name: "yaml with carriage return", + string: testString + "\r\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpFile, err := saveReaderToFile(strings.NewReader(tt.string)) + g.Expect(err).To(BeNil()) + + t.Cleanup(func() { _ = os.Remove(tmpFile) }) + + b, err := os.ReadFile(tmpFile) + if tt.expectErr { + g.Expect(err).To(Not(BeNil())) + return + } + + g.Expect(err).To(BeNil()) + g.Expect(string(b)).To(BeEquivalentTo(testString)) + }) + + } +} diff --git a/cmd/flux/build_kustomization.go b/cmd/flux/build_kustomization.go index 966046b7..96fb5b9b 100644 --- a/cmd/flux/build_kustomization.go +++ b/cmd/flux/build_kustomization.go @@ -23,8 +23,10 @@ import ( "github.com/spf13/cobra" - "github.com/fluxcd/flux2/internal/build" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + 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{ @@ -33,25 +35,50 @@ var buildKsCmd = &cobra.Command{ 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.`, +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`, +flux build kustomization my-app --path ./path/to/local/manifests + +# Build using a local flux kustomization file +flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml + +# Build in dry-run mode without connecting to the cluster. +# Note that variable substitutions from Secrets and ConfigMaps are skipped in dry-run mode. +flux build kustomization my-app --path ./path/to/local/manifests \ + --kustomization-file ./path/to/local/my-app.yaml \ + --dry-run + +# Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat. +flux build kustomization my-app --path ./path/to/local/manifests \ + --kustomization-file ./path/to/local/my-app.yaml \ + --ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: buildKsCmdRun, } type buildKsFlags struct { - path string + kustomizationFile string + path string + ignorePaths []string + dryRun bool + strictSubst bool } var buildKsArgs buildKsFlags func init() { - buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.)") + buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.") + buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") + buildKsCmd.Flags().StringSliceVar(&buildKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format") + buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.") + buildKsCmd.Flags().BoolVar(&buildKsArgs.strictSubst, "strict-substitute", false, + "When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.") buildCmd.AddCommand(buildKsCmd) } -func buildKsCmdRun(cmd *cobra.Command, args []string) error { +func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) { if len(args) < 1 { return fmt.Errorf("%s name is required", kustomizationType.humanKind) } @@ -65,7 +92,36 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid resource path %q", buildKsArgs.path) } - builder, err := build.NewBuilder(kubeconfigArgs, name, buildKsArgs.path, build.WithTimeout(rootArgs.timeout)) + if buildKsArgs.dryRun && buildKsArgs.kustomizationFile == "" { + return fmt.Errorf("dry-run mode requires a kustomization file") + } + + if buildKsArgs.kustomizationFile != "" { + if fs, err := os.Stat(buildKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { + return fmt.Errorf("invalid kustomization file %q", buildKsArgs.kustomizationFile) + } + } + + var builder *build.Builder + if buildKsArgs.dryRun { + builder, err = build.NewBuilder(name, buildKsArgs.path, + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(buildKsArgs.kustomizationFile), + build.WithDryRun(buildKsArgs.dryRun), + build.WithNamespace(*kubeconfigArgs.Namespace), + build.WithIgnore(buildKsArgs.ignorePaths), + build.WithStrictSubstitute(buildKsArgs.strictSubst), + ) + } else { + builder, err = build.NewBuilder(name, buildKsArgs.path, + build.WithClientConfig(kubeconfigArgs, kubeclientOptions), + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(buildKsArgs.kustomizationFile), + build.WithIgnore(buildKsArgs.ignorePaths), + build.WithStrictSubstitute(buildKsArgs.strictSubst), + ) + } + if err != nil { return err } @@ -76,12 +132,17 @@ func buildKsCmdRun(cmd *cobra.Command, args []string) error { errChan := make(chan error) go func() { - manifests, err := builder.Build() + objects, err := builder.Build() + if err != nil { + errChan <- err + } + + manifests, err := ssautil.ObjectsToYAML(objects) if err != nil { errChan <- err } - cmd.Print(string(manifests)) + cmd.Print(manifests) errChan <- nil }() diff --git a/cmd/flux/build_kustomization_test.go b/cmd/flux/build_kustomization_test.go index 99ecb45c..383fb1fc 100644 --- a/cmd/flux/build_kustomization_test.go +++ b/cmd/flux/build_kustomization_test.go @@ -20,7 +20,10 @@ limitations under the License. package main import ( + "bytes" + "os" "testing" + "text/template" ) func setup(t *testing.T, tmpl map[string]string) { @@ -54,6 +57,18 @@ func TestBuildKustomization(t *testing.T) { resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", assertFunc: "assertGoldenTemplateFile", }, + { + name: "build deployment and configmap with var substitution", + args: "build kustomization podinfo --path ./testdata/build-kustomization/var-substitution", + resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build ignore", + args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\"", + resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, } tmpl := map[string]string{ @@ -81,3 +96,107 @@ func TestBuildKustomization(t *testing.T) { }) } } + +func TestBuildLocalKustomization(t *testing.T) { + podinfo := `apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: podinfo + targetNamespace: default + postBuild: + substitute: + cluster_env: "prod" + cluster_region: "eu-central-1" +` + + tests := []struct { + name string + args string + resultFile string + assertFunc string + }{ + { + name: "no args", + args: "build kustomization podinfo --kustomization-file ./wrongfile/ --path ./testdata/build-kustomization/podinfo", + resultFile: "invalid kustomization file \"./wrongfile/\"", + assertFunc: "assertError", + }, + { + name: "build podinfo", + args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/podinfo", + resultFile: "./testdata/build-kustomization/podinfo-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build podinfo without service", + args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/delete-service", + resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build deployment and configmap with var substitution", + args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution", + resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build deployment and configmap with var substitution in dry-run mode", + args: "build kustomization podinfo --kustomization-file ./testdata/build-kustomization/podinfo.yaml --path ./testdata/build-kustomization/var-substitution --dry-run", + resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + } + + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setup(t, tmpl) + + testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t) + + temp, err := template.New("podinfo").Parse(podinfo) + if err != nil { + t.Fatal(err) + } + + var b bytes.Buffer + err = temp.Execute(&b, tmpl) + if err != nil { + t.Fatal(err) + } + + err = os.WriteFile("./testdata/build-kustomization/podinfo.yaml", b.Bytes(), 0666) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Remove("./testdata/build-kustomization/podinfo.yaml") }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var assert assertFunc + + switch tt.assertFunc { + case "assertGoldenTemplateFile": + assert = assertGoldenTemplateFile(tt.resultFile, tmpl) + case "assertError": + assert = assertError(tt.resultFile) + } + + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: assert, + } + + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/check.go b/cmd/flux/check.go index a36159d0..b7108941 100644 --- a/cmd/flux/check.go +++ b/cmd/flux/check.go @@ -18,28 +18,32 @@ package main import ( "context" + "fmt" "os" "time" "github.com/Masterminds/semver/v3" "github.com/spf13/cobra" v1 "k8s.io/api/apps/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/version" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen" - "github.com/fluxcd/flux2/pkg/manifestgen/install" - "github.com/fluxcd/flux2/pkg/status" + "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{ Use: "check", + Args: cobra.NoArgs, Short: "Check requirements and installation", - 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.`, + Long: withPreviewNote(`The check command will perform a series of checks to validate that +the local environment is configured correctly and if the installed components are healthy.`), Example: ` # Run pre-installation checks flux check --pre @@ -56,10 +60,7 @@ type checkFlags struct { } var kubernetesConstraints = []string{ - ">=1.19.0-0", - ">=1.16.11-0 <=1.16.15-0", - ">=1.17.7-0 <=1.17.17-0", - ">=1.18.4-0 <=1.18.20-0", + ">=1.28.0-0", } var checkArgs checkFlags @@ -82,7 +83,20 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { fluxCheck() - if !kubernetesCheck(kubernetesConstraints) { + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) + if err != nil { + return fmt.Errorf("Kubernetes client initialization failed: %s", err.Error()) + } + + kubeClient, err := client.New(cfg, client.Options{Scheme: utils.NewScheme()}) + if err != nil { + return err + } + + if !kubernetesCheck(cfg, kubernetesConstraints) { checkFailed = true } @@ -94,13 +108,26 @@ func runCheckCmd(cmd *cobra.Command, args []string) error { return nil } + logger.Actionf("checking version in cluster") + if !fluxClusterVersionCheck(ctx, kubeClient) { + checkFailed = true + } + logger.Actionf("checking controllers") - if !componentsCheck() { + if !componentsCheck(ctx, kubeClient) { + checkFailed = true + } + + logger.Actionf("checking crds") + if !crdsCheck(ctx, kubeClient) { checkFailed = true } + if checkFailed { + logger.Failuref("check failed") os.Exit(1) } + logger.Successf("all checks passed") return nil } @@ -123,17 +150,11 @@ func fluxCheck() { return } if latestSv.GreaterThan(curSv) { - logger.Failuref("flux %s <%s (new version is available, please upgrade)", curSv, latestSv) + logger.Failuref("flux %s <%s (new CLI version is available, please upgrade)", curSv, latestSv) } } -func kubernetesCheck(constraints []string) bool { - cfg, err := utils.KubeConfig(kubeconfigArgs) - if err != nil { - logger.Failuref("Kubernetes client initialization failed: %s", err.Error()) - return false - } - +func kubernetesCheck(cfg *rest.Config, constraints []string) bool { clientSet, err := kubernetes.NewForConfig(cfg) if err != nil { logger.Failuref("Kubernetes client initialization failed: %s", err.Error()) @@ -172,21 +193,8 @@ func kubernetesCheck(constraints []string) bool { return true } -func componentsCheck() bool { - ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) - defer cancel() - - kubeConfig, err := utils.KubeConfig(kubeconfigArgs) - if err != nil { - return false - } - - statusChecker, err := status.NewStatusChecker(kubeConfig, checkArgs.pollInterval, rootArgs.timeout, logger) - if err != nil { - return false - } - - kubeClient, err := utils.KubeClient(kubeconfigArgs) +func componentsCheck(ctx context.Context, kubeClient client.Client) bool { + statusChecker, err := status.NewStatusCheckerWithClient(kubeClient, checkArgs.pollInterval, rootArgs.timeout, logger) if err != nil { return false } @@ -194,7 +202,14 @@ func componentsCheck() bool { ok := true selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} var list v1.DeploymentList - if err := kubeClient.List(ctx, &list, client.InNamespace(*kubeconfigArgs.Namespace), selector); err == nil { + ns := *kubeconfigArgs.Namespace + if err := kubeClient.List(ctx, &list, client.InNamespace(ns), selector); err == nil { + if len(list.Items) == 0 { + logger.Failuref("no controllers found in the '%s' namespace with the label selector '%s=%s'", + ns, manifestgen.PartOfLabelKey, manifestgen.PartOfLabelValue) + return false + } + for _, d := range list.Items { if ref, err := buildComponentObjectRefs(d.Name); err == nil { if err := statusChecker.Assess(ref...); err != nil { @@ -208,3 +223,41 @@ func componentsCheck() bool { } return ok } + +func crdsCheck(ctx context.Context, kubeClient client.Client) bool { + ok := true + selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} + var list apiextensionsv1.CustomResourceDefinitionList + if err := kubeClient.List(ctx, &list, client.InNamespace(*kubeconfigArgs.Namespace), selector); err == nil { + if len(list.Items) == 0 { + logger.Failuref("no crds found with the label selector '%s=%s'", + manifestgen.PartOfLabelKey, manifestgen.PartOfLabelValue) + return false + } + + for _, crd := range list.Items { + versions := crd.Status.StoredVersions + if len(versions) > 0 { + logger.Successf(crd.Name + "/" + versions[len(versions)-1]) + } else { + ok = false + logger.Failuref("no stored versions for %s", crd.Name) + } + } + } + return ok +} + +func fluxClusterVersionCheck(ctx context.Context, kubeClient client.Client) bool { + clusterInfo, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + logger.Failuref("checking failed: %s", err.Error()) + return false + } + + if clusterInfo.distribution() != "" { + logger.Successf("distribution: %s", clusterInfo.distribution()) + } + logger.Successf("bootstrapped: %t", clusterInfo.bootstrapped) + return true +} diff --git a/cmd/flux/check_test.go b/cmd/flux/check_test.go index e6374535..e5745915 100644 --- a/cmd/flux/check_test.go +++ b/cmd/flux/check_test.go @@ -25,8 +25,7 @@ import ( "strings" "testing" - "github.com/fluxcd/flux2/internal/utils" - "k8s.io/apimachinery/pkg/version" + "github.com/fluxcd/flux2/v2/internal/utils" ) func TestCheckPre(t *testing.T) { @@ -35,17 +34,19 @@ func TestCheckPre(t *testing.T) { t.Fatalf("Error running utils.ExecKubectlCommand: %v", err.Error()) } - var versions map[string]version.Info + var versions map[string]interface{} if err := json.Unmarshal([]byte(jsonOutput), &versions); err != nil { - t.Fatalf("Error unmarshalling: %v", err.Error()) + t.Fatalf("Error unmarshalling '%s': %v", jsonOutput, err.Error()) } - serverVersion := strings.TrimPrefix(versions["serverVersion"].GitVersion, "v") + serverGitVersion := strings.TrimPrefix( + versions["serverVersion"].(map[string]interface{})["gitVersion"].(string), + "v") cmd := cmdTestCase{ args: "check --pre", assert: assertGoldenTemplateFile("testdata/check/check_pre.golden", map[string]string{ - "serverVersion": serverVersion, + "serverVersion": serverGitVersion, }), } cmd.runTestCmd(t) diff --git a/cmd/flux/cluster_info.go b/cmd/flux/cluster_info.go new file mode 100644 index 00000000..2f2f79a3 --- /dev/null +++ b/cmd/flux/cluster_info.go @@ -0,0 +1,126 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + + "github.com/manifoldco/promptui" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + "github.com/fluxcd/flux2/v2/pkg/manifestgen" +) + +// bootstrapLabels are labels put on a resource by kustomize-controller. These labels on the CRD indicates +// that flux has been bootstrapped. +var bootstrapLabels = []string{ + fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group), + fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group), +} + +// fluxClusterInfo contains information about an existing flux installation on a cluster. +type fluxClusterInfo struct { + // bootstrapped indicates that Flux was installed using the `flux bootstrap` command. + bootstrapped bool + // managedBy is the name of the tool being used to manage the installation of Flux. + managedBy string + // partOf indicates which distribution the instance is a part of. + partOf string + // version is the Flux version number in semver format. + version string +} + +// getFluxClusterInfo returns information on the Flux installation running on the cluster. +// If an error occurred, the returned error will be non-nil. +// +// This function retrieves the GitRepository CRD from the cluster and checks it +// for a set of labels used to determine the Flux version and how Flux was installed. +// It returns the NotFound error from the underlying library if it was unable to find +// the GitRepository CRD and this can be used to check if Flux is installed. +func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, error) { + var info fluxClusterInfo + crdMetadata := &metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + APIVersion: apiextensionsv1.SchemeGroupVersion.String(), + Kind: "CustomResourceDefinition", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("gitrepositories.%s", sourcev1.GroupVersion.Group), + }, + } + if err := c.Get(ctx, client.ObjectKeyFromObject(crdMetadata), crdMetadata); err != nil { + return info, err + } + + info.version = crdMetadata.Labels[manifestgen.VersionLabelKey] + + var present bool + for _, l := range bootstrapLabels { + _, present = crdMetadata.Labels[l] + } + if present { + info.bootstrapped = true + } + + // the `app.kubernetes.io/managed-by` label is not set by flux but might be set by other + // tools used to install Flux e.g Helm. + if manager, ok := crdMetadata.Labels["app.kubernetes.io/managed-by"]; ok { + info.managedBy = manager + } + + if partOf, ok := crdMetadata.Labels[manifestgen.PartOfLabelKey]; ok { + info.partOf = partOf + } + return info, nil +} + +// confirmFluxInstallOverride displays a prompt to the user so that they can confirm before overriding +// a Flux installation. It returns nil if the installation should continue, +// promptui.ErrAbort if the user doesn't confirm, or an error encountered. +func confirmFluxInstallOverride(info fluxClusterInfo) error { + // no need to display prompt if installation is managed by Flux + if installManagedByFlux(info.managedBy) { + return nil + } + + display := fmt.Sprintf("Flux %s has been installed on this cluster with %s!", info.version, info.managedBy) + fmt.Fprintln(rootCmd.ErrOrStderr(), display) + prompt := promptui.Prompt{ + Label: fmt.Sprintf("Are you sure you want to override the %s installation? Y/N", info.managedBy), + IsConfirm: true, + } + _, err := prompt.Run() + return err +} + +func (info fluxClusterInfo) distribution() string { + distribution := info.version + if info.partOf != "" { + distribution = fmt.Sprintf("%s-%s", info.partOf, info.version) + } + return distribution +} + +func installManagedByFlux(manager string) bool { + return manager == "" || manager == "flux" +} diff --git a/cmd/flux/cluster_info_test.go b/cmd/flux/cluster_info_test.go new file mode 100644 index 00000000..86b94c9c --- /dev/null +++ b/cmd/flux/cluster_info_test.go @@ -0,0 +1,141 @@ +/* +Copyright 2023 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "os" + "testing" + + . "github.com/onsi/gomega" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + ssautil "github.com/fluxcd/pkg/ssa/utils" +) + +func Test_getFluxClusterInfo(t *testing.T) { + g := NewWithT(t) + f, err := os.Open("./testdata/cluster_info/gitrepositories.yaml") + g.Expect(err).To(BeNil()) + + objs, err := ssautil.ReadObjects(f) + g.Expect(err).To(Not(HaveOccurred())) + gitrepo := objs[0] + + tests := []struct { + name string + labels map[string]string + wantErr bool + wantInfo fluxClusterInfo + }{ + { + name: "no git repository CRD present", + wantErr: true, + }, + { + name: "CRD with kustomize-controller labels", + labels: map[string]string{ + fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): "flux-system", + fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system", + "app.kubernetes.io/version": "v2.1.0", + }, + wantInfo: fluxClusterInfo{ + version: "v2.1.0", + bootstrapped: true, + }, + }, + { + name: "CRD with kustomize-controller labels and managed-by label", + labels: map[string]string{ + fmt.Sprintf("%s/name", kustomizev1.GroupVersion.Group): "flux-system", + fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system", + "app.kubernetes.io/version": "v2.1.0", + "app.kubernetes.io/managed-by": "flux", + }, + wantInfo: fluxClusterInfo{ + version: "v2.1.0", + bootstrapped: true, + managedBy: "flux", + }, + }, + { + name: "CRD with only managed-by label", + labels: map[string]string{ + "app.kubernetes.io/version": "v2.1.0", + "app.kubernetes.io/managed-by": "helm", + }, + wantInfo: fluxClusterInfo{ + version: "v2.1.0", + managedBy: "helm", + }, + }, + { + name: "CRD with no labels", + labels: map[string]string{}, + wantInfo: fluxClusterInfo{}, + }, + { + name: "CRD with only version label", + labels: map[string]string{ + "app.kubernetes.io/version": "v2.1.0", + }, + wantInfo: fluxClusterInfo{ + version: "v2.1.0", + }, + }, + { + name: "CRD with version and part-of labels", + labels: map[string]string{ + "app.kubernetes.io/version": "v2.1.0", + "app.kubernetes.io/part-of": "flux", + }, + wantInfo: fluxClusterInfo{ + version: "v2.1.0", + partOf: "flux", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + newscheme := runtime.NewScheme() + apiextensionsv1.AddToScheme(newscheme) + builder := fake.NewClientBuilder().WithScheme(newscheme) + if tt.labels != nil { + gitrepo.SetLabels(tt.labels) + builder = builder.WithRuntimeObjects(gitrepo) + } + + client := builder.Build() + info, err := getFluxClusterInfo(context.Background(), client) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + } else { + g.Expect(err).To(Not(HaveOccurred())) + } + + g.Expect(info).To(BeEquivalentTo(tt.wantInfo)) + }) + } +} diff --git a/cmd/flux/completion.go b/cmd/flux/completion.go index 06beac2a..a3bc6696 100644 --- a/cmd/flux/completion.go +++ b/cmd/flux/completion.go @@ -20,7 +20,7 @@ import ( "context" "strings" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -31,7 +31,7 @@ import ( var completionCmd = &cobra.Command{ Use: "completion", 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() { @@ -60,7 +60,7 @@ func resourceNamesCompletionFunc(gvk schema.GroupVersionKind) func(cmd *cobra.Co ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - cfg, err := utils.KubeConfig(kubeconfigArgs) + cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return completionError(err) } diff --git a/cmd/flux/completion_bash.go b/cmd/flux/completion_bash.go index 42ce075c..872d4a9e 100644 --- a/cmd/flux/completion_bash.go +++ b/cmd/flux/completion_bash.go @@ -25,6 +25,7 @@ import ( var completionBashCmd = &cobra.Command{ Use: "bash", Short: "Generates bash completion scripts", + Long: `The completion sub-command generates completion scripts for bash.`, Example: `To load completion run . <(flux completion bash) diff --git a/cmd/flux/completion_fish.go b/cmd/flux/completion_fish.go index b8cb8cf8..fad5dcfe 100644 --- a/cmd/flux/completion_fish.go +++ b/cmd/flux/completion_fish.go @@ -25,6 +25,7 @@ import ( var completionFishCmd = &cobra.Command{ Use: "fish", Short: "Generates fish completion scripts", + Long: `The completion sub-command generates completion scripts for fish.`, Example: `To configure your fish shell to load completions for each session write this script to your completions dir: flux completion fish > ~/.config/fish/completions/flux.fish diff --git a/cmd/flux/completion_powershell.go b/cmd/flux/completion_powershell.go index 77efd87a..b0a17abc 100644 --- a/cmd/flux/completion_powershell.go +++ b/cmd/flux/completion_powershell.go @@ -25,6 +25,7 @@ import ( var completionPowerShellCmd = &cobra.Command{ Use: "powershell", Short: "Generates powershell completion scripts", + Long: `The completion sub-command generates completion scripts for powershell.`, Example: `To load completion run . <(flux completion powershell) @@ -34,12 +35,12 @@ To configure your powershell shell to load completions for each session add to y Windows: cd "$env:USERPROFILE\Documents\WindowsPowerShell\Modules" -flux completion >> flux-completion.ps1 +flux completion powershell >> flux-completion.ps1 Linux: cd "${XDG_CONFIG_HOME:-"$HOME/.config/"}/powershell/modules" -flux completion >> flux-completions.ps1`, +flux completion powershell >> flux-completions.ps1`, Run: func(cmd *cobra.Command, args []string) { rootCmd.GenPowerShellCompletion(os.Stdout) }, diff --git a/cmd/flux/completion_zsh.go b/cmd/flux/completion_zsh.go index a1fc0c01..79b26685 100644 --- a/cmd/flux/completion_zsh.go +++ b/cmd/flux/completion_zsh.go @@ -26,6 +26,7 @@ import ( var completionZshCmd = &cobra.Command{ Use: "zsh", Short: "Generates zsh completion scripts", + Long: `The completion sub-command generates completion scripts for zsh.`, Example: `To load completion run . <(flux completion zsh) diff --git a/cmd/flux/create.go b/cmd/flux/create.go index 758a2f54..209c8f0a 100644 --- a/cmd/flux/create.go +++ b/cmd/flux/create.go @@ -19,6 +19,7 @@ package main import ( "context" "fmt" + "regexp" "strings" "time" @@ -29,13 +30,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var createCmd = &cobra.Command{ Use: "create", 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 { @@ -51,6 +52,18 @@ func init() { createCmd.PersistentFlags().BoolVar(&createArgs.export, "export", false, "export in YAML format to stdout") createCmd.PersistentFlags().StringSliceVar(&createArgs.labels, "label", nil, "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) } @@ -66,12 +79,12 @@ type upsertable interface { // want to update. The mutate function is nullary -- you mutate a // value in the closure, e.g., by doing this: // -// var existing Value -// existing.Name = name -// existing.Namespace = ns -// upsert(ctx, client, valueAdapter{&value}, func() error { -// value.Spec = onePreparedEarlier -// }) +// var existing Value +// existing.Name = name +// existing.Namespace = ns +// upsert(ctx, client, valueAdapter{&value}, func() error { +// value.Spec = onePreparedEarlier +// }) func (names apiType) upsert(ctx context.Context, kubeClient client.Client, object upsertable, mutate func() error) (types.NamespacedName, error) { nsname := types.NamespacedName{ Namespace: object.GetNamespace(), @@ -104,7 +117,7 @@ func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) e ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) // NB globals + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) // NB globals if err != nil { return err } @@ -118,8 +131,8 @@ func (names apiType) upsertAndWait(object upsertWaitable, mutate func() error) e } logger.Waitingf("waiting for %s reconciliation", names.kind) - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isReady(ctx, kubeClient, namespacedName, object)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isObjectReadyConditionFunc(kubeClient, namespacedName, object.asClientObject())); err != nil { return err } logger.Successf("%s reconciliation completed", names.kind) @@ -150,3 +163,8 @@ func parseLabels() (map[string]string, error) { 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) +} diff --git a/cmd/flux/create_alert.go b/cmd/flux/create_alert.go index 0a5ea93a..80c8beee 100644 --- a/cmd/flux/create_alert.go +++ b/cmd/flux/create_alert.go @@ -22,22 +22,22 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var createAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Create or update a Alert resource", - Long: "The create alert command generates a Alert resource.", + Long: withPreviewNote(`The create alert command generates a Alert resource.`), Example: ` # Create an Alert for kustomization events flux create alert \ --event-severity info \ @@ -63,9 +63,6 @@ func init() { } func createAlertCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("Alert name is required") - } name := args[0] if alertArgs.providerRef == "" { @@ -99,13 +96,13 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error { logger.Generatef("generating Alert") } - alert := notificationv1.Alert{ + alert := notificationv1b3.Alert{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, - Spec: notificationv1.AlertSpec{ + Spec: notificationv1b3.AlertSpec{ ProviderRef: meta.LocalObjectReference{ Name: alertArgs.providerRef, }, @@ -122,7 +119,7 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -134,8 +131,8 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error { } logger.Waitingf("waiting for Alert reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isAlertReady(ctx, kubeClient, namespacedName, &alert)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isStaticObjectReadyConditionFunc(kubeClient, namespacedName, &alert)); err != nil { return err } logger.Successf("Alert %s is ready", name) @@ -143,13 +140,13 @@ func createAlertCmdRun(cmd *cobra.Command, args []string) error { } func upsertAlert(ctx context.Context, kubeClient client.Client, - alert *notificationv1.Alert) (types.NamespacedName, error) { + alert *notificationv1b3.Alert) (types.NamespacedName, error) { namespacedName := types.NamespacedName{ Namespace: alert.GetNamespace(), Name: alert.GetName(), } - var existing notificationv1.Alert + var existing notificationv1b3.Alert err := kubeClient.Get(ctx, namespacedName, &existing) if err != nil { if errors.IsNotFound(err) { @@ -172,23 +169,3 @@ func upsertAlert(ctx context.Context, kubeClient client.Client, logger.Successf("Alert updated") return namespacedName, nil } - -func isAlertReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, alert *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 - } -} diff --git a/cmd/flux/create_alertprovider.go b/cmd/flux/create_alertprovider.go index 7d5bf640..89a8e289 100644 --- a/cmd/flux/create_alertprovider.go +++ b/cmd/flux/create_alertprovider.go @@ -22,22 +22,21 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var createAlertProviderCmd = &cobra.Command{ Use: "alert-provider [name]", Short: "Create or update a Provider resource", - Long: "The create alert-provider command generates a Provider resource.", + Long: withPreviewNote(`The create alert-provider command generates a Provider resource.`), Example: ` # Create a Provider for a Slack channel flux create alert-provider slack \ --type slack \ @@ -73,9 +72,6 @@ func init() { } func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("Provider name is required") - } name := args[0] if alertProviderArgs.alertType == "" { @@ -118,7 +114,7 @@ func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -130,8 +126,8 @@ func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error { } logger.Waitingf("waiting for Provider reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isAlertProviderReady(ctx, kubeClient, namespacedName, &provider)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isStaticObjectReadyConditionFunc(kubeClient, namespacedName, &provider)); err != nil { return err } @@ -170,23 +166,3 @@ func upsertAlertProvider(ctx context.Context, kubeClient client.Client, logger.Successf("Provider updated") return namespacedName, nil } - -func isAlertProviderReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, provider *notificationv1.Provider) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, provider) - if err != nil { - return false, err - } - - if c := apimeta.FindStatusCondition(provider.Status.Conditions, meta.ReadyCondition); c != nil { - switch c.Status { - case metav1.ConditionTrue: - return true, nil - case metav1.ConditionFalse: - return false, fmt.Errorf(c.Message) - } - } - return false, nil - } -} diff --git a/cmd/flux/create_helmrelease.go b/cmd/flux/create_helmrelease.go index 2072c720..7e2bf3b3 100644 --- a/cmd/flux/create_helmrelease.go +++ b/cmd/flux/create_helmrelease.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +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. @@ -21,30 +21,33 @@ import ( "encoding/json" "fmt" "os" - - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/pkg/runtime/transform" + "strings" + "time" "github.com/spf13/cobra" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/transform" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" ) var createHelmReleaseCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Create or update a HelmRelease resource", - Long: "The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.", + Long: `The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.`, Example: ` # Create a HelmRelease with a chart from a HelmRepository source flux create hr podinfo \ --interval=10m \ @@ -81,9 +84,9 @@ var createHelmReleaseCmd = &cobra.Command{ # Create a HelmRelease with a custom release name flux create hr podinfo \ - --release-name=podinfo-dev + --release-name=podinfo-dev \ --source=HelmRepository/podinfo \ - --chart=podinfo \ + --chart=podinfo # Create a HelmRelease targeting another namespace than the resource flux create hr podinfo \ @@ -103,26 +106,44 @@ var createHelmReleaseCmd = &cobra.Command{ --source=HelmRepository/podinfo \ --chart=podinfo \ --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, } type helmReleaseFlags struct { - name string - source flags.HelmChartSource - dependsOn []string - chart string - chartVersion string - targetNamespace string - createNamespace bool - valuesFiles []string - valuesFrom flags.HelmReleaseValuesFrom - saName string - crds flags.CRDsPolicy + name string + source flags.HelmChartSource + dependsOn []string + chart string + chartVersion string + chartRef string + targetNamespace string + createNamespace bool + valuesFiles []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() { createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.name, "release-name", "", "name used for the Helm release, defaults to a composition of '[-]'") createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.source, "source", helmReleaseArgs.source.Description()) @@ -132,20 +153,21 @@ func init() { createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.targetNamespace, "target-namespace", "", "namespace to install this release, defaults to the HelmRelease namespace") createHelmReleaseCmd.Flags().BoolVar(&helmReleaseArgs.createNamespace, "create-target-namespace", false, "create the target namespace if it does not exist") createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.saName, "service-account", "", "the name of the service account to impersonate when reconciling this HelmRelease") + createHelmReleaseCmd.Flags().StringVar(&helmReleaseArgs.reconcileStrategy, "reconcile-strategy", "ChartVersion", "the reconcile strategy for helm chart created by the helm release(accepted values: Revision and ChartRevision)") + createHelmReleaseCmd.Flags().DurationVarP(&helmReleaseArgs.chartInterval, "chart-interval", "", 0, "the interval of which to check for new chart versions") createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFiles, "values", nil, "local path to values.yaml files, also accepts comma-separated values") - createHelmReleaseCmd.Flags().Var(&helmReleaseArgs.valuesFrom, "values-from", helmReleaseArgs.valuesFrom.Description()) + createHelmReleaseCmd.Flags().StringSliceVar(&helmReleaseArgs.valuesFrom, "values-from", nil, "a Kubernetes object reference that contains the values.yaml data key in the format '/', 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 '/.', where kind must be one of: (OCIRepository,HelmChart)") createCmd.AddCommand(createHelmReleaseCmd) } func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("HelmRelease name is required") - } name := args[0] - if helmReleaseArgs.chart == "" { - return fmt.Errorf("chart name or path is required") + if helmReleaseArgs.chart == "" && helmReleaseArgs.chartRef == "" { + return fmt.Errorf("chart or chart-ref is required") } sourceLabels, err := parseLabels() @@ -157,6 +179,11 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { 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{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -170,20 +197,48 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { Duration: createArgs.interval, }, TargetNamespace: helmReleaseArgs.targetNamespace, + Suspend: false, + }, + } - Chart: helmv2.HelmChartTemplate{ - Spec: helmv2.HelmChartTemplateSpec{ - Chart: helmReleaseArgs.chart, - Version: helmReleaseArgs.chartVersion, - SourceRef: helmv2.CrossNamespaceObjectReference{ - Kind: helmReleaseArgs.source.Kind, - Name: helmReleaseArgs.source.Name, - Namespace: helmReleaseArgs.source.Namespace, - }, + switch { + case helmReleaseArgs.chart != "": + helmRelease.Spec.Chart = &helmv2.HelmChartTemplate{ + Spec: helmv2.HelmChartTemplateSpec{ + Chart: helmReleaseArgs.chart, + Version: helmReleaseArgs.chartVersion, + SourceRef: helmv2.CrossNamespaceObjectReference{ + Kind: helmReleaseArgs.source.Kind, + Name: helmReleaseArgs.source.Name, + Namespace: helmReleaseArgs.source.Namespace, }, + ReconcileStrategy: helmReleaseArgs.reconcileStrategy, }, - Suspend: false, - }, + } + if helmReleaseArgs.chartInterval != 0 { + helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{ + Duration: helmReleaseArgs.chartInterval, + } + } + case helmReleaseArgs.chartRef != "": + kind, name, ns := utils.ParseObjectKindNameNamespace(helmReleaseArgs.chartRef) + if kind != sourcev1.HelmChartKind && kind != sourcev1b2.OCIRepositoryKind { + return fmt.Errorf("chart reference kind '%s' is not supported, must be one of: %s", + kind, strings.Join(supportedHelmReleaseReferenceKinds, ", ")) + } + helmRelease.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ + Kind: kind, + Name: name, + Namespace: ns, + } + } + + if helmReleaseArgs.kubeConfigSecretRef != "" { + helmRelease.Spec.KubeConfig = &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: helmReleaseArgs.kubeConfigSecretRef, + }, + } } if helmReleaseArgs.createNamespace { @@ -236,11 +291,25 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { helmRelease.Spec.Values = &apiextensionsv1.JSON{Raw: jsonRaw} } - if helmReleaseArgs.valuesFrom.String() != "" { - helmRelease.Spec.ValuesFrom = []helmv2.ValuesReference{{ - Kind: helmReleaseArgs.valuesFrom.Kind, - Name: helmReleaseArgs.valuesFrom.Name, - }} + if len(helmReleaseArgs.valuesFrom) != 0 { + 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 /", value) + } + cleanSourceKind, ok := utils.ContainsEqualFoldItemString(supportedHelmReleaseValuesFromKinds, sourceKind) + if !ok { + return fmt.Errorf("reference kind '%s' is not supported, must be one of: %s", + sourceKind, strings.Join(supportedHelmReleaseValuesFromKinds, ", ")) + } + + values = append(values, helmv2.ValuesReference{ + Name: sourceName, + Kind: cleanSourceKind, + }) + } + helmRelease.Spec.ValuesFrom = values } if createArgs.export { @@ -250,7 +319,7 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -262,13 +331,13 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { } logger.Waitingf("waiting for HelmRelease reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isHelmReleaseReady(ctx, kubeClient, namespacedName, &helmRelease)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isObjectReadyConditionFunc(kubeClient, namespacedName, &helmRelease)); err != nil { return err } logger.Successf("HelmRelease %s is ready", name) - logger.Successf("applied revision %s", helmRelease.Status.LastAppliedRevision) + logger.Successf("applied revision %s", getHelmReleaseRevision(helmRelease)) return nil } @@ -303,19 +372,14 @@ func upsertHelmRelease(ctx context.Context, kubeClient client.Client, return namespacedName, nil } -func isHelmReleaseReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, helmRelease *helmv2.HelmRelease) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, helmRelease) - if err != nil { - return false, err - } +func validateStrategy(input string) bool { + allowedStrategy := []string{"Revision", "ChartVersion"} - // Confirm the state we are observing is for the current generation - if helmRelease.Generation != helmRelease.Status.ObservedGeneration { - return false, nil + for _, strategy := range allowedStrategy { + if strategy == input { + return true } - - return apimeta.IsStatusConditionTrue(helmRelease.Status.Conditions, meta.ReadyCondition), nil } + + return false } diff --git a/cmd/flux/create_helmrelease_test.go b/cmd/flux/create_helmrelease_test.go new file mode 100644 index 00000000..ffdef081 --- /dev/null +++ b/cmd/flux/create_helmrelease_test.go @@ -0,0 +1,86 @@ +//go:build unit +// +build unit + +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import "testing" + +func TestCreateHelmRelease(t *testing.T) { + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setupHRSource(t, tmpl) + + tests := []struct { + name string + args string + assert assertFunc + }{ + { + name: "missing name", + args: "create helmrelease --export", + assert: assertError("name is required"), + }, + { + name: "missing chart template and chartRef", + args: "create helmrelease podinfo --export", + assert: assertError("chart or chart-ref is required"), + }, + { + name: "unknown source kind", + args: "create helmrelease podinfo --source foobar/podinfo --chart podinfo --export", + assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`), + }, + { + name: "unknown chart reference kind", + args: "create helmrelease podinfo --chart-ref foobar/podinfo --export", + assert: assertError(`chart reference kind 'foobar' is not supported, must be one of: OCIRepository, HelmChart`), + }, + { + name: "basic helmrelease", + args: "create helmrelease podinfo --source Helmrepository/podinfo --chart podinfo --interval=1m0s --export", + assert: assertGoldenTemplateFile("testdata/create_hr/basic.yaml", tmpl), + }, + { + name: "chart with OCIRepository source", + args: "create helmrelease podinfo --chart-ref OCIRepository/podinfo --interval=1m0s --export", + assert: assertGoldenTemplateFile("testdata/create_hr/or_basic.yaml", tmpl), + }, + { + name: "chart with HelmChart source", + args: "create helmrelease podinfo --chart-ref HelmChart/podinfo --interval=1m0s --export", + assert: assertGoldenTemplateFile("testdata/create_hr/hc_basic.yaml", tmpl), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} + +func setupHRSource(t *testing.T, tmpl map[string]string) { + t.Helper() + testEnv.CreateObjectFile("./testdata/create_hr/setup-source.yaml", tmpl, t) +} diff --git a/cmd/flux/create_image.go b/cmd/flux/create_image.go index b6402906..f1bf165d 100644 --- a/cmd/flux/create_image.go +++ b/cmd/flux/create_image.go @@ -20,14 +20,12 @@ import ( "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{ Use: "image", Short: "Create or update resources dealing with image automation", - Long: createImageLong, + Long: `The create image sub-commands work with image automation objects; +that is, object controlling updates to git based on e.g., new container images +being available.`, } func init() { diff --git a/cmd/flux/create_image_policy.go b/cmd/flux/create_image_policy.go index 05cb5c14..db287ed2 100644 --- a/cmd/flux/create_image_policy.go +++ b/cmd/flux/create_image_policy.go @@ -28,18 +28,18 @@ import ( "github.com/fluxcd/pkg/apis/meta" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var createImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Create or update an ImagePolicy object", - Long: `The create image policy command generates an ImagePolicy resource. + Long: withPreviewNote(`The create image policy command generates an ImagePolicy resource. An ImagePolicy object calculates a "latest image" given an image repository and a policy, e.g., semver. 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 \ @@ -54,13 +54,12 @@ the status of the object.`, RunE: createImagePolicyRun} type imagePolicyFlags struct { - imageRef string - semver string - alpha string - numeric string - filterRegex string - filterExtract string - filterNumerical string + imageRef string + semver string + alpha string + numeric string + filterRegex string + filterExtract string } var imagePolicyArgs = imagePolicyFlags{} @@ -84,9 +83,6 @@ func (obj imagePolicyAdapter) getObservedGeneration() int64 { } func createImagePolicyRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("ImagePolicy name is required") - } objectName := args[0] if imagePolicyArgs.imageRef == "" { @@ -186,7 +182,6 @@ func validateExtractStr(template string, capNames []string) error { name, num, rest, ok := extract(template) if !ok { // Malformed extract string, assume user didn't want this - template = template[1:] return fmt.Errorf("--filter-extract is malformed") } template = rest diff --git a/cmd/flux/create_image_repository.go b/cmd/flux/create_image_repository.go index b5ffdcb8..3a4d351d 100644 --- a/cmd/flux/create_image_repository.go +++ b/cmd/flux/create_image_repository.go @@ -26,14 +26,14 @@ import ( "github.com/fluxcd/pkg/apis/meta" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var createImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Create or update an ImageRepository object", - Long: `The create image repository command generates an ImageRepository resource. -An ImageRepository object specifies an image repository to scan.`, + Long: withPreviewNote(`The create image repository command generates an ImageRepository resource. +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 @@ -83,9 +83,6 @@ func init() { } func createImageRepositoryRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("ImageRepository name is required") - } objectName := args[0] if imageRepoArgs.image == "" { diff --git a/cmd/flux/create_image_update.go b/cmd/flux/create_image_update.go index 2adb562c..8e4e7366 100644 --- a/cmd/flux/create_image_update.go +++ b/cmd/flux/create_image_update.go @@ -22,16 +22,16 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + 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: `The create image update command generates an ImageUpdateAutomation resource. + Long: withPreviewNote(`The create image update command generates an ImageUpdateAutomation resource. An ImageUpdateAutomation object specifies an automated update to images -mentioned in YAMLs in a git repository.`, +mentioned in YAMLs in a git repository.`), Example: ` # Configure image updates for the main repository created by flux bootstrap flux create image update flux-system \ --git-repo-ref=flux-system \ @@ -49,25 +49,40 @@ mentioned in YAMLs in a git repository.`, --push-branch=image-updates \ --author-name=flux \ --author-email=flux@example.com \ - --commit-template="{{range .Updated.Images}}{{println .}}{{end}}"`, + --commit-template="{{range .Updated.Images}}{{println .}}{{end}}" + + # Configure image updates for a Git repository in a different namespace + flux create image update apps \ + --namespace=apps \ + --git-repo-ref=flux-system \ + --git-repo-namespace=flux-system \ + --git-repo-path="./clusters/my-cluster" \ + --checkout-branch=main \ + --push-branch=image-updates \ + --author-name=flux \ + --author-email=flux@example.com \ + --commit-template="{{range .Updated.Images}}{{println .}}{{end}}" +`, RunE: createImageUpdateRun, } type imageUpdateFlags struct { - gitRepoRef string - gitRepoPath string - checkoutBranch string - pushBranch string - commitTemplate string - authorName string - authorEmail string + 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.gitRepoRef, "git-repo-ref", "", "the name of a GitRepository resource with details of the upstream Git repository") + flags.StringVar(&imageUpdateArgs.gitRepoName, "git-repo-ref", "", "the name of a GitRepository resource with details of the upstream Git repository") + flags.StringVar(&imageUpdateArgs.gitRepoNamespace, "git-repo-namespace", "", "the namespace of the GitRepository resource, defaults to the ImageUpdateAutomation namespace") flags.StringVar(&imageUpdateArgs.gitRepoPath, "git-repo-path", "", "path to the directory containing the manifests to be updated, defaults to the repository root") flags.StringVar(&imageUpdateArgs.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") @@ -79,12 +94,9 @@ func init() { } 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 == "" { + if imageUpdateArgs.gitRepoName == "" { return fmt.Errorf("a reference to a GitRepository is required (--git-repo-ref)") } @@ -112,9 +124,10 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error { Labels: labels, }, Spec: autov1.ImageUpdateAutomationSpec{ - SourceRef: autov1.SourceReference{ - Kind: sourcev1.GitRepositoryKind, - Name: imageUpdateArgs.gitRepoRef, + SourceRef: autov1.CrossNamespaceSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: imageUpdateArgs.gitRepoName, + Namespace: imageUpdateArgs.gitRepoNamespace, }, GitSpec: &autov1.GitSpec{ diff --git a/cmd/flux/create_kustomization.go b/cmd/flux/create_kustomization.go index d523374d..6c662928 100644 --- a/cmd/flux/create_kustomization.go +++ b/cmd/flux/create_kustomization.go @@ -24,40 +24,38 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + helmv2 "github.com/fluxcd/helm-controller/api/v2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" ) var createKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Create or update a Kustomization resource", - Long: "The kustomization source create command generates a Kustomize resource for a given source.", + Long: `The create command generates a Kustomization resource for a given source.`, Example: ` # Create a Kustomization resource from a source at a given path - flux create kustomization contour \ - --source=GitRepository/contour \ - --path="./examples/contour/" \ + flux create kustomization kyverno \ + --source=GitRepository/kyverno \ + --path="./config/release" \ --prune=true \ - --interval=10m \ - --health-check="Deployment/contour.projectcontour" \ - --health-check="DaemonSet/envoy.projectcontour" \ + --interval=60m \ + --wait=true \ --health-check-timeout=3m # Create a Kustomization resource that depends on the previous one - flux create kustomization webapp \ - --depends-on=contour \ - --source=GitRepository/webapp \ - --path="./deploy/overlays/dev" \ + flux create kustomization kyverno-policies \ + --depends-on=kyverno \ + --source=GitRepository/kyverno-policies \ + --path="./policies/flux" \ --prune=true \ --interval=5m @@ -65,7 +63,14 @@ var createKsCmd = &cobra.Command{ flux create kustomization podinfo \ --namespace=default \ --source=GitRepository/podinfo.flux-system \ - --path="./deploy/overlays/dev" \ + --path="./kustomize" \ + --prune=true \ + --interval=5m + + # Create a Kustomization resource that references an OCIRepository + flux create kustomization podinfo \ + --source=OCIRepository/podinfo \ + --target-namespace=default \ --prune=true \ --interval=5m @@ -78,18 +83,20 @@ var createKsCmd = &cobra.Command{ } type kustomizationFlags struct { - source flags.KustomizationSource - path flags.SafeRelativePath - prune bool - dependsOn []string - validation string - healthCheck []string - healthTimeout time.Duration - saName string - decryptionProvider flags.DecryptionProvider - decryptionSecret string - targetNamespace string - wait bool + source flags.KustomizationSource + path flags.SafeRelativePath + prune bool + dependsOn []string + validation string + healthCheck []string + healthTimeout time.Duration + saName string + decryptionProvider flags.DecryptionProvider + decryptionSecret string + targetNamespace string + wait bool + kubeConfigSecretRef string + retryInterval time.Duration } var kustomizationArgs = NewKustomizationFlags() @@ -107,7 +114,9 @@ func init() { createKsCmd.Flags().Var(&kustomizationArgs.decryptionProvider, "decryption-provider", kustomizationArgs.decryptionProvider.Description()) createKsCmd.Flags().StringVar(&kustomizationArgs.decryptionSecret, "decryption-secret", "", "set the Kubernetes secret name that contains the OpenPGP private keys used for sops decryption") createKsCmd.Flags().StringVar(&kustomizationArgs.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) } @@ -119,9 +128,6 @@ func NewKustomizationFlags() kustomizationFlags { } func createKsCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("Kustomization name is required") - } name := args[0] if kustomizationArgs.path == "" { @@ -163,6 +169,14 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error { }, } + if kustomizationArgs.kubeConfigSecretRef != "" { + kustomization.Spec.KubeConfig = &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: kustomizationArgs.kubeConfigSecretRef, + }, + } + } + if len(kustomizationArgs.healthCheck) > 0 && !kustomizationArgs.wait { healthChecks := make([]meta.NamespacedObjectKindReference, 0) for _, w := range kustomizationArgs.healthCheck { @@ -225,6 +239,10 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error { } } + if kustomizationArgs.retryInterval > 0 { + kustomization.Spec.RetryInterval = &metav1.Duration{Duration: kustomizationArgs.retryInterval} + } + if createArgs.export { return printExport(exportKs(&kustomization)) } @@ -232,7 +250,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -244,8 +262,8 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error { } logger.Waitingf("waiting for Kustomization reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isKustomizationReady(ctx, kubeClient, namespacedName, &kustomization)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isObjectReadyConditionFunc(kubeClient, namespacedName, &kustomization)); err != nil { return err } logger.Successf("Kustomization %s is ready", name) @@ -284,28 +302,3 @@ func upsertKustomization(ctx context.Context, kubeClient client.Client, logger.Successf("Kustomization updated") return namespacedName, nil } - -func isKustomizationReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, kustomization *kustomizev1.Kustomization) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, kustomization) - if err != nil { - return false, err - } - - // Confirm the state we are observing is for the current generation - if kustomization.Generation != kustomization.Status.ObservedGeneration { - return false, nil - } - - if c := apimeta.FindStatusCondition(kustomization.Status.Conditions, meta.ReadyCondition); c != nil { - switch c.Status { - case metav1.ConditionTrue: - return true, nil - case metav1.ConditionFalse: - return false, fmt.Errorf(c.Message) - } - } - return false, nil - } -} diff --git a/cmd/flux/create_receiver.go b/cmd/flux/create_receiver.go index d6004dc1..ad6436cf 100644 --- a/cmd/flux/create_receiver.go +++ b/cmd/flux/create_receiver.go @@ -22,22 +22,21 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var createReceiverCmd = &cobra.Command{ Use: "receiver [name]", 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 flux create receiver github-receiver \ --type github \ @@ -67,9 +66,6 @@ func init() { } func createReceiverCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("Receiver name is required") - } name := args[0] if receiverArgs.receiverType == "" { @@ -130,7 +126,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -142,13 +138,13 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error { } logger.Waitingf("waiting for Receiver reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isReceiverReady(ctx, kubeClient, namespacedName, &receiver)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isObjectReadyConditionFunc(kubeClient, namespacedName, &receiver)); err != nil { return err } logger.Successf("Receiver %s is ready", name) - logger.Successf("generated webhook URL %s", receiver.Status.URL) + logger.Successf("generated webhook URL %s", receiver.Status.WebhookPath) return nil } @@ -182,23 +178,3 @@ func upsertReceiver(ctx context.Context, kubeClient client.Client, logger.Successf("Receiver updated") return namespacedName, nil } - -func isReceiverReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, receiver *notificationv1.Receiver) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, receiver) - if err != nil { - return false, err - } - - if c := apimeta.FindStatusCondition(receiver.Status.Conditions, meta.ReadyCondition); c != nil { - switch c.Status { - case metav1.ConditionTrue: - return true, nil - case metav1.ConditionFalse: - return false, fmt.Errorf(c.Message) - } - } - return false, nil - } -} diff --git a/cmd/flux/create_secret.go b/cmd/flux/create_secret.go index 9aad039c..376fd24f 100644 --- a/cmd/flux/create_secret.go +++ b/cmd/flux/create_secret.go @@ -29,7 +29,7 @@ import ( var createSecretCmd = &cobra.Command{ Use: "secret", 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() { diff --git a/cmd/flux/create_secret_git.go b/cmd/flux/create_secret_git.go index 769c2526..9865cbc9 100644 --- a/cmd/flux/create_secret_git.go +++ b/cmd/flux/create_secret_git.go @@ -21,22 +21,25 @@ import ( "crypto/elliptic" "fmt" "net/url" + "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var createSecretGitCmd = &cobra.Command{ Use: "git [name]", Short: "Create or update a Kubernetes secret for Git authentication", Long: `The create secret git command generates a Kubernetes secret with Git credentials. -For Git over SSH, the host and SSH keys are automatically generated and stored in the secret. -For Git over HTTP/S, the provided basic authentication credentials are stored 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 +authentication token are stored in the secret.`, Example: ` # Create a Git SSH authentication secret using an ECDSA P-521 curve public key flux create secret git podinfo-auth \ @@ -85,7 +88,9 @@ type secretGitFlags struct { rsaBits flags.RSAKeyBits ecdsaCurve flags.ECDSACurve caFile string + caCrtFile string privateKeyFile string + bearerToken string } var secretGitArgs = NewSecretGitFlags() @@ -98,7 +103,9 @@ func init() { createSecretGitCmd.Flags().Var(&secretGitArgs.rsaBits, "ssh-rsa-bits", secretGitArgs.rsaBits.Description()) createSecretGitCmd.Flags().Var(&secretGitArgs.ecdsaCurve, "ssh-ecdsa-curve", secretGitArgs.ecdsaCurve.Description()) createSecretGitCmd.Flags().StringVar(&secretGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates") + createSecretGitCmd.Flags().StringVar(&secretGitArgs.caCrtFile, "ca-crt-file", "", "path to TLS CA certificate file used for validating self-signed certificates; takes precedence over --ca-file") createSecretGitCmd.Flags().StringVar(&secretGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server") + createSecretGitCmd.Flags().StringVar(&secretGitArgs.bearerToken, "bearer-token", "", "bearer authentication token") createSecretCmd.AddCommand(createSecretGitCmd) } @@ -112,9 +119,6 @@ func NewSecretGitFlags() secretGitFlags { } func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("secret name is required") - } name := args[0] if secretGitArgs.url == "" { return fmt.Errorf("url is required") @@ -138,19 +142,39 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { } switch u.Scheme { case "ssh": + keypair, err := sourcesecret.LoadKeyPairFromPath(secretGitArgs.privateKeyFile, secretGitArgs.password) + if err != nil { + return err + } + opts.Keypair = keypair opts.SSHHostname = u.Host - opts.PrivateKeyPath = secretGitArgs.privateKeyFile opts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(secretGitArgs.keyAlgorithm) opts.RSAKeyBits = int(secretGitArgs.rsaBits) opts.ECDSACurve = secretGitArgs.ecdsaCurve.Curve opts.Password = secretGitArgs.password case "http", "https": - if secretGitArgs.username == "" || secretGitArgs.password == "" { - return fmt.Errorf("for Git over HTTP/S the username and password are required") + 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 - opts.CAFilePath = secretGitArgs.caFile + opts.BearerToken = secretGitArgs.bearerToken + if secretGitArgs.username != "" && secretGitArgs.password != "" && secretGitArgs.bearerToken != "" { + return fmt.Errorf("user credentials and bearer token cannot be used together") + } + + // --ca-crt-file takes precedence over --ca-file. + if secretGitArgs.caCrtFile != "" { + opts.CACrt, err = os.ReadFile(secretGitArgs.caCrtFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } else if secretGitArgs.caFile != "" { + opts.CAFile, err = os.ReadFile(secretGitArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } default: return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme) } @@ -176,7 +200,7 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } diff --git a/cmd/flux/create_secret_git_test.go b/cmd/flux/create_secret_git_test.go index 01a3e929..d4c84d0d 100644 --- a/cmd/flux/create_secret_git_test.go +++ b/cmd/flux/create_secret_git_test.go @@ -1,10 +1,21 @@ 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 @@ -13,7 +24,7 @@ func TestCreateGitSecret(t *testing.T) { { name: "no args", args: "create secret git", - assert: assertError("secret name is required"), + assert: assertError("name is required"), }, { name: "basic secret", @@ -30,6 +41,21 @@ func TestCreateGitSecret(t *testing.T) { args: "create secret git podinfo-auth --url=ssh://git@github.com/stefanprodan/podinfo --private-key-file=./testdata/create_secret/git/ecdsa-password.private --password=password --namespace=my-namespace --export", 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 { diff --git a/cmd/flux/create_secret_helm.go b/cmd/flux/create_secret_helm.go index 8f9df1b5..2a809366 100644 --- a/cmd/flux/create_secret_helm.go +++ b/cmd/flux/create_secret_helm.go @@ -19,13 +19,14 @@ package main import ( "context" "fmt" + "os" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var createSecretHelmCmd = &cobra.Command{ @@ -40,15 +41,8 @@ var createSecretHelmCmd = &cobra.Command{ --export > repo-auth.yaml sops --encrypt --encrypted-regex '^(data|stringData)$' \ - --in-place repo-auth.yaml + --in-place repo-auth.yaml`, - # Create a Helm authentication secret using a custom TLS cert - flux create secret helm repo-auth \ - --username=username \ - --password=password \ - --cert-file=./cert.crt \ - --key-file=./key.crt \ - --ca-file=./ca.crt`, RunE: createSecretHelmCmdRun, } @@ -61,16 +55,20 @@ type secretHelmFlags struct { var secretHelmArgs secretHelmFlags func init() { - createSecretHelmCmd.Flags().StringVarP(&secretHelmArgs.username, "username", "u", "", "basic authentication username") - createSecretHelmCmd.Flags().StringVarP(&secretHelmArgs.password, "password", "p", "", "basic authentication password") - initSecretTLSFlags(createSecretHelmCmd.Flags(), &secretHelmArgs.secretTLSFlags) + flags := createSecretHelmCmd.Flags() + flags.StringVarP(&secretHelmArgs.username, "username", "u", "", "basic authentication username") + flags.StringVarP(&secretHelmArgs.password, "password", "p", "", "basic authentication password") + + initSecretDeprecatedTLSFlags(flags, &secretHelmArgs.secretTLSFlags) + deprecationMsg := "please use the command `flux create secret tls` to generate TLS secrets" + flags.MarkDeprecated("cert-file", deprecationMsg) + flags.MarkDeprecated("key-file", deprecationMsg) + flags.MarkDeprecated("ca-file", deprecationMsg) + createSecretCmd.AddCommand(createSecretHelmCmd) } func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("secret name is required") - } name := args[0] labels, err := parseLabels() @@ -78,15 +76,34 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error { return err } + caBundle := []byte{} + if secretHelmArgs.caFile != "" { + var err error + caBundle, err = os.ReadFile(secretHelmArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } + + var certFile, keyFile []byte + if secretHelmArgs.certFile != "" && secretHelmArgs.keyFile != "" { + if certFile, err = os.ReadFile(secretHelmArgs.certFile); err != nil { + return fmt.Errorf("failed to read cert file: %w", err) + } + if keyFile, err = os.ReadFile(secretHelmArgs.keyFile); err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + } + opts := sourcesecret.Options{ - Name: name, - Namespace: *kubeconfigArgs.Namespace, - Labels: labels, - Username: secretHelmArgs.username, - Password: secretHelmArgs.password, - CAFilePath: secretHelmArgs.caFile, - CertFilePath: secretHelmArgs.certFile, - KeyFilePath: secretHelmArgs.keyFile, + Name: name, + Namespace: *kubeconfigArgs.Namespace, + Labels: labels, + Username: secretHelmArgs.username, + Password: secretHelmArgs.password, + CAFile: caBundle, + CertFile: certFile, + KeyFile: keyFile, } secret, err := sourcesecret.Generate(opts) if err != nil { @@ -100,7 +117,7 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } diff --git a/cmd/flux/create_secret_helm_test.go b/cmd/flux/create_secret_helm_test.go index 04c96dbb..821f8311 100644 --- a/cmd/flux/create_secret_helm_test.go +++ b/cmd/flux/create_secret_helm_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package main import ( @@ -12,7 +28,7 @@ func TestCreateHelmSecret(t *testing.T) { }{ { args: "create secret helm", - assert: assertError("secret name is required"), + assert: assertError("name is required"), }, { args: "create secret helm helm-secret --username=my-username --password=my-password --namespace=my-namespace --export", diff --git a/cmd/flux/create_secret_notation.go b/cmd/flux/create_secret_notation.go new file mode 100644 index 00000000..dae49a4c --- /dev/null +++ b/cmd/flux/create_secret_notation.go @@ -0,0 +1,161 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +var createSecretNotationCmd = &cobra.Command{ + Use: "notation [name]", + Short: "Create or update a Kubernetes secret for verifications of artifacts signed by Notation", + Long: withPreviewNote(`The create secret notation command generates a Kubernetes secret with root ca certificates and trust policy.`), + Example: ` # Create a Notation configuration secret on disk and encrypt it with Mozilla SOPS + flux create secret notation my-notation-cert \ + --namespace=my-namespace \ + --trust-policy-file=./my-trust-policy.json \ + --ca-cert-file=./my-cert.crt \ + --export > my-notation-cert.yaml + + sops --encrypt --encrypted-regex '^(data|stringData)$' \ + --in-place my-notation-cert.yaml`, + + RunE: createSecretNotationCmdRun, +} + +type secretNotationFlags struct { + trustPolicyFile string + caCrtFile []string +} + +var secretNotationArgs secretNotationFlags + +func init() { + createSecretNotationCmd.Flags().StringVar(&secretNotationArgs.trustPolicyFile, "trust-policy-file", "", "notation trust policy file path") + createSecretNotationCmd.Flags().StringSliceVar(&secretNotationArgs.caCrtFile, "ca-cert-file", []string{}, "root ca cert file path") + + createSecretCmd.AddCommand(createSecretNotationCmd) +} + +func createSecretNotationCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("name is required") + } + + if secretNotationArgs.caCrtFile == nil || len(secretNotationArgs.caCrtFile) == 0 { + return fmt.Errorf("--ca-cert-file is required") + } + + if secretNotationArgs.trustPolicyFile == "" { + return fmt.Errorf("--trust-policy-file is required") + } + + name := args[0] + + labels, err := parseLabels() + if err != nil { + return err + } + + policy, err := os.ReadFile(secretNotationArgs.trustPolicyFile) + if err != nil { + return fmt.Errorf("unable to read trust policy file: %w", err) + } + + var doc trustpolicy.Document + + if err := json.Unmarshal(policy, &doc); err != nil { + return fmt.Errorf("failed to unmarshal trust policy %s: %w", secretNotationArgs.trustPolicyFile, err) + } + + if err := doc.Validate(); err != nil { + return fmt.Errorf("invalid trust policy: %w", err) + } + + var ( + caCerts []sourcesecret.VerificationCrt + fileErr error + ) + for _, caCrtFile := range secretNotationArgs.caCrtFile { + fileName := filepath.Base(caCrtFile) + if !strings.HasSuffix(fileName, ".crt") && !strings.HasSuffix(fileName, ".pem") { + fileErr = errors.Join(fileErr, fmt.Errorf("%s must end with either .crt or .pem", fileName)) + continue + } + caBundle, err := os.ReadFile(caCrtFile) + if err != nil { + fileErr = errors.Join(fileErr, fmt.Errorf("unable to read TLS CA file: %w", err)) + continue + } + caCerts = append(caCerts, sourcesecret.VerificationCrt{Name: fileName, CACrt: caBundle}) + } + + if fileErr != nil { + return fileErr + } + + if len(caCerts) == 0 { + return fmt.Errorf("no CA certs found") + } + + opts := sourcesecret.Options{ + Name: name, + Namespace: *kubeconfigArgs.Namespace, + Labels: labels, + VerificationCrts: caCerts, + TrustPolicy: policy, + } + secret, err := sourcesecret.Generate(opts) + if err != nil { + return err + } + + if createArgs.export { + rootCmd.Println(secret.Content) + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + var s corev1.Secret + if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { + return err + } + if err := upsertSecret(ctx, kubeClient, s); err != nil { + return err + } + + logger.Actionf("notation configuration secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) + return nil +} diff --git a/cmd/flux/create_secret_notation_test.go b/cmd/flux/create_secret_notation_test.go new file mode 100644 index 00000000..c5944c38 --- /dev/null +++ b/cmd/flux/create_secret_notation_test.go @@ -0,0 +1,124 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "testing" +) + +const ( + trustPolicy = "./testdata/create_secret/notation/test-trust-policy.json" + invalidTrustPolicy = "./testdata/create_secret/notation/invalid-trust-policy.json" + invalidJson = "./testdata/create_secret/notation/invalid.json" + testCertFolder = "./testdata/create_secret/notation" +) + +func TestCreateNotationSecret(t *testing.T) { + crt, err := os.Create(filepath.Join(t.TempDir(), "ca.crt")) + if err != nil { + t.Fatal("could not create ca.crt file") + } + + pem, err := os.Create(filepath.Join(t.TempDir(), "ca.pem")) + if err != nil { + t.Fatal("could not create ca.pem file") + } + + invalidCert, err := os.Create(filepath.Join(t.TempDir(), "ca.p12")) + if err != nil { + t.Fatal("could not create ca.p12 file") + } + + _, err = crt.Write([]byte("ca-data-crt")) + if err != nil { + t.Fatal("could not write to crt certificate file") + } + + _, err = pem.Write([]byte("ca-data-pem")) + if err != nil { + t.Fatal("could not write to pem certificate file") + } + + tests := []struct { + name string + args string + assert assertFunc + }{ + { + name: "no args", + args: "create secret notation", + assert: assertError("name is required"), + }, + { + name: "no trust policy", + args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s", testCertFolder), + assert: assertError("--trust-policy-file is required"), + }, + { + name: "no cert", + args: fmt.Sprintf("create secret notation notation-config --trust-policy-file=%s", trustPolicy), + assert: assertError("--ca-cert-file is required"), + }, + { + name: "non pem and crt cert", + args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", invalidCert.Name(), trustPolicy), + assert: assertError("ca.p12 must end with either .crt or .pem"), + }, + { + name: "invalid trust policy", + args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", t.TempDir(), invalidTrustPolicy), + assert: assertError("invalid trust policy: a trust policy statement is missing a name, every statement requires a name"), + }, + { + name: "invalid trust policy json", + args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s", t.TempDir(), invalidJson), + assert: assertError(fmt.Sprintf("failed to unmarshal trust policy %s: json: cannot unmarshal string into Go value of type trustpolicy.Document", invalidJson)), + }, + { + name: "crt secret", + args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", crt.Name(), trustPolicy), + assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-crt.yaml"), + }, + { + name: "pem secret", + args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", pem.Name(), trustPolicy), + assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-pem.yaml"), + }, + { + name: "multi secret", + args: fmt.Sprintf("create secret notation notation-config --ca-cert-file=%s --ca-cert-file=%s --trust-policy-file=%s --namespace=my-namespace --export", crt.Name(), pem.Name(), trustPolicy), + assert: assertGoldenFile("./testdata/create_secret/notation/secret-ca-multi.yaml"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + secretNotationArgs = secretNotationFlags{} + }() + + cmd := cmdTestCase{ + args: tt.args, + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/create_secret_oci.go b/cmd/flux/create_secret_oci.go new file mode 100644 index 00000000..b1a51cd6 --- /dev/null +++ b/cmd/flux/create_secret_oci.go @@ -0,0 +1,121 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" + "github.com/google/go-containerregistry/pkg/name" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +var createSecretOCICmd = &cobra.Command{ + Use: "oci [name]", + Short: "Create or update a Kubernetes image pull secret", + Long: withPreviewNote(`The create secret oci command generates a Kubernetes secret that can be used for OCIRepository authentication`), + Example: ` # Create an OCI authentication secret on disk and encrypt it with Mozilla SOPS + flux create secret oci podinfo-auth \ + --url=ghcr.io \ + --username=username \ + --password=password \ + --export > repo-auth.yaml + + sops --encrypt --encrypted-regex '^(data|stringData)$' \ + --in-place repo-auth.yaml + `, + RunE: createSecretOCICmdRun, +} + +type secretOCIFlags struct { + url string + password string + username string +} + +var secretOCIArgs = secretOCIFlags{} + +func init() { + createSecretOCICmd.Flags().StringVar(&secretOCIArgs.url, "url", "", "oci repository address e.g ghcr.io/stefanprodan/charts") + createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.username, "username", "u", "", "basic authentication username") + createSecretOCICmd.Flags().StringVarP(&secretOCIArgs.password, "password", "p", "", "basic authentication password") + + createSecretCmd.AddCommand(createSecretOCICmd) +} + +func createSecretOCICmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("name is required") + } + + secretName := args[0] + + if secretOCIArgs.url == "" { + return fmt.Errorf("--url is required") + } + + if secretOCIArgs.username == "" { + return fmt.Errorf("--username is required") + } + + if secretOCIArgs.password == "" { + return fmt.Errorf("--password is required") + } + + if _, err := name.ParseReference(secretOCIArgs.url); err != nil { + return fmt.Errorf("error parsing url: '%s'", err) + } + + opts := sourcesecret.Options{ + Name: secretName, + Namespace: *kubeconfigArgs.Namespace, + Registry: secretOCIArgs.url, + Password: secretOCIArgs.password, + Username: secretOCIArgs.username, + } + + secret, err := sourcesecret.Generate(opts) + if err != nil { + return err + } + + if createArgs.export { + rootCmd.Println(secret.Content) + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + var s corev1.Secret + if err := yaml.Unmarshal([]byte(secret.Content), &s); err != nil { + return err + } + if err := upsertSecret(ctx, kubeClient, s); err != nil { + return err + } + + logger.Actionf("oci secret '%s' created in '%s' namespace", secretName, *kubeconfigArgs.Namespace) + return nil +} diff --git a/cmd/flux/create_secret_oci_test.go b/cmd/flux/create_secret_oci_test.go new file mode 100644 index 00000000..a7897796 --- /dev/null +++ b/cmd/flux/create_secret_oci_test.go @@ -0,0 +1,51 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func TestCreateSecretOCI(t *testing.T) { + tests := []struct { + name string + args string + assert assertFunc + }{ + { + args: "create secret oci", + assert: assertError("name is required"), + }, + { + args: "create secret oci ghcr", + assert: assertError("--url is required"), + }, + { + args: "create secret oci ghcr --namespace=my-namespace --url ghcr.io --username stefanprodan --password=password --export", + assert: assertGoldenFile("testdata/create_secret/oci/create-secret.yaml"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args, + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/create_secret_tls.go b/cmd/flux/create_secret_tls.go index a308066e..82f36743 100644 --- a/cmd/flux/create_secret_tls.go +++ b/cmd/flux/create_secret_tls.go @@ -19,26 +19,28 @@ package main import ( "context" "fmt" + "os" "github.com/spf13/cobra" "github.com/spf13/pflag" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/yaml" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" + "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.`, + Long: withPreviewNote(`The create secret tls command generates a Kubernetes secret with certificates for use with TLS.`), Example: ` # Create a TLS secret on disk and encrypt it with Mozilla SOPS. # Files are expected to be PEM-encoded. flux create secret tls certs \ --namespace=my-namespace \ - --cert-file=./client.crt \ - --key-file=./client.key \ + --tls-crt-file=./client.crt \ + --tls-key-file=./client.key \ + --ca-crt-file=./ca.crt \ --export > certs.yaml sops --encrypt --encrypted-regex '^(data|stringData)$' \ @@ -47,29 +49,41 @@ var createSecretTLSCmd = &cobra.Command{ } type secretTLSFlags struct { - certFile string - keyFile string - caFile string + certFile string + keyFile string + caFile string + caCrtFile string + tlsKeyFile string + tlsCrtFile string } var secretTLSArgs secretTLSFlags -func initSecretTLSFlags(flags *pflag.FlagSet, args *secretTLSFlags) { +func initSecretDeprecatedTLSFlags(flags *pflag.FlagSet, args *secretTLSFlags) { flags.StringVar(&args.certFile, "cert-file", "", "TLS authentication cert file path") flags.StringVar(&args.keyFile, "key-file", "", "TLS authentication key file path") flags.StringVar(&args.caFile, "ca-file", "", "TLS authentication CA file path") } +func initSecretTLSFlags(flags *pflag.FlagSet, args *secretTLSFlags) { + flags.StringVar(&args.tlsCrtFile, "tls-crt-file", "", "TLS authentication cert file path") + flags.StringVar(&args.tlsKeyFile, "tls-key-file", "", "TLS authentication key file path") + flags.StringVar(&args.caCrtFile, "ca-crt-file", "", "TLS authentication CA file path") +} + func init() { flags := createSecretTLSCmd.Flags() + initSecretDeprecatedTLSFlags(flags, &secretTLSArgs) initSecretTLSFlags(flags, &secretTLSArgs) + + flags.MarkDeprecated("cert-file", "please use --tls-crt-file instead") + flags.MarkDeprecated("key-file", "please use --tls-key-file instead") + flags.MarkDeprecated("ca-file", "please use --ca-crt-file instead") + createSecretCmd.AddCommand(createSecretTLSCmd) } func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("secret name is required") - } name := args[0] labels, err := parseLabels() @@ -78,13 +92,39 @@ func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error { } opts := sourcesecret.Options{ - Name: name, - Namespace: *kubeconfigArgs.Namespace, - Labels: labels, - CAFilePath: secretTLSArgs.caFile, - CertFilePath: secretTLSArgs.certFile, - KeyFilePath: secretTLSArgs.keyFile, + 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) + } + } else if secretTLSArgs.caFile != "" { + opts.CAFile, err = os.ReadFile(secretTLSArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } } + + if secretTLSArgs.tlsCrtFile != "" && secretTLSArgs.tlsKeyFile != "" { + if opts.TLSCrt, err = os.ReadFile(secretTLSArgs.tlsCrtFile); err != nil { + return fmt.Errorf("failed to read cert file: %w", err) + } + if opts.TLSKey, err = os.ReadFile(secretTLSArgs.tlsKeyFile); err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + } else if secretTLSArgs.certFile != "" && secretTLSArgs.keyFile != "" { + if opts.CertFile, err = os.ReadFile(secretTLSArgs.certFile); err != nil { + return fmt.Errorf("failed to read cert file: %w", err) + } + if opts.KeyFile, err = os.ReadFile(secretTLSArgs.keyFile); err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + } + secret, err := sourcesecret.Generate(opts) if err != nil { return err @@ -97,7 +137,7 @@ func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } diff --git a/cmd/flux/create_secret_tls_test.go b/cmd/flux/create_secret_tls_test.go index 8085c584..226ed8e3 100644 --- a/cmd/flux/create_secret_tls_test.go +++ b/cmd/flux/create_secret_tls_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func TestCreateTlsSecretNoArgs(t *testing.T) { +func TestCreateTlsSecret(t *testing.T) { tests := []struct { name string args string @@ -12,12 +12,16 @@ func TestCreateTlsSecretNoArgs(t *testing.T) { }{ { args: "create secret tls", - assert: assertError("secret name is required"), + assert: assertError("name is required"), }, { - args: "create secret tls certs --namespace=my-namespace --cert-file=./testdata/create_secret/tls/test-cert.pem --key-file=./testdata/create_secret/tls/test-key.pem --export", + args: "create secret tls certs --namespace=my-namespace --tls-crt-file=./testdata/create_secret/tls/test-cert.pem --tls-key-file=./testdata/create_secret/tls/test-key.pem --ca-crt-file=./testdata/create_secret/tls/test-ca.pem --export", assert: assertGoldenFile("testdata/create_secret/tls/secret-tls.yaml"), }, + { + args: "create secret tls certs --namespace=my-namespace --cert-file=./testdata/create_secret/tls/test-cert.pem --key-file=./testdata/create_secret/tls/test-key.pem --ca-file=./testdata/create_secret/tls/test-ca.pem --export", + assert: assertGoldenFile("testdata/create_secret/tls/deprecated-secret-tls.yaml"), + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/flux/create_source.go b/cmd/flux/create_source.go index 9e1e93f5..cdd4e3b2 100644 --- a/cmd/flux/create_source.go +++ b/cmd/flux/create_source.go @@ -25,7 +25,7 @@ import ( var createSourceCmd = &cobra.Command{ Use: "source", Short: "Create or update sources", - Long: "The create source sub-commands generate sources.", + Long: `The create source sub-commands generate sources.`, } type createSourceFlags struct { diff --git a/cmd/flux/create_source_bucket.go b/cmd/flux/create_source_bucket.go index 50858c5b..32c88730 100644 --- a/cmd/flux/create_source_bucket.go +++ b/cmd/flux/create_source_bucket.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" @@ -30,17 +31,18 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" ) var createSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Create or update a Bucket source", - Long: `The create source bucket command generates a Bucket resource and waits for it to be downloaded. -For Buckets with static authentication, the credentials are stored in a Kubernetes secret.`, + Long: withPreviewNote(`The create source bucket command generates a Bucket resource and waits for it to be downloaded. +For Buckets with static authentication, the credentials are stored in a Kubernetes secret.`), Example: ` # Create a source for a Bucket using static authentication flux create source bucket podinfo \ --bucket-name=podinfo \ @@ -61,17 +63,18 @@ For Buckets with static authentication, the credentials are stored in a Kubernet } type sourceBucketFlags struct { - name string - provider flags.SourceBucketProvider - endpoint string - accessKey string - secretKey string - region string - insecure bool - secretRef string + name string + provider flags.SourceBucketProvider + endpoint string + accessKey string + secretKey string + region string + insecure bool + secretRef string + ignorePaths []string } -var sourceBucketArgs = NewSourceBucketFlags() +var sourceBucketArgs = newSourceBucketFlags() func init() { createSourceBucketCmd.Flags().Var(&sourceBucketArgs.provider, "provider", sourceBucketArgs.provider.Description()) @@ -82,20 +85,18 @@ func init() { createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.region, "region", "", "the bucket region") createSourceBucketCmd.Flags().BoolVar(&sourceBucketArgs.insecure, "insecure", false, "for when connecting to a non-TLS S3 HTTP endpoint") createSourceBucketCmd.Flags().StringVar(&sourceBucketArgs.secretRef, "secret-ref", "", "the name of an existing secret containing credentials") + createSourceBucketCmd.Flags().StringSliceVar(&sourceBucketArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in bucket resource (can specify multiple paths with commas: path1,path2)") createSourceCmd.AddCommand(createSourceBucketCmd) } -func NewSourceBucketFlags() sourceBucketFlags { +func newSourceBucketFlags() sourceBucketFlags { return sourceBucketFlags{ provider: flags.SourceBucketProvider(sourcev1.GenericBucketProvider), } } func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("Bucket source name is required") - } name := args[0] if sourceBucketArgs.name == "" { @@ -117,6 +118,12 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error { } defer os.RemoveAll(tmpDir) + var ignorePaths *string + if len(sourceBucketArgs.ignorePaths) > 0 { + ignorePathsStr := strings.Join(sourceBucketArgs.ignorePaths, "\n") + ignorePaths = &ignorePathsStr + } + bucket := &sourcev1.Bucket{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -132,6 +139,7 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error { Interval: metav1.Duration{ Duration: createArgs.interval, }, + Ignore: ignorePaths, }, } @@ -152,7 +160,7 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -195,8 +203,8 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error { } logger.Waitingf("waiting for Bucket source reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isBucketReady(ctx, kubeClient, namespacedName, bucket)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isObjectReadyConditionFunc(kubeClient, namespacedName, bucket)); err != nil { return err } logger.Successf("Bucket source reconciliation completed") diff --git a/cmd/flux/create_source_chart.go b/cmd/flux/create_source_chart.go new file mode 100644 index 00000000..aab02196 --- /dev/null +++ b/cmd/flux/create_source_chart.go @@ -0,0 +1,217 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" +) + +var createSourceChartCmd = &cobra.Command{ + Use: "chart [name]", + Short: "Create or update a HelmChart source", + Long: `The create source chart command generates a HelmChart resource and waits for the chart to be available.`, + Example: ` # Create a source for a chart residing in a HelmRepository + flux create source chart podinfo \ + --source=HelmRepository/podinfo \ + --chart=podinfo \ + --chart-version=6.x + + # Create a source for a chart residing in a Git repository + flux create source chart podinfo \ + --source=GitRepository/podinfo \ + --chart=./charts/podinfo + + # Create a source for a chart residing in a S3 Bucket + flux create source chart podinfo \ + --source=Bucket/podinfo \ + --chart=./charts/podinfo + + # Create a source for a chart from OCI and verify its signature + flux create source chart podinfo \ + --source HelmRepository/podinfo \ + --chart podinfo \ + --chart-version=6.6.2 \ + --verify-provider=cosign \ + --verify-issuer=https://token.actions.githubusercontent.com \ + --verify-subject=https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2`, + RunE: createSourceChartCmdRun, +} + +type sourceChartFlags struct { + chart string + chartVersion string + source flags.LocalHelmChartSource + reconcileStrategy string + verifyProvider flags.SourceOCIVerifyProvider + verifySecretRef string + verifyOIDCIssuer string + verifySubject string +} + +var sourceChartArgs sourceChartFlags + +func init() { + createSourceChartCmd.Flags().StringVar(&sourceChartArgs.chart, "chart", "", "Helm chart name or path") + createSourceChartCmd.Flags().StringVar(&sourceChartArgs.chartVersion, "chart-version", "", "Helm chart version, accepts a semver range (ignored for charts from GitRepository sources)") + createSourceChartCmd.Flags().Var(&sourceChartArgs.source, "source", sourceChartArgs.source.Description()) + createSourceChartCmd.Flags().StringVar(&sourceChartArgs.reconcileStrategy, "reconcile-strategy", "ChartVersion", "the reconcile strategy for helm chart (accepted values: Revision and ChartRevision)") + createSourceChartCmd.Flags().Var(&sourceChartArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description()) + createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification") + createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification") + createSourceChartCmd.Flags().StringVar(&sourceChartArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification") + + createSourceCmd.AddCommand(createSourceChartCmd) +} + +func createSourceChartCmdRun(cmd *cobra.Command, args []string) error { + name := args[0] + + if sourceChartArgs.source.Kind == "" || sourceChartArgs.source.Name == "" { + return fmt.Errorf("chart source is required") + } + + if sourceChartArgs.chart == "" { + return fmt.Errorf("chart name or path is required") + } + + logger.Generatef("generating HelmChart source") + + sourceLabels, err := parseLabels() + if err != nil { + return err + } + + helmChart := &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: *kubeconfigArgs.Namespace, + Labels: sourceLabels, + }, + Spec: sourcev1.HelmChartSpec{ + Chart: sourceChartArgs.chart, + Version: sourceChartArgs.chartVersion, + Interval: metav1.Duration{ + Duration: createArgs.interval, + }, + ReconcileStrategy: sourceChartArgs.reconcileStrategy, + SourceRef: sourcev1.LocalHelmChartSourceReference{ + Kind: sourceChartArgs.source.Kind, + Name: sourceChartArgs.source.Name, + }, + }, + } + + if provider := sourceChartArgs.verifyProvider.String(); provider != "" { + helmChart.Spec.Verify = &sourcev1.OCIRepositoryVerification{ + Provider: provider, + } + if secretName := sourceChartArgs.verifySecretRef; secretName != "" { + helmChart.Spec.Verify.SecretRef = &meta.LocalObjectReference{ + Name: secretName, + } + } + verifyIssuer := sourceChartArgs.verifyOIDCIssuer + verifySubject := sourceChartArgs.verifySubject + if verifyIssuer != "" || verifySubject != "" { + helmChart.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{ + Issuer: verifyIssuer, + Subject: verifySubject, + }} + } + } else if sourceChartArgs.verifySecretRef != "" { + return fmt.Errorf("a verification provider must be specified when a secret is specified") + } else if sourceChartArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" { + return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified") + } + + if createArgs.export { + return printExport(exportHelmChart(helmChart)) + } + + logger.Actionf("applying HelmChart source") + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + + namespacedName, err := upsertHelmChart(ctx, kubeClient, helmChart) + if err != nil { + return err + } + + logger.Waitingf("waiting for HelmChart source reconciliation") + readyConditionFunc := isObjectReadyConditionFunc(kubeClient, namespacedName, helmChart) + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil { + return err + } + logger.Successf("HelmChart source reconciliation completed") + + if helmChart.Status.Artifact == nil { + return fmt.Errorf("HelmChart source reconciliation completed but no artifact was found") + } + logger.Successf("fetched revision: %s", helmChart.Status.Artifact.Revision) + return nil +} + +func upsertHelmChart(ctx context.Context, kubeClient client.Client, + helmChart *sourcev1.HelmChart) (types.NamespacedName, error) { + namespacedName := types.NamespacedName{ + Namespace: helmChart.GetNamespace(), + Name: helmChart.GetName(), + } + + var existing sourcev1.HelmChart + err := kubeClient.Get(ctx, namespacedName, &existing) + if err != nil { + if errors.IsNotFound(err) { + if err := kubeClient.Create(ctx, helmChart); err != nil { + return namespacedName, err + } else { + logger.Successf("source created") + return namespacedName, nil + } + } + return namespacedName, err + } + + existing.Labels = helmChart.Labels + existing.Spec = helmChart.Spec + if err := kubeClient.Update(ctx, &existing); err != nil { + return namespacedName, err + } + helmChart = &existing + logger.Successf("source updated") + return namespacedName, nil +} diff --git a/cmd/flux/create_source_chart_test.go b/cmd/flux/create_source_chart_test.go new file mode 100644 index 00000000..95667086 --- /dev/null +++ b/cmd/flux/create_source_chart_test.go @@ -0,0 +1,91 @@ +//go:build unit +// +build unit + +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import "testing" + +func TestCreateSourceChart(t *testing.T) { + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setupSourceChart(t, tmpl) + + tests := []struct { + name string + args string + assert assertFunc + }{ + { + name: "missing name", + args: "create source chart --export", + assert: assertError("name is required"), + }, + { + name: "missing source reference", + args: "create source chart podinfo --export ", + assert: assertError("chart source is required"), + }, + { + name: "missing chart name", + args: "create source chart podinfo --source helmrepository/podinfo --export", + assert: assertError("chart name or path is required"), + }, + { + name: "unknown source kind", + args: "create source chart podinfo --source foobar/podinfo --export", + assert: assertError(`invalid argument "foobar/podinfo" for "--source" flag: source kind 'foobar' is not supported, must be one of: HelmRepository, GitRepository, Bucket`), + }, + { + name: "basic chart", + args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --export", + assert: assertGoldenTemplateFile("testdata/create_source_chart/basic.yaml", tmpl), + }, + { + name: "chart with basic signature verification", + args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --export", + assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_basic.yaml", tmpl), + }, + { + name: "unknown signature verification provider", + args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider foobar --export", + assert: assertError(`invalid argument "foobar" for "--verify-provider" flag: source OCI verify provider 'foobar' is not supported, must be one of: cosign`), + }, + { + name: "chart with complete signature verification", + args: "create source chart podinfo --source helmrepository/podinfo --chart podinfo --verify-provider cosign --verify-issuer foo --verify-subject bar --export", + assert: assertGoldenTemplateFile("testdata/create_source_chart/verify_complete.yaml", tmpl), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} + +func setupSourceChart(t *testing.T, tmpl map[string]string) { + t.Helper() + testEnv.CreateObjectFile("./testdata/create_source_chart/setup-source.yaml", tmpl, t) +} diff --git a/cmd/flux/create_source_git.go b/cmd/flux/create_source_git.go index 17a928b6..67b03dab 100644 --- a/cmd/flux/create_source_git.go +++ b/cmd/flux/create_source_git.go @@ -22,23 +22,25 @@ import ( "fmt" "net/url" "os" + "strings" - "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" "github.com/manifoldco/promptui" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" + "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" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) type sourceGitFlags struct { @@ -46,17 +48,19 @@ type sourceGitFlags struct { branch string tag string semver string + refName string + commit string username string password string keyAlgorithm flags.PublicKeyAlgorithm keyRSABits flags.RSAKeyBits keyECDSACurve flags.ECDSACurve secretRef string - gitImplementation flags.GitImplementation caFile string privateKeyFile string recurseSubmodules bool silent bool + ignorePaths []string } var createSourceGitCmd = &cobra.Command{ @@ -113,6 +117,7 @@ For private Git repositories, the basic authentication credentials are stored in # Create a source for a Git repository using basic authentication flux create source git podinfo \ --url=https://github.com/stefanprodan/podinfo \ + --branch=master \ --username=username \ --password=password`, RunE: createSourceGitCmdRun, @@ -125,18 +130,20 @@ func init() { createSourceGitCmd.Flags().StringVar(&sourceGitArgs.branch, "branch", "", "git branch") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.tag, "tag", "", "git tag") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.semver, "tag-semver", "", "git tag semver range") + createSourceGitCmd.Flags().StringVar(&sourceGitArgs.refName, "ref-name", "", " git reference name") + createSourceGitCmd.Flags().StringVar(&sourceGitArgs.commit, "commit", "", "git commit") createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.username, "username", "u", "", "basic authentication username") createSourceGitCmd.Flags().StringVarP(&sourceGitArgs.password, "password", "p", "", "basic authentication password") createSourceGitCmd.Flags().Var(&sourceGitArgs.keyAlgorithm, "ssh-key-algorithm", sourceGitArgs.keyAlgorithm.Description()) createSourceGitCmd.Flags().Var(&sourceGitArgs.keyRSABits, "ssh-rsa-bits", sourceGitArgs.keyRSABits.Description()) createSourceGitCmd.Flags().Var(&sourceGitArgs.keyECDSACurve, "ssh-ecdsa-curve", sourceGitArgs.keyECDSACurve.Description()) createSourceGitCmd.Flags().StringVar(&sourceGitArgs.secretRef, "secret-ref", "", "the name of an existing secret containing SSH or basic credentials") - createSourceGitCmd.Flags().Var(&sourceGitArgs.gitImplementation, "git-implementation", sourceGitArgs.gitImplementation.Description()) createSourceGitCmd.Flags().StringVar(&sourceGitArgs.caFile, "ca-file", "", "path to TLS CA file used for validating self-signed certificates") createSourceGitCmd.Flags().StringVar(&sourceGitArgs.privateKeyFile, "private-key-file", "", "path to a passwordless private key file used for authenticating to the Git SSH server") createSourceGitCmd.Flags().BoolVar(&sourceGitArgs.recurseSubmodules, "recurse-submodules", false, "when enabled, configures the GitRepository source to initialize and include Git submodules in the artifact it produces") createSourceGitCmd.Flags().BoolVarP(&sourceGitArgs.silent, "silent", "s", false, "assumes the deploy key is already setup, skips confirmation") + createSourceGitCmd.Flags().StringSliceVar(&sourceGitArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in git resource (can specify multiple paths with commas: path1,path2)") createSourceCmd.AddCommand(createSourceGitCmd) } @@ -150,9 +157,6 @@ func newSourceGitFlags() sourceGitFlags { } func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("GitRepository source name is required") - } name := args[0] if sourceGitArgs.url == "" { @@ -167,16 +171,12 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("git URL scheme '%s' not supported, can be: ssh, http and https", u.Scheme) } - if sourceGitArgs.branch == "" && sourceGitArgs.tag == "" && sourceGitArgs.semver == "" { - return fmt.Errorf("a Git ref is required, use one of the following: --branch, --tag or --tag-semver") + if sourceGitArgs.branch == "" && sourceGitArgs.tag == "" && sourceGitArgs.semver == "" && sourceGitArgs.commit == "" && sourceGitArgs.refName == "" { + return fmt.Errorf("a Git ref is required, use one of the following: --branch, --tag, --commit, --ref-name or --tag-semver") } if sourceGitArgs.caFile != "" && u.Scheme == "ssh" { - return fmt.Errorf("specifing a CA file is not supported for Git over SSH") - } - - if sourceGitArgs.recurseSubmodules && sourceGitArgs.gitImplementation == sourcev1.LibGit2Implementation { - return fmt.Errorf("recurse submodules requires --git-implementation=%s", sourcev1.GoGitImplementation) + return fmt.Errorf("specifying a CA file is not supported for Git over SSH") } tmpDir, err := os.MkdirTemp("", name) @@ -190,6 +190,12 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { return err } + var ignorePaths *string + if len(sourceGitArgs.ignorePaths) > 0 { + ignorePathsStr := strings.Join(sourceGitArgs.ignorePaths, "\n") + ignorePaths = &ignorePathsStr + } + gitRepository := sourcev1.GitRepository{ ObjectMeta: metav1.ObjectMeta{ Name: name, @@ -203,6 +209,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { }, RecurseSubmodules: sourceGitArgs.recurseSubmodules, Reference: &sourcev1.GitRepositoryRef{}, + Ignore: ignorePaths, }, } @@ -210,11 +217,12 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { gitRepository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} } - if sourceGitArgs.gitImplementation != "" { - gitRepository.Spec.GitImplementation = sourceGitArgs.gitImplementation.String() - } - - if sourceGitArgs.semver != "" { + if sourceGitArgs.commit != "" { + gitRepository.Spec.Reference.Commit = sourceGitArgs.commit + gitRepository.Spec.Reference.Branch = sourceGitArgs.branch + } else if sourceGitArgs.refName != "" { + gitRepository.Spec.Reference.Name = sourceGitArgs.refName + } else if sourceGitArgs.semver != "" { gitRepository.Spec.Reference.SemVer = sourceGitArgs.semver } else if sourceGitArgs.tag != "" { gitRepository.Spec.Reference.Tag = sourceGitArgs.tag @@ -235,7 +243,7 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -249,16 +257,26 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { } switch u.Scheme { case "ssh": + keypair, err := sourcesecret.LoadKeyPairFromPath(sourceGitArgs.privateKeyFile, sourceGitArgs.password) + if err != nil { + return err + } + secretOpts.Keypair = keypair secretOpts.SSHHostname = u.Host - secretOpts.PrivateKeyPath = sourceGitArgs.privateKeyFile secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(sourceGitArgs.keyAlgorithm) secretOpts.RSAKeyBits = int(sourceGitArgs.keyRSABits) secretOpts.ECDSACurve = sourceGitArgs.keyECDSACurve.Curve secretOpts.Password = sourceGitArgs.password case "https": + if sourceGitArgs.caFile != "" { + caBundle, err := os.ReadFile(sourceGitArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + secretOpts.CAFile = caBundle + } secretOpts.Username = sourceGitArgs.username secretOpts.Password = sourceGitArgs.password - secretOpts.CAFilePath = sourceGitArgs.caFile case "http": logger.Warningf("insecure configuration: credentials configured for an HTTP URL") secretOpts.Username = sourceGitArgs.username @@ -306,8 +324,8 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { } logger.Waitingf("waiting for GitRepository source reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isGitRepositoryReady(ctx, kubeClient, namespacedName, &gitRepository)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isObjectReadyConditionFunc(kubeClient, namespacedName, &gitRepository)); err != nil { return err } logger.Successf("GitRepository source reconciliation completed") @@ -349,23 +367,3 @@ func upsertGitRepository(ctx context.Context, kubeClient client.Client, logger.Successf("GitRepository source updated") return namespacedName, nil } - -func isGitRepositoryReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, gitRepository *sourcev1.GitRepository) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, gitRepository) - if err != nil { - return false, err - } - - if c := apimeta.FindStatusCondition(gitRepository.Status.Conditions, meta.ReadyCondition); c != nil { - switch c.Status { - case metav1.ConditionTrue: - return true, nil - case metav1.ConditionFalse: - return false, fmt.Errorf(c.Message) - } - } - return false, nil - } -} diff --git a/cmd/flux/create_source_git_test.go b/cmd/flux/create_source_git_test.go index 1b391bce..b34e4208 100644 --- a/cmd/flux/create_source_git_test.go +++ b/cmd/flux/create_source_git_test.go @@ -21,15 +21,18 @@ package main import ( "context" - "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "testing" + "time" + "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - "testing" - "time" + + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var pollInterval = 50 * time.Millisecond @@ -83,6 +86,66 @@ func (r *reconciler) conditionFunc() (bool, error) { return true, err } +func TestCreateSourceGitExport(t *testing.T) { + var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --ignore-paths .cosign,non-existent-dir/ -n default --interval 1m --export --timeout=" + testTimeout.String() + + cases := []struct { + name string + args string + assert assertFunc + }{ + { + "ExportSucceeded", + command, + assertGoldenFile("testdata/create_source_git/export.golden"), + }, + { + name: "no args", + args: "create secret git", + assert: assertError("name is required"), + }, + { + name: "source with commit", + args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --commit=c88a2f41 --interval=1m0s --export", + assert: assertGoldenFile("./testdata/create_source_git/source-git-commit.yaml"), + }, + { + name: "source with ref name", + args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --ref-name=refs/heads/main --interval=1m0s --export", + assert: assertGoldenFile("testdata/create_source_git/source-git-refname.yaml"), + }, + { + name: "source with branch name and commit", + args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --branch=main --commit=c88a2f41 --interval=1m0s --export", + assert: assertGoldenFile("testdata/create_source_git/source-git-branch-commit.yaml"), + }, + { + name: "source with semver", + args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --tag-semver=v1.01 --interval=1m0s --export", + assert: assertGoldenFile("testdata/create_source_git/source-git-semver.yaml"), + }, + { + name: "source with git tag", + args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --tag=test --interval=1m0s --export", + assert: assertGoldenFile("testdata/create_source_git/source-git-tag.yaml"), + }, + { + name: "source with git branch", + args: "create source git podinfo --namespace=flux-system --url=https://github.com/stefanprodan/podinfo --branch=test --interval=1m0s --export", + assert: assertGoldenFile("testdata/create_source_git/source-git-branch.yaml"), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tc.args, + assert: tc.assert, + } + cmd.runTestCmd(t) + }) + } +} + func TestCreateSourceGit(t *testing.T) { // Default command used for multiple tests var command = "create source git podinfo --url=https://github.com/stefanprodan/podinfo --branch=master --timeout=" + testTimeout.String() @@ -96,25 +159,52 @@ func TestCreateSourceGit(t *testing.T) { { "NoArgs", "create source git", - assertError("GitRepository source name is required"), + assertError("name is required"), nil, }, { "Succeeded", command, assertGoldenFile("testdata/create_source_git/success.golden"), func(repo *sourcev1.GitRepository) { - meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionTrue, sourcev1.GitOperationSucceedReason, "succeeded message") + newCondition := metav1.Condition{ + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: sourcev1.GitOperationSucceedReason, + Message: "succeeded message", + ObservedGeneration: repo.GetGeneration(), + } + apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition) repo.Status.Artifact = &sourcev1.Artifact{ Path: "some-path", Revision: "v1", + LastUpdateTime: metav1.Time{ + Time: time.Now(), + }, } + repo.Status.ObservedGeneration = repo.GetGeneration() }, }, { "Failed", command, assertError("failed message"), func(repo *sourcev1.GitRepository) { - meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionFalse, sourcev1.URLInvalidReason, "failed message") + stalledCondition := metav1.Condition{ + Type: meta.StalledCondition, + Status: metav1.ConditionTrue, + Reason: sourcev1.URLInvalidReason, + Message: "failed message", + ObservedGeneration: repo.GetGeneration(), + } + apimeta.SetStatusCondition(&repo.Status.Conditions, stalledCondition) + newCondition := metav1.Condition{ + Type: meta.ReadyCondition, + Status: metav1.ConditionFalse, + Reason: sourcev1.URLInvalidReason, + Message: "failed message", + ObservedGeneration: repo.GetGeneration(), + } + apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition) + repo.Status.ObservedGeneration = repo.GetGeneration() }, }, { "NoArtifact", @@ -122,7 +212,15 @@ func TestCreateSourceGit(t *testing.T) { assertError("GitRepository source reconciliation completed but no artifact was found"), func(repo *sourcev1.GitRepository) { // Updated with no artifact - meta.SetResourceCondition(repo, meta.ReadyCondition, metav1.ConditionTrue, sourcev1.GitOperationSucceedReason, "succeeded message") + newCondition := metav1.Condition{ + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + Reason: sourcev1.GitOperationSucceedReason, + Message: "succeeded message", + ObservedGeneration: repo.GetGeneration(), + } + apimeta.SetStatusCondition(&repo.Status.Conditions, newCondition) + repo.Status.ObservedGeneration = repo.GetGeneration() }, }, } diff --git a/cmd/flux/create_source_helm.go b/cmd/flux/create_source_helm.go index 81b5a05f..d299c43b 100644 --- a/cmd/flux/create_source_helm.go +++ b/cmd/flux/create_source_helm.go @@ -22,21 +22,20 @@ import ( "net/url" "os" - "github.com/fluxcd/pkg/apis/meta" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" ) var createSourceHelmCmd = &cobra.Command{ @@ -44,23 +43,34 @@ var createSourceHelmCmd = &cobra.Command{ Short: "Create or update a HelmRepository source", Long: `The create source helm command generates a HelmRepository resource and waits for it to fetch the index. For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`, - Example: ` # Create a source for a public Helm repository + Example: ` # Create a source for an HTTPS public Helm repository flux create source helm podinfo \ --url=https://stefanprodan.github.io/podinfo \ --interval=10m - # Create a source for a Helm repository using basic authentication + # Create a source for an HTTPS Helm repository using basic authentication flux create source helm podinfo \ --url=https://stefanprodan.github.io/podinfo \ --username=username \ --password=password - # Create a source for a Helm repository using TLS authentication + # Create a source for an HTTPS Helm repository using TLS authentication flux create source helm podinfo \ --url=https://stefanprodan.github.io/podinfo \ --cert-file=./cert.crt \ --key-file=./key.crt \ - --ca-file=./ca.crt`, + --ca-file=./ca.crt + + # Create a source for an OCI Helm repository + flux create source helm podinfo \ + --url=oci://ghcr.io/stefanprodan/charts/podinfo \ + --username=username \ + --password=password + + # Create a source for an OCI Helm repository using an existing secret with basic auth or dockerconfig credentials + flux create source helm podinfo \ + --url=oci://ghcr.io/stefanprodan/charts/podinfo \ + --secret-ref=docker-config`, RunE: createSourceHelmCmdRun, } @@ -72,6 +82,7 @@ type sourceHelmFlags struct { keyFile string caFile string secretRef string + ociProvider string passCredentials bool } @@ -84,16 +95,14 @@ func init() { createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.certFile, "cert-file", "", "TLS authentication cert file path") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.keyFile, "key-file", "", "TLS authentication key file path") createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.caFile, "ca-file", "", "TLS authentication CA file path") - createSourceHelmCmd.Flags().StringVarP(&sourceHelmArgs.secretRef, "secret-ref", "", "", "the name of an existing secret containing TLS or basic auth credentials") + createSourceHelmCmd.Flags().StringVarP(&sourceHelmArgs.secretRef, "secret-ref", "", "", "the name of an existing secret containing TLS, basic auth or docker-config credentials") + createSourceHelmCmd.Flags().StringVar(&sourceHelmArgs.ociProvider, "oci-provider", "", "OCI provider for authentication") createSourceHelmCmd.Flags().BoolVarP(&sourceHelmArgs.passCredentials, "pass-credentials", "", false, "pass credentials to all domains") createSourceCmd.AddCommand(createSourceHelmCmd) } func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("HelmRepository source name is required") - } name := args[0] if sourceHelmArgs.url == "" { @@ -129,6 +138,15 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { }, } + url, err := url.Parse(sourceHelmArgs.url) + if err != nil { + return fmt.Errorf("failed to parse URL: %w", err) + } + if url.Scheme == sourcev1.HelmRepositoryTypeOCI { + helmRepository.Spec.Type = sourcev1.HelmRepositoryTypeOCI + helmRepository.Spec.Provider = sourceHelmArgs.ociProvider + } + if createSourceArgs.fetchTimeout > 0 { helmRepository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} } @@ -147,11 +165,30 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } + caBundle := []byte{} + if sourceHelmArgs.caFile != "" { + var err error + caBundle, err = os.ReadFile(sourceHelmArgs.caFile) + if err != nil { + return fmt.Errorf("unable to read TLS CA file: %w", err) + } + } + + var certFile, keyFile []byte + if sourceHelmArgs.certFile != "" && sourceHelmArgs.keyFile != "" { + if certFile, err = os.ReadFile(sourceHelmArgs.certFile); err != nil { + return fmt.Errorf("failed to read cert file: %w", err) + } + if keyFile, err = os.ReadFile(sourceHelmArgs.keyFile); err != nil { + return fmt.Errorf("failed to read key file: %w", err) + } + } + logger.Generatef("generating HelmRepository source") if sourceHelmArgs.secretRef == "" { secretName := fmt.Sprintf("helm-%s", name) @@ -160,9 +197,9 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Username: sourceHelmArgs.username, Password: sourceHelmArgs.password, - CertFilePath: sourceHelmArgs.certFile, - KeyFilePath: sourceHelmArgs.keyFile, - CAFilePath: sourceHelmArgs.caFile, + CAFile: caBundle, + CertFile: certFile, + KeyFile: keyFile, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } secret, err := sourcesecret.Generate(secretOpts) @@ -193,12 +230,21 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { } logger.Waitingf("waiting for HelmRepository source reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isHelmRepositoryReady(ctx, kubeClient, namespacedName, helmRepository)); err != nil { + readyConditionFunc := isObjectReadyConditionFunc(kubeClient, namespacedName, helmRepository) + if helmRepository.Spec.Type == sourcev1.HelmRepositoryTypeOCI { + // HelmRepository type OCI is a static object. + readyConditionFunc = isStaticObjectReadyConditionFunc(kubeClient, namespacedName, helmRepository) + } + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil { return err } logger.Successf("HelmRepository source reconciliation completed") + if helmRepository.Spec.Type == sourcev1.HelmRepositoryTypeOCI { + // OCI repos don't expose any artifact so we just return early here + return nil + } + if helmRepository.Status.Artifact == nil { return fmt.Errorf("HelmRepository source reconciliation completed but no artifact was found") } @@ -236,28 +282,3 @@ func upsertHelmRepository(ctx context.Context, kubeClient client.Client, logger.Successf("source updated") return namespacedName, nil } - -func isHelmRepositoryReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, helmRepository *sourcev1.HelmRepository) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, helmRepository) - if err != nil { - return false, err - } - - // Confirm the state we are observing is for the current generation - if helmRepository.Generation != helmRepository.Status.ObservedGeneration { - return false, nil - } - - if c := apimeta.FindStatusCondition(helmRepository.Status.Conditions, meta.ReadyCondition); c != nil { - switch c.Status { - case metav1.ConditionTrue: - return true, nil - case metav1.ConditionFalse: - return false, fmt.Errorf(c.Message) - } - } - return false, nil - } -} diff --git a/cmd/flux/create_source_helm_test.go b/cmd/flux/create_source_helm_test.go new file mode 100644 index 00000000..ceaa959f --- /dev/null +++ b/cmd/flux/create_source_helm_test.go @@ -0,0 +1,81 @@ +//go:build unit +// +build unit + +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func TestCreateSourceHelm(t *testing.T) { + tests := []struct { + name string + args string + resultFile string + assertFunc string + }{ + { + name: "no args", + args: "create source helm", + resultFile: "name is required", + assertFunc: "assertError", + }, + { + name: "OCI repo", + args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --export", + resultFile: "./testdata/create_source_helm/oci.golden", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "OCI repo with Secret ref", + args: "create source helm podinfo --url=oci://ghcr.io/stefanprodan/charts/podinfo --interval 5m --secret-ref=creds --export", + resultFile: "./testdata/create_source_helm/oci-with-secret.golden", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "HTTPS repo", + args: "create source helm podinfo --url=https://stefanprodan.github.io/charts/podinfo --interval 5m --export", + resultFile: "./testdata/create_source_helm/https.golden", + assertFunc: "assertGoldenTemplateFile", + }, + } + + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setup(t, tmpl) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var assert assertFunc + switch tt.assertFunc { + case "assertGoldenTemplateFile": + assert = assertGoldenTemplateFile(tt.resultFile, tmpl) + case "assertError": + assert = assertError(tt.resultFile) + } + + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/create_source_oci.go b/cmd/flux/create_source_oci.go new file mode 100644 index 00000000..4713334a --- /dev/null +++ b/cmd/flux/create_source_oci.go @@ -0,0 +1,260 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/fluxcd/pkg/apis/meta" + + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" +) + +var createSourceOCIRepositoryCmd = &cobra.Command{ + Use: "oci [name]", + Short: "Create or update an OCIRepository", + Long: withPreviewNote(`The create source oci command generates an OCIRepository resource and waits for it to be ready.`), + Example: ` # Create an OCIRepository for a public container image + flux create source oci podinfo \ + --url=oci://ghcr.io/stefanprodan/manifests/podinfo \ + --tag=6.6.2 \ + --interval=10m + + # Create an OCIRepository with OIDC signature verification + flux create source oci podinfo \ + --url=oci://ghcr.io/stefanprodan/manifests/podinfo \ + --tag=6.6.2 \ + --interval=10m \ + --verify-provider=cosign \ + --verify-subject="^https://github.com/stefanprodan/podinfo/.github/workflows/release.yml@refs/tags/6.6.2$" \ + --verify-issuer="^https://token.actions.githubusercontent.com$" +`, + RunE: createSourceOCIRepositoryCmdRun, +} + +type sourceOCIRepositoryFlags struct { + url string + tag string + semver string + digest string + secretRef string + serviceAccount string + certSecretRef string + verifyProvider flags.SourceOCIVerifyProvider + verifySecretRef string + verifyOIDCIssuer string + verifySubject string + ignorePaths []string + provider flags.SourceOCIProvider + insecure bool +} + +var sourceOCIRepositoryArgs = newSourceOCIFlags() + +func newSourceOCIFlags() sourceOCIRepositoryFlags { + return sourceOCIRepositoryFlags{ + provider: flags.SourceOCIProvider(sourcev1b2.GenericOCIProvider), + } +} + +func init() { + createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description()) + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.url, "url", "", "the OCI repository URL") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.tag, "tag", "", "the OCI artifact tag") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.semver, "tag-semver", "", "the OCI artifact tag semver range") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.digest, "digest", "", "the OCI artifact digest") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.secretRef, "secret-ref", "", "the name of the Kubernetes image pull secret (type 'kubernetes.io/dockerconfigjson')") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.serviceAccount, "service-account", "", "the name of the Kubernetes service account that refers to an image pull secret") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.certSecretRef, "cert-ref", "", "the name of a secret to use for TLS certificates") + createSourceOCIRepositoryCmd.Flags().Var(&sourceOCIRepositoryArgs.verifyProvider, "verify-provider", sourceOCIRepositoryArgs.verifyProvider.Description()) + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySecretRef, "verify-secret-ref", "", "the name of a secret to use for signature verification") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifySubject, "verify-subject", "", "regular expression to use for the OIDC subject during signature verification") + createSourceOCIRepositoryCmd.Flags().StringVar(&sourceOCIRepositoryArgs.verifyOIDCIssuer, "verify-issuer", "", "regular expression to use for the OIDC issuer during signature verification") + createSourceOCIRepositoryCmd.Flags().StringSliceVar(&sourceOCIRepositoryArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore resources (can specify multiple paths with commas: path1,path2)") + createSourceOCIRepositoryCmd.Flags().BoolVar(&sourceOCIRepositoryArgs.insecure, "insecure", false, "for when connecting to a non-TLS registries over plain HTTP") + + createSourceCmd.AddCommand(createSourceOCIRepositoryCmd) +} + +func createSourceOCIRepositoryCmdRun(cmd *cobra.Command, args []string) error { + name := args[0] + + if sourceOCIRepositoryArgs.url == "" { + return fmt.Errorf("url is required") + } + + if sourceOCIRepositoryArgs.semver == "" && sourceOCIRepositoryArgs.tag == "" && sourceOCIRepositoryArgs.digest == "" { + return fmt.Errorf("--tag, --tag-semver or --digest is required") + } + + sourceLabels, err := parseLabels() + if err != nil { + return err + } + + var ignorePaths *string + if len(sourceOCIRepositoryArgs.ignorePaths) > 0 { + ignorePathsStr := strings.Join(sourceOCIRepositoryArgs.ignorePaths, "\n") + ignorePaths = &ignorePathsStr + } + + repository := &sourcev1b2.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: *kubeconfigArgs.Namespace, + Labels: sourceLabels, + }, + Spec: sourcev1b2.OCIRepositorySpec{ + Provider: sourceOCIRepositoryArgs.provider.String(), + URL: sourceOCIRepositoryArgs.url, + Insecure: sourceOCIRepositoryArgs.insecure, + Interval: metav1.Duration{ + Duration: createArgs.interval, + }, + Reference: &sourcev1b2.OCIRepositoryRef{}, + Ignore: ignorePaths, + }, + } + + if digest := sourceOCIRepositoryArgs.digest; digest != "" { + repository.Spec.Reference.Digest = digest + } + if semver := sourceOCIRepositoryArgs.semver; semver != "" { + repository.Spec.Reference.SemVer = semver + } + if tag := sourceOCIRepositoryArgs.tag; tag != "" { + repository.Spec.Reference.Tag = tag + } + + if createSourceArgs.fetchTimeout > 0 { + repository.Spec.Timeout = &metav1.Duration{Duration: createSourceArgs.fetchTimeout} + } + + if saName := sourceOCIRepositoryArgs.serviceAccount; saName != "" { + repository.Spec.ServiceAccountName = saName + } + + if secretName := sourceOCIRepositoryArgs.secretRef; secretName != "" { + repository.Spec.SecretRef = &meta.LocalObjectReference{ + Name: secretName, + } + } + + if secretName := sourceOCIRepositoryArgs.certSecretRef; secretName != "" { + repository.Spec.CertSecretRef = &meta.LocalObjectReference{ + Name: secretName, + } + } + + if provider := sourceOCIRepositoryArgs.verifyProvider.String(); provider != "" { + repository.Spec.Verify = &sourcev1.OCIRepositoryVerification{ + Provider: provider, + } + if secretName := sourceOCIRepositoryArgs.verifySecretRef; secretName != "" { + repository.Spec.Verify.SecretRef = &meta.LocalObjectReference{ + Name: secretName, + } + } + verifyIssuer := sourceOCIRepositoryArgs.verifyOIDCIssuer + verifySubject := sourceOCIRepositoryArgs.verifySubject + if verifyIssuer != "" || verifySubject != "" { + repository.Spec.Verify.MatchOIDCIdentity = []sourcev1.OIDCIdentityMatch{{ + Issuer: verifyIssuer, + Subject: verifySubject, + }} + } + } else if sourceOCIRepositoryArgs.verifySecretRef != "" { + return fmt.Errorf("a verification provider must be specified when a secret is specified") + } else if sourceOCIRepositoryArgs.verifyOIDCIssuer != "" || sourceOCIRepositoryArgs.verifySubject != "" { + return fmt.Errorf("a verification provider must be specified when OIDC issuer/subject is specified") + } + + if createArgs.export { + return printExport(exportOCIRepository(repository)) + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + + logger.Actionf("applying OCIRepository") + namespacedName, err := upsertOCIRepository(ctx, kubeClient, repository) + if err != nil { + return err + } + + logger.Waitingf("waiting for OCIRepository reconciliation") + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + isObjectReadyConditionFunc(kubeClient, namespacedName, repository)); err != nil { + return err + } + logger.Successf("OCIRepository reconciliation completed") + + if repository.Status.Artifact == nil { + return fmt.Errorf("no artifact was found") + } + logger.Successf("fetched revision: %s", repository.Status.Artifact.Revision) + return nil +} + +func upsertOCIRepository(ctx context.Context, kubeClient client.Client, + ociRepository *sourcev1b2.OCIRepository) (types.NamespacedName, error) { + namespacedName := types.NamespacedName{ + Namespace: ociRepository.GetNamespace(), + Name: ociRepository.GetName(), + } + + var existing sourcev1b2.OCIRepository + err := kubeClient.Get(ctx, namespacedName, &existing) + if err != nil { + if errors.IsNotFound(err) { + if err := kubeClient.Create(ctx, ociRepository); err != nil { + return namespacedName, err + } else { + logger.Successf("OCIRepository created") + return namespacedName, nil + } + } + return namespacedName, err + } + + existing.Labels = ociRepository.Labels + existing.Spec = ociRepository.Spec + if err := kubeClient.Update(ctx, &existing); err != nil { + return namespacedName, err + } + ociRepository = &existing + logger.Successf("OCIRepository updated") + return namespacedName, nil +} diff --git a/cmd/flux/create_source_oci_test.go b/cmd/flux/create_source_oci_test.go new file mode 100644 index 00000000..da08d9f6 --- /dev/null +++ b/cmd/flux/create_source_oci_test.go @@ -0,0 +1,96 @@ +/* +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func TestCreateSourceOCI(t *testing.T) { + tests := []struct { + name string + args string + assertFunc assertFunc + }{ + { + name: "NoArgs", + args: "create source oci", + assertFunc: assertError("name is required"), + }, + { + name: "NoURL", + args: "create source oci podinfo", + assertFunc: assertError("url is required"), + }, + { + name: "verify secret specified but provider missing", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-secret-ref=cosign-pub", + assertFunc: assertError("a verification provider must be specified when a secret is specified"), + }, + { + name: "verify issuer specified but provider missing", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github.com", + assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"), + }, + { + name: "verify identity specified but provider missing", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=developer", + assertFunc: assertError("a verification provider must be specified when OIDC issuer/subject is specified"), + }, + { + name: "verify issuer specified but subject missing", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github --verify-provider=cosign --export", + assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"), + }, + { + name: "all verify fields set", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-issuer=github verify-subject=stefanprodan --verify-provider=cosign --export", + assertFunc: assertGoldenFile("./testdata/oci/export_with_issuer.golden"), + }, + { + name: "verify subject specified but issuer missing", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --verify-subject=stefanprodan --verify-provider=cosign --export", + assertFunc: assertGoldenFile("./testdata/oci/export_with_subject.golden"), + }, + { + name: "export manifest", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --export", + assertFunc: assertGoldenFile("./testdata/oci/export.golden"), + }, + { + name: "export manifest with secret", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --secret-ref=creds --export", + assertFunc: assertGoldenFile("./testdata/oci/export_with_secret.golden"), + }, + { + name: "export manifest with verify secret", + args: "create source oci podinfo --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m --verify-provider=cosign --verify-secret-ref=cosign-pub --export", + assertFunc: assertGoldenFile("./testdata/oci/export_with_verify_secret.golden"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args, + assert: tt.assertFunc, + } + + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/create_tenant.go b/cmd/flux/create_tenant.go index 88b8eda6..1b071ecb 100644 --- a/cmd/flux/create_tenant.go +++ b/cmd/flux/create_tenant.go @@ -21,7 +21,7 @@ import ( "context" "fmt" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" @@ -37,8 +37,8 @@ import ( var createTenantCmd = &cobra.Command{ Use: "tenant", Short: "Create or update a tenant", - Long: `The create tenant command generates namespaces, service accounts and role bindings to limit the -reconcilers scope to the tenant namespaces.`, + Long: withPreviewNote(`The create tenant command generates namespaces, service accounts and role bindings to limit the +reconcilers scope to the tenant namespaces.`), Example: ` # Create a tenant with access to a namespace flux create tenant dev-team \ --with-namespace=frontend \ @@ -70,9 +70,6 @@ func init() { } func createTenantCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("tenant name is required") - } tenant := args[0] if err := validation.IsQualifiedName(tenant); len(err) > 0 { return fmt.Errorf("invalid tenant name '%s': %v", tenant, err) @@ -159,7 +156,7 @@ func createTenantCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } diff --git a/cmd/flux/create_test.go b/cmd/flux/create_test.go new file mode 100644 index 00000000..ee38fe50 --- /dev/null +++ b/cmd/flux/create_test.go @@ -0,0 +1,55 @@ +package main + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/rand" +) + +func Test_validateObjectName(t *testing.T) { + tests := []struct { + name string + valid bool + }{ + { + name: "flux-system", + valid: true, + }, + { + name: "-flux-system", + valid: false, + }, + { + name: "-flux-system-", + valid: false, + }, + { + name: "third.first", + valid: false, + }, + { + name: "THirdfirst", + valid: false, + }, + { + name: "THirdfirst", + valid: false, + }, + { + name: rand.String(63), + valid: true, + }, + { + name: rand.String(64), + valid: false, + }, + } + + for _, tt := range tests { + valid := validateObjectName(tt.name) + if valid != tt.valid { + t.Errorf("expected name %q to return %t for validateObjectName func but got %t", + tt.name, tt.valid, valid) + } + } +} diff --git a/cmd/flux/delete.go b/cmd/flux/delete.go index 7facf933..f3c43c3c 100644 --- a/cmd/flux/delete.go +++ b/cmd/flux/delete.go @@ -24,13 +24,13 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var deleteCmd = &cobra.Command{ Use: "delete", Short: "Delete sources and resources", - Long: "The delete sub-commands delete sources and resources.", + Long: `The delete sub-commands delete sources and resources.`, } type deleteFlags struct { @@ -60,7 +60,7 @@ func (del deleteCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } diff --git a/cmd/flux/delete_alert.go b/cmd/flux/delete_alert.go index c9e6f1ab..cda9f04d 100644 --- a/cmd/flux/delete_alert.go +++ b/cmd/flux/delete_alert.go @@ -19,13 +19,13 @@ package main import ( "github.com/spf13/cobra" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var deleteAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Delete a Alert resource", - Long: "The delete alert command removes the given Alert from the cluster.", + Long: withPreviewNote("The delete alert command removes the given Alert from the cluster."), Example: ` # Delete an Alert and the Kubernetes resources created by it flux delete alert main`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), diff --git a/cmd/flux/delete_alertprovider.go b/cmd/flux/delete_alertprovider.go index 08f0526c..73a54b2a 100644 --- a/cmd/flux/delete_alertprovider.go +++ b/cmd/flux/delete_alertprovider.go @@ -19,13 +19,13 @@ package main import ( "github.com/spf13/cobra" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var deleteAlertProviderCmd = &cobra.Command{ Use: "alert-provider [name]", Short: "Delete a Provider resource", - Long: "The delete alert-provider command removes the given Provider from the cluster.", + Long: withPreviewNote("The delete alert-provider command removes the given Provider from the cluster."), Example: ` # Delete a Provider and the Kubernetes resources created by it flux delete alert-provider slack`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), diff --git a/cmd/flux/delete_helmrelease.go b/cmd/flux/delete_helmrelease.go index c721d3dc..d2cfd675 100644 --- a/cmd/flux/delete_helmrelease.go +++ b/cmd/flux/delete_helmrelease.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +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. @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var deleteHelmReleaseCmd = &cobra.Command{ diff --git a/cmd/flux/delete_image.go b/cmd/flux/delete_image.go index 7547ae7f..cd527fa8 100644 --- a/cmd/flux/delete_image.go +++ b/cmd/flux/delete_image.go @@ -23,7 +23,7 @@ import ( var deleteImageCmd = &cobra.Command{ Use: "image", Short: "Delete image automation objects", - Long: "The delete image sub-commands delete image automation objects.", + Long: `The delete image sub-commands delete image automation objects.`, } func init() { diff --git a/cmd/flux/delete_image_policy.go b/cmd/flux/delete_image_policy.go index e86924da..28115900 100644 --- a/cmd/flux/delete_image_policy.go +++ b/cmd/flux/delete_image_policy.go @@ -19,13 +19,13 @@ package main import ( "github.com/spf13/cobra" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var deleteImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Delete an ImagePolicy object", - Long: "The delete image policy command deletes the given ImagePolicy from the cluster.", + Long: withPreviewNote(`The delete image policy command deletes the given ImagePolicy from the cluster.`), Example: ` # Delete an image policy flux delete image policy alpine3.x`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), diff --git a/cmd/flux/delete_image_repository.go b/cmd/flux/delete_image_repository.go index a8769788..5adc3a80 100644 --- a/cmd/flux/delete_image_repository.go +++ b/cmd/flux/delete_image_repository.go @@ -19,13 +19,13 @@ package main import ( "github.com/spf13/cobra" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var deleteImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Delete an ImageRepository object", - Long: "The delete image repository command deletes the given ImageRepository from the cluster.", + Long: withPreviewNote("The delete image repository command deletes the given ImageRepository from the cluster."), Example: ` # Delete an image repository flux delete image repository alpine`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), diff --git a/cmd/flux/delete_image_update.go b/cmd/flux/delete_image_update.go index 3c38f078..5470c303 100644 --- a/cmd/flux/delete_image_update.go +++ b/cmd/flux/delete_image_update.go @@ -19,13 +19,13 @@ package main import ( "github.com/spf13/cobra" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" ) var deleteImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Delete an ImageUpdateAutomation object", - Long: "The delete image update command deletes the given ImageUpdateAutomation from the cluster.", + Long: withPreviewNote(`The delete image update command deletes the given ImageUpdateAutomation from the cluster.`), Example: ` # Delete an image update automation flux delete image update latest-images`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), diff --git a/cmd/flux/delete_kustomization.go b/cmd/flux/delete_kustomization.go index e3366f2f..9bca28b0 100644 --- a/cmd/flux/delete_kustomization.go +++ b/cmd/flux/delete_kustomization.go @@ -19,15 +19,15 @@ package main import ( "github.com/spf13/cobra" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var deleteKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Delete a Kustomization resource", - Long: "The delete kustomization command deletes the given Kustomization from the cluster.", - Example: ` # Delete a kustomization and the Kubernetes resources created by it + Long: `The delete kustomization command deletes the given Kustomization from the cluster.`, + Example: ` # Delete a kustomization and the Kubernetes resources created by it when prune is enabled flux delete kustomization podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: deleteCommand{ diff --git a/cmd/flux/delete_receiver.go b/cmd/flux/delete_receiver.go index bfdc5dbc..070f91ab 100644 --- a/cmd/flux/delete_receiver.go +++ b/cmd/flux/delete_receiver.go @@ -19,13 +19,13 @@ package main import ( "github.com/spf13/cobra" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var deleteReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Delete a Receiver resource", - Long: "The delete receiver command removes the given Receiver from the cluster.", + Long: `The delete receiver command removes the given Receiver from the cluster.`, Example: ` # Delete an Receiver and the Kubernetes resources created by it flux delete receiver main`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), diff --git a/cmd/flux/delete_source.go b/cmd/flux/delete_source.go index 84ec1d04..c28f6418 100644 --- a/cmd/flux/delete_source.go +++ b/cmd/flux/delete_source.go @@ -23,7 +23,7 @@ import ( var deleteSourceCmd = &cobra.Command{ Use: "source", Short: "Delete sources", - Long: "The delete source sub-commands delete sources.", + Long: `The delete source sub-commands delete sources.`, } func init() { diff --git a/cmd/flux/delete_source_bucket.go b/cmd/flux/delete_source_bucket.go index 56d9bdf8..c3eca532 100644 --- a/cmd/flux/delete_source_bucket.go +++ b/cmd/flux/delete_source_bucket.go @@ -19,13 +19,13 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" ) var deleteSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Delete a Bucket source", - Long: "The delete source bucket command deletes the given Bucket from the cluster.", + Long: withPreviewNote("The delete source bucket command deletes the given Bucket from the cluster."), Example: ` # Delete a Bucket source flux delete source bucket podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), diff --git a/cmd/flux/delete_source_chart.go b/cmd/flux/delete_source_chart.go new file mode 100644 index 00000000..db8f2090 --- /dev/null +++ b/cmd/flux/delete_source_chart.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/spf13/cobra" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" +) + +var deleteSourceChartCmd = &cobra.Command{ + Use: "chart [name]", + Short: "Delete a HelmChart source", + Long: "The delete source chart command deletes the given HelmChart from the cluster.", + Example: ` # Delete a HelmChart + flux delete source chart podinfo`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), + RunE: deleteCommand{ + apiType: helmChartType, + object: universalAdapter{&sourcev1.HelmChart{}}, + }.run, +} + +func init() { + deleteSourceCmd.AddCommand(deleteSourceChartCmd) +} diff --git a/cmd/flux/delete_source_git.go b/cmd/flux/delete_source_git.go index 5c521a5b..add16fb5 100644 --- a/cmd/flux/delete_source_git.go +++ b/cmd/flux/delete_source_git.go @@ -19,13 +19,13 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var deleteSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Delete a GitRepository source", - Long: "The delete source git command deletes the given GitRepository from the cluster.", + Long: `The delete source git command deletes the given GitRepository from the cluster.`, Example: ` # Delete a Git repository flux delete source git podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), diff --git a/cmd/flux/delete_source_helm.go b/cmd/flux/delete_source_helm.go index 87b708ad..b4860fbb 100644 --- a/cmd/flux/delete_source_helm.go +++ b/cmd/flux/delete_source_helm.go @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var deleteSourceHelmCmd = &cobra.Command{ diff --git a/cmd/flux/delete_source_oci.go b/cmd/flux/delete_source_oci.go new file mode 100644 index 00000000..c869be69 --- /dev/null +++ b/cmd/flux/delete_source_oci.go @@ -0,0 +1,40 @@ +/* +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 ( + "github.com/spf13/cobra" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" +) + +var deleteSourceOCIRepositoryCmd = &cobra.Command{ + Use: "oci [name]", + Short: "Delete an OCIRepository source", + Long: withPreviewNote("The delete source oci command deletes the given OCIRepository from the cluster."), + Example: ` # Delete an OCIRepository + flux delete source oci podinfo`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), + RunE: deleteCommand{ + apiType: ociRepositoryType, + object: universalAdapter{&sourcev1.OCIRepository{}}, + }.run, +} + +func init() { + deleteSourceCmd.AddCommand(deleteSourceOCIRepositoryCmd) +} diff --git a/cmd/flux/diff.go b/cmd/flux/diff.go index d4ee4804..9f0ec4a9 100644 --- a/cmd/flux/diff.go +++ b/cmd/flux/diff.go @@ -23,7 +23,7 @@ import ( var diffCmd = &cobra.Command{ Use: "diff", Short: "Diff a flux resource", - Long: "The diff command is used to do a server-side dry-run on flux resources, then output the diff.", + Long: `The diff command is used to do a server-side dry-run on flux resources, then prints the diff.`, } func init() { diff --git a/cmd/flux/diff_artifact.go b/cmd/flux/diff_artifact.go new file mode 100644 index 00000000..edfd9382 --- /dev/null +++ b/cmd/flux/diff_artifact.go @@ -0,0 +1,112 @@ +/* +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" + "os" + + oci "github.com/fluxcd/pkg/oci/client" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/v2/internal/flags" +) + +var diffArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Diff Artifact", + Long: withPreviewNote(`The diff artifact command computes the diff between the remote OCI artifact and a local directory or file`), + Example: `# Check if local files differ from remote +flux diff artifact oci://ghcr.io/stefanprodan/manifests:podinfo:6.2.0 --path=./kustomize`, + RunE: diffArtifactCmdRun, +} + +type diffArtifactFlags struct { + path string + creds string + provider flags.SourceOCIProvider + ignorePaths []string +} + +var diffArtifactArgs = newDiffArtifactArgs() + +func newDiffArtifactArgs() diffArtifactFlags { + return diffArtifactFlags{ + provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), + } +} + +func init() { + diffArtifactCmd.Flags().StringVar(&diffArtifactArgs.path, "path", "", "path to the directory where the Kubernetes manifests are located") + diffArtifactCmd.Flags().StringVar(&diffArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") + diffArtifactCmd.Flags().Var(&diffArtifactArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description()) + diffArtifactCmd.Flags().StringSliceVar(&diffArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") + diffCmd.AddCommand(diffArtifactCmd) +} + +func diffArtifactCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact URL is required") + } + ociURL := args[0] + + if diffArtifactArgs.path == "" { + return fmt.Errorf("invalid path %q", diffArtifactArgs.path) + } + + url, err := oci.ParseArtifactURL(ociURL) + if err != nil { + return err + } + + if _, err := os.Stat(diffArtifactArgs.path); err != nil { + return fmt.Errorf("invalid path '%s', must point to an existing directory or file", diffArtifactArgs.path) + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + ociClient := oci.NewClient(oci.DefaultOptions()) + + if diffArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && diffArtifactArgs.creds != "" { + logger.Actionf("logging in to registry with credentials") + if err := ociClient.LoginWithCredentials(diffArtifactArgs.creds); err != nil { + return fmt.Errorf("could not login with credentials: %w", err) + } + } + + if diffArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { + logger.Actionf("logging in to registry with provider credentials") + ociProvider, err := diffArtifactArgs.provider.ToOCIProvider() + if err != nil { + return fmt.Errorf("provider not supported: %w", err) + } + + if err := ociClient.LoginWithProvider(ctx, url, ociProvider); err != nil { + return fmt.Errorf("error during login with provider: %w", err) + } + } + + if err := ociClient.Diff(ctx, url, diffArtifactArgs.path, diffArtifactArgs.ignorePaths); err != nil { + return err + } + + logger.Successf("no changes detected") + return nil +} diff --git a/cmd/flux/diff_artifact_test.go b/cmd/flux/diff_artifact_test.go new file mode 100644 index 00000000..05e29915 --- /dev/null +++ b/cmd/flux/diff_artifact_test.go @@ -0,0 +1,109 @@ +//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 ( + "context" + "fmt" + "testing" + "time" + + "github.com/distribution/distribution/v3/configuration" + "github.com/distribution/distribution/v3/registry" + _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" + _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" + "github.com/phayes/freeport" + ctrl "sigs.k8s.io/controller-runtime" +) + +var dockerReg string + +func setupRegistryServer(ctx context.Context) error { + // Registry config + config := &configuration.Configuration{} + port, err := freeport.GetFreePort() + if err != nil { + return fmt.Errorf("failed to get free port: %s", err) + } + + dockerReg = fmt.Sprintf("localhost:%d", port) + config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port) + config.HTTP.DrainTimeout = time.Duration(10) * time.Second + config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + dockerRegistry, err := registry.NewRegistry(ctx, config) + if err != nil { + return fmt.Errorf("failed to create docker registry: %w", err) + } + + // Start Docker registry + go dockerRegistry.ListenAndServe() + + return nil +} + +func TestDiffArtifact(t *testing.T) { + tests := []struct { + name string + url string + argsTpl string + pushFile string + diffFile string + assert assertFunc + }{ + { + name: "should not fail if there is no diff", + url: "oci://%s/podinfo:1.0.0", + argsTpl: "diff artifact %s --path=%s", + pushFile: "./testdata/diff-artifact/deployment.yaml", + diffFile: "./testdata/diff-artifact/deployment.yaml", + assert: assertGoldenFile("testdata/diff-artifact/success.golden"), + }, + { + name: "should fail if there is a diff", + url: "oci://%s/podinfo:2.0.0", + argsTpl: "diff artifact %s --path=%s", + pushFile: "./testdata/diff-artifact/deployment.yaml", + diffFile: "./testdata/diff-artifact/deployment-diff.yaml", + assert: assertError("the remote artifact contents differs from the local one"), + }, + } + + ctx := ctrl.SetupSignalHandler() + err := setupRegistryServer(ctx) + if err != nil { + panic(fmt.Sprintf("failed to start docker registry: %s", err)) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.url = fmt.Sprintf(tt.url, dockerReg) + _, err := executeCommand("push artifact " + tt.url + " --path=" + tt.pushFile + " --source=test --revision=test") + if err != nil { + t.Fatalf(fmt.Errorf("failed to push image: %w", err).Error()) + } + + cmd := cmdTestCase{ + args: fmt.Sprintf(tt.argsTpl, tt.url, tt.diffFile), + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/diff_kustomization.go b/cmd/flux/diff_kustomization.go index e40f72c9..0b85a2b1 100644 --- a/cmd/flux/diff_kustomization.go +++ b/cmd/flux/diff_kustomization.go @@ -23,29 +23,49 @@ import ( "github.com/spf13/cobra" - "github.com/fluxcd/flux2/internal/build" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/build" ) var diffKsCmd = &cobra.Command{ Use: "kustomization", Aliases: []string{"ks"}, Short: "Diff Kustomization", - Long: `The diff command does a build, then it performs a server-side dry-run and output the diff.`, - Example: `# Preview changes local changes as they were applied on the cluster -flux diff kustomization my-app --path ./path/to/local/manifests`, + Long: `The diff command does a build, then it performs a server-side dry-run and prints the diff. +Exit status: 0 No differences were found. 1 Differences were found. >1 diff failed with an error.`, + Example: `# Preview local changes as they were applied on the cluster +flux diff kustomization my-app --path ./path/to/local/manifests + +# Preview using a local flux kustomization file +flux diff kustomization my-app --path ./path/to/local/manifests \ + --kustomization-file ./path/to/local/my-app.yaml + +# Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat. +flux diff kustomization my-app --path ./path/to/local/manifests \ + --kustomization-file ./path/to/local/my-app.yaml \ + --ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: diffKsCmdRun, } type diffKsFlags struct { - path string + kustomizationFile string + path string + ignorePaths []string + progressBar bool + strictSubst bool } var diffKsArgs diffKsFlags func init() { - diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Path to a local directory that matches the specified Kustomization.spec.path.)") + diffKsCmd.Flags().StringVar(&diffKsArgs.path, "path", "", "Path to a local directory that matches the specified Kustomization.spec.path.") + diffKsCmd.Flags().BoolVar(&diffKsArgs.progressBar, "progress-bar", true, "Boolean to set the progress bar. The default value is true.") + diffKsCmd.Flags().StringSliceVar(&diffKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format") + diffKsCmd.Flags().StringVar(&diffKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") + diffKsCmd.Flags().BoolVar(&diffKsArgs.strictSubst, "strict-substitute", false, + "When enabled, the post build substitutions will fail if a var without a default value is declared in files but is missing from the input vars.") diffCmd.AddCommand(diffKsCmd) } @@ -56,16 +76,44 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { name := args[0] if diffKsArgs.path == "" { - return fmt.Errorf("invalid resource path %q", diffKsArgs.path) + return &RequestError{StatusCode: 2, Err: fmt.Errorf("invalid resource path %q", diffKsArgs.path)} } if fs, err := os.Stat(diffKsArgs.path); err != nil || !fs.IsDir() { - return fmt.Errorf("invalid resource path %q", diffKsArgs.path) + return &RequestError{StatusCode: 2, Err: fmt.Errorf("invalid resource path %q", diffKsArgs.path)} + } + + if diffKsArgs.kustomizationFile != "" { + if fs, err := os.Stat(diffKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { + return fmt.Errorf("invalid kustomization file %q", diffKsArgs.kustomizationFile) + } + } + + var ( + builder *build.Builder + err error + ) + if diffKsArgs.progressBar { + builder, err = build.NewBuilder(name, diffKsArgs.path, + build.WithClientConfig(kubeconfigArgs, kubeclientOptions), + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(diffKsArgs.kustomizationFile), + build.WithProgressBar(), + build.WithIgnore(diffKsArgs.ignorePaths), + build.WithStrictSubstitute(diffKsArgs.strictSubst), + ) + } else { + builder, err = build.NewBuilder(name, diffKsArgs.path, + build.WithClientConfig(kubeconfigArgs, kubeclientOptions), + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(diffKsArgs.kustomizationFile), + build.WithIgnore(diffKsArgs.ignorePaths), + build.WithStrictSubstitute(diffKsArgs.strictSubst), + ) } - builder, err := build.NewBuilder(kubeconfigArgs, name, diffKsArgs.path, build.WithTimeout(rootArgs.timeout)) if err != nil { - return err + return &RequestError{StatusCode: 2, Err: err} } // create a signal channel @@ -74,13 +122,18 @@ func diffKsCmdRun(cmd *cobra.Command, args []string) error { errChan := make(chan error) go func() { - output, err := builder.Diff() + output, hasChanged, err := builder.Diff() if err != nil { - errChan <- err + errChan <- &RequestError{StatusCode: 2, Err: err} } cmd.Print(output) - errChan <- nil + + if hasChanged { + errChan <- &RequestError{StatusCode: 1, Err: fmt.Errorf("identified at least one change, exiting with non-zero exit code")} + } else { + errChan <- nil + } }() select { diff --git a/cmd/flux/diff_kustomization_test.go b/cmd/flux/diff_kustomization_test.go index e2f62e75..a381a61f 100644 --- a/cmd/flux/diff_kustomization_test.go +++ b/cmd/flux/diff_kustomization_test.go @@ -25,7 +25,7 @@ import ( "strings" "testing" - "github.com/fluxcd/flux2/internal/build" + "github.com/fluxcd/flux2/v2/internal/build" "github.com/fluxcd/pkg/ssa" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -45,47 +45,65 @@ func TestDiffKustomization(t *testing.T) { }, { name: "diff nothing deployed", - args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "", assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), }, { name: "diff with a deployment object", - args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/deployment.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-deployment.golden"), }, { name: "diff with a drifted service object", - args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/service.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-service.golden"), }, { name: "diff with a drifted secret object", - args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/secret.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-secret.golden"), }, { name: "diff with a drifted key in sops secret object", - args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/key-sops-secret.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden"), }, { name: "diff with a drifted value in sops secret object", - args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", objectFile: "./testdata/diff-kustomization/value-sops-secret.yaml", assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden"), }, + { + name: "diff with a sops dockerconfigjson secret object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", + objectFile: "./testdata/diff-kustomization/dockerconfigjson-sops-secret.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-dockerconfigjson-sops-secret.golden"), + }, + { + name: "diff with a sops stringdata secret object", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false", + objectFile: "./testdata/diff-kustomization/stringdata-sops-secret.yaml", + assert: assertGoldenFile("./testdata/diff-kustomization/diff-with-drifted-stringdata-sops-secret.golden"), + }, + { + name: "diff where kustomization file has multiple objects with the same name", + args: "diff kustomization podinfo --path ./testdata/build-kustomization/podinfo --progress-bar=false --kustomization-file ./testdata/diff-kustomization/flux-kustomization-multiobj.yaml", + objectFile: "", + assert: assertGoldenFile("./testdata/diff-kustomization/nothing-is-deployed.golden"), + }, } tmpl := map[string]string{ "fluxns": allocateNamespace("flux-system"), } - b, _ := build.NewBuilder(kubeconfigArgs, "podinfo", "") + b, _ := build.NewBuilder("podinfo", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions)) resourceManager, err := b.Manager() if err != nil { @@ -97,7 +115,9 @@ func TestDiffKustomization(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.objectFile != "" { - resourceManager.ApplyAll(context.Background(), createObjectFromFile(tt.objectFile, tmpl, t), ssa.DefaultApplyOptions()) + if _, err := resourceManager.ApplyAll(context.Background(), createObjectFromFile(tt.objectFile, tmpl, t), ssa.DefaultApplyOptions()); err != nil { + t.Error(err) + } } cmd := cmdTestCase{ args: tt.args + " -n " + tmpl["fluxns"], @@ -125,5 +145,9 @@ func createObjectFromFile(objectFile string, templateValues map[string]string, t t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) } + if err := ssa.SetNativeKindsDefaults(clientObjects); err != nil { + t.Fatalf("Error setting native kinds defaults for '%s': %v", objectFile, err) + } + return clientObjects } diff --git a/cmd/flux/envsubst.go b/cmd/flux/envsubst.go new file mode 100644 index 00000000..96ddefa7 --- /dev/null +++ b/cmd/flux/envsubst.go @@ -0,0 +1,74 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "fmt" + + "github.com/fluxcd/pkg/envsubst" + "github.com/spf13/cobra" +) + +var envsubstCmd = &cobra.Command{ + Use: "envsubst", + Args: cobra.NoArgs, + Short: "envsubst substitutes the values of environment variables", + Long: withPreviewNote(`The envsubst command substitutes the values of environment variables +in the string piped as standard input and writes the result to the standard output. This command can be used +to replicate the behavior of the Flux Kustomization post-build substitutions.`), + Example: ` # Run env var substitutions on the kustomization build output + export cluster_region=eu-central-1 + kustomize build . | flux envsubst + + # Run env var substitutions and error out if a variable is not set + kustomize build . | flux envsubst --strict +`, + RunE: runEnvsubstCmd, +} + +type envsubstFlags struct { + strict bool +} + +var envsubstArgs envsubstFlags + +func init() { + envsubstCmd.Flags().BoolVar(&envsubstArgs.strict, "strict", false, + "fail if a variable without a default value is declared in the input but is missing from the environment") + rootCmd.AddCommand(envsubstCmd) +} + +func runEnvsubstCmd(cmd *cobra.Command, args []string) error { + stdin := bufio.NewScanner(rootCmd.InOrStdin()) + stdout := bufio.NewWriter(rootCmd.OutOrStdout()) + for stdin.Scan() { + line, err := envsubst.EvalEnv(stdin.Text(), envsubstArgs.strict) + if err != nil { + return err + } + _, err = fmt.Fprintln(stdout, line) + if err != nil { + return err + } + err = stdout.Flush() + if err != nil { + return err + } + } + return nil +} diff --git a/cmd/flux/envsubst_test.go b/cmd/flux/envsubst_test.go new file mode 100644 index 00000000..38010b5f --- /dev/null +++ b/cmd/flux/envsubst_test.go @@ -0,0 +1,50 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "os" + "testing" + + . "github.com/onsi/gomega" +) + +func TestEnvsubst(t *testing.T) { + g := NewWithT(t) + input, err := os.ReadFile("testdata/envsubst/file.yaml") + g.Expect(err).NotTo(HaveOccurred()) + + t.Setenv("REPO_NAME", "test") + + output, err := executeCommandWithIn("envsubst", bytes.NewReader(input)) + g.Expect(err).NotTo(HaveOccurred()) + + expected, err := os.ReadFile("testdata/envsubst/file.gold") + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(output).To(Equal(string(expected))) +} + +func TestEnvsubst_Strinct(t *testing.T) { + g := NewWithT(t) + input, err := os.ReadFile("testdata/envsubst/file.yaml") + g.Expect(err).NotTo(HaveOccurred()) + + _, err = executeCommandWithIn("envsubst --strict", bytes.NewReader(input)) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("variable not set (strict mode)")) +} diff --git a/cmd/flux/events.go b/cmd/flux/events.go new file mode 100644 index 00000000..5ac5afd2 --- /dev/null +++ b/cmd/flux/events.go @@ -0,0 +1,547 @@ +/* +Copyright 2023 The Kubernetes Authors. +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" + "sort" + "strings" + "time" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/duration" + "k8s.io/apimachinery/pkg/watch" + runtimeresource "k8s.io/cli-runtime/pkg/resource" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/controller-runtime/pkg/client" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/printers" +) + +var eventsCmd = &cobra.Command{ + Use: "events", + Short: "Display Kubernetes events for Flux resources", + Long: withPreviewNote("The events sub-command shows Kubernetes events from Flux resources"), + Example: ` # Display events for flux resources in default namespace + flux events -n default + + # Display events for flux resources in all namespaces + flux events -A + + # Display events for a Kustomization named podinfo + flux events --for Kustomization/podinfo + + # Display events for all Kustomizations in default namespace + flux events --for Kustomization -n default + + # Display warning events for alert resources + flux events --for Alert/podinfo --types warning +`, + RunE: eventsCmdRun, +} + +type eventFlags struct { + allNamespaces bool + watch bool + forSelector string + filterTypes []string +} + +var eventArgs eventFlags + +func init() { + eventsCmd.Flags().BoolVarP(&eventArgs.allNamespaces, "all-namespaces", "A", false, + "display events from Flux resources across all namespaces") + eventsCmd.Flags().BoolVarP(&eventArgs.watch, "watch", "w", false, + "indicate if the events should be streamed") + eventsCmd.Flags().StringVar(&eventArgs.forSelector, "for", "", + "get events for a particular object") + eventsCmd.Flags().StringSliceVar(&eventArgs.filterTypes, "types", []string{}, "filter events for certain types (valid types are: Normal, Warning)") + rootCmd.AddCommand(eventsCmd) +} + +func eventsCmdRun(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + if err := validateEventTypes(eventArgs.filterTypes); err != nil { + return err + } + + kubeclient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + + namespace := *kubeconfigArgs.Namespace + if eventArgs.allNamespaces { + namespace = "" + } + + var diffRefNs bool + clientListOpts := []client.ListOption{client.InNamespace(*kubeconfigArgs.Namespace)} + var refListOpts [][]client.ListOption + if eventArgs.forSelector != "" { + kind, name := getKindNameFromSelector(eventArgs.forSelector) + if kind == "" { + return fmt.Errorf("--for selector must be of format [/]") + } + + refInfoKind, err := fluxKindMap.getRefInfo(kind) + if err != nil { + return err + } + clientListOpts = append(clientListOpts, getListOpt(refInfoKind.gvk.Kind, name)) + if name != "" { + refs, err := getObjectRef(ctx, kubeclient, refInfoKind, name, *kubeconfigArgs.Namespace) + if err != nil { + return err + } + + for _, ref := range refs { + refKind, refName, refNs := utils.ParseObjectKindNameNamespace(ref) + if refNs != namespace { + diffRefNs = true + } + refOpt := []client.ListOption{getListOpt(refKind, refName), client.InNamespace(refNs)} + refListOpts = append(refListOpts, refOpt) + } + } + } + + showNamespace := namespace == "" || diffRefNs + if eventArgs.watch { + return eventsCmdWatchRun(ctx, kubeclient, clientListOpts, refListOpts, showNamespace) + } + + rows, err := getRows(ctx, kubeclient, clientListOpts, refListOpts, showNamespace) + if err != nil { + return err + } + if len(rows) == 0 { + if eventArgs.allNamespaces { + logger.Failuref("No events found.") + } else { + logger.Failuref("No events found in %s namespace.", *kubeconfigArgs.Namespace) + } + + return nil + } + headers := getHeaders(showNamespace) + return printers.TablePrinter(headers).Print(cmd.OutOrStdout(), rows) +} + +func getRows(ctx context.Context, kubeclient client.Client, clientListOpts []client.ListOption, refListOpts [][]client.ListOption, showNs bool) ([][]string, error) { + el := &corev1.EventList{} + if err := addEventsToList(ctx, kubeclient, el, clientListOpts); err != nil { + return nil, err + } + + for _, refOpts := range refListOpts { + if err := addEventsToList(ctx, kubeclient, el, refOpts); err != nil { + return nil, err + } + } + + sort.Sort(SortableEvents(el.Items)) + + var rows [][]string + for _, item := range el.Items { + if ignoreEvent(item) { + continue + } + rows = append(rows, getEventRow(item, showNs)) + } + + return rows, nil +} + +func addEventsToList(ctx context.Context, kubeclient client.Client, el *corev1.EventList, clientListOpts []client.ListOption) error { + listOpts := &metav1.ListOptions{} + clientListOpts = append(clientListOpts, client.Limit(cmdutil.DefaultChunkSize)) + err := runtimeresource.FollowContinue(listOpts, + func(options metav1.ListOptions) (runtime.Object, error) { + newEvents := &corev1.EventList{} + if err := kubeclient.List(ctx, newEvents, clientListOpts...); err != nil { + return nil, fmt.Errorf("error getting events: %w", err) + } + el.Items = append(el.Items, newEvents.Items...) + return newEvents, nil + }) + + return err +} + +func getListOpt(kind, name string) client.ListOption { + var sel fields.Selector + if name == "" { + sel = fields.OneTermEqualSelector("involvedObject.kind", kind) + } else { + sel = fields.AndSelectors( + fields.OneTermEqualSelector("involvedObject.kind", kind), + fields.OneTermEqualSelector("involvedObject.name", name)) + } + + return client.MatchingFieldsSelector{Selector: sel} +} + +func eventsCmdWatchRun(ctx context.Context, kubeclient client.WithWatch, listOpts []client.ListOption, refListOpts [][]client.ListOption, showNs bool) error { + event := &corev1.EventList{} + listOpts = append(listOpts, client.Limit(cmdutil.DefaultChunkSize)) + eventWatch, err := kubeclient.Watch(ctx, event, listOpts...) + if err != nil { + return err + } + + firstIteration := true + + handleEvent := func(e watch.Event) error { + if e.Type == watch.Deleted { + return nil + } + + event, ok := e.Object.(*corev1.Event) + if !ok { + return nil + } + if ignoreEvent(*event) { + return nil + } + rows := getEventRow(*event, showNs) + var hdr []string + if firstIteration { + hdr = getHeaders(showNs) + firstIteration = false + } + return printers.TablePrinter(hdr).Print(os.Stdout, [][]string{rows}) + } + + for _, refOpts := range refListOpts { + refEventWatch, err := kubeclient.Watch(ctx, event, refOpts...) + if err != nil { + return err + } + go func() { + if err := receiveEventChan(ctx, refEventWatch, handleEvent); err != nil { + logger.Failuref("error watching events: %s", err.Error()) + } + }() + } + + return receiveEventChan(ctx, eventWatch, handleEvent) +} + +func receiveEventChan(ctx context.Context, eventWatch watch.Interface, f func(e watch.Event) error) error { + defer eventWatch.Stop() + for { + select { + case e, ok := <-eventWatch.ResultChan(): + if !ok { + return nil + } + err := f(e) + if err != nil { + return err + } + case <-ctx.Done(): + return nil + } + } +} + +func getHeaders(showNs bool) []string { + headers := []string{"Last seen", "Type", "Reason", "Object", "Message"} + if showNs { + headers = append(namespaceHeader, headers...) + } + + return headers +} + +func getEventRow(e corev1.Event, showNs bool) []string { + var row []string + if showNs { + row = []string{e.Namespace} + } + row = append(row, getLastSeen(e), e.Type, e.Reason, fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name), e.Message) + + return row +} + +// getObjectRef is used to get the metadata of a resource that the selector(in the format ) references. +// It returns an empty string if the resource doesn't reference any resource +// and a string with the format `/.` if it does. +func getObjectRef(ctx context.Context, kubeclient client.Client, ref refInfo, name, ns string) ([]string, error) { + // the resource has no source ref + if len(ref.field) == 0 { + return nil, nil + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(schema.GroupVersionKind{ + Kind: ref.gvk.Kind, + Version: ref.gvk.Version, + Group: ref.gvk.Group, + }) + objName := types.NamespacedName{ + Namespace: ns, + Name: name, + } + + if err := kubeclient.Get(ctx, objName, obj); err != nil { + return nil, err + } + + refKind := ref.kind + if refKind == "" { + kindField := append(ref.field, "kind") + specKind, ok, err := unstructured.NestedString(obj.Object, kindField...) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("field '%s' for '%s' not found", strings.Join(kindField, "."), objName) + } + refKind = specKind + } + + nameField := append(ref.field, "name") + refName, ok, err := unstructured.NestedString(obj.Object, nameField...) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("field '%s' for '%s' not found", strings.Join(nameField, "."), objName) + } + + var allRefs []string + refNamespace := ns + if ref.crossNamespaced { + namespaceField := append(ref.field, "namespace") + namespace, ok, err := unstructured.NestedString(obj.Object, namespaceField...) + if err != nil { + return nil, err + } + if ok { + refNamespace = namespace + } + } + + allRefs = append(allRefs, fmt.Sprintf("%s/%s.%s", refKind, refName, refNamespace)) + if ref.otherRefs != nil { + for _, otherRef := range ref.otherRefs(ns, name) { + allRefs = append(allRefs, fmt.Sprintf("%s.%s", otherRef, refNamespace)) + } + } + return allRefs, nil +} + +type refMap map[string]refInfo + +func (r refMap) getRefInfo(kind string) (refInfo, error) { + for key, ref := range r { + if strings.EqualFold(key, kind) { + return ref, nil + } + } + return refInfo{}, fmt.Errorf("'%s' is not a recognized Flux kind", kind) +} + +func (r refMap) hasKind(kind string) bool { + _, err := r.getRefInfo(kind) + return err == nil +} + +// validateEventTypes checks that the event types passed into the function +// is either equal to `Normal` or `Warning` which are currently the two supported types. +// https://github.com/kubernetes/kubernetes/blob/a8a1abc25cad87333840cd7d54be2efaf31a3177/staging/src/k8s.io/api/core/v1/types.go#L6212 +func validateEventTypes(eventTypes []string) error { + for _, t := range eventTypes { + if !strings.EqualFold(corev1.EventTypeWarning, t) && !strings.EqualFold(corev1.EventTypeNormal, t) { + return fmt.Errorf("type '%s' not supported. Supported types are Normal, Warning", t) + } + } + + return nil +} + +type refInfo struct { + // gvk is the group version kind of the resource + gvk schema.GroupVersionKind + // kind is the kind that the resource references if it's not static + kind string + // crossNamespaced indicates if this resource uses cross namespaced references + crossNamespaced bool + // otherRefs returns other reference that might not be directly accessible + // from the spec of the object + otherRefs func(namespace, name string) []string + field []string +} + +var fluxKindMap = refMap{ + kustomizev1.KustomizationKind: { + gvk: kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind), + crossNamespaced: true, + field: []string{"spec", "sourceRef"}, + }, + helmv2.HelmReleaseKind: { + gvk: helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind), + crossNamespaced: true, + otherRefs: func(namespace, name string) []string { + return []string{fmt.Sprintf("%s/%s-%s", sourcev1.HelmChartKind, namespace, name)} + }, + field: []string{"spec", "chart", "spec", "sourceRef"}, + }, + notificationv1b3.AlertKind: { + gvk: notificationv1b3.GroupVersion.WithKind(notificationv1b3.AlertKind), + kind: notificationv1b3.ProviderKind, + crossNamespaced: false, + field: []string{"spec", "providerRef"}, + }, + notificationv1.ReceiverKind: {gvk: notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)}, + notificationv1b3.ProviderKind: {gvk: notificationv1b3.GroupVersion.WithKind(notificationv1b3.ProviderKind)}, + imagev1.ImagePolicyKind: { + gvk: imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind), + kind: imagev1.ImageRepositoryKind, + crossNamespaced: true, + field: []string{"spec", "imageRepositoryRef"}, + }, + sourcev1.HelmChartKind: { + gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind), + crossNamespaced: true, + field: []string{"spec", "sourceRef"}, + }, + sourcev1.GitRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)}, + sourcev1b2.OCIRepositoryKind: {gvk: sourcev1b2.GroupVersion.WithKind(sourcev1b2.OCIRepositoryKind)}, + sourcev1b2.BucketKind: {gvk: sourcev1b2.GroupVersion.WithKind(sourcev1b2.BucketKind)}, + sourcev1.HelmRepositoryKind: {gvk: sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)}, + autov1.ImageUpdateAutomationKind: {gvk: autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)}, + imagev1.ImageRepositoryKind: {gvk: imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)}, +} + +func ignoreEvent(e corev1.Event) bool { + if !fluxKindMap.hasKind(e.InvolvedObject.Kind) { + return true + } + + if len(eventArgs.filterTypes) > 0 { + _, equal := utils.ContainsEqualFoldItemString(eventArgs.filterTypes, e.Type) + if !equal { + return true + } + } + + return false +} + +func getKindNameFromSelector(selector string) (string, string) { + kind, name := utils.ParseObjectKindName(selector) + // if there's no slash in the selector utils.ParseObjectKindName returns the + // input string as the name but here we want it as the kind instead + if kind == "" && name != "" { + kind = name + name = "" + } + + return kind, name +} + +// The functions below are copied from: https://github.com/kubernetes/kubectl/blob/4ecd7bd0f0799f191335a331ca3c6a397a888233/pkg/cmd/events/events.go#L294 + +// SortableEvents implements sort.Interface for []api.Event by time +type SortableEvents []corev1.Event + +func (list SortableEvents) Len() int { + return len(list) +} + +func (list SortableEvents) Swap(i, j int) { + list[i], list[j] = list[j], list[i] +} + +// Return the time that should be used for sorting, which can come from +// various places in corev1.Event. +func eventTime(event corev1.Event) time.Time { + if event.Series != nil { + return event.Series.LastObservedTime.Time + } + if !event.LastTimestamp.Time.IsZero() { + return event.LastTimestamp.Time + } + return event.EventTime.Time +} + +func (list SortableEvents) Less(i, j int) bool { + return eventTime(list[i]).Before(eventTime(list[j])) +} + +func getLastSeen(e corev1.Event) string { + var interval string + firstTimestampSince := translateMicroTimestampSince(e.EventTime) + if e.EventTime.IsZero() { + firstTimestampSince = translateTimestampSince(e.FirstTimestamp) + } + if e.Series != nil { + interval = fmt.Sprintf("%s (x%d over %s)", translateMicroTimestampSince(e.Series.LastObservedTime), e.Series.Count, firstTimestampSince) + } else if e.Count > 1 { + interval = fmt.Sprintf("%s (x%d over %s)", translateTimestampSince(e.LastTimestamp), e.Count, firstTimestampSince) + } else { + interval = firstTimestampSince + } + + return interval +} + +// translateMicroTimestampSince returns the elapsed time since timestamp in +// human-readable approximation. +func translateMicroTimestampSince(timestamp metav1.MicroTime) string { + if timestamp.IsZero() { + return "" + } + + return duration.HumanDuration(time.Since(timestamp.Time)) +} + +// translateTimestampSince returns the elapsed time since timestamp in +// human-readable approximation. +func translateTimestampSince(timestamp metav1.Time) string { + if timestamp.IsZero() { + return "" + } + + return duration.HumanDuration(time.Since(timestamp.Time)) +} diff --git a/cmd/flux/events_test.go b/cmd/flux/events_test.go new file mode 100644 index 00000000..151cd55c --- /dev/null +++ b/cmd/flux/events_test.go @@ -0,0 +1,438 @@ +/* +Copyright 2023 The Kubernetes Authors. +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" + "strings" + "testing" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" + ssautil "github.com/fluxcd/pkg/ssa/utils" + + "github.com/fluxcd/flux2/v2/internal/utils" +) + +var objects = ` +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: flux-system + namespace: flux-system +spec: + interval: 5m0s + path: ./infrastructure/ + prune: true + sourceRef: + kind: GitRepository + name: flux-system +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: default +spec: + interval: 5m0s + path: ./infrastructure/ + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: flux-system + namespace: flux-system +spec: + interval: 5m0s + ref: + branch: main + secretRef: + name: flux-system + timeout: 1m0s + url: ssh://git@github.com/example/repo +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: podinfo + namespace: default +spec: + chart: + spec: + chart: podinfo + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo + namespace: flux-system + version: '*' + interval: 5m0s +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + url: https://stefanprodan.github.io/podinfo +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmChart +metadata: + name: default-podinfo + namespace: flux-system +spec: + chart: podinfo + interval: 1m0s + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo-chart + version: '*' +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Alert +metadata: + name: webapp + namespace: flux-system +spec: + eventSeverity: info + eventSources: + - kind: GitRepository + name: '*' + providerRef: + name: slack +--- +apiVersion: notification.toolkit.fluxcd.io/v1beta3 +kind: Provider +metadata: + name: slack + namespace: flux-system +spec: + address: https://hooks.slack.com/services/mock + type: slack +--- +apiVersion: image.toolkit.fluxcd.io/v1beta2 +kind: ImagePolicy +metadata: + name: podinfo + namespace: default +spec: + imageRepositoryRef: + name: acr-podinfo + namespace: flux-system + policy: + semver: + range: 5.0.x +--- +apiVersion: v1 +kind: Namespace +metadata: + name: flux-system` + +func Test_getObjectRef(t *testing.T) { + g := NewWithT(t) + objs, err := ssautil.ReadObjects(strings.NewReader(objects)) + g.Expect(err).To(Not(HaveOccurred())) + + builder := fake.NewClientBuilder().WithScheme(utils.NewScheme()) + for _, obj := range objs { + builder = builder.WithObjects(obj) + } + c := builder.Build() + + tests := []struct { + name string + selector string + namespace string + want []string + wantErr bool + }{ + { + name: "Source Ref for Kustomization", + selector: "Kustomization/flux-system", + namespace: "flux-system", + want: []string{"GitRepository/flux-system.flux-system"}, + }, + { + name: "Crossnamespace Source Ref for Kustomization", + selector: "Kustomization/podinfo", + namespace: "default", + want: []string{"GitRepository/flux-system.flux-system"}, + }, + { + name: "Source Ref for HelmRelease", + selector: "HelmRelease/podinfo", + namespace: "default", + want: []string{"HelmRepository/podinfo.flux-system", "HelmChart/default-podinfo.flux-system"}, + }, + { + name: "Source Ref for Alert", + selector: "Alert/webapp", + namespace: "flux-system", + want: []string{"Provider/slack.flux-system"}, + }, + { + name: "Source Ref for ImagePolicy", + selector: "ImagePolicy/podinfo", + namespace: "default", + want: []string{"ImageRepository/acr-podinfo.flux-system"}, + }, + { + name: "Source Ref for ImagePolicy (lowercased)", + selector: "imagepolicy/podinfo", + namespace: "default", + want: []string{"ImageRepository/acr-podinfo.flux-system"}, + }, + { + name: "Empty Ref for Provider", + selector: "Provider/slack", + namespace: "flux-system", + want: nil, + }, + { + name: "Non flux resource", + selector: "Namespace/flux-system", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + kind, name := getKindNameFromSelector(tt.selector) + infoRef, err := fluxKindMap.getRefInfo(kind) + if tt.wantErr { + g.Expect(err).To(HaveOccurred()) + return + } + got, err := getObjectRef(context.Background(), c, infoRef, name, tt.namespace) + + g.Expect(err).To(Not(HaveOccurred())) + g.Expect(got).To(Equal(tt.want)) + }) + } +} + +func Test_getRows(t *testing.T) { + g := NewWithT(t) + objs, err := ssautil.ReadObjects(strings.NewReader(objects)) + g.Expect(err).To(Not(HaveOccurred())) + + builder := fake.NewClientBuilder().WithScheme(utils.NewScheme()) + for _, obj := range objs { + builder = builder.WithObjects(obj) + } + eventList := &corev1.EventList{} + for _, obj := range objs { + infoEvent := createEvent(obj, eventv1.EventSeverityInfo, "Info Message", "Info Reason") + warningEvent := createEvent(obj, eventv1.EventSeverityError, "Error Message", "Error Reason") + eventList.Items = append(eventList.Items, infoEvent, warningEvent) + } + builder = builder.WithLists(eventList) + builder.WithIndex(&corev1.Event{}, "involvedObject.kind/name", kindNameIndexer) + builder.WithIndex(&corev1.Event{}, "involvedObject.kind", kindIndexer) + c := builder.Build() + + tests := []struct { + name string + selector string + refSelector string + namespace string + refNs string + expected [][]string + }{ + { + name: "events from all namespaces", + selector: "", + namespace: "", + expected: [][]string{ + {"default", "", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, + {"default", "", "error", "Error Reason", "ImagePolicy/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "ImagePolicy/podinfo", "Info Message"}, + {"default", "", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "Alert/webapp", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "Alert/webapp", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "GitRepository/flux-system", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "GitRepository/flux-system", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "Kustomization/flux-system", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "Kustomization/flux-system", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "Provider/slack", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "Provider/slack", "Info Message"}, + }, + }, + { + name: "events from default namespaces", + selector: "", + namespace: "default", + expected: [][]string{ + {"", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, + {"", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, + {"", "error", "Error Reason", "ImagePolicy/podinfo", "Error Message"}, + {"", "info", "Info Reason", "ImagePolicy/podinfo", "Info Message"}, + {"", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, + {"", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, + }, + }, + { + name: "Kustomization with crossnamespaced GitRepository", + selector: "Kustomization/podinfo", + namespace: "default", + expected: [][]string{ + {"default", "", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "GitRepository/flux-system", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "GitRepository/flux-system", "Info Message"}, + }, + }, + { + name: "All Kustomization (lowercased selector)", + selector: "kustomization", + expected: [][]string{ + {"default", "", "error", "Error Reason", "Kustomization/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "Kustomization/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "Kustomization/flux-system", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "Kustomization/flux-system", "Info Message"}, + }, + }, + { + name: "HelmRelease with crossnamespaced HelmRepository", + selector: "HelmRelease/podinfo", + namespace: "default", + expected: [][]string{ + {"default", "", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"}, + }, + }, + { + name: "HelmRelease with crossnamespaced HelmRepository (lowercased)", + selector: "helmrelease/podinfo", + namespace: "default", + expected: [][]string{ + {"default", "", "error", "Error Reason", "HelmRelease/podinfo", "Error Message"}, + {"default", "", "info", "Info Reason", "HelmRelease/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmRepository/podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmRepository/podinfo", "Info Message"}, + {"flux-system", "", "error", "Error Reason", "HelmChart/default-podinfo", "Error Message"}, + {"flux-system", "", "info", "Info Reason", "HelmChart/default-podinfo", "Info Message"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + var refs []string + var refNs, refKind, refName string + var clientOpts = []client.ListOption{client.InNamespace(tt.namespace)} + if tt.selector != "" { + kind, name := getKindNameFromSelector(tt.selector) + infoRef, err := fluxKindMap.getRefInfo(kind) + clientOpts = append(clientOpts, getTestListOpt(infoRef.gvk.Kind, name)) + if name != "" { + g.Expect(err).To(Not(HaveOccurred())) + refs, err = getObjectRef(context.Background(), c, infoRef, name, tt.namespace) + g.Expect(err).To(Not(HaveOccurred())) + } + } + + g.Expect(err).To(Not(HaveOccurred())) + + var refOpts [][]client.ListOption + for _, ref := range refs { + refKind, refName, refNs = utils.ParseObjectKindNameNamespace(ref) + refOpts = append(refOpts, []client.ListOption{client.InNamespace(refNs), getTestListOpt(refKind, refName)}) + } + + showNs := tt.namespace == "" || (refNs != "" && refNs != tt.namespace) + rows, err := getRows(context.Background(), c, clientOpts, refOpts, showNs) + g.Expect(err).To(Not(HaveOccurred())) + g.Expect(rows).To(ConsistOf(tt.expected)) + }) + } +} + +func getTestListOpt(kind, name string) client.ListOption { + var sel fields.Selector + if name == "" { + sel = fields.OneTermEqualSelector("involvedObject.kind", kind) + } else { + sel = fields.OneTermEqualSelector("involvedObject.kind/name", fmt.Sprintf("%s/%s", kind, name)) + } + return client.MatchingFieldsSelector{Selector: sel} +} + +func createEvent(obj client.Object, eventType, msg, reason string) corev1.Event { + return corev1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: obj.GetNamespace(), + // name of event needs to be unique + Name: obj.GetNamespace() + obj.GetNamespace() + obj.GetObjectKind().GroupVersionKind().Kind + eventType, + }, + Reason: reason, + Message: msg, + Type: eventType, + InvolvedObject: corev1.ObjectReference{ + Kind: obj.GetObjectKind().GroupVersionKind().Kind, + Namespace: obj.GetNamespace(), + Name: obj.GetName(), + }, + } +} + +func kindNameIndexer(obj client.Object) []string { + e, ok := obj.(*corev1.Event) + if !ok { + panic(fmt.Sprintf("Expected a Event, got %T", e)) + } + + return []string{fmt.Sprintf("%s/%s", e.InvolvedObject.Kind, e.InvolvedObject.Name)} +} + +func kindIndexer(obj client.Object) []string { + e, ok := obj.(*corev1.Event) + if !ok { + panic(fmt.Sprintf("Expected a Event, got %T", e)) + } + + return []string{e.InvolvedObject.Kind} +} diff --git a/cmd/flux/export.go b/cmd/flux/export.go index e9b3be11..3ac9e215 100644 --- a/cmd/flux/export.go +++ b/cmd/flux/export.go @@ -26,13 +26,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var exportCmd = &cobra.Command{ Use: "export", Short: "Export resources in YAML format", - Long: "The export sub-commands export resources in YAML format.", + Long: `The export sub-commands export resources in YAML format.`, } type exportFlags struct { @@ -74,7 +74,7 @@ func (export exportCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -122,5 +122,6 @@ func printExport(export interface{}) error { func resourceToString(data []byte) string { data = bytes.Replace(data, []byte(" creationTimestamp: null\n"), []byte(""), 1) data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1) + data = bytes.TrimSpace(data) return string(data) } diff --git a/cmd/flux/export_alert.go b/cmd/flux/export_alert.go index 9c358e0c..2f158711 100644 --- a/cmd/flux/export_alert.go +++ b/cmd/flux/export_alert.go @@ -20,13 +20,13 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var exportAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Export Alert resources in YAML format", - Long: "The export alert command exports one or all Alert resources in YAML format.", + Long: withPreviewNote("The export alert command exports one or all Alert resources in YAML format."), Example: ` # Export all Alert resources flux export alert --all > alerts.yaml diff --git a/cmd/flux/export_alertprovider.go b/cmd/flux/export_alertprovider.go index eb47294f..e4c3ab9a 100644 --- a/cmd/flux/export_alertprovider.go +++ b/cmd/flux/export_alertprovider.go @@ -20,13 +20,13 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var exportAlertProviderCmd = &cobra.Command{ Use: "alert-provider [name]", Short: "Export Provider resources in YAML format", - Long: "The export alert-provider command exports one or all Provider resources in YAML format.", + Long: withPreviewNote("The export alert-provider command exports one or all Provider resources in YAML format."), Example: ` # Export all Provider resources flux export alert-provider --all > alert-providers.yaml diff --git a/cmd/flux/export_helmrelease.go b/cmd/flux/export_helmrelease.go index 8fd8c87f..aaa209f5 100644 --- a/cmd/flux/export_helmrelease.go +++ b/cmd/flux/export_helmrelease.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +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. @@ -20,7 +20,7 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var exportHelmReleaseCmd = &cobra.Command{ diff --git a/cmd/flux/export_image.go b/cmd/flux/export_image.go index 24410c97..074cff73 100644 --- a/cmd/flux/export_image.go +++ b/cmd/flux/export_image.go @@ -23,7 +23,7 @@ import ( var exportImageCmd = &cobra.Command{ Use: "image", Short: "Export image automation objects", - Long: "The export image sub-commands export image automation objects in YAML format.", + Long: `The export image sub-commands export image automation objects in YAML format.`, } func init() { diff --git a/cmd/flux/export_image_policy.go b/cmd/flux/export_image_policy.go index 5302dd63..835c8a20 100644 --- a/cmd/flux/export_image_policy.go +++ b/cmd/flux/export_image_policy.go @@ -20,13 +20,13 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var exportImagePolicyCmd = &cobra.Command{ Use: "policy [name]", Short: "Export ImagePolicy resources in YAML format", - Long: "The export image policy command exports one or all ImagePolicy resources in YAML format.", + Long: withPreviewNote("The export image policy command exports one or all ImagePolicy resources in YAML format."), Example: ` # Export all ImagePolicy resources flux export image policy --all > image-policies.yaml diff --git a/cmd/flux/export_image_repository.go b/cmd/flux/export_image_repository.go index 13f72f7d..48df926d 100644 --- a/cmd/flux/export_image_repository.go +++ b/cmd/flux/export_image_repository.go @@ -20,13 +20,13 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var exportImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Export ImageRepository resources in YAML format", - Long: "The export image repository command exports one or all ImageRepository resources in YAML format.", + Long: withPreviewNote("The export image repository command exports one or all ImageRepository resources in YAML format."), Example: ` # Export all ImageRepository resources flux export image repository --all > image-repositories.yaml diff --git a/cmd/flux/export_image_update.go b/cmd/flux/export_image_update.go index 44f4b4a1..59aae7d9 100644 --- a/cmd/flux/export_image_update.go +++ b/cmd/flux/export_image_update.go @@ -20,13 +20,13 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" ) var exportImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Export ImageUpdateAutomation resources in YAML format", - Long: "The export image update command exports one or all ImageUpdateAutomation resources in YAML format.", + Long: withPreviewNote("The export image update command exports one or all ImageUpdateAutomation resources in YAML format."), Example: ` # Export all ImageUpdateAutomation resources flux export image update --all > updates.yaml diff --git a/cmd/flux/export_kustomization.go b/cmd/flux/export_kustomization.go index ed613dcc..d46c5729 100644 --- a/cmd/flux/export_kustomization.go +++ b/cmd/flux/export_kustomization.go @@ -20,14 +20,14 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var exportKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Export Kustomization resources in YAML format", - Long: "The export kustomization command exports one or all Kustomization resources in YAML format.", + Long: `The export kustomization command exports one or all Kustomization resources in YAML format.`, Example: ` # Export all Kustomization resources flux export kustomization --all > kustomizations.yaml diff --git a/cmd/flux/export_receiver.go b/cmd/flux/export_receiver.go index 0a79d853..358d0b42 100644 --- a/cmd/flux/export_receiver.go +++ b/cmd/flux/export_receiver.go @@ -20,13 +20,13 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var exportReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Export Receiver resources in YAML format", - Long: "The export receiver command exports one or all Receiver resources in YAML format.", + Long: `The export receiver command exports one or all Receiver resources in YAML format.`, Example: ` # Export all Receiver resources flux export receiver --all > receivers.yaml @@ -44,7 +44,7 @@ func init() { } func exportReceiver(receiver *notificationv1.Receiver) interface{} { - gvk := notificationv1.GroupVersion.WithKind("Receiver") + gvk := notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind) export := notificationv1.Receiver{ TypeMeta: metav1.TypeMeta{ Kind: gvk.Kind, diff --git a/cmd/flux/export_secret.go b/cmd/flux/export_secret.go index eb7dfb05..76d6ec56 100644 --- a/cmd/flux/export_secret.go +++ b/cmd/flux/export_secret.go @@ -26,7 +26,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) // exportableWithSecret represents a type that you can fetch from the Kubernetes @@ -46,7 +46,6 @@ type exportableWithSecretList interface { } type exportWithSecretCommand struct { - apiType object exportableWithSecret list exportableWithSecretList } @@ -59,7 +58,7 @@ func (export exportWithSecretCommand) run(cmd *cobra.Command, args []string) err ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } diff --git a/cmd/flux/export_source.go b/cmd/flux/export_source.go index 3ba6f6bb..84ddc741 100644 --- a/cmd/flux/export_source.go +++ b/cmd/flux/export_source.go @@ -23,7 +23,7 @@ import ( var exportSourceCmd = &cobra.Command{ Use: "source", Short: "Export sources", - Long: "The export source sub-commands export sources in YAML format.", + Long: `The export source sub-commands export sources in YAML format.`, } var ( diff --git a/cmd/flux/export_source_bucket.go b/cmd/flux/export_source_bucket.go index 33e36ebd..b95d266a 100644 --- a/cmd/flux/export_source_bucket.go +++ b/cmd/flux/export_source_bucket.go @@ -21,13 +21,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" ) var exportSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Export Bucket sources in YAML format", - Long: "The export source git command exports one or all Bucket sources in YAML format.", + Long: withPreviewNote("The export source git command exports one or all Bucket sources in YAML format."), Example: ` # Export all Bucket sources flux export source bucket --all > sources.yaml diff --git a/cmd/flux/export_source_chart.go b/cmd/flux/export_source_chart.go new file mode 100644 index 00000000..ea9b7207 --- /dev/null +++ b/cmd/flux/export_source_chart.go @@ -0,0 +1,67 @@ +/* +Copyright 2024 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + sourcev1 "github.com/fluxcd/source-controller/api/v1" +) + +var exportSourceChartCmd = &cobra.Command{ + Use: "chart [name]", + Short: "Export HelmChart sources in YAML format", + Long: withPreviewNote("The export source chart command exports one or all HelmChart sources in YAML format."), + Example: ` # Export all chart sources + flux export source chart --all > sources.yaml`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), + RunE: exportCommand{ + list: helmChartListAdapter{&sourcev1.HelmChartList{}}, + object: helmChartAdapter{&sourcev1.HelmChart{}}, + }.run, +} + +func init() { + exportSourceCmd.AddCommand(exportSourceChartCmd) +} + +func exportHelmChart(source *sourcev1.HelmChart) interface{} { + gvk := sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind) + export := sourcev1.HelmChart{ + TypeMeta: metav1.TypeMeta{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: source.Name, + Namespace: source.Namespace, + Labels: source.Labels, + Annotations: source.Annotations, + }, + Spec: source.Spec, + } + return export +} + +func (ex helmChartAdapter) export() interface{} { + return exportHelmChart(ex.HelmChart) +} + +func (ex helmChartListAdapter) exportItem(i int) interface{} { + return exportHelmChart(&ex.HelmChartList.Items[i]) +} diff --git a/cmd/flux/export_source_git.go b/cmd/flux/export_source_git.go index c93e0fd1..0e2921e4 100644 --- a/cmd/flux/export_source_git.go +++ b/cmd/flux/export_source_git.go @@ -21,13 +21,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var exportSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Export GitRepository sources in YAML format", - Long: "The export source git command exports one or all GitRepository sources in YAML format.", + Long: `The export source git command exports one or all GitRepository sources in YAML format.`, Example: ` # Export all GitRepository sources flux export source git --all > sources.yaml diff --git a/cmd/flux/export_source_helm.go b/cmd/flux/export_source_helm.go index d2215335..2c8da3a1 100644 --- a/cmd/flux/export_source_helm.go +++ b/cmd/flux/export_source_helm.go @@ -21,7 +21,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var exportSourceHelmCmd = &cobra.Command{ diff --git a/cmd/flux/export_source_oci.go b/cmd/flux/export_source_oci.go new file mode 100644 index 00000000..66e942d9 --- /dev/null +++ b/cmd/flux/export_source_oci.go @@ -0,0 +1,92 @@ +/* +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 ( + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" +) + +var exportSourceOCIRepositoryCmd = &cobra.Command{ + Use: "oci [name]", + Short: "Export OCIRepository sources in YAML format", + Long: withPreviewNote("The export source oci command exports one or all OCIRepository sources in YAML format."), + Example: ` # Export all OCIRepository sources + flux export source oci --all > sources.yaml + + # Export a OCIRepository including the static credentials + flux export source oci my-app --with-credentials > source.yaml`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), + RunE: exportWithSecretCommand{ + list: ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, + object: ociRepositoryAdapter{&sourcev1.OCIRepository{}}, + }.run, +} + +func init() { + exportSourceCmd.AddCommand(exportSourceOCIRepositoryCmd) +} + +func exportOCIRepository(source *sourcev1.OCIRepository) interface{} { + gvk := sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind) + export := sourcev1.OCIRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: source.Name, + Namespace: source.Namespace, + Labels: source.Labels, + Annotations: source.Annotations, + }, + Spec: source.Spec, + } + return export +} + +func getOCIRepositorySecret(source *sourcev1.OCIRepository) *types.NamespacedName { + if source.Spec.SecretRef != nil { + namespacedName := types.NamespacedName{ + Namespace: source.Namespace, + Name: source.Spec.SecretRef.Name, + } + + return &namespacedName + } + + return nil +} + +func (ex ociRepositoryAdapter) secret() *types.NamespacedName { + return getOCIRepositorySecret(ex.OCIRepository) +} + +func (ex ociRepositoryListAdapter) secretItem(i int) *types.NamespacedName { + return getOCIRepositorySecret(&ex.OCIRepositoryList.Items[i]) +} + +func (ex ociRepositoryAdapter) export() interface{} { + return exportOCIRepository(ex.OCIRepository) +} + +func (ex ociRepositoryListAdapter) exportItem(i int) interface{} { + return exportOCIRepository(&ex.OCIRepositoryList.Items[i]) +} diff --git a/cmd/flux/export_test.go b/cmd/flux/export_test.go index dc59caa2..5488305b 100644 --- a/cmd/flux/export_test.go +++ b/cmd/flux/export_test.go @@ -8,78 +8,98 @@ import ( ) func TestExport(t *testing.T) { + namespace := allocateNamespace("flux-system") + + objectFile := "testdata/export/objects.yaml" + tmpl := map[string]string{ + "fluxns": namespace, + } + testEnv.CreateObjectFile(objectFile, tmpl, t) + cases := []struct { name string arg string goldenFile string + tmpl map[string]string }{ { "alert-provider", "export alert-provider slack", "testdata/export/provider.yaml", + tmpl, }, { "alert", "export alert flux-system", "testdata/export/alert.yaml", + tmpl, }, { "image policy", "export image policy flux-system", "testdata/export/image-policy.yaml", + tmpl, }, { "image repository", "export image repository flux-system", "testdata/export/image-repo.yaml", + tmpl, }, { "image update", "export image update flux-system", "testdata/export/image-update.yaml", + tmpl, }, { "source git", "export source git flux-system", "testdata/export/git-repo.yaml", + tmpl, + }, + { + "source chart", + "export source chart flux-system", + "testdata/export/helm-chart.yaml", + tmpl, }, { "source helm", "export source helm flux-system", "testdata/export/helm-repo.yaml", + tmpl, }, { "receiver", "export receiver flux-system", "testdata/export/receiver.yaml", + tmpl, }, { "kustomization", "export kustomization flux-system", "testdata/export/ks.yaml", + tmpl, }, { "helmrelease", "export helmrelease flux-system", "testdata/export/helm-release.yaml", + tmpl, }, { "bucket", "export source bucket flux-system", "testdata/export/bucket.yaml", + tmpl, }, } - objectFile := "testdata/export/objects.yaml" - tmpl := map[string]string{ - "fluxns": allocateNamespace("flux-system"), - } - testEnv.CreateObjectFile(objectFile, tmpl, t) - for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { cmd := cmdTestCase{ - args: tt.arg + " -n=" + tmpl["fluxns"], + args: tt.arg + " -n=" + namespace, assert: assertGoldenTemplateFile(tt.goldenFile, tmpl), } diff --git a/cmd/flux/get.go b/cmd/flux/get.go index f52fd496..17766f38 100644 --- a/cmd/flux/get.go +++ b/cmd/flux/get.go @@ -32,7 +32,8 @@ import ( "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/printers" ) type deriveType func(runtime.Object) (summarisable, error) @@ -58,13 +59,14 @@ func (m typeMap) execute(t string, obj runtime.Object) (summarisable, error) { var getCmd = &cobra.Command{ Use: "get", Short: "Get the resources and their status", - Long: "The get sub-commands print the statuses of Flux resources.", + Long: `The get sub-commands print the statuses of Flux resources.`, } type GetFlags struct { allNamespaces bool noHeader bool statusSelector string + labelSelector string watch bool } @@ -77,6 +79,8 @@ func init() { getCmd.PersistentFlags().BoolVarP(&getArgs.watch, "watch", "w", false, "After listing/getting the requested object, watch for changes.") getCmd.PersistentFlags().StringVar(&getArgs.statusSelector, "status-selector", "", "specify the status condition name and the desired state to filter the get result, e.g. ready=false") + getCmd.PersistentFlags().StringVarP(&getArgs.labelSelector, "label-selector", "l", "", + "filter objects by label selector") rootCmd.AddCommand(getCmd) } @@ -135,7 +139,7 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -149,6 +153,21 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error { listOpts = append(listOpts, client.MatchingFields{"metadata.name": args[0]}) } + if getArgs.labelSelector != "" { + label, err := metav1.ParseToLabelSelector(getArgs.labelSelector) + if err != nil { + return fmt.Errorf("unable to parse label selector: %w", err) + } + + sel, err := metav1.LabelSelectorAsSelector(label) + if err != nil { + return err + } + listOpts = append(listOpts, client.MatchingLabelsSelector{ + Selector: sel, + }) + } + getAll := cmd.Use == "all" if getArgs.watch { @@ -157,12 +176,24 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error { err = kubeClient.List(ctx, get.list.asClientList(), listOpts...) if err != nil { + if getAll && apimeta.IsNoMatchError(err) { + return nil + } return err } if get.list.len() == 0 { - if !getAll { - logger.Failuref("no %s objects found in %s namespace", get.kind, *kubeconfigArgs.Namespace) + if len(args) > 0 { + logger.Failuref("%s object '%s' not found in %s namespace", + get.kind, + args[0], + namespaceNameOrAny(getArgs.allNamespaces, *kubeconfigArgs.Namespace), + ) + } else if !getAll { + logger.Failuref("no %s objects found in %s namespace", + get.kind, + namespaceNameOrAny(getArgs.allNamespaces, *kubeconfigArgs.Namespace), + ) } return nil } @@ -177,7 +208,10 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error { return err } - utils.PrintTable(cmd.OutOrStdout(), header, rows) + err = printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows) + if err != nil { + return err + } if getAll { fmt.Println() @@ -186,6 +220,13 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error { return nil } +func namespaceNameOrAny(allNamespaces bool, namespaceName string) string { + if allNamespaces { + return "any" + } + return fmt.Sprintf("%q", namespaceName) +} + func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) { noFilter := true var conditionType, conditionStatus string @@ -208,7 +249,6 @@ func getRowsToPrint(getAll bool, list summarisable) ([][]string, error) { return rows, nil } -// // watch starts a client-side watch of one or more resources. func (get *getCommand) watch(ctx context.Context, kubeClient client.WithWatch, cmd *cobra.Command, args []string, listOpts []client.ListOption) error { w, err := kubeClient.Watch(ctx, get.list.asClientList(), listOpts...) @@ -242,10 +282,16 @@ func watchUntil(ctx context.Context, w watch.Interface, get *getCommand) (bool, return false, err } if firstIteration { - utils.PrintTable(os.Stdout, header, rows) + err = printers.TablePrinter(header).Print(os.Stdout, rows) + if err != nil { + return false, err + } firstIteration = false } else { - utils.PrintTable(os.Stdout, []string{}, rows) + err = printers.TablePrinter([]string{}).Print(os.Stdout, rows) + if err != nil { + return false, err + } } return false, nil diff --git a/cmd/flux/get_alert.go b/cmd/flux/get_alert.go index 126dbe56..54b2afe6 100644 --- a/cmd/flux/get_alert.go +++ b/cmd/flux/get_alert.go @@ -19,19 +19,21 @@ package main import ( "fmt" "strconv" - "strings" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var getAlertCmd = &cobra.Command{ Use: "alerts", Aliases: []string{"alert"}, Short: "Get Alert statuses", - Long: "The get alert command prints the statuses of the resources.", + Long: withPreviewNote("The get alert command prints the statuses of the resources."), Example: ` # List all Alerts and their status flux get alerts`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), @@ -76,12 +78,13 @@ func init() { func (s alertListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] - status, msg := statusAndMessage(item.Status.Conditions) - return append(nameColumns(&item, includeNamespace, includeKind), status, msg, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + status, msg := string(metav1.ConditionTrue), "Alert is Ready" + return append(nameColumns(&item, includeNamespace, includeKind), + cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (s alertListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Suspended"} + headers := []string{"Name", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } @@ -89,6 +92,5 @@ func (s alertListAdapter) headers(includeNamespace bool) []string { } func (s alertListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { - item := s.Items[i] - return statusMatches(conditionType, conditionStatus, item.Status.Conditions) + return false } diff --git a/cmd/flux/get_alertprovider.go b/cmd/flux/get_alertprovider.go index cb27973f..11d50cb3 100644 --- a/cmd/flux/get_alertprovider.go +++ b/cmd/flux/get_alertprovider.go @@ -20,16 +20,17 @@ import ( "fmt" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var getAlertProviderCmd = &cobra.Command{ Use: "alert-providers", Aliases: []string{"alert-provider"}, Short: "Get Provider statuses", - Long: "The get alert-provider command prints the statuses of the resources.", + Long: withPreviewNote("The get alert-provider command prints the statuses of the resources."), Example: ` # List all Providers and their status flux get alert-providers`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), @@ -74,7 +75,7 @@ func init() { func (s alertProviderListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] - status, msg := statusAndMessage(item.Status.Conditions) + status, msg := string(metav1.ConditionTrue), "Provider is Ready" return append(nameColumns(&item, includeNamespace, includeKind), status, msg) } @@ -87,6 +88,5 @@ func (s alertProviderListAdapter) headers(includeNamespace bool) []string { } func (s alertProviderListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { - item := s.Items[i] - return statusMatches(conditionType, conditionStatus, item.Status.Conditions) + return false } diff --git a/cmd/flux/get_all.go b/cmd/flux/get_all.go index 5d4d03f2..69122ffd 100644 --- a/cmd/flux/get_all.go +++ b/cmd/flux/get_all.go @@ -17,19 +17,19 @@ limitations under the License. package main import ( - "strings" - "github.com/spf13/cobra" + apimeta "k8s.io/apimachinery/pkg/api/meta" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" ) var getAllCmd = &cobra.Command{ Use: "all", Short: "Get all resources and statuses", - Long: "The get all command print the statuses of all resources.", + Long: withPreviewNote("The get all command print the statuses of all resources."), Example: ` # List all resources in a namespace flux get all --namespace=flux-system @@ -62,11 +62,11 @@ var getAllCmd = &cobra.Command{ }, { apiType: alertProviderType, - list: alertProviderListAdapter{¬ificationv1.ProviderList{}}, + list: alertProviderListAdapter{¬ificationv1b3.ProviderList{}}, }, { apiType: alertType, - list: &alertListAdapter{¬ificationv1.AlertList{}}, + list: &alertListAdapter{¬ificationv1b3.AlertList{}}, }, } @@ -86,7 +86,7 @@ var getAllCmd = &cobra.Command{ } func logError(err error) { - if !strings.Contains(err.Error(), "no matches for kind") { + if !apimeta.IsNoMatchError(err) { logger.Failuref(err.Error()) } } diff --git a/cmd/flux/get_helmrelease.go b/cmd/flux/get_helmrelease.go index 5478e29c..c5a4b67e 100644 --- a/cmd/flux/get_helmrelease.go +++ b/cmd/flux/get_helmrelease.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +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. @@ -19,11 +19,13 @@ package main import ( "fmt" "strconv" - "strings" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var getHelmReleaseCmd = &cobra.Command{ @@ -70,16 +72,23 @@ func init() { getCmd.AddCommand(getHelmReleaseCmd) } +func getHelmReleaseRevision(helmRelease helmv2.HelmRelease) string { + if helmRelease.Status.History != nil && len(helmRelease.Status.History) > 0 { + return helmRelease.Status.History[0].ChartVersion + } + return helmRelease.Status.LastAttemptedRevision +} + func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := a.Items[i] - revision := item.Status.LastAppliedRevision + revision := getHelmReleaseRevision(item) status, msg := statusAndMessage(item.Status.Conditions) return append(nameColumns(&item, includeNamespace, includeKind), - status, msg, revision, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a helmReleaseListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Revision", "Suspended"} + headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } diff --git a/cmd/flux/get_image.go b/cmd/flux/get_image.go index 5cde4c24..0cffc78d 100644 --- a/cmd/flux/get_image.go +++ b/cmd/flux/get_image.go @@ -24,10 +24,7 @@ var getImageCmd = &cobra.Command{ Use: "images", Aliases: []string{"image"}, Short: "Get image automation object status", - Long: "The get image sub-commands print the status of image automation objects.", - RunE: func(cmd *cobra.Command, args []string) error { - return validateWatchOption(cmd, "images") - }, + Long: `The get image sub-commands print the status of image automation objects.`, } func init() { diff --git a/cmd/flux/get_image_all.go b/cmd/flux/get_image_all.go index f554232e..bf0fbd52 100644 --- a/cmd/flux/get_image_all.go +++ b/cmd/flux/get_image_all.go @@ -17,18 +17,16 @@ limitations under the License. package main import ( - "strings" - "github.com/spf13/cobra" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var getImageAllCmd = &cobra.Command{ Use: "all", Short: "Get all image statuses", - Long: "The get image sub-commands print the statuses of all image objects.", + Long: withPreviewNote("The get image sub-commands print the statuses of all image objects."), Example: ` # List all image objects in a namespace flux get images all --namespace=flux-system @@ -57,9 +55,7 @@ var getImageAllCmd = &cobra.Command{ for _, c := range allImageCmd { if err := c.run(cmd, args); err != nil { - if !strings.Contains(err.Error(), "no matches for kind") { - logger.Failuref(err.Error()) - } + logger.Failuref(err.Error()) } } diff --git a/cmd/flux/get_image_policy.go b/cmd/flux/get_image_policy.go index 7974c380..cc10f307 100644 --- a/cmd/flux/get_image_policy.go +++ b/cmd/flux/get_image_policy.go @@ -22,13 +22,13 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/runtime" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var getImagePolicyCmd = &cobra.Command{ Use: "policy", Short: "Get ImagePolicy status", - Long: "The get image policy command prints the status of ImagePolicy objects.", + Long: withPreviewNote("The get image policy command prints the status of ImagePolicy objects."), Example: ` # List all image policies and their status flux get image policy @@ -74,11 +74,11 @@ func init() { func (s imagePolicyListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := statusAndMessage(item.Status.Conditions) - return append(nameColumns(&item, includeNamespace, includeKind), status, msg, item.Status.LatestImage) + return append(nameColumns(&item, includeNamespace, includeKind), item.Status.LatestImage, status, msg) } func (s imagePolicyListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Latest image"} + headers := []string{"Name", "Latest image", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } diff --git a/cmd/flux/get_image_repository.go b/cmd/flux/get_image_repository.go index 0878a29c..e160de8d 100644 --- a/cmd/flux/get_image_repository.go +++ b/cmd/flux/get_image_repository.go @@ -19,19 +19,20 @@ package main import ( "fmt" "strconv" - "strings" "time" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var getImageRepositoryCmd = &cobra.Command{ Use: "repository", Short: "Get ImageRepository status", - Long: "The get image repository command prints the status of ImageRepository objects.", + Long: withPreviewNote("The get image repository command prints the status of ImageRepository objects."), Example: ` # List all image repositories and their status flux get image repository @@ -82,11 +83,11 @@ func (s imageRepositoryListAdapter) summariseItem(i int, includeNamespace bool, lastScan = item.Status.LastScanResult.ScanTime.Time.Format(time.RFC3339) } return append(nameColumns(&item, includeNamespace, includeKind), - status, msg, lastScan, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + lastScan, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (s imageRepositoryListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Last scan", "Suspended"} + headers := []string{"Name", "Last scan", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } diff --git a/cmd/flux/get_image_update.go b/cmd/flux/get_image_update.go index da16c576..f653a055 100644 --- a/cmd/flux/get_image_update.go +++ b/cmd/flux/get_image_update.go @@ -19,19 +19,20 @@ package main import ( "fmt" "strconv" - "strings" "time" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" ) var getImageUpdateCmd = &cobra.Command{ Use: "update", Short: "Get ImageUpdateAutomation status", - Long: "The get image update command prints the status of ImageUpdateAutomation objects.", + Long: withPreviewNote("The get image update command prints the status of ImageUpdateAutomation objects."), Example: ` # List all image update automation object and their status flux get image update @@ -81,11 +82,12 @@ func (s imageUpdateAutomationListAdapter) summariseItem(i int, includeNamespace if item.Status.LastAutomationRunTime != nil { lastRun = item.Status.LastAutomationRunTime.Time.Format(time.RFC3339) } - return append(nameColumns(&item, includeNamespace, includeKind), status, msg, lastRun, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + return append(nameColumns(&item, includeNamespace, includeKind), lastRun, + cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (s imageUpdateAutomationListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Last run", "Suspended"} + headers := []string{"Name", "Last run", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } diff --git a/cmd/flux/get_kustomization.go b/cmd/flux/get_kustomization.go index d2991e93..a95ec655 100644 --- a/cmd/flux/get_kustomization.go +++ b/cmd/flux/get_kustomization.go @@ -18,22 +18,23 @@ package main import ( "fmt" - "regexp" "strconv" - "strings" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var getKsCmd = &cobra.Command{ Use: "kustomizations", Aliases: []string{"ks", "kustomization"}, Short: "Get Kustomization statuses", - Long: "The get kustomizations command prints the statuses of the resources.", + Long: `The get kustomizations command prints the statuses of the resources.`, Example: ` # List all kustomizations and their status flux get kustomizations`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), @@ -80,16 +81,14 @@ func (a kustomizationListAdapter) summariseItem(i int, includeNamespace bool, in item := a.Items[i] revision := item.Status.LastAppliedRevision status, msg := statusAndMessage(item.Status.Conditions) - if status == string(metav1.ConditionTrue) { - revision = shortenCommitSha(revision) - msg = shortenCommitSha(msg) - } + revision = utils.TruncateHex(revision) + msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), - status, msg, revision, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a kustomizationListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Revision", "Suspended"} + headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } @@ -100,13 +99,3 @@ func (a kustomizationListAdapter) statusSelectorMatches(i int, conditionType, co item := a.Items[i] return statusMatches(conditionType, conditionStatus, item.Status.Conditions) } - -func shortenCommitSha(msg string) string { - r := regexp.MustCompile("/([a-f0-9]{40})$") - sha := r.FindString(msg) - if sha != "" { - msg = strings.Replace(msg, sha, string([]rune(sha)[:8]), -1) - } - - return msg -} diff --git a/cmd/flux/get_receiver.go b/cmd/flux/get_receiver.go index d2147d1b..43f8f5a4 100644 --- a/cmd/flux/get_receiver.go +++ b/cmd/flux/get_receiver.go @@ -19,19 +19,20 @@ package main import ( "fmt" "strconv" - "strings" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var getReceiverCmd = &cobra.Command{ Use: "receivers", Aliases: []string{"receiver"}, Short: "Get Receiver statuses", - Long: "The get receiver command prints the statuses of the resources.", + Long: `The get receiver command prints the statuses of the resources.`, Example: ` # List all Receiver and their status flux get receivers`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), @@ -74,11 +75,12 @@ func init() { func (s receiverListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { item := s.Items[i] status, msg := statusAndMessage(item.Status.Conditions) - return append(nameColumns(&item, includeNamespace, includeKind), status, msg, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + return append(nameColumns(&item, includeNamespace, includeKind), + cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (s receiverListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Suspended"} + headers := []string{"Name", "Suspended", "Ready", "Message"} if includeNamespace { return append(namespaceHeader, headers...) } diff --git a/cmd/flux/get_source.go b/cmd/flux/get_source.go index 0e3cd5e5..b70d959f 100644 --- a/cmd/flux/get_source.go +++ b/cmd/flux/get_source.go @@ -24,11 +24,7 @@ var getSourceCmd = &cobra.Command{ Use: "sources", Aliases: []string{"source"}, Short: "Get source statuses", - Long: "The get source sub-commands print the statuses of the sources.", - RunE: func(cmd *cobra.Command, args []string) error { - - return validateWatchOption(cmd, "sources") - }, + Long: `The get source sub-commands print the statuses of the sources.`, } func init() { diff --git a/cmd/flux/get_source_all.go b/cmd/flux/get_source_all.go index 08c12de1..5dfb9f97 100644 --- a/cmd/flux/get_source_all.go +++ b/cmd/flux/get_source_all.go @@ -17,17 +17,17 @@ limitations under the License. package main import ( - "strings" - "github.com/spf13/cobra" + apimeta "k8s.io/apimachinery/pkg/api/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" ) var getSourceAllCmd = &cobra.Command{ Use: "all", Short: "Get all source statuses", - Long: "The get sources all command print the statuses of all sources.", + Long: withPreviewNote("The get sources all command print the statuses of all sources."), Example: ` # List all sources in a namespace flux get sources all --namespace=flux-system @@ -40,9 +40,13 @@ var getSourceAllCmd = &cobra.Command{ } var allSourceCmd = []getCommand{ + { + apiType: ociRepositoryType, + list: &ociRepositoryListAdapter{&sourcev1b2.OCIRepositoryList{}}, + }, { apiType: bucketType, - list: &bucketListAdapter{&sourcev1.BucketList{}}, + list: &bucketListAdapter{&sourcev1b2.BucketList{}}, }, { apiType: gitRepositoryType, @@ -60,7 +64,7 @@ var getSourceAllCmd = &cobra.Command{ for _, c := range allSourceCmd { if err := c.run(cmd, args); err != nil { - if !strings.Contains(err.Error(), "no matches for kind") { + if !apimeta.IsNoMatchError(err) { logger.Failuref(err.Error()) } } diff --git a/cmd/flux/get_source_bucket.go b/cmd/flux/get_source_bucket.go index 963a8ed4..b7355fdb 100644 --- a/cmd/flux/get_source_bucket.go +++ b/cmd/flux/get_source_bucket.go @@ -19,18 +19,21 @@ package main import ( "fmt" "strconv" - "strings" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceBucketCmd = &cobra.Command{ Use: "bucket", Short: "Get Bucket source statuses", - Long: "The get sources bucket command prints the status of the Bucket sources.", + Long: withPreviewNote("The get sources bucket command prints the status of the Bucket sources."), Example: ` # List all Buckets and their status flux get sources bucket @@ -80,12 +83,14 @@ func (a *bucketListAdapter) summariseItem(i int, includeNamespace bool, includeK revision = item.GetArtifact().Revision } status, msg := statusAndMessage(item.Status.Conditions) + revision = utils.TruncateHex(revision) + msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), - status, msg, revision, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a bucketListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Revision", "Suspended"} + headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } diff --git a/cmd/flux/get_source_chart.go b/cmd/flux/get_source_chart.go index f5401791..4f078453 100644 --- a/cmd/flux/get_source_chart.go +++ b/cmd/flux/get_source_chart.go @@ -19,12 +19,15 @@ package main import ( "fmt" "strconv" - "strings" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceHelmChartCmd = &cobra.Command{ @@ -80,12 +83,15 @@ func (a *helmChartListAdapter) summariseItem(i int, includeNamespace bool, inclu revision = item.GetArtifact().Revision } status, msg := statusAndMessage(item.Status.Conditions) + // NB: do not shorten revision as it contains a SemVer + // Message may still contain reference of e.g. commit chart was build from + msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), - status, msg, revision, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a helmChartListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Revision", "Suspended"} + headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } diff --git a/cmd/flux/get_source_git.go b/cmd/flux/get_source_git.go index 95b8ab4f..78476de5 100644 --- a/cmd/flux/get_source_git.go +++ b/cmd/flux/get_source_git.go @@ -19,19 +19,21 @@ package main import ( "fmt" "strconv" - "strings" "github.com/spf13/cobra" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/apimachinery/pkg/runtime" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceGitCmd = &cobra.Command{ Use: "git", Short: "Get GitRepository source statuses", - Long: "The get sources git command prints the status of the GitRepository sources.", + Long: `The get sources git command prints the status of the GitRepository sources.`, Example: ` # List all Git repositories and their status flux get sources git @@ -81,16 +83,14 @@ func (a *gitRepositoryListAdapter) summariseItem(i int, includeNamespace bool, i revision = item.GetArtifact().Revision } status, msg := statusAndMessage(item.Status.Conditions) - if status == string(metav1.ConditionTrue) { - revision = shortenCommitSha(revision) - msg = shortenCommitSha(msg) - } + revision = utils.TruncateHex(revision) + msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), - status, msg, revision, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a gitRepositoryListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Revision", "Suspended"} + headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } diff --git a/cmd/flux/get_source_helm.go b/cmd/flux/get_source_helm.go index cf98246b..eef666d4 100644 --- a/cmd/flux/get_source_helm.go +++ b/cmd/flux/get_source_helm.go @@ -19,12 +19,16 @@ package main import ( "fmt" "strconv" - "strings" "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceHelmCmd = &cobra.Command{ @@ -79,13 +83,20 @@ func (a *helmRepositoryListAdapter) summariseItem(i int, includeNamespace bool, if item.GetArtifact() != nil { revision = item.GetArtifact().Revision } - status, msg := statusAndMessage(item.Status.Conditions) + var status, msg string + if item.Spec.Type == sourcev1.HelmRepositoryTypeOCI { + status, msg = string(metav1.ConditionTrue), "Helm repository is Ready" + } else { + status, msg = statusAndMessage(item.Status.Conditions) + } + revision = utils.TruncateHex(revision) + msg = utils.TruncateHex(msg) return append(nameColumns(&item, includeNamespace, includeKind), - status, msg, revision, strings.Title(strconv.FormatBool(item.Spec.Suspend))) + revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) } func (a helmRepositoryListAdapter) headers(includeNamespace bool) []string { - headers := []string{"Name", "Ready", "Message", "Revision", "Suspended"} + headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} if includeNamespace { headers = append([]string{"Namespace"}, headers...) } diff --git a/cmd/flux/get_source_oci.go b/cmd/flux/get_source_oci.go new file mode 100644 index 00000000..e5bbe409 --- /dev/null +++ b/cmd/flux/get_source_oci.go @@ -0,0 +1,103 @@ +/* +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 ( + "fmt" + "strconv" + + "github.com/spf13/cobra" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "k8s.io/apimachinery/pkg/runtime" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/utils" +) + +var getSourceOCIRepositoryCmd = &cobra.Command{ + Use: "oci", + Short: "Get OCIRepository status", + Long: withPreviewNote("The get sources oci command prints the status of the OCIRepository sources."), + Example: ` # List all OCIRepositories and their status + flux get sources oci + + # List OCIRepositories from all namespaces + flux get sources oci --all-namespaces`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), + RunE: func(cmd *cobra.Command, args []string) error { + get := getCommand{ + apiType: ociRepositoryType, + list: &ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, + funcMap: make(typeMap), + } + + err := get.funcMap.registerCommand(get.apiType.kind, func(obj runtime.Object) (summarisable, error) { + o, ok := obj.(*sourcev1.OCIRepository) + if !ok { + return nil, fmt.Errorf("impossible to cast type %#v to OCIRepository", obj) + } + + sink := &ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{ + Items: []sourcev1.OCIRepository{ + *o, + }}} + return sink, nil + }) + + if err != nil { + return err + } + + if err := get.run(cmd, args); err != nil { + return err + } + + return nil + }, +} + +func init() { + getSourceCmd.AddCommand(getSourceOCIRepositoryCmd) +} + +func (a *ociRepositoryListAdapter) summariseItem(i int, includeNamespace bool, includeKind bool) []string { + item := a.Items[i] + var revision string + if item.GetArtifact() != nil { + revision = item.GetArtifact().Revision + } + status, msg := statusAndMessage(item.Status.Conditions) + revision = utils.TruncateHex(revision) + msg = utils.TruncateHex(msg) + return append(nameColumns(&item, includeNamespace, includeKind), + revision, cases.Title(language.English).String(strconv.FormatBool(item.Spec.Suspend)), status, msg) +} + +func (a ociRepositoryListAdapter) headers(includeNamespace bool) []string { + headers := []string{"Name", "Revision", "Suspended", "Ready", "Message"} + if includeNamespace { + headers = append([]string{"Namespace"}, headers...) + } + return headers +} + +func (a ociRepositoryListAdapter) statusSelectorMatches(i int, conditionType, conditionStatus string) bool { + item := a.Items[i] + return statusMatches(conditionType, conditionStatus, item.Status.Conditions) +} diff --git a/cmd/flux/get_test.go b/cmd/flux/get_test.go new file mode 100644 index 00000000..62a1b725 --- /dev/null +++ b/cmd/flux/get_test.go @@ -0,0 +1,61 @@ +//go:build unit +// +build unit + +/* +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 "testing" + +func Test_GetCmd(t *testing.T) { + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + testEnv.CreateObjectFile("./testdata/get/objects.yaml", tmpl, t) + + tests := []struct { + name string + args string + expected string + }{ + { + name: "no label selector", + expected: "testdata/get/get.golden", + }, + { + name: "equal label selector", + args: "-l sharding.fluxcd.io/key=shard1", + expected: "testdata/get/get_label_one.golden", + }, + { + name: "notin label selector", + args: `-l "sharding.fluxcd.io/key notin (shard1, shard2)"`, + expected: "testdata/get/get_label_two.golden", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: "get sources git " + tt.args + " -n " + tmpl["fluxns"], + assert: assertGoldenTemplateFile(tt.expected, nil), + } + + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/helmrelease.go b/cmd/flux/helmrelease.go index c4ae564a..08cad027 100644 --- a/cmd/flux/helmrelease.go +++ b/cmd/flux/helmrelease.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Flux authors +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. @@ -19,14 +19,15 @@ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" ) // helmv2.HelmRelease var helmReleaseType = apiType{ - kind: helmv2.HelmReleaseKind, - humanKind: "helmreleases", + kind: helmv2.HelmReleaseKind, + humanKind: "helmrelease", + groupVersion: helmv2.GroupVersion, } type helmReleaseAdapter struct { diff --git a/cmd/flux/helmrelease_test.go b/cmd/flux/helmrelease_test.go index dcb9ff58..bdf7d87c 100644 --- a/cmd/flux/helmrelease_test.go +++ b/cmd/flux/helmrelease_test.go @@ -22,51 +22,61 @@ package main import "testing" func TestHelmReleaseFromGit(t *testing.T) { + namespace := allocateNamespace("thrfg") + del, err := execSetupTestNamespace(namespace) + if err != nil { + t.Fatal(err) + } + t.Cleanup(del) + + tmpl := map[string]string{"ns": namespace} + cases := []struct { args string goldenFile string + tmpl map[string]string }{ { - "create source git thrfg --url=https://github.com/stefanprodan/podinfo --branch=main --tag=6.0.0", + "create source git thrfg --url=https://github.com/stefanprodan/podinfo --branch=main --tag=6.3.5", "testdata/helmrelease/create_source_git.golden", + nil, }, { "create helmrelease thrfg --source=GitRepository/thrfg --chart=./charts/podinfo", "testdata/helmrelease/create_helmrelease_from_git.golden", + nil, }, { "get helmrelease thrfg", "testdata/helmrelease/get_helmrelease_from_git.golden", + nil, }, { "reconcile helmrelease thrfg --with-source", "testdata/helmrelease/reconcile_helmrelease_from_git.golden", + tmpl, }, { "suspend helmrelease thrfg", "testdata/helmrelease/suspend_helmrelease_from_git.golden", + tmpl, }, { "resume helmrelease thrfg", "testdata/helmrelease/resume_helmrelease_from_git.golden", + tmpl, }, { "delete helmrelease thrfg --silent", "testdata/helmrelease/delete_helmrelease_from_git.golden", + tmpl, }, } - namespace := allocateNamespace("thrfg") - del, err := setupTestNamespace(namespace) - if err != nil { - t.Fatal(err) - } - defer del() - for _, tc := range cases { cmd := cmdTestCase{ args: tc.args + " -n=" + namespace, - assert: assertGoldenTemplateFile(tc.goldenFile, map[string]string{"ns": namespace}), + assert: assertGoldenTemplateFile(tc.goldenFile, tc.tmpl), } cmd.runTestCmd(t) } diff --git a/cmd/flux/image.go b/cmd/flux/image.go index b54aa25d..621dabfc 100644 --- a/cmd/flux/image.go +++ b/cmd/flux/image.go @@ -19,8 +19,8 @@ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) // These are general-purpose adapters for attaching methods to, for @@ -30,8 +30,9 @@ import ( // imagev1.ImageRepository var imageRepositoryType = apiType{ - kind: imagev1.ImageRepositoryKind, - humanKind: "image repository", + kind: imagev1.ImageRepositoryKind, + humanKind: "image repository", + groupVersion: imagev1.GroupVersion, } type imageRepositoryAdapter struct { @@ -63,8 +64,9 @@ func (a imageRepositoryListAdapter) len() int { // imagev1.ImagePolicy var imagePolicyType = apiType{ - kind: imagev1.ImagePolicyKind, - humanKind: "image policy", + kind: imagev1.ImagePolicyKind, + humanKind: "image policy", + groupVersion: imagev1.GroupVersion, } type imagePolicyAdapter struct { @@ -92,8 +94,9 @@ func (a imagePolicyListAdapter) len() int { // autov1.ImageUpdateAutomation var imageUpdateAutomationType = apiType{ - kind: autov1.ImageUpdateAutomationKind, - humanKind: "image update automation", + kind: autov1.ImageUpdateAutomationKind, + humanKind: "image update automation", + groupVersion: autov1.GroupVersion, } type imageUpdateAutomationAdapter struct { diff --git a/cmd/flux/image_test.go b/cmd/flux/image_test.go index a0e0b257..8fe76a1e 100644 --- a/cmd/flux/image_test.go +++ b/cmd/flux/image_test.go @@ -1,11 +1,34 @@ //go:build e2e // +build e2e +/* +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 TestImageScanning(t *testing.T) { + namespace := allocateNamespace("tis") + del, err := execSetupTestNamespace(namespace) + if err != nil { + t.Fatal(err) + } + t.Cleanup(del) + cases := []struct { args string goldenFile string @@ -32,13 +55,6 @@ func TestImageScanning(t *testing.T) { }, } - namespace := allocateNamespace("tis") - del, err := setupTestNamespace(namespace) - if err != nil { - t.Fatal(err) - } - defer del() - for _, tc := range cases { cmd := cmdTestCase{ args: tc.args + " -n=" + namespace, diff --git a/cmd/flux/install.go b/cmd/flux/install.go index 9032929b..390f8616 100644 --- a/cmd/flux/install.go +++ b/cmd/flux/install.go @@ -21,26 +21,37 @@ import ( "fmt" "os" "path/filepath" + "strings" "time" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" - - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen/install" - "github.com/fluxcd/flux2/pkg/status" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/yaml" + + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" + "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/status" ) var installCmd = &cobra.Command{ Use: "install", + Args: cobra.NoArgs, Short: "Install or upgrade Flux", Long: `The install command deploys Flux in the specified namespace. If a previous version is installed, then an in-place upgrade will be performed.`, Example: ` # Install the latest version in the flux-system namespace - flux install --version=latest --namespace=flux-system + flux install --namespace=flux-system + + # Install a specific series of components + flux install --components="source-controller,kustomize-controller" - # Install a specific version and a series of components - flux install --version=v0.0.7 --components="source-controller,kustomize-controller" + # Install all components including the image automation ones + flux install --components-extra="image-reflector-controller,image-automation-controller" # Install Flux onto tainted Kubernetes nodes flux install --toleration-keys=node.kubernetes.io/dedicated-to-flux @@ -55,21 +66,21 @@ If a previous version is installed, then an in-place upgrade will be performed.` type installFlags struct { export bool - dryRun bool version string defaultComponents []string extraComponents []string registry string + registryCredential string imagePullSecret string branch string watchAllNamespaces bool networkPolicy bool manifestsPath string - arch flags.Arch logLevel flags.LogLevel tokenAuth bool clusterDomain string tolerationKeys []string + force bool } var installArgs = NewInstallFlags() @@ -77,20 +88,19 @@ var installArgs = NewInstallFlags() func init() { installCmd.Flags().BoolVar(&installArgs.export, "export", false, "write the install manifests to stdout and exit") - installCmd.Flags().BoolVarP(&installArgs.dryRun, "dry-run", "", false, - "only print the object that would be applied") installCmd.Flags().StringVarP(&installArgs.version, "version", "v", "", "toolkit version, when specified the manifests are downloaded from https://github.com/fluxcd/flux2/releases") installCmd.Flags().StringSliceVar(&installArgs.defaultComponents, "components", rootArgs.defaults.Components, "list of components, accepts comma-separated values") installCmd.Flags().StringSliceVar(&installArgs.extraComponents, "components-extra", nil, - "list of components in addition to those supplied or defaulted, accepts comma-separated values") + "list of components in addition to those supplied or defaulted, accepts values such as 'image-reflector-controller,image-automation-controller'") installCmd.Flags().StringVar(&installArgs.manifestsPath, "manifests", "", "path to the manifest directory") installCmd.Flags().StringVar(&installArgs.registry, "registry", rootArgs.defaults.Registry, "container registry where the toolkit images are published") + installCmd.Flags().StringVar(&installArgs.registryCredential, "registry-creds", "", + "container registry credentials in the format 'user:password', requires --image-pull-secret to be set") installCmd.Flags().StringVar(&installArgs.imagePullSecret, "image-pull-secret", "", "Kubernetes secret name used for pulling the toolkit images from a private registry") - installCmd.Flags().Var(&installArgs.arch, "arch", installArgs.arch.Description()) installCmd.Flags().BoolVar(&installArgs.watchAllNamespaces, "watch-all-namespaces", rootArgs.defaults.WatchAllNamespaces, "watch for custom resources in all namespaces, if set to false it will only watch the namespace where the toolkit is installed") installCmd.Flags().Var(&installArgs.logLevel, "log-level", installArgs.logLevel.Description()) @@ -99,9 +109,9 @@ func init() { installCmd.Flags().StringVar(&installArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain") installCmd.Flags().StringSliceVar(&installArgs.tolerationKeys, "toleration-keys", nil, "list of toleration keys used to schedule the components pods onto nodes with matching taints") + installCmd.Flags().BoolVar(&installArgs.force, "force", false, "override existing Flux installation if it's managed by a different tool such as Helm") installCmd.Flags().MarkHidden("manifests") - installCmd.Flags().MarkDeprecated("arch", "multi-arch container image is now available for AMD64, ARMv7 and ARM64") - installCmd.Flags().MarkDeprecated("dry-run", "use 'flux install --export | kubectl apply --dry-run=client -f-'") + rootCmd.AddCommand(installCmd) } @@ -121,6 +131,14 @@ func installCmdRun(cmd *cobra.Command, args []string) error { return err } + if installArgs.registryCredential != "" && installArgs.imagePullSecret == "" { + return fmt.Errorf("--registry-creds requires --image-pull-secret to be set") + } + + if installArgs.registryCredential != "" && len(strings.Split(installArgs.registryCredential, ":")) != 2 { + return fmt.Errorf("invalid --registry-creds format, expected 'user:password'") + } + if ver, err := getVersion(installArgs.version); err != nil { return err } else { @@ -131,7 +149,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error { logger.Generatef("generating manifests") } - tmpDir, err := os.MkdirTemp("", *kubeconfigArgs.Namespace) + tmpDir, err := manifestgen.MkdirTempAbs("", *kubeconfigArgs.Namespace) if err != nil { return err } @@ -151,6 +169,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error { Namespace: *kubeconfigArgs.Namespace, Components: components, Registry: installArgs.registry, + RegistryCredential: installArgs.registryCredential, ImagePullSecret: installArgs.imagePullSecret, WatchAllNamespaces: installArgs.watchAllNamespaces, NetworkPolicy: installArgs.networkPolicy, @@ -185,19 +204,66 @@ func installCmdRun(cmd *cobra.Command, args []string) error { logger.Successf("manifests build completed") logger.Actionf("installing components in %s namespace", *kubeconfigArgs.Namespace) - if installArgs.dryRun { - logger.Successf("install dry-run finished") - return nil + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + + installed := true + info, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("cluster info unavailable: %w", err) + } + installed = false + } + + if info.bootstrapped { + return fmt.Errorf("this cluster has already been bootstrapped with Flux %s! Please use 'flux bootstrap' to upgrade", + info.version) + } + + if installed && !installArgs.force { + err := confirmFluxInstallOverride(info) + if err != nil { + if err == promptui.ErrAbort { + return fmt.Errorf("installation cancelled") + } + return err + } } - applyOutput, err := utils.Apply(ctx, kubeconfigArgs, filepath.Join(tmpDir, manifest.Path)) + applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path)) if err != nil { return fmt.Errorf("install failed: %w", err) } fmt.Fprintln(os.Stderr, applyOutput) - kubeConfig, err := utils.KubeConfig(kubeconfigArgs) + if opts.ImagePullSecret != "" && opts.RegistryCredential != "" { + logger.Actionf("generating image pull secret %s", opts.ImagePullSecret) + credentials := strings.SplitN(opts.RegistryCredential, ":", 2) + secretOpts := sourcesecret.Options{ + Name: opts.ImagePullSecret, + Namespace: opts.Namespace, + Registry: opts.Registry, + Username: credentials[0], + Password: credentials[1], + } + imagePullSecret, err := sourcesecret.Generate(secretOpts) + if err != nil { + return fmt.Errorf("install failed: %w", err) + } + var s corev1.Secret + if err := yaml.Unmarshal([]byte(imagePullSecret.Content), &s); err != nil { + return fmt.Errorf("install failed: %w", err) + } + if err := upsertSecret(ctx, kubeClient, s); err != nil { + return fmt.Errorf("install failed: %w", err) + } + } + + kubeConfig, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return fmt.Errorf("install failed: %w", err) } diff --git a/cmd/flux/install_test.go b/cmd/flux/install_test.go new file mode 100644 index 00000000..161a3d36 --- /dev/null +++ b/cmd/flux/install_test.go @@ -0,0 +1,61 @@ +/* +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 TestInstall(t *testing.T) { + // The pointer to kubeconfigArgs.Namespace is shared across + // the tests. When a new value is set, it will linger and + // impact subsequent tests. + // Given that this test uses an invalid namespace, it ensures + // to restore whatever value it had previously. + currentNamespace := *kubeconfigArgs.Namespace + t.Cleanup(func() { *kubeconfigArgs.Namespace = currentNamespace }) + + tests := []struct { + name string + args string + assert assertFunc + }{ + { + name: "invalid namespace", + args: "install --namespace='@#[]'", + assert: assertError("namespace must be a valid DNS label: \"@#[]\""), + }, + { + name: "invalid sub-command", + args: "install unexpectedPosArg --namespace=example", + assert: assertError(`unknown command "unexpectedPosArg" for "flux install"`), + }, + { + name: "missing image pull secret", + args: "install --registry-creds=fluxcd:test", + assert: assertError(`--registry-creds requires --image-pull-secret to be set`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := cmdTestCase{ + args: tt.args, + assert: tt.assert, + } + cmd.runTestCmd(t) + }) + } +} diff --git a/cmd/flux/kustomization.go b/cmd/flux/kustomization.go index 1151b7aa..56e6abc5 100644 --- a/cmd/flux/kustomization.go +++ b/cmd/flux/kustomization.go @@ -19,14 +19,15 @@ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) // kustomizev1.Kustomization var kustomizationType = apiType{ - kind: kustomizev1.KustomizationKind, - humanKind: "kustomizations", + kind: kustomizev1.KustomizationKind, + humanKind: "kustomization", + groupVersion: kustomizev1.GroupVersion, } type kustomizationAdapter struct { diff --git a/cmd/flux/kustomization_test.go b/cmd/flux/kustomization_test.go index f2a16fb0..2f6176ac 100644 --- a/cmd/flux/kustomization_test.go +++ b/cmd/flux/kustomization_test.go @@ -22,51 +22,76 @@ package main import "testing" func TestKustomizationFromGit(t *testing.T) { + namespace := allocateNamespace("tkfg") + del, err := execSetupTestNamespace(namespace) + if err != nil { + t.Fatal(err) + } + t.Cleanup(del) + + tmpl := map[string]string{"ns": namespace} + cases := []struct { args string goldenFile string + tmpl map[string]string }{ { - "create source git tkfg --url=https://github.com/stefanprodan/podinfo --branch=main --tag=6.0.0", + "create source git tkfg --url=https://github.com/stefanprodan/podinfo --branch=main --tag=6.3.5", "testdata/kustomization/create_source_git.golden", + nil, }, { "create kustomization tkfg --source=tkfg --path=./deploy/overlays/dev --prune=true --interval=5m --health-check=Deployment/frontend.dev --health-check=Deployment/backend.dev --health-check-timeout=3m", "testdata/kustomization/create_kustomization_from_git.golden", + nil, }, { "get kustomization tkfg", "testdata/kustomization/get_kustomization_from_git.golden", + nil, }, { "reconcile kustomization tkfg --with-source", "testdata/kustomization/reconcile_kustomization_from_git.golden", + tmpl, }, { "suspend kustomization tkfg", "testdata/kustomization/suspend_kustomization_from_git.golden", + tmpl, + }, + { + "suspend kustomization tkfg foo tkfg bar", + "testdata/kustomization/suspend_kustomization_from_git_multiple_args.golden", + tmpl, + }, + { + "resume kustomization tkfg foo --wait", + "testdata/kustomization/resume_kustomization_from_git_multiple_args_wait.golden", + tmpl, }, { "resume kustomization tkfg", "testdata/kustomization/resume_kustomization_from_git.golden", + tmpl, + }, + { + "resume kustomization tkfg tkfg", + "testdata/kustomization/resume_kustomization_from_git_multiple_args.golden", + tmpl, }, { "delete kustomization tkfg --silent", "testdata/kustomization/delete_kustomization_from_git.golden", + tmpl, }, } - namespace := allocateNamespace("tkfg") - del, err := setupTestNamespace(namespace) - if err != nil { - t.Fatal(err) - } - defer del() - for _, tc := range cases { cmd := cmdTestCase{ args: tc.args + " -n=" + namespace, - assert: assertGoldenTemplateFile(tc.goldenFile, map[string]string{"ns": namespace}), + assert: assertGoldenTemplateFile(tc.goldenFile, tc.tmpl), } cmd.runTestCmd(t) } diff --git a/tests/azure/terraform.go b/cmd/flux/list.go similarity index 59% rename from tests/azure/terraform.go rename to cmd/flux/list.go index ee6cd156..51c3784f 100644 --- a/tests/azure/terraform.go +++ b/cmd/flux/list.go @@ -1,5 +1,5 @@ /* -Copyright 2021 The Flux authors +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. @@ -14,22 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -package test +package main import ( - "context" - "os/exec" - "strings" + "github.com/spf13/cobra" ) -type whichTerraform struct{} +var listCmd = &cobra.Command{ + Use: "list", + Short: "List artifacts", + Long: `The list command is used for printing the OCI artifacts metadata.`, +} -func (w *whichTerraform) ExecPath(ctx context.Context) (string, error) { - cmd := exec.CommandContext(ctx, "which", "terraform") - output, err := cmd.Output() - if err != nil { - return "", err - } - path := strings.TrimSuffix(string(output), "\n") - return path, nil +func init() { + rootCmd.AddCommand(listCmd) } diff --git a/cmd/flux/list_artifact.go b/cmd/flux/list_artifact.go new file mode 100644 index 00000000..23a61b94 --- /dev/null +++ b/cmd/flux/list_artifact.go @@ -0,0 +1,123 @@ +/* +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/spf13/cobra" + + oci "github.com/fluxcd/pkg/oci/client" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/pkg/printers" +) + +type listArtifactFlags struct { + semverFilter string + regexFilter string + creds string + provider flags.SourceOCIProvider +} + +var listArtifactArgs = newListArtifactFlags() + +func newListArtifactFlags() listArtifactFlags { + return listArtifactFlags{ + provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), + } +} + +var listArtifactsCmd = &cobra.Command{ + Use: "artifacts", + Short: "list artifacts", + Long: withPreviewNote(`The list command fetches the tags and their metadata from a remote OCI repository. +The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`), + Example: ` # List the artifacts stored in an OCI repository + flux list artifact oci://ghcr.io/org/config/app +`, + RunE: listArtifactsCmdRun, +} + +func init() { + listArtifactsCmd.Flags().StringVar(&listArtifactArgs.semverFilter, "filter-semver", "", "filter tags returned from the oci repository using semver") + listArtifactsCmd.Flags().StringVar(&listArtifactArgs.regexFilter, "filter-regex", "", "filter tags returned from the oci repository using regex") + listArtifactsCmd.Flags().StringVar(&listArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") + listArtifactsCmd.Flags().Var(&listArtifactArgs.provider, "provider", listArtifactArgs.provider.Description()) + + listCmd.AddCommand(listArtifactsCmd) +} + +func listArtifactsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact repository URL is required") + } + ociURL := args[0] + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + url, err := oci.ParseArtifactURL(ociURL) + if err != nil { + return err + } + + ociClient := oci.NewClient(oci.DefaultOptions()) + + if listArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && listArtifactArgs.creds != "" { + logger.Actionf("logging in to registry with credentials") + if err := ociClient.LoginWithCredentials(listArtifactArgs.creds); err != nil { + return fmt.Errorf("could not login with credentials: %w", err) + } + } + + if listArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { + logger.Actionf("logging in to registry with provider credentials") + ociProvider, err := listArtifactArgs.provider.ToOCIProvider() + if err != nil { + return fmt.Errorf("provider not supported: %w", err) + } + + if err := ociClient.LoginWithProvider(ctx, url, ociProvider); err != nil { + return fmt.Errorf("error during login with provider: %w", err) + } + } + + opts := oci.ListOptions{ + RegexFilter: listArtifactArgs.regexFilter, + SemverFilter: listArtifactArgs.semverFilter, + } + + metas, err := ociClient.List(ctx, url, opts) + if err != nil { + return err + } + + var rows [][]string + for _, meta := range metas { + rows = append(rows, []string{meta.URL, meta.Digest, meta.Source, meta.Revision}) + } + + err = printers.TablePrinter([]string{"artifact", "digest", "source", "revision"}).Print(cmd.OutOrStdout(), rows) + if err != nil { + return err + } + + return nil +} diff --git a/cmd/flux/logs.go b/cmd/flux/logs.go index 3dc0f903..602c85dd 100644 --- a/cmd/flux/logs.go +++ b/cmd/flux/logs.go @@ -20,13 +20,14 @@ import ( "bufio" "context" "encoding/json" + "errors" "fmt" - "html/template" "io" "os" "sort" "strings" "sync" + "text/template" "time" "github.com/spf13/cobra" @@ -37,15 +38,15 @@ import ( "k8s.io/kubectl/pkg/util" "k8s.io/kubectl/pkg/util/podutils" - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen" + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) var logsCmd = &cobra.Command{ Use: "logs", Short: "Display formatted logs for Flux components", - Long: "The logs command displays formatted logs from various Flux components.", + Long: withPreviewNote("The logs command displays formatted logs from various Flux components."), Example: ` # Print the reconciliation logs of all Flux custom resources in your cluster flux logs --all-namespaces @@ -73,13 +74,15 @@ type logsFlags struct { fluxNamespace string allNamespaces bool sinceTime string - sinceSeconds time.Duration + sinceDuration time.Duration } -var logsArgs = &logsFlags{ +var logsArgs = logsFlags{ tail: -1, } +const controllerContainer = "manager" + func init() { logsCmd.Flags().Var(&logsArgs.logLevel, "level", logsArgs.logLevel.Description()) logsCmd.Flags().StringVarP(&logsArgs.kind, "kind", "", logsArgs.kind, "displays errors of a particular toolkit kind e.g GitRepository") @@ -88,7 +91,7 @@ func init() { logsCmd.Flags().Int64VarP(&logsArgs.tail, "tail", "", logsArgs.tail, "lines of recent log file to display") logsCmd.Flags().StringVarP(&logsArgs.fluxNamespace, "flux-namespace", "", rootArgs.defaults.Namespace, "the namespace where the Flux components are running") logsCmd.Flags().BoolVarP(&logsArgs.allNamespaces, "all-namespaces", "A", false, "displays logs for objects across all namespaces") - logsCmd.Flags().DurationVar(&logsArgs.sinceSeconds, "since", logsArgs.sinceSeconds, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") + logsCmd.Flags().DurationVar(&logsArgs.sinceDuration, "since", logsArgs.sinceDuration, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used.") logsCmd.Flags().StringVar(&logsArgs.sinceTime, "since-time", logsArgs.sinceTime, "Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used.") rootCmd.AddCommand(logsCmd) } @@ -99,7 +102,7 @@ func logsCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - cfg, err := utils.KubeConfig(kubeconfigArgs) + cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -113,7 +116,7 @@ func logsCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("no argument required") } - pods, err := getPods(ctx, clientset, fluxSelector) + pods, err := getPods(ctx, clientset, logsArgs.fluxNamespace, fluxSelector) if err != nil { return err } @@ -126,8 +129,8 @@ func logsCmdRun(cmd *cobra.Command, args []string) error { logOpts.TailLines = &logsArgs.tail } - if len(logsArgs.sinceTime) > 0 && logsArgs.sinceSeconds != 0 { - return fmt.Errorf("at most one of `sinceTime` or `sinceSeconds` may be specified") + if len(logsArgs.sinceTime) > 0 && logsArgs.sinceDuration != 0 { + return fmt.Errorf("at most one of `sinceTime` or `sinceDuration` may be specified") } if len(logsArgs.sinceTime) > 0 { @@ -138,14 +141,18 @@ func logsCmdRun(cmd *cobra.Command, args []string) error { logOpts.SinceTime = &t } - if logsArgs.sinceSeconds != 0 { + if logsArgs.sinceDuration != 0 { // round up to the nearest second - sec := int64(logsArgs.sinceSeconds.Round(time.Second).Seconds()) + sec := int64(logsArgs.sinceDuration.Round(time.Second).Seconds()) logOpts.SinceSeconds = &sec } var requests []rest.ResponseWrapper for _, pod := range pods { + logOpts := logOpts.DeepCopy() + if len(pod.Spec.Containers) > 1 { + logOpts.Container = controllerContainer + } req := clientset.CoreV1().Pods(logsArgs.fluxNamespace).GetLogs(pod.Name, logOpts) requests = append(requests, req) } @@ -157,13 +164,16 @@ func logsCmdRun(cmd *cobra.Command, args []string) error { return podLogs(ctx, requests) } -func getPods(ctx context.Context, c *kubernetes.Clientset, label string) ([]corev1.Pod, error) { +// getPods searches for all Deployments in the given namespace that match the given label and returns a list of Pods +// from these Deployments. For each Deployment a single Pod is chosen (based on various factors such as the running +// state). If no Pod is found, an error is returned. +func getPods(ctx context.Context, c *kubernetes.Clientset, ns string, label string) ([]corev1.Pod, error) { var ret []corev1.Pod opts := metav1.ListOptions{ LabelSelector: label, } - deployList, err := c.AppsV1().Deployments(logsArgs.fluxNamespace).List(ctx, opts) + deployList, err := c.AppsV1().Deployments(ns).List(ctx, opts) if err != nil { return ret, err } @@ -173,7 +183,7 @@ func getPods(ctx context.Context, c *kubernetes.Clientset, label string) ([]core opts := metav1.ListOptions{ LabelSelector: createLabelStringFromMap(label), } - podList, err := c.CoreV1().Pods(logsArgs.fluxNamespace).List(ctx, opts) + podList, err := c.CoreV1().Pods(ns).List(ctx, opts) if err != nil { return ret, err } @@ -190,21 +200,24 @@ func getPods(ctx context.Context, c *kubernetes.Clientset, label string) ([]core } } + if len(ret) == 0 { + return nil, fmt.Errorf("no Flux pods found in namespace %q", ns) + } + return ret, nil } func parallelPodLogs(ctx context.Context, requests []rest.ResponseWrapper) error { reader, writer := io.Pipe() + errReader, errWriter := io.Pipe() wg := &sync.WaitGroup{} wg.Add(len(requests)) - var mutex = &sync.Mutex{} - for _, request := range requests { go func(req rest.ResponseWrapper) { defer wg.Done() - if err := logRequest(mutex, ctx, req, os.Stdout); err != nil { - writer.CloseWithError(err) + if err := logRequest(ctx, req, writer); err != nil { + fmt.Fprintf(errWriter, "failed getting logs: %s\n", err) return } }(request) @@ -213,21 +226,40 @@ func parallelPodLogs(ctx context.Context, requests []rest.ResponseWrapper) error go func() { wg.Wait() writer.Close() + errWriter.Close() }() - _, err := io.Copy(os.Stdout, reader) - return err + stdoutErrCh := asyncCopy(os.Stdout, reader) + stderrErrCh := asyncCopy(os.Stderr, errReader) + + return errors.Join(<-stdoutErrCh, <-stderrErrCh) +} + +// asyncCopy copies all data from dst to src asynchronously and returns a channel for reading an error value. +// This is basically an asynchronous wrapper around `io.Copy`. The returned channel is unbuffered and always is sent +// a value (either nil or the error from `io.Copy`) as soon as `io.Copy` returns. +// This function lets you copy from multiple sources into multiple destinations in parallel. +func asyncCopy(dst io.Writer, src io.Reader) <-chan error { + errCh := make(chan error) + go func(errCh chan error) { + _, err := io.Copy(dst, src) + errCh <- err + }(errCh) + + return errCh } func podLogs(ctx context.Context, requests []rest.ResponseWrapper) error { - mutex := &sync.Mutex{} + var retErr error for _, req := range requests { - if err := logRequest(mutex, ctx, req, os.Stdout); err != nil { - return err + if err := logRequest(ctx, req, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "failed getting logs: %s\n", err) + retErr = fmt.Errorf("failed to collect logs from all Flux pods") + continue } } - return nil + return retErr } func createLabelStringFromMap(m map[string]string) string { @@ -240,7 +272,7 @@ func createLabelStringFromMap(m map[string]string) string { return strings.Join(strArr, ",") } -func logRequest(mu *sync.Mutex, ctx context.Context, request rest.ResponseWrapper, w io.Writer) error { +func logRequest(ctx context.Context, request rest.ResponseWrapper, w io.Writer) error { stream, err := request.Stream(ctx) if err != nil { return err @@ -249,12 +281,13 @@ func logRequest(mu *sync.Mutex, ctx context.Context, request rest.ResponseWrappe scanner := bufio.NewScanner(stream) - const logTmpl = "{{.Timestamp}} {{.Level}} {{.Kind}}{{if .Name}}/{{.Name}}.{{.Namespace}}{{end}} - {{.Message}} {{.Error}}\n" + const logTmpl = "{{.Timestamp}} {{.Level}} {{or .Kind .ControllerKind}}{{if .Name}}/{{.Name}}.{{.Namespace}}{{end}} - {{.Message}} {{.Error}}\n" t, err := template.New("log").Parse(logTmpl) if err != nil { return fmt.Errorf("unable to create template, err: %s", err) } + bw := bufio.NewWriter(w) for scanner.Scan() { line := scanner.Text() if !strings.HasPrefix(line, "{") { @@ -265,36 +298,33 @@ func logRequest(mu *sync.Mutex, ctx context.Context, request rest.ResponseWrappe logger.Failuref("parse error: %s", err) break } - - mu.Lock() - filterPrintLog(t, &l) - mu.Unlock() + filterPrintLog(t, &l, bw) + bw.Flush() } return nil } -func filterPrintLog(t *template.Template, l *ControllerLogEntry) { - if logsArgs.logLevel != "" && logsArgs.logLevel != l.Level || - logsArgs.kind != "" && strings.ToLower(logsArgs.kind) != strings.ToLower(l.Kind) || - logsArgs.name != "" && strings.ToLower(logsArgs.name) != strings.ToLower(l.Name) || - !logsArgs.allNamespaces && strings.ToLower(*kubeconfigArgs.Namespace) != strings.ToLower(l.Namespace) { - return - } - - err := t.Execute(os.Stdout, l) - if err != nil { - logger.Failuref("log template error: %s", err) +func filterPrintLog(t *template.Template, l *ControllerLogEntry, w io.Writer) { + if (logsArgs.logLevel == "" || logsArgs.logLevel == l.Level) && + (logsArgs.kind == "" || strings.EqualFold(logsArgs.kind, l.Kind) || strings.EqualFold(logsArgs.kind, l.ControllerKind)) && + (logsArgs.name == "" || strings.EqualFold(logsArgs.name, l.Name)) && + (logsArgs.allNamespaces || strings.EqualFold(*kubeconfigArgs.Namespace, l.Namespace)) { + err := t.Execute(w, l) + if err != nil { + logger.Failuref("log template error: %s", err) + } } } type ControllerLogEntry struct { - Timestamp string `json:"ts"` - Level flags.LogLevel `json:"level"` - Message string `json:"msg"` - Error string `json:"error,omitempty"` - Logger string `json:"logger"` - Kind string `json:"reconciler kind,omitempty"` - Name string `json:"name,omitempty"` - Namespace string `json:"namespace,omitempty"` + Timestamp string `json:"ts"` + Level flags.LogLevel `json:"level"` + Message string `json:"msg"` + Error string `json:"error,omitempty"` + Logger string `json:"logger"` + Kind string `json:"reconciler kind,omitempty"` + ControllerKind string `json:"controllerKind,omitempty"` + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` } diff --git a/cmd/flux/logs_test.go b/cmd/flux/logs_e2e_test.go similarity index 84% rename from cmd/flux/logs_test.go rename to cmd/flux/logs_e2e_test.go index 9355e37b..46c1c11e 100644 --- a/cmd/flux/logs_test.go +++ b/cmd/flux/logs_e2e_test.go @@ -1,5 +1,5 @@ -//go:build unit -// +build unit +//go:build e2e +// +build e2e /* Copyright 2021 The Flux authors @@ -31,6 +31,14 @@ func TestLogsNoArgs(t *testing.T) { cmd.runTestCmd(t) } +func TestLogsWrongNamespace(t *testing.T) { + cmd := cmdTestCase{ + args: "logs --flux-namespace=default", + assert: assertError(`no Flux pods found in namespace "default"`), + } + cmd.runTestCmd(t) +} + func TestLogsAllNamespaces(t *testing.T) { cmd := cmdTestCase{ args: "logs --all-namespaces", @@ -74,7 +82,7 @@ func TestLogsSinceTimeInvalid(t *testing.T) { func TestLogsSinceOnlyOneAllowed(t *testing.T) { cmd := cmdTestCase{ args: "logs --since=2m --since-time=2021-08-06T14:26:25.546Z", - assert: assertError("at most one of `sinceTime` or `sinceSeconds` may be specified"), + assert: assertError("at most one of `sinceTime` or `sinceDuration` may be specified"), } cmd.runTestCmd(t) } diff --git a/cmd/flux/logs_unit_test.go b/cmd/flux/logs_unit_test.go new file mode 100644 index 00000000..336f3314 --- /dev/null +++ b/cmd/flux/logs_unit_test.go @@ -0,0 +1,134 @@ +//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" + "context" + "io" + "os" + "strings" + "testing" + + . "github.com/onsi/gomega" +) + +func TestLogRequest(t *testing.T) { + mapper := &testResponseMapper{} + tests := []struct { + name string + namespace string + flags logsFlags + assertFile string + }{ + { + name: "all logs", + flags: logsFlags{ + tail: -1, + allNamespaces: true, + }, + assertFile: "testdata/logs/all-logs.txt", + }, + { + name: "filter by namespace", + namespace: "default", + flags: logsFlags{ + tail: -1, + }, + assertFile: "testdata/logs/namespace.txt", + }, + { + name: "filter by kind and namespace", + flags: logsFlags{ + tail: -1, + kind: "Kustomization", + }, + assertFile: "testdata/logs/kind.txt", + }, + { + name: "filter by loglevel", + flags: logsFlags{ + tail: -1, + logLevel: "error", + allNamespaces: true, + }, + assertFile: "testdata/logs/log-level.txt", + }, + { + name: "filter by namespace, name, loglevel and kind", + namespace: "flux-system", + flags: logsFlags{ + tail: -1, + logLevel: "error", + kind: "Kustomization", + name: "podinfo", + }, + assertFile: "testdata/logs/multiple-filters.txt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + logsArgs = tt.flags + if tt.namespace != "" { + *kubeconfigArgs.Namespace = tt.namespace + } + w := bytes.NewBuffer([]byte{}) + err := logRequest(context.Background(), mapper, w) + g.Expect(err).To(BeNil()) + + got := make([]byte, w.Len()) + _, err = w.Read(got) + g.Expect(err).To(BeNil()) + + expected, err := os.ReadFile(tt.assertFile) + g.Expect(err).To(BeNil()) + + g.Expect(string(got)).To(Equal(string(expected))) + + // reset flags to default + *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace + logsArgs = logsFlags{ + tail: -1, + } + }) + } +} + +var testPodLogs = `{"level":"info","ts":"2022-08-02T12:55:34.419Z","msg":"no changes since last reconcilation: observed revision","controller":"gitrepository","controllerGroup":"source.toolkit.fluxcd.io","controllerKind":"GitRepository","gitRepository":{"name":"podinfo","namespace":"default"},"namespace":"default","name":"podinfo","reconcileID":"5ef9b2ef-4ea5-47b7-b887-a247cafc1bce"} +{"level":"error","ts":"2022-08-02T12:56:04.679Z","logger":"controller.gitrepository","msg":"no changes since last reconcilation: observed revision","controllerGroup":"source.toolkit.fluxcd.io","controllerKind":"GitRepository","gitRepository":{"name":"podinfo","namespace":"flux-system"},"name":"flux-system","namespace":"flux-system","reconcileID":"543ef9b2ef-4ea5-47b7-b887-a247cafc1bce"} +{"level":"error","ts":"2022-08-02T12:56:34.961Z","logger":"controller.kustomization","msg":"no changes since last reconcilation: observed revision","reconciler group":"kustomize.toolkit.fluxcd.io","reconciler kind":"Kustomization","name":"flux-system","namespace":"flux-system"} +{"level":"info","ts":"2022-08-02T12:56:34.961Z","logger":"controller.kustomization","msg":"no changes since last reconcilation: observed revision","reconciler group":"kustomize.toolkit.fluxcd.io","reconciler kind":"Kustomization","name":"podinfo","namespace":"default"} +{"level":"info","ts":"2022-08-02T12:56:34.961Z","logger":"controller.gitrepository","msg":"no changes since last reconcilation: observed revision","reconciler group":"source.toolkit.fluxcd.io","reconciler kind":"GitRepository","name":"podinfo","namespace":"default"} +{"level":"error","ts":"2022-08-02T12:56:34.961Z","logger":"controller.kustomization","msg":"no changes since last reconcilation: observed revision","reconciler group":"kustomize.toolkit.fluxcd.io","reconciler kind":"Kustomization","name":"podinfo","namespace":"flux-system"} +` + +type testResponseMapper struct { +} + +func (t *testResponseMapper) DoRaw(_ context.Context) ([]byte, error) { + return nil, nil +} + +func (t *testResponseMapper) Stream(_ context.Context) (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(testPodLogs)), nil +} diff --git a/cmd/flux/main.go b/cmd/flux/main.go index d61bcda9..1226a347 100644 --- a/cmd/flux/main.go +++ b/cmd/flux/main.go @@ -24,13 +24,18 @@ import ( "strings" "time" + "github.com/go-logr/logr" "github.com/spf13/cobra" "golang.org/x/term" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/fluxcd/flux2/pkg/manifestgen/install" + runclient "github.com/fluxcd/pkg/runtime/client" + + "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" ) var VERSION = "0.0.0-dev.0" @@ -94,6 +99,18 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`, # Uninstall Flux and delete CRDs flux uninstall`, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + ns, err := cmd.Flags().GetString("namespace") + if err != nil { + return fmt.Errorf("error getting namespace: %w", err) + } + + if e := validation.IsDNS1123Label(ns); len(e) > 0 { + return fmt.Errorf("namespace must be a valid DNS label: %q", ns) + } + + return nil + }, } var logger = stderrLogger{stderr: os.Stderr} @@ -105,8 +122,19 @@ type rootFlags struct { defaults install.Options } +// RequestError is a custom error type that wraps an error returned by the flux api. +type RequestError struct { + StatusCode int + Err error +} + +func (r *RequestError) Error() string { + return r.Err.Error() +} + var rootArgs = NewRootFlags() var kubeconfigArgs = genericclioptions.NewConfigFlags(false) +var kubeclientOptions = new(runclient.Options) func init() { rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation") @@ -123,6 +151,13 @@ func init() { apiServer := "" kubeconfigArgs.APIServer = &apiServer rootCmd.PersistentFlags().StringVar(kubeconfigArgs.APIServer, "server", *kubeconfigArgs.APIServer, "The address and port of the Kubernetes API server") + // Update the description for kubeconfig TLS flags so that user's don't mistake it for a Flux specific flag + rootCmd.Flag("insecure-skip-tls-verify").Usage = "If true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure" + rootCmd.Flag("client-certificate").Usage = "Path to a client certificate file for TLS authentication to the Kubernetes API server" + rootCmd.Flag("certificate-authority").Usage = "Path to a cert file for the certificate authority to authenticate the Kubernetes API server" + rootCmd.Flag("client-key").Usage = "Path to a client key file for TLS authentication to the Kubernetes API server" + + kubeclientOptions.BindFlags(rootCmd.PersistentFlags()) rootCmd.RegisterFlagCompletionFunc("context", contextsCompletionFunc) rootCmd.RegisterFlagCompletionFunc("namespace", resourceNamesCompletionFunc(corev1.SchemeGroupVersion.WithKind("Namespace"))) @@ -142,7 +177,27 @@ func NewRootFlags() rootFlags { func main() { log.SetFlags(0) + + // This is required because controller-runtime expects its consumers to + // set a logger through log.SetLogger within 30 seconds of the program's + // initalization. If not set, the entire debug stack is printed as an + // error, see: https://github.com/kubernetes-sigs/controller-runtime/blob/ed8be90/pkg/log/log.go#L59 + // Since we have our own logging and don't care about controller-runtime's + // logger, we configure it's logger to do nothing. + ctrllog.SetLogger(logr.New(ctrllog.NullLogSink{})) + if err := rootCmd.Execute(); err != nil { + + if err, ok := err.(*RequestError); ok { + if err.StatusCode == 1 { + logger.Warningf("%v", err) + } else { + logger.Failuref("%v", err) + } + + os.Exit(err.StatusCode) + } + logger.Failuref("%v", err) os.Exit(1) } @@ -152,15 +207,16 @@ func configureDefaultNamespace() { *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE") if fromEnv != "" { - kubeconfigArgs.Namespace = &fromEnv - } -} + // namespace must be a valid DNS label. Assess against validation + // used upstream, and ignore invalid values as environment vars + // may not be actively provided by end-user. + if e := validation.IsDNS1123Label(fromEnv); len(e) > 0 { + logger.Warningf(" ignoring invalid FLUX_SYSTEM_NAMESPACE: %q", fromEnv) + return + } -func homeDir() string { - if h := os.Getenv("HOME"); h != "" { - return h + kubeconfigArgs.Namespace = &fromEnv } - return os.Getenv("USERPROFILE") // windows } // readPasswordFromStdin reads a password from stdin and returns the input @@ -184,3 +240,10 @@ func readPasswordFromStdin(prompt string) (string, error) { fmt.Println() return strings.TrimRight(out, "\r\n"), nil } + +func withPreviewNote(desc string) string { + previewNote := `⚠️ Please note that this command is in preview and under development. +While we try our best to not introduce breaking changes, they may occur when +we adapt to new features and/or find better ways to facilitate what it does.` + return fmt.Sprintf("%s\n\n%s", strings.TrimSpace(desc), previewNote) +} diff --git a/cmd/flux/main_e2e_test.go b/cmd/flux/main_e2e_test.go index 20a51c5f..f2b01cc6 100644 --- a/cmd/flux/main_e2e_test.go +++ b/cmd/flux/main_e2e_test.go @@ -25,10 +25,15 @@ import ( "os" "testing" - "github.com/fluxcd/flux2/internal/utils" + "github.com/go-logr/logr" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/fluxcd/flux2/v2/internal/utils" ) func TestMain(m *testing.M) { + log.SetLogger(logr.New(log.NullLogSink{})) + // Ensure tests print consistent timestamps regardless of timezone os.Setenv("TZ", "UTC") @@ -41,7 +46,7 @@ func TestMain(m *testing.M) { // Install Flux. output, err := executeCommand("install --components-extra=image-reflector-controller,image-automation-controller") if err != nil { - panic(fmt.Errorf("install falied: %s error:'%w'", output, err)) + panic(fmt.Errorf("install failed: %s error:'%w'", output, err)) } // Run tests @@ -50,7 +55,7 @@ func TestMain(m *testing.M) { // Uninstall Flux output, err = executeCommand("uninstall -s --keep-namespace") if err != nil { - panic(fmt.Errorf("uninstall falied: %s error:'%w'", output, err)) + panic(fmt.Errorf("uninstall failed: %s error:'%w'", output, err)) } // Delete namespace and wait for finalisation @@ -65,7 +70,7 @@ func TestMain(m *testing.M) { os.Exit(code) } -func setupTestNamespace(namespace string) (func(), error) { +func execSetupTestNamespace(namespace string) (func(), error) { kubectlArgs := []string{"create", "namespace", namespace} _, err := utils.ExecKubectlCommand(context.TODO(), utils.ModeStderrOS, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, kubectlArgs...) if err != nil { diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 26b4e99e..5f2afe60 100644 --- a/cmd/flux/main_test.go +++ b/cmd/flux/main_test.go @@ -20,6 +20,7 @@ import ( "bufio" "bytes" "context" + "flag" "fmt" "io" "os" @@ -30,18 +31,23 @@ import ( "text/template" "time" - "github.com/fluxcd/flux2/internal/utils" "github.com/google/go-cmp/cmp" "github.com/mattn/go-shellwords" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" k8syaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var nextNamespaceId int64 +// update allows golden files to be updated based on the current output. +var update = flag.Bool("update", false, "update golden files") + // Return a unique namespace with the specified prefix, for tests to create // objects that won't collide with each other. func allocateNamespace(prefix string) string { @@ -108,7 +114,8 @@ func (m *testEnvKubeManager) CreateObjects(clientObjects []*unstructured.Unstruc } obj.SetResourceVersion(createObj.GetResourceVersion()) err = m.client.Status().Update(context.Background(), obj) - if err != nil { + // Updating status of static objects results in not found error. + if err != nil && !errors.IsNotFound(err) { return err } } @@ -178,7 +185,7 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager } tmpFilename := filepath.Join("/tmp", "kubeconfig-"+time.Nanosecond.String()) - os.WriteFile(tmpFilename, kubeConfig, 0644) + os.WriteFile(tmpFilename, kubeConfig, 0o600) k8sClient, err := client.NewWithWatch(cfg, client.Options{ Scheme: utils.NewScheme(), }) @@ -199,6 +206,9 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager useExistingCluster := true config, err := clientcmd.BuildConfigFromFlags("", testKubeConfig) + if err != nil { + return nil, err + } testEnv := &envtest.Environment{ UseExistingCluster: &useExistingCluster, Config: config, @@ -284,24 +294,38 @@ func assertGoldenFile(goldenFile string) assertFunc { // is pre-processed with the specified templateValues. func assertGoldenTemplateFile(goldenFile string, templateValues map[string]string) assertFunc { goldenFileContents, fileErr := os.ReadFile(goldenFile) - return func(output string, err error) error { - if fileErr != nil { - return fmt.Errorf("Error reading golden file '%s': %s", goldenFile, fileErr) - } - var expectedOutput string - if len(templateValues) > 0 { - expectedOutput, err = executeTemplate(string(goldenFileContents), templateValues) - if err != nil { - return fmt.Errorf("Error executing golden template file '%s': %s", goldenFile, err) + return assert( + assertSuccess(), + func(output string, err error) error { + if fileErr != nil { + return fmt.Errorf("Error reading golden file '%s': %s", goldenFile, fileErr) } - } else { - expectedOutput = string(goldenFileContents) - } - if assertErr := assertGoldenValue(expectedOutput)(output, err); assertErr != nil { - return fmt.Errorf("Mismatch from golden file '%s': %v", goldenFile, assertErr) - } - return nil - } + var expectedOutput string + if len(templateValues) > 0 { + expectedOutput, err = executeTemplate(string(goldenFileContents), templateValues) + if err != nil { + return fmt.Errorf("Error executing golden template file '%s': %s", goldenFile, err) + } + } else { + expectedOutput = string(goldenFileContents) + } + if assertErr := assertGoldenValue(expectedOutput)(output, err); assertErr != nil { + // Update the golden files if comparison fails and the update flag is set. + if *update && output != "" { + // Skip update if there are template values. + if len(templateValues) > 0 { + fmt.Println("NOTE: -update flag passed but golden template files can't be updated, please update it manually") + } else { + if err := os.WriteFile(goldenFile, []byte(output), 0o600); err != nil { + return fmt.Errorf("failed to update golden file '%s': %v", goldenFile, err) + } + return nil + } + } + return fmt.Errorf("Mismatch from golden file '%s': %v", goldenFile, assertErr) + } + return nil + }) } type TestClusterMode int @@ -319,12 +343,15 @@ type cmdTestCase struct { // Tests use assertFunc to assert on an output, success or failure. This // can be a function defined by the test or existing function above. assert assertFunc - // Filename that contains yaml objects to load into Kubernetes - objectFile string } func (cmd *cmdTestCase) runTestCmd(t *testing.T) { actual, testErr := executeCommand(cmd.args) + // If the cmd error is a change, discard it + if isChangeError(testErr) { + testErr = nil + } + if assertErr := cmd.assert(actual, testErr); assertErr != nil { t.Error(assertErr) } @@ -342,6 +369,12 @@ func executeTemplate(content string, templateValues map[string]string) (string, // Run the command and return the captured output. func executeCommand(cmd string) (string, error) { defer resetCmdArgs() + defer func() { + // need to set this explicitly because apparently its value isn't changed + // in subsequent executions which causes tests to fail that rely on the value + // of "Changed". + resumeCmd.PersistentFlags().Lookup("wait").Changed = false + }() args, err := shellwords.Parse(cmd) if err != nil { return "", err @@ -361,8 +394,85 @@ func executeCommand(cmd string) (string, error) { return result, err } +// Run the command while passing the string as input and return the captured output. +func executeCommandWithIn(cmd string, in io.Reader) (string, error) { + defer resetCmdArgs() + args, err := shellwords.Parse(cmd) + if err != nil { + return "", err + } + + buf := new(bytes.Buffer) + + rootCmd.SetOut(buf) + rootCmd.SetErr(buf) + rootCmd.SetArgs(args) + if in != nil { + rootCmd.SetIn(in) + } + + _, err = rootCmd.ExecuteC() + result := buf.String() + + return result, err +} + +// resetCmdArgs resets the flags for various cmd +// Note: this will also clear default value of the flags set in init() func resetCmdArgs() { + *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace + alertArgs = alertFlags{} + alertProviderArgs = alertProviderFlags{} + bootstrapArgs = NewBootstrapFlags() + bServerArgs = bServerFlags{} + logsArgs = logsFlags{ + tail: -1, + fluxNamespace: rootArgs.defaults.Namespace, + } + buildKsArgs = buildKsFlags{} + checkArgs = checkFlags{} createArgs = createFlags{} + deleteArgs = deleteFlags{} + diffKsArgs = diffKsFlags{} + exportArgs = exportFlags{} getArgs = GetFlags{} + gitArgs = gitFlags{} + githubArgs = githubFlags{} + gitlabArgs = gitlabFlags{} + helmReleaseArgs = helmReleaseFlags{ + reconcileStrategy: "ChartVersion", + } + imagePolicyArgs = imagePolicyFlags{} + imageRepoArgs = imageRepoFlags{} + imageUpdateArgs = imageUpdateFlags{} + kustomizationArgs = NewKustomizationFlags() + receiverArgs = receiverFlags{} + resumeArgs = ResumeFlags{} + rhrArgs = reconcileHelmReleaseFlags{} + rksArgs = reconcileKsFlags{} secretGitArgs = NewSecretGitFlags() + secretHelmArgs = secretHelmFlags{} + secretTLSArgs = secretTLSFlags{} + sourceBucketArgs = sourceBucketFlags{} + sourceGitArgs = newSourceGitFlags() + sourceHelmArgs = sourceHelmFlags{} + sourceOCIRepositoryArgs = sourceOCIRepositoryFlags{} + suspendArgs = SuspendFlags{} + tenantArgs = tenantFlags{} + traceArgs = traceFlags{} + treeKsArgs = TreeKsFlags{} + uninstallArgs = uninstallFlags{} + versionArgs = versionFlags{ + output: "yaml", + } + envsubstArgs = envsubstFlags{} +} + +func isChangeError(err error) bool { + if reqErr, ok := err.(*RequestError); ok { + if strings.Contains(err.Error(), "identified at least one change, exiting with non-zero exit code") && reqErr.StatusCode == 1 { + return true + } + } + return false } diff --git a/cmd/flux/main_unit_test.go b/cmd/flux/main_unit_test.go index 8d3e6398..8e0af1aa 100644 --- a/cmd/flux/main_unit_test.go +++ b/cmd/flux/main_unit_test.go @@ -22,10 +22,13 @@ package main import ( "context" "fmt" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "os" "testing" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/log" ) // The test environment is long running process shared between tests, initialized @@ -34,6 +37,8 @@ import ( var testEnv *testEnvKubeManager func TestMain(m *testing.M) { + log.SetLogger(logr.New(log.NullLogSink{})) + // Ensure tests print consistent timestamps regardless of timezone os.Setenv("TZ", "UTC") diff --git a/cmd/flux/object.go b/cmd/flux/object.go index 0c68fcba..681d86e0 100644 --- a/cmd/flux/object.go +++ b/cmd/flux/object.go @@ -28,6 +28,7 @@ import ( // implementation can pick whichever it wants to use. type apiType struct { kind, humanKind string + groupVersion schema.GroupVersion } // adapter is an interface for a wrapper or alias from which we can @@ -46,7 +47,7 @@ type copyable interface { deepCopyClientObject() client.Object } -// listAdapater is the analogue to adapter, but for lists; the +// listAdapter is the analogue to adapter, but for lists; the // controller runtime distinguishes between methods dealing with // objects and lists. type listAdapter interface { diff --git a/cmd/flux/pull.go b/cmd/flux/pull.go new file mode 100644 index 00000000..9a5953b2 --- /dev/null +++ b/cmd/flux/pull.go @@ -0,0 +1,31 @@ +/* +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 ( + "github.com/spf13/cobra" +) + +var pullCmd = &cobra.Command{ + Use: "pull", + Short: "Pull artifacts", + Long: `The pull command is used to download OCI artifacts.`, +} + +func init() { + rootCmd.AddCommand(pullCmd) +} diff --git a/cmd/flux/pull_artifact.go b/cmd/flux/pull_artifact.go new file mode 100644 index 00000000..114368bd --- /dev/null +++ b/cmd/flux/pull_artifact.go @@ -0,0 +1,124 @@ +/* +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" + "os" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/v2/internal/flags" + + oci "github.com/fluxcd/pkg/oci/client" +) + +var pullArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Pull artifact", + Long: withPreviewNote(`The pull artifact command downloads and extracts the OCI artifact content to the given path. +The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`), + Example: ` # Pull an OCI artifact created by flux from GHCR + flux pull artifact oci://ghcr.io/org/manifests/app:v0.0.1 --output ./path/to/local/manifests +`, + RunE: pullArtifactCmdRun, +} + +type pullArtifactFlags struct { + output string + creds string + provider flags.SourceOCIProvider +} + +var pullArtifactArgs = newPullArtifactFlags() + +func newPullArtifactFlags() pullArtifactFlags { + return pullArtifactFlags{ + provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), + } +} + +func init() { + pullArtifactCmd.Flags().StringVarP(&pullArtifactArgs.output, "output", "o", "", "path where the artifact content should be extracted.") + pullArtifactCmd.Flags().StringVar(&pullArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") + pullArtifactCmd.Flags().Var(&pullArtifactArgs.provider, "provider", sourceOCIRepositoryArgs.provider.Description()) + pullCmd.AddCommand(pullArtifactCmd) +} + +func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact URL is required") + } + ociURL := args[0] + + if pullArtifactArgs.output == "" { + return fmt.Errorf("output path cannot be empty") + } + + if fs, err := os.Stat(pullArtifactArgs.output); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid output path %q: %w", pullArtifactArgs.output, err) + } + + url, err := oci.ParseArtifactURL(ociURL) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + ociClient := oci.NewClient(oci.DefaultOptions()) + + if pullArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && pullArtifactArgs.creds != "" { + logger.Actionf("logging in to registry with credentials") + if err := ociClient.LoginWithCredentials(pullArtifactArgs.creds); err != nil { + return fmt.Errorf("could not login with credentials: %w", err) + } + } + + if pullArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { + logger.Actionf("logging in to registry with provider credentials") + ociProvider, err := pullArtifactArgs.provider.ToOCIProvider() + if err != nil { + return fmt.Errorf("provider not supported: %w", err) + } + + if err := ociClient.LoginWithProvider(ctx, url, ociProvider); err != nil { + return fmt.Errorf("error during login with provider: %w", err) + } + } + + logger.Actionf("pulling artifact from %s", url) + + meta, err := ociClient.Pull(ctx, url, pullArtifactArgs.output) + if err != nil { + return err + } + + if meta.Source != "" { + logger.Successf("source %s", meta.Source) + } + if meta.Revision != "" { + logger.Successf("revision %s", meta.Revision) + } + logger.Successf("digest %s", meta.Digest) + logger.Successf("artifact content extracted to %s", pullArtifactArgs.output) + + return nil +} diff --git a/cmd/flux/push.go b/cmd/flux/push.go new file mode 100644 index 00000000..8488a83b --- /dev/null +++ b/cmd/flux/push.go @@ -0,0 +1,31 @@ +/* +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 ( + "github.com/spf13/cobra" +) + +var pushCmd = &cobra.Command{ + Use: "push", + Short: "Push artifacts", + Long: `The push command is used to publish OCI artifacts.`, +} + +func init() { + rootCmd.AddCommand(pushCmd) +} diff --git a/cmd/flux/push_artifact.go b/cmd/flux/push_artifact.go new file mode 100644 index 00000000..735b78d5 --- /dev/null +++ b/cmd/flux/push_artifact.go @@ -0,0 +1,332 @@ +/* +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" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/crane" + "github.com/google/go-containerregistry/pkg/logs" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + "github.com/fluxcd/pkg/oci" + "github.com/fluxcd/pkg/oci/auth/login" + "github.com/fluxcd/pkg/oci/client" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/flags" +) + +var pushArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Push artifact", + Long: withPreviewNote(`The push artifact command creates a tarball from the given directory or the single file and uploads the artifact to an OCI repository. +The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`), + Example: ` # Push manifests to GHCR using the short Git SHA as the OCI artifact tag + echo $GITHUB_PAT | docker login ghcr.io --username flux --password-stdin + flux push artifact oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) \ + --path="./path/to/local/manifests" \ + --source="$(git config --get remote.origin.url)" \ + --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" + + # Push and sign artifact with cosign + digest_url = $(flux push artifact \ + oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) \ + --source="$(git config --get remote.origin.url)" \ + --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" \ + --path="./path/to/local/manifest.yaml" \ + --output json | \ + jq -r '. | .repository + "@" + .digest') + cosign sign $digest_url + + # Push manifests passed into stdin to GHCR and set custom OCI annotations + kustomize build . | flux push artifact oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) -f - \ + --source="$(git config --get remote.origin.url)" \ + --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" \ + --annotations='org.opencontainers.image.licenses=Apache-2.0' \ + --annotations='org.opencontainers.image.documentation=https://app.org/docs' \ + --annotations='org.opencontainers.image.description=Production config.' + + # Push single manifest file to GHCR using the short Git SHA as the OCI artifact tag + echo $GITHUB_PAT | docker login ghcr.io --username flux --password-stdin + flux push artifact oci://ghcr.io/org/config/app:$(git rev-parse --short HEAD) \ + --path="./path/to/local/manifest.yaml" \ + --source="$(git config --get remote.origin.url)" \ + --revision="$(git branch --show-current)@sha1:$(git rev-parse HEAD)" + + # Push manifests to Docker Hub using the Git tag as the OCI artifact tag + echo $DOCKER_PAT | docker login --username flux --password-stdin + flux push artifact oci://docker.io/org/app-config:$(git tag --points-at HEAD) \ + --path="./path/to/local/manifests" \ + --source="$(git config --get remote.origin.url)" \ + --revision="$(git tag --points-at HEAD)@sha1:$(git rev-parse HEAD)" + + # Login directly to the registry provider + # You might need to export the following variable if you use local config files for AWS: + # export AWS_SDK_LOAD_CONFIG=1 + flux push artifact oci://.dkr.ecr..amazonaws.com/app-config:$(git tag --points-at HEAD) \ + --path="./path/to/local/manifests" \ + --source="$(git config --get remote.origin.url)" \ + --revision="$(git tag --points-at HEAD)@sha1:$(git rev-parse HEAD)" \ + --provider aws + + # Login by passing credentials directly + flux push artifact oci://docker.io/org/app-config:$(git tag --points-at HEAD) \ + --path="./path/to/local/manifests" \ + --source="$(git config --get remote.origin.url)" \ + --revision="$(git tag --points-at HEAD)@sha1:$(git rev-parse HEAD)" \ + --creds flux:$DOCKER_PAT +`, + RunE: pushArtifactCmdRun, +} + +type pushArtifactFlags struct { + path string + source string + revision string + creds string + provider flags.SourceOCIProvider + ignorePaths []string + annotations []string + output string + debug bool + reproducible bool +} + +var pushArtifactArgs = newPushArtifactFlags() + +func newPushArtifactFlags() pushArtifactFlags { + return pushArtifactFlags{ + provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), + } +} + +func init() { + pushArtifactCmd.Flags().StringVarP(&pushArtifactArgs.path, "path", "f", "", "path to the directory where the Kubernetes manifests are located") + pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.source, "source", "", "the source address, e.g. the Git URL") + pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.revision, "revision", "", "the source revision in the format '@sha1:'") + pushArtifactCmd.Flags().StringVar(&pushArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") + pushArtifactCmd.Flags().Var(&pushArtifactArgs.provider, "provider", pushArtifactArgs.provider.Description()) + pushArtifactCmd.Flags().StringSliceVar(&pushArtifactArgs.ignorePaths, "ignore-paths", excludeOCI, "set paths to ignore in .gitignore format") + pushArtifactCmd.Flags().StringArrayVarP(&pushArtifactArgs.annotations, "annotations", "a", nil, "Set custom OCI annotations in the format '='") + pushArtifactCmd.Flags().StringVarP(&pushArtifactArgs.output, "output", "o", "", + "the format in which the artifact digest should be printed, can be 'json' or 'yaml'") + pushArtifactCmd.Flags().BoolVarP(&pushArtifactArgs.debug, "debug", "", false, "display logs from underlying library") + pushArtifactCmd.Flags().BoolVar(&pushArtifactArgs.reproducible, "reproducible", false, "ensure reproducible image digests by setting the created timestamp to '1970-01-01T00:00:00Z'") + + pushCmd.AddCommand(pushArtifactCmd) +} + +func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact URL is required") + } + ociURL := args[0] + + if pushArtifactArgs.source == "" { + return fmt.Errorf("--source is required") + } + + if pushArtifactArgs.revision == "" { + return fmt.Errorf("--revision is required") + } + + if pushArtifactArgs.path == "" { + return fmt.Errorf("invalid path %q", pushArtifactArgs.path) + } + + url, err := client.ParseArtifactURL(ociURL) + if err != nil { + return err + } + + ref, err := name.ParseReference(url) + if err != nil { + return err + } + + path := pushArtifactArgs.path + if pushArtifactArgs.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: %w", path, err) + } + + annotations := map[string]string{} + for _, annotation := range pushArtifactArgs.annotations { + kv := strings.Split(annotation, "=") + if len(kv) != 2 { + return fmt.Errorf("invalid annotation %s, must be in the format key=value", annotation) + } + annotations[kv[0]] = kv[1] + } + + if pushArtifactArgs.debug { + // direct logs from crane library to stderr + // this can be useful to figure out things happening underneath e.g when the library is retrying a request + logs.Warn.SetOutput(os.Stderr) + } + + meta := client.Metadata{ + Source: pushArtifactArgs.source, + Revision: pushArtifactArgs.revision, + Annotations: annotations, + } + + if pushArtifactArgs.reproducible { + zeroTime := time.Unix(0, 0) + meta.Created = zeroTime.Format(time.RFC3339) + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + var auth authn.Authenticator + opts := client.DefaultOptions() + if pushArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && pushArtifactArgs.creds != "" { + logger.Actionf("logging in to registry with credentials") + auth, err = client.GetAuthFromCredentials(pushArtifactArgs.creds) + if err != nil { + return fmt.Errorf("could not login with credentials: %w", err) + } + opts = append(opts, crane.WithAuth(auth)) + } + + if pushArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { + logger.Actionf("logging in to registry with provider credentials") + ociProvider, err := pushArtifactArgs.provider.ToOCIProvider() + if err != nil { + return fmt.Errorf("provider not supported: %w", err) + } + + auth, err = login.NewManager().Login(ctx, url, ref, getProviderLoginOption(ociProvider)) + if err != nil { + return fmt.Errorf("error during login with provider: %w", err) + } + opts = append(opts, crane.WithAuth(auth)) + } + + if rootArgs.timeout != 0 { + backoff := remote.Backoff{ + Duration: 1.0 * time.Second, + Factor: 3, + Jitter: 0.1, + // timeout happens when the cap is exceeded or number of steps is reached + // 10 steps is big enough that most reasonable cap(under 30min) will be exceeded before + // the number of steps are completed. + Steps: 10, + Cap: rootArgs.timeout, + } + + if auth == nil { + auth, err = authn.DefaultKeychain.Resolve(ref.Context()) + if err != nil { + return err + } + } + transportOpts, err := client.WithRetryTransport(ctx, ref, auth, backoff, []string{ref.Context().Scope(transport.PushScope)}) + if err != nil { + return fmt.Errorf("error setting up transport: %w", err) + } + opts = append(opts, transportOpts, client.WithRetryBackOff(backoff)) + } + + if pushArtifactArgs.output == "" { + logger.Actionf("pushing artifact to %s", url) + } + + ociClient := client.NewClient(opts) + digestURL, err := ociClient.Push(ctx, url, path, + client.WithPushMetadata(meta), + client.WithPushIgnorePaths(pushArtifactArgs.ignorePaths...), + ) + if err != nil { + return fmt.Errorf("pushing artifact failed: %w", err) + } + + digest, err := name.NewDigest(digestURL) + if err != nil { + return fmt.Errorf("artifact digest parsing failed: %w", err) + } + + tag, err := name.NewTag(url) + if err != nil { + return fmt.Errorf("artifact tag parsing failed: %w", err) + } + + info := struct { + URL string `json:"url"` + Repository string `json:"repository"` + Tag string `json:"tag"` + Digest string `json:"digest"` + }{ + URL: fmt.Sprintf("oci://%s", digestURL), + Repository: digest.Repository.Name(), + Tag: tag.TagStr(), + Digest: digest.DigestStr(), + } + + switch pushArtifactArgs.output { + case "json": + marshalled, err := json.MarshalIndent(&info, "", " ") + if err != nil { + return fmt.Errorf("artifact digest JSON conversion failed: %w", err) + } + marshalled = append(marshalled, "\n"...) + rootCmd.Print(string(marshalled)) + case "yaml": + marshalled, err := yaml.Marshal(&info) + if err != nil { + return fmt.Errorf("artifact digest YAML conversion failed: %w", err) + } + rootCmd.Print(string(marshalled)) + default: + logger.Successf("artifact successfully pushed to %s", digestURL) + } + + return nil +} + +func getProviderLoginOption(provider oci.Provider) login.ProviderOptions { + var opts login.ProviderOptions + switch provider { + case oci.ProviderAzure: + opts.AzureAutoLogin = true + case oci.ProviderAWS: + opts.AwsAutoLogin = true + case oci.ProviderGCP: + opts.GcpAutoLogin = true + } + return opts +} diff --git a/cmd/flux/readiness.go b/cmd/flux/readiness.go new file mode 100644 index 00000000..f201625f --- /dev/null +++ b/cmd/flux/readiness.go @@ -0,0 +1,149 @@ +/* +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" + + kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" + apimeta "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/object" + "github.com/fluxcd/pkg/runtime/patch" +) + +// objectStatusType is the type of object in terms of status when computing the +// readiness of an object. Readiness check method depends on the type of object. +// For a dynamic object, Ready status condition is considered only for the +// latest generation of the object. For a static object that don't have any +// condition, the object generation is not considered. +type objectStatusType int + +const ( + objectStatusDynamic objectStatusType = iota + objectStatusStatic +) + +// isObjectReady determines if an object is ready using the kstatus.Compute() +// result. statusType helps differenciate between static and dynamic objects to +// accurately check the object's readiness. A dynamic object may have some extra +// considerations depending on the object. +func isObjectReady(obj client.Object, statusType objectStatusType) (bool, error) { + observedGen, err := object.GetStatusObservedGeneration(obj) + if err != nil && err != object.ErrObservedGenerationNotFound { + return false, err + } + + if statusType == objectStatusDynamic { + // Object not reconciled yet. + if observedGen < 1 { + return false, nil + } + + cobj, ok := obj.(meta.ObjectWithConditions) + if !ok { + return false, fmt.Errorf("unable to get conditions from object") + } + + if c := apimeta.FindStatusCondition(cobj.GetConditions(), meta.ReadyCondition); c != nil { + // Ensure that the ready condition is for the latest generation of + // the object. + // NOTE: Some APIs like ImageUpdateAutomation and HelmRelease don't + // support per condition observed generation yet. Per condition + // observed generation for them are always zero. + // There are two strategies used across different object kinds to + // check the latest ready condition: + // - check that the ready condition's generation matches the + // object's generation. + // - check that the observed generation of the object in the + // status matches the object's generation. + // + // TODO: Once ImageUpdateAutomation and HelmRelease APIs have per + // condition observed generation, remove the object's observed + // generation and object's generation check (the second condition + // below). Also, try replacing this readiness check function with + // fluxcd/pkg/ssa's ResourceManager.Wait(), which uses kstatus + // internally to check readiness of the objects. + if c.ObservedGeneration != 0 && c.ObservedGeneration != obj.GetGeneration() { + return false, nil + } + if c.ObservedGeneration == 0 && observedGen != obj.GetGeneration() { + return false, nil + } + } else { + return false, nil + } + } + + u, err := patch.ToUnstructured(obj) + if err != nil { + return false, err + } + result, err := kstatus.Compute(u) + if err != nil { + return false, err + } + switch result.Status { + case kstatus.CurrentStatus: + return true, nil + case kstatus.InProgressStatus: + return false, nil + default: + return false, fmt.Errorf(result.Message) + } +} + +// isObjectReadyConditionFunc returns a wait.ConditionFunc to be used with +// wait.Poll* while polling for an object with dynamic status to be ready. +func isObjectReadyConditionFunc(kubeClient client.Client, namespaceName types.NamespacedName, obj client.Object) wait.ConditionWithContextFunc { + return func(ctx context.Context) (bool, error) { + err := kubeClient.Get(ctx, namespaceName, obj) + if err != nil { + return false, err + } + + return isObjectReady(obj, objectStatusDynamic) + } +} + +// isStaticObjectReadyConditionFunc returns a wait.ConditionFunc to be used with +// wait.Poll* while polling for an object with static or no status to be +// ready. +func isStaticObjectReadyConditionFunc(kubeClient client.Client, namespaceName types.NamespacedName, obj client.Object) wait.ConditionWithContextFunc { + return func(ctx context.Context) (bool, error) { + err := kubeClient.Get(ctx, namespaceName, obj) + if err != nil { + return false, err + } + + return isObjectReady(obj, objectStatusStatic) + } +} + +// kstatusCompute returns the kstatus computed result of a given object. +func kstatusCompute(obj client.Object) (result *kstatus.Result, err error) { + u, err := patch.ToUnstructured(obj) + if err != nil { + return result, err + } + return kstatus.Compute(u) +} diff --git a/cmd/flux/readiness_test.go b/cmd/flux/readiness_test.go new file mode 100644 index 00000000..4d987042 --- /dev/null +++ b/cmd/flux/readiness_test.go @@ -0,0 +1,139 @@ +/* +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 ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/conditions" + sourcev1 "github.com/fluxcd/source-controller/api/v1" +) + +func Test_isObjectReady(t *testing.T) { + // Ready object. + readyObj := &sourcev1.GitRepository{} + readyObj.Generation = 1 + readyObj.Status.ObservedGeneration = 1 + conditions.MarkTrue(readyObj, meta.ReadyCondition, "foo1", "bar1") + + // Not ready object. + notReadyObj := readyObj.DeepCopy() + conditions.MarkFalse(notReadyObj, meta.ReadyCondition, "foo2", "bar2") + + // Not reconciled object. + notReconciledObj := readyObj.DeepCopy() + notReconciledObj.Status = sourcev1.GitRepositoryStatus{ObservedGeneration: -1} + + // No condition. + noConditionObj := readyObj.DeepCopy() + noConditionObj.Status = sourcev1.GitRepositoryStatus{ObservedGeneration: 1} + + // Outdated condition. + readyObjOutdated := readyObj.DeepCopy() + readyObjOutdated.Generation = 2 + + // Object without per condition observed generation. + oldObj := readyObj.DeepCopy() + readyTrueCondn := conditions.TrueCondition(meta.ReadyCondition, "foo3", "bar3") + oldObj.Status.Conditions = []metav1.Condition{*readyTrueCondn} + + // Outdated object without per condition observed generation. + oldObjOutdated := oldObj.DeepCopy() + oldObjOutdated.Generation = 2 + + // Empty status object. + staticObj := readyObj.DeepCopy() + staticObj.Status = sourcev1.GitRepositoryStatus{} + + // No status object. + noStatusObj := ¬ificationv1.Provider{} + noStatusObj.Generation = 1 + + type args struct { + obj client.Object + statusType objectStatusType + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "dynamic ready", + args: args{obj: readyObj, statusType: objectStatusDynamic}, + want: true, + }, + { + name: "dynamic not ready", + args: args{obj: notReadyObj, statusType: objectStatusDynamic}, + want: false, + }, + { + name: "dynamic not reconciled", + args: args{obj: notReconciledObj, statusType: objectStatusDynamic}, + want: false, + }, + { + name: "dynamic not condition", + args: args{obj: noConditionObj, statusType: objectStatusDynamic}, + want: false, + }, + { + name: "dynamic ready outdated", + args: args{obj: readyObjOutdated, statusType: objectStatusDynamic}, + want: false, + }, + { + name: "dynamic ready without per condition gen", + args: args{obj: oldObj, statusType: objectStatusDynamic}, + want: true, + }, + { + name: "dynamic outdated ready status without per condition gen", + args: args{obj: oldObjOutdated, statusType: objectStatusDynamic}, + want: false, + }, + { + name: "static empty status", + args: args{obj: staticObj, statusType: objectStatusStatic}, + want: true, + }, + { + name: "static no status", + args: args{obj: noStatusObj, statusType: objectStatusStatic}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := isObjectReady(tt.args.obj, tt.args.statusType) + if (err != nil) != tt.wantErr { + t.Errorf("isObjectReady() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("isObjectReady() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/cmd/flux/receiver.go b/cmd/flux/receiver.go index 9266e99a..428f8378 100644 --- a/cmd/flux/receiver.go +++ b/cmd/flux/receiver.go @@ -19,14 +19,15 @@ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) // notificationv1.Receiver var receiverType = apiType{ - kind: notificationv1.ReceiverKind, - humanKind: "receiver", + kind: notificationv1.ReceiverKind, + humanKind: "receiver", + groupVersion: notificationv1.GroupVersion, } type receiverAdapter struct { diff --git a/cmd/flux/reconcile.go b/cmd/flux/reconcile.go index c3e53a73..c37f3d32 100644 --- a/cmd/flux/reconcile.go +++ b/cmd/flux/reconcile.go @@ -21,24 +21,26 @@ import ( "fmt" "time" + kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" "github.com/spf13/cobra" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/util/retry" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/notification-controller/api/v1beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var reconcileCmd = &cobra.Command{ Use: "reconcile", Short: "Reconcile sources and resources", - Long: "The reconcile sub-commands trigger a reconciliation of sources and resources.", + Long: `The reconcile sub-commands trigger a reconciliation of sources and resources.`, } func init() { @@ -59,13 +61,23 @@ type reconcilable interface { GetAnnotations() map[string]string SetAnnotations(map[string]string) - // this is usually implemented by GOTK types, since it's used for meta.SetResourceCondition - GetStatusConditions() *[]metav1.Condition - + isStatic() bool // is it a static object that does not have a reconciler? lastHandledReconcileRequest() string // what was the last handled reconcile request? successMessage() string // what do you want to tell people when successfully reconciled? } +func reconcilableConditions(object reconcilable) []metav1.Condition { + if s, ok := object.(meta.ObjectWithConditions); ok { + return s.GetConditions() + } + + if s, ok := object.(oldConditions); ok { + return *s.GetStatusConditions() + } + + return []metav1.Condition{} +} + func (reconcile reconcileCommand) run(cmd *cobra.Command, args []string) error { if len(args) < 1 { return fmt.Errorf("%s name is required", reconcile.kind) @@ -75,7 +87,7 @@ func (reconcile reconcileCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -90,33 +102,29 @@ func (reconcile reconcileCommand) run(cmd *cobra.Command, args []string) error { return err } + if reconcile.object.isStatic() { + logger.Successf("reconciliation not supported by the object") + return nil + } + if reconcile.object.isSuspended() { return fmt.Errorf("resource is suspended") } logger.Actionf("annotating %s %s in %s namespace", reconcile.kind, name, *kubeconfigArgs.Namespace) - if err := requestReconciliation(ctx, kubeClient, namespacedName, reconcile.object); err != nil { + if err := requestReconciliation(ctx, kubeClient, namespacedName, + reconcile.groupVersion.WithKind(reconcile.kind)); err != nil { return err } logger.Successf("%s annotated", reconcile.kind) - if reconcile.kind == v1beta1.AlertKind || reconcile.kind == v1beta1.ReceiverKind { - if err = wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isReconcileReady(ctx, kubeClient, namespacedName, reconcile.object)); err != nil { - return err - } - - logger.Successf(reconcile.object.successMessage()) - return nil - } - lastHandledReconcileAt := reconcile.object.lastHandledReconcileRequest() logger.Waitingf("waiting for %s reconciliation", reconcile.kind) - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - reconciliationHandled(ctx, kubeClient, namespacedName, reconcile.object, lastHandledReconcileAt)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + reconciliationHandled(kubeClient, namespacedName, reconcile.object, lastHandledReconcileAt)); err != nil { return err } - readyCond := apimeta.FindStatusCondition(*reconcile.object.GetStatusConditions(), meta.ReadyCondition) + readyCond := apimeta.FindStatusCondition(reconcilableConditions(reconcile.object), meta.ReadyCondition) if readyCond == nil { return fmt.Errorf("status can't be determined") } @@ -128,54 +136,57 @@ func (reconcile reconcileCommand) run(cmd *cobra.Command, args []string) error { return nil } -func reconciliationHandled(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, obj reconcilable, lastHandledReconcileAt string) wait.ConditionFunc { - return func() (bool, error) { +func reconciliationHandled(kubeClient client.Client, namespacedName types.NamespacedName, obj reconcilable, lastHandledReconcileAt string) wait.ConditionWithContextFunc { + return func(ctx context.Context) (bool, error) { err := kubeClient.Get(ctx, namespacedName, obj.asClientObject()) if err != nil { return false, err } - isProgressing := apimeta.IsStatusConditionPresentAndEqual(*obj.GetStatusConditions(), - meta.ReadyCondition, metav1.ConditionUnknown) - return obj.lastHandledReconcileRequest() != lastHandledReconcileAt && !isProgressing, nil + + if obj.lastHandledReconcileRequest() == lastHandledReconcileAt { + return false, nil + } + + result, err := kstatusCompute(obj.asClientObject()) + if err != nil { + return false, err + } + + return result.Status == kstatus.CurrentStatus, nil } } func requestReconciliation(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, obj reconcilable) error { + namespacedName types.NamespacedName, gvk schema.GroupVersionKind) error { return retry.RetryOnConflict(retry.DefaultBackoff, func() (err error) { - if err := kubeClient.Get(ctx, namespacedName, obj.asClientObject()); err != nil { + object := &metav1.PartialObjectMetadata{} + object.SetGroupVersionKind(gvk) + object.SetName(namespacedName.Name) + object.SetNamespace(namespacedName.Namespace) + if err := kubeClient.Get(ctx, namespacedName, object); err != nil { return err } - patch := client.MergeFrom(obj.deepCopyClientObject()) - if ann := obj.GetAnnotations(); ann == nil { - obj.SetAnnotations(map[string]string{ - meta.ReconcileRequestAnnotation: time.Now().Format(time.RFC3339Nano), - }) - } else { - ann[meta.ReconcileRequestAnnotation] = time.Now().Format(time.RFC3339Nano) - obj.SetAnnotations(ann) - } - return kubeClient.Patch(ctx, obj.asClientObject(), patch) - }) -} + patch := client.MergeFrom(object.DeepCopy()) -func isReconcileReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, obj reconcilable) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, obj.asClientObject()) - if err != nil { - return false, err + // Add a timestamp annotation to trigger a reconciliation. + ts := time.Now().Format(time.RFC3339Nano) + annotations := object.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string, 1) } + annotations[meta.ReconcileRequestAnnotation] = ts - if c := apimeta.FindStatusCondition(*obj.GetStatusConditions(), meta.ReadyCondition); c != nil { - switch c.Status { - case metav1.ConditionTrue: - return true, nil - case metav1.ConditionFalse: - return false, fmt.Errorf(c.Message) + // HelmRelease specific annotations to force or reset a release. + if gvk.Kind == helmv2.HelmReleaseKind { + if rhrArgs.syncForce { + annotations[helmv2.ForceRequestAnnotation] = ts + } + if rhrArgs.syncReset { + annotations[helmv2.ResetRequestAnnotation] = ts } } - return false, nil - } + + object.SetAnnotations(annotations) + return kubeClient.Patch(ctx, object, patch) + }) } diff --git a/cmd/flux/reconcile_alert.go b/cmd/flux/reconcile_alert.go deleted file mode 100644 index 6027da3e..00000000 --- a/cmd/flux/reconcile_alert.go +++ /dev/null @@ -1,44 +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 ( - "github.com/spf13/cobra" - - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" -) - -var reconcileAlertCmd = &cobra.Command{ - Use: "alert [name]", - Short: "Reconcile an Alert", - Long: `The reconcile alert command triggers a reconciliation of an Alert resource and waits for it to finish.`, - Example: ` # Trigger a reconciliation for an existing alert - flux reconcile alert main`, - ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), - RunE: reconcileCommand{ - apiType: alertType, - object: alertAdapter{¬ificationv1.Alert{}}, - }.run, -} - -func init() { - reconcileCmd.AddCommand(reconcileAlertCmd) -} - -func (obj alertAdapter) lastHandledReconcileRequest() string { - return "" -} diff --git a/cmd/flux/reconcile_alertprovider.go b/cmd/flux/reconcile_alertprovider.go deleted file mode 100644 index df24a68e..00000000 --- a/cmd/flux/reconcile_alertprovider.go +++ /dev/null @@ -1,93 +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 ( - "context" - "fmt" - "time" - - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" - "github.com/fluxcd/pkg/apis/meta" - - "github.com/fluxcd/flux2/internal/utils" -) - -var reconcileAlertProviderCmd = &cobra.Command{ - Use: "alert-provider [name]", - Short: "Reconcile a Provider", - Long: `The reconcile alert-provider command triggers a reconciliation of a Provider resource and waits for it to finish.`, - Example: ` # Trigger a reconciliation for an existing provider - flux reconcile alert-provider slack`, - ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), - RunE: reconcileAlertProviderCmdRun, -} - -func init() { - reconcileCmd.AddCommand(reconcileAlertProviderCmd) -} - -func reconcileAlertProviderCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("Provider name is required") - } - name := args[0] - - ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) - defer cancel() - - kubeClient, err := utils.KubeClient(kubeconfigArgs) - if err != nil { - return err - } - - namespacedName := types.NamespacedName{ - Namespace: *kubeconfigArgs.Namespace, - Name: name, - } - - logger.Actionf("annotating Provider %s in %s namespace", name, *kubeconfigArgs.Namespace) - var alertProvider notificationv1.Provider - err = kubeClient.Get(ctx, namespacedName, &alertProvider) - if err != nil { - return err - } - - if alertProvider.Annotations == nil { - alertProvider.Annotations = map[string]string{ - meta.ReconcileRequestAnnotation: time.Now().Format(time.RFC3339Nano), - } - } else { - alertProvider.Annotations[meta.ReconcileRequestAnnotation] = time.Now().Format(time.RFC3339Nano) - } - if err := kubeClient.Update(ctx, &alertProvider); err != nil { - return err - } - logger.Successf("Provider annotated") - - logger.Waitingf("waiting for reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isAlertProviderReady(ctx, kubeClient, namespacedName, &alertProvider)); err != nil { - return err - } - logger.Successf("Provider reconciliation completed") - return nil -} diff --git a/cmd/flux/reconcile_helmrelease.go b/cmd/flux/reconcile_helmrelease.go index 38558a0f..134cfa47 100644 --- a/cmd/flux/reconcile_helmrelease.go +++ b/cmd/flux/reconcile_helmrelease.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +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. @@ -17,11 +17,14 @@ limitations under the License. package main import ( + "fmt" + "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" ) var reconcileHrCmd = &cobra.Command{ @@ -44,13 +47,16 @@ The reconcile kustomization command triggers a reconciliation of a HelmRelease r type reconcileHelmReleaseFlags struct { syncHrWithSource bool + syncForce bool + syncReset bool } var rhrArgs reconcileHelmReleaseFlags func init() { reconcileHrCmd.Flags().BoolVar(&rhrArgs.syncHrWithSource, "with-source", false, "reconcile HelmRelease source") - + reconcileHrCmd.Flags().BoolVar(&rhrArgs.syncForce, "force", false, "force a one-off install or upgrade of the HelmRelease resource") + reconcileHrCmd.Flags().BoolVar(&rhrArgs.syncReset, "reset", false, "reset the failure count for this HelmRelease resource") reconcileCmd.AddCommand(reconcileHrCmd) } @@ -62,28 +68,50 @@ func (obj helmReleaseAdapter) reconcileSource() bool { return rhrArgs.syncHrWithSource } -func (obj helmReleaseAdapter) getSource() (reconcileCommand, types.NamespacedName) { - var cmd reconcileCommand - switch obj.Spec.Chart.Spec.SourceRef.Kind { - case sourcev1.HelmRepositoryKind: - cmd = reconcileCommand{ - apiType: helmRepositoryType, - object: helmRepositoryAdapter{&sourcev1.HelmRepository{}}, +func (obj helmReleaseAdapter) getSource() (reconcileSource, types.NamespacedName) { + var ( + name string + ns string + ) + switch { + case obj.Spec.ChartRef != nil: + name, ns = obj.Spec.ChartRef.Name, obj.Spec.ChartRef.Namespace + if ns == "" { + ns = obj.Namespace + } + namespacedName := types.NamespacedName{ + Name: name, + Namespace: ns, } - case sourcev1.GitRepositoryKind: - cmd = reconcileCommand{ - apiType: gitRepositoryType, - object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, + if obj.Spec.ChartRef.Kind == sourcev1.HelmChartKind { + return reconcileWithSourceCommand{ + apiType: helmChartType, + object: helmChartAdapter{&sourcev1.HelmChart{}}, + force: true, + }, namespacedName } - case sourcev1.BucketKind: - cmd = reconcileCommand{ - apiType: bucketType, - object: bucketAdapter{&sourcev1.Bucket{}}, + return reconcileCommand{ + apiType: ociRepositoryType, + object: ociRepositoryAdapter{&sourcev1b2.OCIRepository{}}, + }, namespacedName + default: + // default case assumes the HelmRelease is using a HelmChartTemplate + ns = obj.Spec.Chart.Spec.SourceRef.Namespace + if ns == "" { + ns = obj.Namespace } + name = fmt.Sprintf("%s-%s", obj.Namespace, obj.Name) + return reconcileWithSourceCommand{ + apiType: helmChartType, + object: helmChartAdapter{&sourcev1.HelmChart{}}, + force: true, + }, types.NamespacedName{ + Name: name, + Namespace: ns, + } } +} - return cmd, types.NamespacedName{ - Name: obj.Spec.Chart.Spec.SourceRef.Name, - Namespace: obj.Spec.Chart.Spec.SourceRef.Namespace, - } +func (obj helmReleaseAdapter) isStatic() bool { + return false } diff --git a/cmd/flux/reconcile_image.go b/cmd/flux/reconcile_image.go index 8d1d432e..00110754 100644 --- a/cmd/flux/reconcile_image.go +++ b/cmd/flux/reconcile_image.go @@ -23,7 +23,7 @@ import ( var reconcileImageCmd = &cobra.Command{ Use: "image", Short: "Reconcile image automation objects", - Long: "The reconcile sub-commands trigger a reconciliation of image automation objects.", + Long: `The reconcile sub-commands trigger a reconciliation of image automation objects.`, } func init() { diff --git a/cmd/flux/reconcile_image_repository.go b/cmd/flux/reconcile_image_repository.go index efd89e9c..6a386c8b 100644 --- a/cmd/flux/reconcile_image_repository.go +++ b/cmd/flux/reconcile_image_repository.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var reconcileImageRepositoryCmd = &cobra.Command{ @@ -30,7 +30,7 @@ var reconcileImageRepositoryCmd = &cobra.Command{ Long: `The reconcile image repository command triggers a reconciliation of an ImageRepository resource and waits for it to finish.`, Example: ` # Trigger an scan for an existing image repository flux reconcile image repository alpine`, - ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImagePolicyKind)), + ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: reconcileCommand{ apiType: imageRepositoryType, object: imageRepositoryAdapter{&imagev1.ImageRepository{}}, @@ -48,3 +48,7 @@ func (obj imageRepositoryAdapter) lastHandledReconcileRequest() string { func (obj imageRepositoryAdapter) successMessage() string { return fmt.Sprintf("scan fetched %d tags", obj.Status.LastScanResult.TagCount) } + +func (obj imageRepositoryAdapter) isStatic() bool { + return false +} diff --git a/cmd/flux/reconcile_image_updateauto.go b/cmd/flux/reconcile_image_updateauto.go index 8fba13cc..95ec6e95 100644 --- a/cmd/flux/reconcile_image_updateauto.go +++ b/cmd/flux/reconcile_image_updateauto.go @@ -22,7 +22,7 @@ import ( "github.com/spf13/cobra" apimeta "k8s.io/apimachinery/pkg/api/meta" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" meta "github.com/fluxcd/pkg/apis/meta" ) @@ -43,10 +43,6 @@ func init() { reconcileImageCmd.AddCommand(reconcileImageUpdateCmd) } -func (obj imageUpdateAutomationAdapter) suspended() bool { - return obj.ImageUpdateAutomation.Spec.Suspend -} - func (obj imageUpdateAutomationAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } @@ -60,3 +56,7 @@ func (obj imageUpdateAutomationAdapter) successMessage() string { } return "automation not yet run" } + +func (obj imageUpdateAutomationAdapter) isStatic() bool { + return false +} diff --git a/cmd/flux/reconcile_kustomization.go b/cmd/flux/reconcile_kustomization.go index fd8e3f8b..7a18ec02 100644 --- a/cmd/flux/reconcile_kustomization.go +++ b/cmd/flux/reconcile_kustomization.go @@ -20,8 +20,9 @@ import ( "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" ) var reconcileKsCmd = &cobra.Command{ @@ -62,18 +63,23 @@ func (obj kustomizationAdapter) reconcileSource() bool { return rksArgs.syncKsWithSource } -func (obj kustomizationAdapter) getSource() (reconcileCommand, types.NamespacedName) { +func (obj kustomizationAdapter) getSource() (reconcileSource, types.NamespacedName) { var cmd reconcileCommand switch obj.Spec.SourceRef.Kind { + case sourcev1b2.OCIRepositoryKind: + cmd = reconcileCommand{ + apiType: ociRepositoryType, + object: ociRepositoryAdapter{&sourcev1b2.OCIRepository{}}, + } case sourcev1.GitRepositoryKind: cmd = reconcileCommand{ apiType: gitRepositoryType, object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, } - case sourcev1.BucketKind: + case sourcev1b2.BucketKind: cmd = reconcileCommand{ apiType: bucketType, - object: bucketAdapter{&sourcev1.Bucket{}}, + object: bucketAdapter{&sourcev1b2.Bucket{}}, } } @@ -82,3 +88,7 @@ func (obj kustomizationAdapter) getSource() (reconcileCommand, types.NamespacedN Namespace: obj.Spec.SourceRef.Namespace, } } + +func (obj kustomizationAdapter) isStatic() bool { + return false +} diff --git a/cmd/flux/reconcile_receiver.go b/cmd/flux/reconcile_receiver.go index 676aa30a..1691a95d 100644 --- a/cmd/flux/reconcile_receiver.go +++ b/cmd/flux/reconcile_receiver.go @@ -17,18 +17,9 @@ limitations under the License. package main import ( - "context" - "fmt" - "time" - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" - "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/flux2/internal/utils" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var reconcileReceiverCmd = &cobra.Command{ @@ -38,62 +29,20 @@ var reconcileReceiverCmd = &cobra.Command{ Example: ` # Trigger a reconciliation for an existing receiver flux reconcile receiver main`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), - RunE: reconcileReceiverCmdRun, + RunE: reconcileCommand{ + apiType: receiverType, + object: receiverAdapter{¬ificationv1.Receiver{}}, + }.run, } func init() { reconcileCmd.AddCommand(reconcileReceiverCmd) } -func reconcileReceiverCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("receiver name is required") - } - name := args[0] - - ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) - defer cancel() - - kubeClient, err := utils.KubeClient(kubeconfigArgs) - if err != nil { - return err - } - - namespacedName := types.NamespacedName{ - Namespace: *kubeconfigArgs.Namespace, - Name: name, - } - - var receiver notificationv1.Receiver - err = kubeClient.Get(ctx, namespacedName, &receiver) - if err != nil { - return err - } - - if receiver.Spec.Suspend { - return fmt.Errorf("resource is suspended") - } - - logger.Actionf("annotating Receiver %s in %s namespace", name, *kubeconfigArgs.Namespace) - if receiver.Annotations == nil { - receiver.Annotations = map[string]string{ - meta.ReconcileRequestAnnotation: time.Now().Format(time.RFC3339Nano), - } - } else { - receiver.Annotations[meta.ReconcileRequestAnnotation] = time.Now().Format(time.RFC3339Nano) - } - if err := kubeClient.Update(ctx, &receiver); err != nil { - return err - } - logger.Successf("Receiver annotated") - - logger.Waitingf("waiting for Receiver reconciliation") - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isReceiverReady(ctx, kubeClient, namespacedName, &receiver)); err != nil { - return err - } - - logger.Successf("Receiver reconciliation completed") +func (obj receiverAdapter) lastHandledReconcileRequest() string { + return obj.Status.GetLastHandledReconcileRequest() +} - return nil +func (obj receiverAdapter) isStatic() bool { + return false } diff --git a/cmd/flux/reconcile_source.go b/cmd/flux/reconcile_source.go index 3079978c..473e3f25 100644 --- a/cmd/flux/reconcile_source.go +++ b/cmd/flux/reconcile_source.go @@ -23,7 +23,7 @@ import ( var reconcileSourceCmd = &cobra.Command{ Use: "source", Short: "Reconcile sources", - Long: "The reconcile source sub-commands trigger a reconciliation of sources.", + Long: `The reconcile source sub-commands trigger a reconciliation of sources.`, } func init() { diff --git a/cmd/flux/reconcile_source_bucket.go b/cmd/flux/reconcile_source_bucket.go index 768071da..68079a66 100644 --- a/cmd/flux/reconcile_source_bucket.go +++ b/cmd/flux/reconcile_source_bucket.go @@ -17,18 +17,11 @@ limitations under the License. package main import ( - "context" "fmt" "github.com/spf13/cobra" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" ) var reconcileSourceBucketCmd = &cobra.Command{ @@ -48,31 +41,6 @@ func init() { reconcileSourceCmd.AddCommand(reconcileSourceBucketCmd) } -func isBucketReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, bucket *sourcev1.Bucket) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, bucket) - if err != nil { - return false, err - } - - // Confirm the state we are observing is for the current generation - if bucket.Generation != bucket.Status.ObservedGeneration { - return false, nil - } - - if c := apimeta.FindStatusCondition(bucket.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 - } -} - func (obj bucketAdapter) lastHandledReconcileRequest() string { return obj.Status.GetLastHandledReconcileRequest() } @@ -80,3 +48,7 @@ func (obj bucketAdapter) lastHandledReconcileRequest() string { func (obj bucketAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } + +func (obj bucketAdapter) isStatic() bool { + return false +} diff --git a/cmd/flux/reconcile_source_chart.go b/cmd/flux/reconcile_source_chart.go new file mode 100644 index 00000000..77887b75 --- /dev/null +++ b/cmd/flux/reconcile_source_chart.go @@ -0,0 +1,90 @@ +/* +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 ( + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" +) + +var reconcileSourceHelmChartCmd = &cobra.Command{ + Use: "chart [name]", + Short: "Reconcile a HelmChart source", + Long: `The reconcile source command triggers a reconciliation of a HelmChart resource and waits for it to finish.`, + Example: ` # Trigger a reconciliation for an existing source + flux reconcile source chart podinfo + + # Trigger a reconciliation of the HelmCharts's source and apply changes + flux reconcile helmchart podinfo --with-source`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), + RunE: reconcileWithSourceCommand{ + apiType: helmChartType, + object: helmChartAdapter{&sourcev1.HelmChart{}}, + }.run, +} + +func init() { + reconcileSourceHelmChartCmd.Flags().BoolVar(&rhcArgs.syncHrWithSource, "with-source", false, "reconcile HelmChart source") + reconcileSourceCmd.AddCommand(reconcileSourceHelmChartCmd) +} + +func (obj helmChartAdapter) lastHandledReconcileRequest() string { + return obj.Status.GetLastHandledReconcileRequest() +} + +type reconcileHelmChartFlags struct { + syncHrWithSource bool +} + +var rhcArgs reconcileHelmChartFlags + +func (obj helmChartAdapter) reconcileSource() bool { + return rhcArgs.syncHrWithSource +} + +func (obj helmChartAdapter) getSource() (reconcileSource, types.NamespacedName) { + var cmd reconcileCommand + switch obj.Spec.SourceRef.Kind { + case sourcev1.HelmRepositoryKind: + cmd = reconcileCommand{ + apiType: helmRepositoryType, + object: helmRepositoryAdapter{&sourcev1.HelmRepository{}}, + } + case sourcev1.GitRepositoryKind: + cmd = reconcileCommand{ + apiType: gitRepositoryType, + object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, + } + case sourcev1b2.BucketKind: + cmd = reconcileCommand{ + apiType: bucketType, + object: bucketAdapter{&sourcev1b2.Bucket{}}, + } + } + + return cmd, types.NamespacedName{ + Name: obj.Spec.SourceRef.Name, + Namespace: obj.Namespace, + } +} + +func (obj helmChartAdapter) isStatic() bool { + return false +} diff --git a/cmd/flux/reconcile_source_git.go b/cmd/flux/reconcile_source_git.go index 9b5a75be..28256496 100644 --- a/cmd/flux/reconcile_source_git.go +++ b/cmd/flux/reconcile_source_git.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileSourceGitCmd = &cobra.Command{ @@ -48,3 +48,7 @@ func (obj gitRepositoryAdapter) lastHandledReconcileRequest() string { func (obj gitRepositoryAdapter) successMessage() string { return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } + +func (obj gitRepositoryAdapter) isStatic() bool { + return false +} diff --git a/cmd/flux/reconcile_source_helm.go b/cmd/flux/reconcile_source_helm.go index e0836259..a9b89d3d 100644 --- a/cmd/flux/reconcile_source_helm.go +++ b/cmd/flux/reconcile_source_helm.go @@ -21,7 +21,9 @@ import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/conditions" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var reconcileSourceHelmCmd = &cobra.Command{ @@ -46,5 +48,19 @@ func (obj helmRepositoryAdapter) lastHandledReconcileRequest() string { } func (obj helmRepositoryAdapter) successMessage() string { + // HelmRepository of type OCI don't set an Artifact + if obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI { + readyCondition := conditions.Get(obj.HelmRepository, meta.ReadyCondition) + // This shouldn't happen, successMessage shouldn't be called if + // object isn't ready + if readyCondition == nil { + return "" + } + return readyCondition.Message + } return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) } + +func (obj helmRepositoryAdapter) isStatic() bool { + return obj.Spec.Type == sourcev1.HelmRepositoryTypeOCI +} diff --git a/cmd/flux/reconcile_source_oci.go b/cmd/flux/reconcile_source_oci.go new file mode 100644 index 00000000..52237f2f --- /dev/null +++ b/cmd/flux/reconcile_source_oci.go @@ -0,0 +1,54 @@ +/* +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 ( + "fmt" + + "github.com/spf13/cobra" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" +) + +var reconcileSourceOCIRepositoryCmd = &cobra.Command{ + Use: "oci [name]", + Short: "Reconcile an OCIRepository", + Long: `The reconcile source command triggers a reconciliation of an OCIRepository resource and waits for it to finish.`, + Example: ` # Trigger a reconciliation for an existing source + flux reconcile source oci podinfo`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), + RunE: reconcileCommand{ + apiType: ociRepositoryType, + object: ociRepositoryAdapter{&sourcev1.OCIRepository{}}, + }.run, +} + +func init() { + reconcileSourceCmd.AddCommand(reconcileSourceOCIRepositoryCmd) +} + +func (obj ociRepositoryAdapter) lastHandledReconcileRequest() string { + return obj.Status.GetLastHandledReconcileRequest() +} + +func (obj ociRepositoryAdapter) successMessage() string { + return fmt.Sprintf("fetched revision %s", obj.Status.Artifact.Revision) +} + +func (obj ociRepositoryAdapter) isStatic() bool { + return false +} diff --git a/cmd/flux/reconcile_with_source.go b/cmd/flux/reconcile_with_source.go index 4eff1ee8..92d5b445 100644 --- a/cmd/flux/reconcile_with_source.go +++ b/cmd/flux/reconcile_with_source.go @@ -12,19 +12,24 @@ import ( "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) type reconcileWithSource interface { adapter reconcilable reconcileSource() bool - getSource() (reconcileCommand, types.NamespacedName) + getSource() (reconcileSource, types.NamespacedName) +} + +type reconcileSource interface { + run(cmd *cobra.Command, args []string) error } type reconcileWithSourceCommand struct { apiType object reconcileWithSource + force bool } func (reconcile reconcileWithSourceCommand) run(cmd *cobra.Command, args []string) error { @@ -36,7 +41,7 @@ func (reconcile reconcileWithSourceCommand) run(cmd *cobra.Command, args []strin ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -55,7 +60,7 @@ func (reconcile reconcileWithSourceCommand) run(cmd *cobra.Command, args []strin return fmt.Errorf("resource is suspended") } - if reconcile.object.reconcileSource() { + if reconcile.object.reconcileSource() || reconcile.force { reconcileCmd, nsName := reconcile.object.getSource() nsCopy := *kubeconfigArgs.Namespace if nsName.Namespace != "" { @@ -71,18 +76,19 @@ func (reconcile reconcileWithSourceCommand) run(cmd *cobra.Command, args []strin lastHandledReconcileAt := reconcile.object.lastHandledReconcileRequest() logger.Actionf("annotating %s %s in %s namespace", reconcile.kind, name, *kubeconfigArgs.Namespace) - if err := requestReconciliation(ctx, kubeClient, namespacedName, reconcile.object); err != nil { + if err := requestReconciliation(ctx, kubeClient, namespacedName, + reconcile.groupVersion.WithKind(reconcile.kind)); err != nil { return err } logger.Successf("%s annotated", reconcile.kind) logger.Waitingf("waiting for %s reconciliation", reconcile.kind) - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - reconciliationHandled(ctx, kubeClient, namespacedName, reconcile.object, lastHandledReconcileAt)); err != nil { + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, + reconciliationHandled(kubeClient, namespacedName, reconcile.object, lastHandledReconcileAt)); err != nil { return err } - readyCond := apimeta.FindStatusCondition(*reconcile.object.GetStatusConditions(), meta.ReadyCondition) + readyCond := apimeta.FindStatusCondition(reconcilableConditions(reconcile.object), meta.ReadyCondition) if readyCond == nil { return fmt.Errorf("status can't be determined") } diff --git a/cmd/flux/resume.go b/cmd/flux/resume.go index 163fbb90..5f3c8b81 100644 --- a/cmd/flux/resume.go +++ b/cmd/flux/resume.go @@ -19,23 +19,26 @@ package main import ( "context" "fmt" + "sort" + "sync" "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var resumeCmd = &cobra.Command{ Use: "resume", Short: "Resume suspended resources", - Long: "The resume sub-commands resume a suspended resource.", + Long: `The resume sub-commands resume a suspended resource.`, } type ResumeFlags struct { - all bool + all bool + wait bool } var resumeArgs ResumeFlags @@ -43,20 +46,26 @@ var resumeArgs ResumeFlags func init() { resumeCmd.PersistentFlags().BoolVarP(&resumeArgs.all, "all", "", false, "resume all resources in that namespace") + resumeCmd.PersistentFlags().BoolVarP(&resumeArgs.wait, "wait", "", false, + "waits for one resource to reconcile before moving to the next one") rootCmd.AddCommand(resumeCmd) } type resumable interface { adapter + copyable statusable setUnsuspended() + isStatic() bool successMessage() string } type resumeCommand struct { apiType - object resumable - list listResumable + client client.WithWatch + list listResumable + namespace string + shouldReconcile bool } type listResumable interface { @@ -64,6 +73,11 @@ type listResumable interface { resumeItem(i int) resumable } +type reconcileResponse struct { + resumable + err error +} + func (resume resumeCommand) run(cmd *cobra.Command, args []string) error { if len(args) < 1 && !resumeArgs.all { return fmt.Errorf("%s name is required", resume.humanKind) @@ -72,51 +86,170 @@ func (resume resumeCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } + resume.client = kubeClient + resume.namespace = *kubeconfigArgs.Namespace - var listOpts []client.ListOption - listOpts = append(listOpts, client.InNamespace(*kubeconfigArgs.Namespace)) - if len(args) > 0 { - listOpts = append(listOpts, client.MatchingFields{ - "metadata.name": args[0], - }) - } + // require waiting for the object(s) if the user has not provided the --wait flag and gave exactly + // one object to resume. This is necessary to maintain backwards compatibility with prior versions + // of this command. Otherwise just follow the value of the --wait flag (including its default). + resume.shouldReconcile = !resumeCmd.PersistentFlags().Changed("wait") && len(args) == 1 || resumeArgs.wait - err = kubeClient.List(ctx, resume.list.asClientList(), listOpts...) + resumables, err := resume.getPatchedResumables(ctx, args) if err != nil { return err } + var wg sync.WaitGroup + wg.Add(len(resumables)) + + resultChan := make(chan reconcileResponse, len(resumables)) + for _, r := range resumables { + go func(res resumable) { + defer wg.Done() + resultChan <- resume.reconcile(ctx, res) + }(r) + } + + go func() { + defer close(resultChan) + wg.Wait() + }() + + reconcileResps := make([]reconcileResponse, 0, len(resumables)) + for c := range resultChan { + reconcileResps = append(reconcileResps, c) + } + + resume.printMessage(reconcileResps) + + return nil +} + +// getPatchedResumables returns a list of the given resumable objects that have been patched to be resumed. +// If the args slice is empty, it patches all resumable objects in the given namespace. +func (resume *resumeCommand) getPatchedResumables(ctx context.Context, args []string) ([]resumable, error) { + if len(args) < 1 { + objs, err := resume.patch(ctx, []client.ListOption{ + client.InNamespace(resume.namespace), + }) + if err != nil { + return nil, fmt.Errorf("failed patching objects: %w", err) + } + + return objs, nil + } + + var resumables []resumable + processed := make(map[string]struct{}, len(args)) + for _, arg := range args { + if _, has := processed[arg]; has { + continue // skip object that user might have provided more than once + } + processed[arg] = struct{}{} + + objs, err := resume.patch(ctx, []client.ListOption{ + client.InNamespace(resume.namespace), + client.MatchingFields{ + "metadata.name": arg, + }, + }) + if err != nil { + return nil, err + } + + resumables = append(resumables, objs...) + } + + return resumables, nil +} + +// Patches resumable objects by setting their status to unsuspended. +// Returns a slice of resumables that have been patched and any error encountered during patching. +func (resume resumeCommand) patch(ctx context.Context, listOpts []client.ListOption) ([]resumable, error) { + if err := resume.client.List(ctx, resume.list.asClientList(), listOpts...); err != nil { + return nil, err + } + if resume.list.len() == 0 { - logger.Failuref("no %s objects found in %s namespace", resume.kind, *kubeconfigArgs.Namespace) - return nil + logger.Failuref("no %s objects found in %s namespace", resume.kind, resume.namespace) + return nil, nil } + var resumables []resumable + for i := 0; i < resume.list.len(); i++ { - logger.Actionf("resuming %s %s in %s namespace", resume.humanKind, resume.list.resumeItem(i).asClientObject().GetName(), *kubeconfigArgs.Namespace) - resume.list.resumeItem(i).setUnsuspended() - if err := kubeClient.Update(ctx, resume.list.resumeItem(i).asClientObject()); err != nil { - return err + obj := resume.list.resumeItem(i) + logger.Actionf("resuming %s %s in %s namespace", resume.humanKind, obj.asClientObject().GetName(), resume.namespace) + + patch := client.MergeFrom(obj.deepCopyClientObject()) + obj.setUnsuspended() + if err := resume.client.Patch(ctx, obj.asClientObject(), patch); err != nil { + return nil, err } + + resumables = append(resumables, obj) + logger.Successf("%s resumed", resume.humanKind) + } + + return resumables, nil +} + +// Waits for resumable object to be reconciled and returns the object and any error encountered while waiting. +// Returns an empty reconcileResponse, if shouldReconcile is false. +func (resume resumeCommand) reconcile(ctx context.Context, res resumable) reconcileResponse { + if !resume.shouldReconcile { + return reconcileResponse{} + } + + namespacedName := types.NamespacedName{ + Name: res.asClientObject().GetName(), + Namespace: resume.namespace, + } + + logger.Waitingf("waiting for %s reconciliation", resume.kind) + + readyConditionFunc := isObjectReadyConditionFunc(resume.client, namespacedName, res.asClientObject()) + if res.isStatic() { + readyConditionFunc = isStaticObjectReadyConditionFunc(resume.client, namespacedName, res.asClientObject()) + } - namespacedName := types.NamespacedName{ - Name: resume.list.resumeItem(i).asClientObject().GetName(), - Namespace: *kubeconfigArgs.Namespace, + if err := wait.PollUntilContextTimeout(ctx, rootArgs.pollInterval, rootArgs.timeout, true, readyConditionFunc); err != nil { + return reconcileResponse{ + resumable: res, + err: err, } + } - logger.Waitingf("waiting for %s reconciliation", resume.kind) - if err := wait.PollImmediate(rootArgs.pollInterval, rootArgs.timeout, - isReady(ctx, kubeClient, namespacedName, resume.list.resumeItem(i))); err != nil { - logger.Failuref(err.Error()) + return reconcileResponse{ + resumable: res, + err: nil, + } +} + +// Sorts the given reconcileResponses by resumable name and prints the success/error message for each response. +func (resume resumeCommand) printMessage(responses []reconcileResponse) { + sort.Slice(responses, func(i, j int) bool { + r1, r2 := responses[i], responses[j] + if r1.resumable == nil || r2.resumable == nil { + return false + } + return r1.asClientObject().GetName() <= r2.asClientObject().GetName() + }) + + // Print success/error message. + for _, r := range responses { + if r.resumable == nil { continue } - logger.Successf("%s reconciliation completed", resume.kind) - logger.Successf(resume.list.resumeItem(i).successMessage()) + if r.err != nil { + logger.Failuref(r.err.Error()) + } + logger.Successf("%s %s reconciliation completed", resume.kind, r.asClientObject().GetName()) + logger.Successf(r.successMessage()) } - - return nil } diff --git a/cmd/flux/resume_alert.go b/cmd/flux/resume_alert.go index bede1e3e..a33d4f5b 100644 --- a/cmd/flux/resume_alert.go +++ b/cmd/flux/resume_alert.go @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var resumeAlertCmd = &cobra.Command{ @@ -28,11 +28,13 @@ var resumeAlertCmd = &cobra.Command{ Long: `The resume command marks a previously suspended Alert resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Alert - flux resume alert main`, + flux resume alert main + + # Resume reconciliation for multiple Alerts + flux resume alert main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), RunE: resumeCommand{ apiType: alertType, - object: alertAdapter{¬ificationv1.Alert{}}, list: &alertListAdapter{¬ificationv1.AlertList{}}, }.run, } @@ -42,7 +44,7 @@ func init() { } func (obj alertAdapter) getObservedGeneration() int64 { - return obj.Alert.Status.ObservedGeneration + return 0 } func (obj alertAdapter) setUnsuspended() { @@ -53,6 +55,10 @@ func (obj alertAdapter) successMessage() string { return "Alert reconciliation completed" } +func (a alertAdapter) isStatic() bool { + return true +} + func (a alertListAdapter) resumeItem(i int) resumable { return &alertAdapter{&a.AlertList.Items[i]} } diff --git a/cmd/flux/resume_alertprovider.go b/cmd/flux/resume_alertprovider.go new file mode 100644 index 00000000..de95d48d --- /dev/null +++ b/cmd/flux/resume_alertprovider.go @@ -0,0 +1,64 @@ +/* +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 ( + "github.com/spf13/cobra" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" +) + +var resumeAlertProviderCmd = &cobra.Command{ + Use: "alert-provider [name]", + Short: "Resume a suspended Provider", + Long: `The resume command marks a previously suspended Provider resource for reconciliation and waits for it to +finish the apply.`, + Example: ` # Resume reconciliation for an existing Provider + flux resume alert-provider main + + # Resume reconciliation for multiple Providers + flux resume alert-provider main-1 main-2`, + ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), + RunE: resumeCommand{ + apiType: alertProviderType, + list: &alertProviderListAdapter{¬ificationv1.ProviderList{}}, + }.run, +} + +func init() { + resumeCmd.AddCommand(resumeAlertProviderCmd) +} + +func (obj alertProviderAdapter) getObservedGeneration() int64 { + return 0 +} + +func (obj alertProviderAdapter) setUnsuspended() { + obj.Provider.Spec.Suspend = false +} + +func (obj alertProviderAdapter) successMessage() string { + return "Provider reconciliation completed" +} + +func (a alertProviderAdapter) isStatic() bool { + return true +} + +func (a alertProviderListAdapter) resumeItem(i int) resumable { + return &alertProviderAdapter{&a.ProviderList.Items[i]} +} diff --git a/cmd/flux/resume_helmrelease.go b/cmd/flux/resume_helmrelease.go index 0de4fb90..6c320b30 100644 --- a/cmd/flux/resume_helmrelease.go +++ b/cmd/flux/resume_helmrelease.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +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. @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var resumeHrCmd = &cobra.Command{ @@ -31,11 +31,13 @@ var resumeHrCmd = &cobra.Command{ Long: `The resume command marks a previously suspended HelmRelease resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Helm release - flux resume hr podinfo`, + flux resume hr podinfo + + # Resume reconciliation for multiple Helm releases + flux resume hr podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: resumeCommand{ apiType: helmReleaseType, - object: helmReleaseAdapter{&helmv2.HelmRelease{}}, list: helmReleaseListAdapter{&helmv2.HelmReleaseList{}}, }.run, } @@ -53,7 +55,7 @@ func (obj helmReleaseAdapter) setUnsuspended() { } func (obj helmReleaseAdapter) successMessage() string { - return fmt.Sprintf("applied revision %s", obj.Status.LastAppliedRevision) + return fmt.Sprintf("applied revision %s", getHelmReleaseRevision(*obj.HelmRelease)) } func (a helmReleaseListAdapter) resumeItem(i int) resumable { diff --git a/cmd/flux/resume_image.go b/cmd/flux/resume_image.go index ef6cf54b..529458d0 100644 --- a/cmd/flux/resume_image.go +++ b/cmd/flux/resume_image.go @@ -23,7 +23,7 @@ import ( var resumeImageCmd = &cobra.Command{ Use: "image", Short: "Resume image automation objects", - Long: "The resume image sub-commands resume suspended image automation objects.", + Long: `The resume image sub-commands resume suspended image automation objects.`, } func init() { diff --git a/cmd/flux/resume_image_repository.go b/cmd/flux/resume_image_repository.go index b0c15b18..a9ab36cd 100644 --- a/cmd/flux/resume_image_repository.go +++ b/cmd/flux/resume_image_repository.go @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var resumeImageRepositoryCmd = &cobra.Command{ @@ -27,11 +27,13 @@ var resumeImageRepositoryCmd = &cobra.Command{ Short: "Resume a suspended ImageRepository", Long: `The resume command marks a previously suspended ImageRepository resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing ImageRepository - flux resume image repository alpine`, + flux resume image repository alpine + + # Resume reconciliation for multiple ImageRepositories + flux resume image repository alpine-1 alpine-2`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: resumeCommand{ apiType: imageRepositoryType, - object: imageRepositoryAdapter{&imagev1.ImageRepository{}}, list: imageRepositoryListAdapter{&imagev1.ImageRepositoryList{}}, }.run, } diff --git a/cmd/flux/resume_image_updateauto.go b/cmd/flux/resume_image_updateauto.go index 8cc40bfc..1b29cbaf 100644 --- a/cmd/flux/resume_image_updateauto.go +++ b/cmd/flux/resume_image_updateauto.go @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" ) var resumeImageUpdateCmd = &cobra.Command{ @@ -27,11 +27,13 @@ var resumeImageUpdateCmd = &cobra.Command{ Short: "Resume a suspended ImageUpdateAutomation", Long: `The resume command marks a previously suspended ImageUpdateAutomation resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing ImageUpdateAutomation - flux resume image update latest-images`, + flux resume image update latest-images + + # Resume reconciliation for multiple ImageUpdateAutomations + flux resume image update latest-images-1 latest-images-2`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), RunE: resumeCommand{ apiType: imageUpdateAutomationType, - object: imageUpdateAutomationAdapter{&autov1.ImageUpdateAutomation{}}, list: imageUpdateAutomationListAdapter{&autov1.ImageUpdateAutomationList{}}, }.run, } diff --git a/cmd/flux/resume_kustomization.go b/cmd/flux/resume_kustomization.go index b1ac2072..a03aed62 100644 --- a/cmd/flux/resume_kustomization.go +++ b/cmd/flux/resume_kustomization.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var resumeKsCmd = &cobra.Command{ @@ -31,11 +31,13 @@ var resumeKsCmd = &cobra.Command{ Long: `The resume command marks a previously suspended Kustomization resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Kustomization - flux resume ks podinfo`, + flux resume ks podinfo + + # Resume reconciliation for multiple Kustomizations + flux resume ks podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: resumeCommand{ apiType: kustomizationType, - object: kustomizationAdapter{&kustomizev1.Kustomization{}}, list: kustomizationListAdapter{&kustomizev1.KustomizationList{}}, }.run, } diff --git a/cmd/flux/resume_receiver.go b/cmd/flux/resume_receiver.go index d4d3c64b..c99bd755 100644 --- a/cmd/flux/resume_receiver.go +++ b/cmd/flux/resume_receiver.go @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var resumeReceiverCmd = &cobra.Command{ @@ -28,11 +28,13 @@ var resumeReceiverCmd = &cobra.Command{ Long: `The resume command marks a previously suspended Receiver resource for reconciliation and waits for it to finish the apply.`, Example: ` # Resume reconciliation for an existing Receiver - flux resume receiver main`, + flux resume receiver main + + # Resume reconciliation for multiple Receivers + flux resume receiver main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), RunE: resumeCommand{ apiType: receiverType, - object: receiverAdapter{¬ificationv1.Receiver{}}, list: receiverListAdapter{¬ificationv1.ReceiverList{}}, }.run, } diff --git a/cmd/flux/resume_source.go b/cmd/flux/resume_source.go index dae04830..38bff6a2 100644 --- a/cmd/flux/resume_source.go +++ b/cmd/flux/resume_source.go @@ -23,7 +23,7 @@ import ( var resumeSourceCmd = &cobra.Command{ Use: "source", Short: "Resume sources", - Long: "The resume sub-commands resume a suspended source.", + Long: `The resume sub-commands resume a suspended source.`, } func init() { diff --git a/cmd/flux/resume_source_bucket.go b/cmd/flux/resume_source_bucket.go index c16d2090..ea1fe37d 100644 --- a/cmd/flux/resume_source_bucket.go +++ b/cmd/flux/resume_source_bucket.go @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" ) var resumeSourceBucketCmd = &cobra.Command{ @@ -27,11 +27,14 @@ var resumeSourceBucketCmd = &cobra.Command{ Short: "Resume a suspended Bucket", Long: `The resume command marks a previously suspended Bucket resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing Bucket - flux resume source bucket podinfo`, + flux resume source bucket podinfo + + # Resume reconciliation for multiple Buckets + flux resume source bucket podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), RunE: resumeCommand{ apiType: bucketType, - object: &bucketAdapter{&sourcev1.Bucket{}}, + list: bucketListAdapter{&sourcev1.BucketList{}}, }.run, } diff --git a/cmd/flux/resume_source_chart.go b/cmd/flux/resume_source_chart.go index 6da61eed..9e7ec759 100644 --- a/cmd/flux/resume_source_chart.go +++ b/cmd/flux/resume_source_chart.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var resumeSourceHelmChartCmd = &cobra.Command{ @@ -29,11 +29,13 @@ var resumeSourceHelmChartCmd = &cobra.Command{ Short: "Resume a suspended HelmChart", Long: `The resume command marks a previously suspended HelmChart resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing HelmChart - flux resume source chart podinfo`, + flux resume source chart podinfo + + # Resume reconciliation for multiple HelmCharts + flux resume source chart podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), RunE: resumeCommand{ apiType: helmChartType, - object: &helmChartAdapter{&sourcev1.HelmChart{}}, list: &helmChartListAdapter{&sourcev1.HelmChartList{}}, }.run, } diff --git a/cmd/flux/resume_source_git.go b/cmd/flux/resume_source_git.go index 31784281..751714a4 100644 --- a/cmd/flux/resume_source_git.go +++ b/cmd/flux/resume_source_git.go @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var resumeSourceGitCmd = &cobra.Command{ @@ -27,11 +27,13 @@ var resumeSourceGitCmd = &cobra.Command{ Short: "Resume a suspended GitRepository", Long: `The resume command marks a previously suspended GitRepository resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing GitRepository - flux resume source git podinfo`, + flux resume source git podinfo + + # Resume reconciliation for multiple GitRepositories + flux resume source git podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), RunE: resumeCommand{ apiType: gitRepositoryType, - object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, list: gitRepositoryListAdapter{&sourcev1.GitRepositoryList{}}, }.run, } diff --git a/cmd/flux/resume_source_helm.go b/cmd/flux/resume_source_helm.go index 674a81b1..b4cb1316 100644 --- a/cmd/flux/resume_source_helm.go +++ b/cmd/flux/resume_source_helm.go @@ -19,7 +19,7 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var resumeSourceHelmCmd = &cobra.Command{ @@ -27,11 +27,13 @@ var resumeSourceHelmCmd = &cobra.Command{ Short: "Resume a suspended HelmRepository", Long: `The resume command marks a previously suspended HelmRepository resource for reconciliation and waits for it to finish.`, Example: ` # Resume reconciliation for an existing HelmRepository - flux resume source helm bitnami`, + flux resume source helm bitnami + + # Resume reconciliation for multiple HelmRepositories + flux resume source helm bitnami-1 bitnami-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), RunE: resumeCommand{ apiType: helmRepositoryType, - object: helmRepositoryAdapter{&sourcev1.HelmRepository{}}, list: helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{}}, }.run, } diff --git a/cmd/flux/resume_source_oci.go b/cmd/flux/resume_source_oci.go new file mode 100644 index 00000000..04b20a4b --- /dev/null +++ b/cmd/flux/resume_source_oci.go @@ -0,0 +1,55 @@ +/* +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 ( + "github.com/spf13/cobra" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" +) + +var resumeSourceOCIRepositoryCmd = &cobra.Command{ + Use: "oci [name]", + Short: "Resume a suspended OCIRepository", + Long: `The resume command marks a previously suspended OCIRepository resource for reconciliation and waits for it to finish.`, + Example: ` # Resume reconciliation for an existing OCIRepository + flux resume source oci podinfo + + # Resume reconciliation for multiple OCIRepositories + flux resume source oci podinfo-1 podinfo-2`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), + RunE: resumeCommand{ + apiType: ociRepositoryType, + list: ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, + }.run, +} + +func init() { + resumeSourceCmd.AddCommand(resumeSourceOCIRepositoryCmd) +} + +func (obj ociRepositoryAdapter) getObservedGeneration() int64 { + return obj.OCIRepository.Status.ObservedGeneration +} + +func (obj ociRepositoryAdapter) setUnsuspended() { + obj.OCIRepository.Spec.Suspend = false +} + +func (a ociRepositoryListAdapter) resumeItem(i int) resumable { + return &ociRepositoryAdapter{&a.OCIRepositoryList.Items[i]} +} diff --git a/cmd/flux/source.go b/cmd/flux/source.go index 29dce7d5..64f755be 100644 --- a/cmd/flux/source.go +++ b/cmd/flux/source.go @@ -19,22 +19,58 @@ package main import ( "sigs.k8s.io/controller-runtime/pkg/client" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" ) // These are general-purpose adapters for attaching methods to, for // the various commands. The *List adapters implement len(), since // it's used in at least a couple of commands. -// sourcev1.Bucket +// sourcev1.ociRepository + +var ociRepositoryType = apiType{ + kind: sourcev1b2.OCIRepositoryKind, + humanKind: "source oci", + groupVersion: sourcev1b2.GroupVersion, +} + +type ociRepositoryAdapter struct { + *sourcev1b2.OCIRepository +} + +func (a ociRepositoryAdapter) asClientObject() client.Object { + return a.OCIRepository +} + +func (a ociRepositoryAdapter) deepCopyClientObject() client.Object { + return a.OCIRepository.DeepCopy() +} + +// sourcev1b2.OCIRepositoryList + +type ociRepositoryListAdapter struct { + *sourcev1b2.OCIRepositoryList +} + +func (a ociRepositoryListAdapter) asClientList() client.ObjectList { + return a.OCIRepositoryList +} + +func (a ociRepositoryListAdapter) len() int { + return len(a.OCIRepositoryList.Items) +} + +// sourcev1b2.Bucket var bucketType = apiType{ - kind: sourcev1.BucketKind, - humanKind: "source bucket", + kind: sourcev1b2.BucketKind, + humanKind: "source bucket", + groupVersion: sourcev1b2.GroupVersion, } type bucketAdapter struct { - *sourcev1.Bucket + *sourcev1b2.Bucket } func (a bucketAdapter) asClientObject() client.Object { @@ -45,10 +81,10 @@ func (a bucketAdapter) deepCopyClientObject() client.Object { return a.Bucket.DeepCopy() } -// sourcev1.BucketList +// sourcev1b2.BucketList type bucketListAdapter struct { - *sourcev1.BucketList + *sourcev1b2.BucketList } func (a bucketListAdapter) asClientList() client.ObjectList { @@ -62,8 +98,9 @@ func (a bucketListAdapter) len() int { // sourcev1.HelmChart var helmChartType = apiType{ - kind: sourcev1.HelmChartKind, - humanKind: "source chart", + kind: sourcev1.HelmChartKind, + humanKind: "source chart", + groupVersion: sourcev1.GroupVersion, } type helmChartAdapter struct { @@ -95,8 +132,9 @@ func (a helmChartListAdapter) len() int { // sourcev1.GitRepository var gitRepositoryType = apiType{ - kind: sourcev1.GitRepositoryKind, - humanKind: "source git", + kind: sourcev1.GitRepositoryKind, + humanKind: "source git", + groupVersion: sourcev1.GroupVersion, } type gitRepositoryAdapter struct { @@ -128,8 +166,9 @@ func (a gitRepositoryListAdapter) len() int { // sourcev1.HelmRepository var helmRepositoryType = apiType{ - kind: sourcev1.HelmRepositoryKind, - humanKind: "source helm", + kind: sourcev1.HelmRepositoryKind, + humanKind: "source helm", + groupVersion: sourcev1.GroupVersion, } type helmRepositoryAdapter struct { diff --git a/cmd/flux/source_oci_test.go b/cmd/flux/source_oci_test.go new file mode 100644 index 00000000..8f8fa47c --- /dev/null +++ b/cmd/flux/source_oci_test.go @@ -0,0 +1,80 @@ +//go:build e2e +// +build e2e + +/* +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 TestSourceOCI(t *testing.T) { + namespace := allocateNamespace("oci-test") + del, err := execSetupTestNamespace(namespace) + if err != nil { + t.Fatal(err) + } + t.Cleanup(del) + + tmpl := map[string]string{"ns": namespace} + + cases := []struct { + args string + goldenFile string + tmpl map[string]string + }{ + { + "create source oci thrfg --url=oci://ghcr.io/stefanprodan/manifests/podinfo --tag=6.3.5 --interval 10m", + "testdata/oci/create_source_oci.golden", + nil, + }, + { + "get source oci thrfg", + "testdata/oci/get_oci.golden", + nil, + }, + { + "reconcile source oci thrfg", + "testdata/oci/reconcile_oci.golden", + tmpl, + }, + { + "suspend source oci thrfg", + "testdata/oci/suspend_oci.golden", + tmpl, + }, + { + "resume source oci thrfg", + "testdata/oci/resume_oci.golden", + tmpl, + }, + { + "delete source oci thrfg --silent", + "testdata/oci/delete_oci.golden", + tmpl, + }, + } + + for _, tc := range cases { + cmd := cmdTestCase{ + args: tc.args + " -n=" + namespace, + assert: assertGoldenTemplateFile(tc.goldenFile, tc.tmpl), + } + cmd.runTestCmd(t) + } +} diff --git a/cmd/flux/stats.go b/cmd/flux/stats.go new file mode 100644 index 00000000..08ef3294 --- /dev/null +++ b/cmd/flux/stats.go @@ -0,0 +1,225 @@ +/* +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/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/fluxcd/cli-utils/pkg/kstatus/status" + helmv2 "github.com/fluxcd/helm-controller/api/v2" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/printers" +) + +var statsCmd = &cobra.Command{ + Use: "stats", + Args: cobra.NoArgs, + Short: "Stats of Flux reconciles", + Long: withPreviewNote(`The stats command prints a report of Flux custom resources present on a cluster, +including their reconcile status and the amount of cumulative storage used for each source type`), + Example: ` # Print the stats report for a namespace + flux stats --namespace default + + # Print the stats report for the whole cluster + flux stats -A`, + RunE: runStatsCmd, +} + +type StatsFlags struct { + allNamespaces bool +} + +var statsArgs StatsFlags + +func init() { + statsCmd.PersistentFlags().BoolVarP(&statsArgs.allNamespaces, "all-namespaces", "A", false, + "list the statistics for objects across all namespaces") + rootCmd.AddCommand(statsCmd) +} + +func runStatsCmd(cmd *cobra.Command, args []string) error { + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) + if err != nil { + return err + } + + types := []metav1.GroupVersionKind{ + { + Kind: sourcev1.GitRepositoryKind, + Version: sourcev1.GroupVersion.Version, + Group: sourcev1.GroupVersion.Group, + }, + { + Kind: sourcev1b2.OCIRepositoryKind, + Version: sourcev1b2.GroupVersion.Version, + Group: sourcev1b2.GroupVersion.Group, + }, + { + Kind: sourcev1.HelmRepositoryKind, + Version: sourcev1.GroupVersion.Version, + Group: sourcev1.GroupVersion.Group, + }, + { + Kind: sourcev1.HelmChartKind, + Version: sourcev1.GroupVersion.Version, + Group: sourcev1.GroupVersion.Group, + }, + { + Kind: sourcev1b2.BucketKind, + Version: sourcev1b2.GroupVersion.Version, + Group: sourcev1b2.GroupVersion.Group, + }, + { + Kind: kustomizev1.KustomizationKind, + Version: kustomizev1.GroupVersion.Version, + Group: kustomizev1.GroupVersion.Group, + }, + { + Kind: helmv2.HelmReleaseKind, + Version: helmv2.GroupVersion.Version, + Group: helmv2.GroupVersion.Group, + }, + { + Kind: notificationv1b3.AlertKind, + Version: notificationv1b3.GroupVersion.Version, + Group: notificationv1b3.GroupVersion.Group, + }, + { + Kind: notificationv1b3.ProviderKind, + Version: notificationv1b3.GroupVersion.Version, + Group: notificationv1b3.GroupVersion.Group, + }, + { + Kind: notificationv1.ReceiverKind, + Version: notificationv1.GroupVersion.Version, + Group: notificationv1.GroupVersion.Group, + }, + { + Kind: autov1.ImageUpdateAutomationKind, + Version: autov1.GroupVersion.Version, + Group: autov1.GroupVersion.Group, + }, + { + Kind: imagev1.ImagePolicyKind, + Version: imagev1.GroupVersion.Version, + Group: imagev1.GroupVersion.Group, + }, + { + Kind: imagev1.ImageRepositoryKind, + Version: imagev1.GroupVersion.Version, + Group: imagev1.GroupVersion.Group, + }, + } + + header := []string{"Reconcilers", "Running", "Failing", "Suspended", "Storage"} + var rows [][]string + + for _, t := range types { + var total int + var suspended int + var failing int + var totalSize int64 + + list := unstructured.UnstructuredList{ + Object: map[string]interface{}{ + "apiVersion": t.Group + "/" + t.Version, + "kind": t.Kind, + }, + } + + scope := client.InNamespace("") + if !statsArgs.allNamespaces { + scope = client.InNamespace(*kubeconfigArgs.Namespace) + } + + if err := kubeClient.List(ctx, &list, scope); err == nil { + total = len(list.Items) + + for _, item := range list.Items { + if s, _, _ := unstructured.NestedBool(item.Object, "spec", "suspend"); s { + suspended++ + } + + if obj, err := status.GetObjectWithConditions(item.Object); err == nil { + for _, cond := range obj.Status.Conditions { + if cond.Type == "Ready" && cond.Status == corev1.ConditionFalse { + failing++ + } + } + } + + if size, found, _ := unstructured.NestedInt64(item.Object, "status", "artifact", "size"); found { + totalSize += size + } + } + } + + rows = append(rows, []string{ + t.Kind, + formatInt(total - suspended), + formatInt(failing), + formatInt(suspended), + formatSize(totalSize), + }) + } + + err = printers.TablePrinter(header).Print(cmd.OutOrStdout(), rows) + if err != nil { + return err + } + + return nil +} + +func formatInt(i int) string { + return fmt.Sprintf("%d", i) +} + +func formatSize(b int64) string { + if b == 0 { + return "-" + } + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", + float64(b)/float64(div), "KMGTPE"[exp]) +} diff --git a/cmd/flux/status.go b/cmd/flux/status.go index 3c7bb253..95c6e9f3 100644 --- a/cmd/flux/status.go +++ b/cmd/flux/status.go @@ -17,18 +17,9 @@ limitations under the License. package main import ( - "context" - "fmt" - - apimeta "k8s.io/apimachinery/pkg/api/meta" + "github.com/fluxcd/cli-utils/pkg/object" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/wait" - "sigs.k8s.io/cli-utils/pkg/object" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/fluxcd/pkg/apis/meta" ) // statusable is used to see if a resource is considered ready in the usual way @@ -37,33 +28,12 @@ type statusable interface { // this is implemented by ObjectMeta GetGeneration() int64 getObservedGeneration() int64 - // this is usually implemented by GOTK API objects because it's used by pkg/apis/meta - GetStatusConditions() *[]metav1.Condition } -func isReady(ctx context.Context, kubeClient client.Client, - namespacedName types.NamespacedName, object statusable) wait.ConditionFunc { - return func() (bool, error) { - err := kubeClient.Get(ctx, namespacedName, object.asClientObject()) - if err != nil { - return false, err - } - - // Confirm the state we are observing is for the current generation - if object.GetGeneration() != object.getObservedGeneration() { - return false, nil - } - - if c := apimeta.FindStatusCondition(*object.GetStatusConditions(), 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 - } +// oldConditions represents the deprecated API which is sunsetting. +type oldConditions interface { + // this is usually implemented by GOTK API objects because it's used by pkg/apis/meta + GetStatusConditions() *[]metav1.Condition } func buildComponentObjectRefs(components ...string) ([]object.ObjMetadata, error) { diff --git a/cmd/flux/suspend.go b/cmd/flux/suspend.go index 02a44bd1..e0023c7e 100644 --- a/cmd/flux/suspend.go +++ b/cmd/flux/suspend.go @@ -18,18 +18,19 @@ package main import ( "context" + "errors" "fmt" "github.com/spf13/cobra" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var suspendCmd = &cobra.Command{ Use: "suspend", Short: "Suspend resources", - Long: "The suspend sub-commands suspend the reconciliation of a resource.", + Long: `The suspend sub-commands suspend the reconciliation of a resource.`, } type SuspendFlags struct { @@ -46,6 +47,7 @@ func init() { type suspendable interface { adapter + copyable isSuspended() bool setSuspended() } @@ -69,37 +71,71 @@ func (suspend suspendCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } - var listOpts []client.ListOption - listOpts = append(listOpts, client.InNamespace(*kubeconfigArgs.Namespace)) - if len(args) > 0 { - listOpts = append(listOpts, client.MatchingFields{ - "metadata.name": args[0], - }) + if len(args) < 1 && suspendArgs.all { + listOpts := []client.ListOption{ + client.InNamespace(*kubeconfigArgs.Namespace), + } + + if err := suspend.patch(ctx, kubeClient, listOpts); err != nil { + return err + } + + return nil } - err = kubeClient.List(ctx, suspend.list.asClientList(), listOpts...) - if err != nil { + processed := make(map[string]struct{}, len(args)) + for _, arg := range args { + if _, has := processed[arg]; has { + continue // skip object that user might have provided more than once + } + processed[arg] = struct{}{} + + listOpts := []client.ListOption{ + client.InNamespace(*kubeconfigArgs.Namespace), + client.MatchingFields{ + "metadata.name": arg, + }, + } + + if err := suspend.patch(ctx, kubeClient, listOpts); err != nil { + if err == ErrNoObjectsFound { + logger.Failuref("%s %s not found in %s namespace", suspend.kind, arg, *kubeconfigArgs.Namespace) + } else { + logger.Failuref("failed suspending %s %s in %s namespace: %s", suspend.kind, arg, *kubeconfigArgs.Namespace, err.Error()) + } + } + } + + return nil +} + +var ErrNoObjectsFound = errors.New("no objects found") + +func (suspend suspendCommand) patch(ctx context.Context, kubeClient client.WithWatch, listOpts []client.ListOption) error { + if err := kubeClient.List(ctx, suspend.list.asClientList(), listOpts...); err != nil { return err } if suspend.list.len() == 0 { - logger.Failuref("no %s objects found in %s namespace", suspend.kind, *kubeconfigArgs.Namespace) - return nil + return ErrNoObjectsFound } for i := 0; i < suspend.list.len(); i++ { logger.Actionf("suspending %s %s in %s namespace", suspend.humanKind, suspend.list.item(i).asClientObject().GetName(), *kubeconfigArgs.Namespace) - suspend.list.item(i).setSuspended() - if err := kubeClient.Update(ctx, suspend.list.item(i).asClientObject()); err != nil { + + obj := suspend.list.item(i) + patch := client.MergeFrom(obj.deepCopyClientObject()) + obj.setSuspended() + if err := kubeClient.Patch(ctx, obj.asClientObject(), patch); err != nil { return err } - logger.Successf("%s suspended", suspend.humanKind) + logger.Successf("%s suspended", suspend.humanKind) } return nil diff --git a/cmd/flux/suspend_alert.go b/cmd/flux/suspend_alert.go index 42ee60df..67fa201a 100644 --- a/cmd/flux/suspend_alert.go +++ b/cmd/flux/suspend_alert.go @@ -19,15 +19,18 @@ package main import ( "github.com/spf13/cobra" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" ) var suspendAlertCmd = &cobra.Command{ Use: "alert [name]", Short: "Suspend reconciliation of Alert", - Long: "The suspend command disables the reconciliation of a Alert resource.", + Long: `The suspend command disables the reconciliation of a Alert resource.`, Example: ` # Suspend reconciliation for an existing Alert - flux suspend alert main`, + flux suspend alert main + + # Suspend reconciliation for multiple Alerts + flux suspend alert main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.AlertKind)), RunE: suspendCommand{ apiType: alertType, diff --git a/cmd/flux/suspend_alertprovider.go b/cmd/flux/suspend_alertprovider.go new file mode 100644 index 00000000..fa58c80e --- /dev/null +++ b/cmd/flux/suspend_alertprovider.go @@ -0,0 +1,56 @@ +/* +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 ( + "github.com/spf13/cobra" + + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta3" +) + +var suspendAlertProviderCmd = &cobra.Command{ + Use: "alert-provider [name]", + Short: "Suspend reconciliation of Provider", + Long: `The suspend command disables the reconciliation of a Provider resource.`, + Example: ` # Suspend reconciliation for an existing Provider + flux suspend alert-provider main + + # Suspend reconciliation for multiple Providers + flux suspend alert-providers main-1 main-2`, + ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ProviderKind)), + RunE: suspendCommand{ + apiType: alertProviderType, + object: &alertProviderAdapter{¬ificationv1.Provider{}}, + list: &alertProviderListAdapter{¬ificationv1.ProviderList{}}, + }.run, +} + +func init() { + suspendCmd.AddCommand(suspendAlertProviderCmd) +} + +func (obj alertProviderAdapter) isSuspended() bool { + return obj.Provider.Spec.Suspend +} + +func (obj alertProviderAdapter) setSuspended() { + obj.Provider.Spec.Suspend = true +} + +func (a alertProviderListAdapter) item(i int) suspendable { + return &alertProviderAdapter{&a.ProviderList.Items[i]} +} diff --git a/cmd/flux/suspend_helmrelease.go b/cmd/flux/suspend_helmrelease.go index 5e552278..762e70eb 100644 --- a/cmd/flux/suspend_helmrelease.go +++ b/cmd/flux/suspend_helmrelease.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +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. @@ -19,16 +19,19 @@ package main import ( "github.com/spf13/cobra" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" ) var suspendHrCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Suspend reconciliation of HelmRelease", - Long: "The suspend command disables the reconciliation of a HelmRelease resource.", + Long: `The suspend command disables the reconciliation of a HelmRelease resource.`, Example: ` # Suspend reconciliation for an existing Helm release - flux suspend hr podinfo`, + flux suspend hr podinfo + + # Suspend reconciliation for multiple Helm releases + flux suspend hr podinfo-1 podinfo-2 `, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), RunE: suspendCommand{ apiType: helmReleaseType, diff --git a/cmd/flux/suspend_image.go b/cmd/flux/suspend_image.go index 468c8c3f..3a820ec7 100644 --- a/cmd/flux/suspend_image.go +++ b/cmd/flux/suspend_image.go @@ -23,7 +23,7 @@ import ( var suspendImageCmd = &cobra.Command{ Use: "image", Short: "Suspend image automation objects", - Long: "The suspend image sub-commands suspend the reconciliation of an image automation object.", + Long: `The suspend image sub-commands suspend the reconciliation of an image automation object.`, } func init() { diff --git a/cmd/flux/suspend_image_repository.go b/cmd/flux/suspend_image_repository.go index 826c4de4..c6e562d1 100644 --- a/cmd/flux/suspend_image_repository.go +++ b/cmd/flux/suspend_image_repository.go @@ -19,15 +19,18 @@ package main import ( "github.com/spf13/cobra" - imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" ) var suspendImageRepositoryCmd = &cobra.Command{ Use: "repository [name]", Short: "Suspend reconciliation of an ImageRepository", - Long: "The suspend image repository command disables the reconciliation of a ImageRepository resource.", + Long: `The suspend image repository command disables the reconciliation of a ImageRepository resource.`, Example: ` # Suspend reconciliation for an existing ImageRepository - flux suspend image repository alpine`, + flux suspend image repository alpine + + # Suspend reconciliation for multiple ImageRepositories + flux suspend image repository alpine-1 alpine-2`, ValidArgsFunction: resourceNamesCompletionFunc(imagev1.GroupVersion.WithKind(imagev1.ImageRepositoryKind)), RunE: suspendCommand{ apiType: imageRepositoryType, diff --git a/cmd/flux/suspend_image_updateauto.go b/cmd/flux/suspend_image_updateauto.go index cd6e8f3a..a1035117 100644 --- a/cmd/flux/suspend_image_updateauto.go +++ b/cmd/flux/suspend_image_updateauto.go @@ -19,15 +19,18 @@ package main import ( "github.com/spf13/cobra" - autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" ) var suspendImageUpdateCmd = &cobra.Command{ Use: "update [name]", Short: "Suspend reconciliation of an ImageUpdateAutomation", - Long: "The suspend image update command disables the reconciliation of a ImageUpdateAutomation resource.", + Long: `The suspend image update command disables the reconciliation of a ImageUpdateAutomation resource.`, Example: ` # Suspend reconciliation for an existing ImageUpdateAutomation - flux suspend image update latest-images`, + flux suspend image update latest-images + + # Suspend reconciliation for multiple ImageUpdateAutomations + flux suspend image update latest-images-1 latest-images-2`, ValidArgsFunction: resourceNamesCompletionFunc(autov1.GroupVersion.WithKind(autov1.ImageUpdateAutomationKind)), RunE: suspendCommand{ apiType: imageUpdateAutomationType, diff --git a/cmd/flux/suspend_kustomization.go b/cmd/flux/suspend_kustomization.go index 46d12276..ac6c72e1 100644 --- a/cmd/flux/suspend_kustomization.go +++ b/cmd/flux/suspend_kustomization.go @@ -19,16 +19,19 @@ package main import ( "github.com/spf13/cobra" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" ) var suspendKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks"}, Short: "Suspend reconciliation of Kustomization", - Long: "The suspend command disables the reconciliation of a Kustomization resource.", + Long: `The suspend command disables the reconciliation of a Kustomization resource.`, Example: ` # Suspend reconciliation for an existing Kustomization - flux suspend ks podinfo`, + flux suspend ks podinfo + + # Suspend reconciliation for multiple Kustomizations + flux suspend ks podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), RunE: suspendCommand{ apiType: kustomizationType, diff --git a/cmd/flux/suspend_receiver.go b/cmd/flux/suspend_receiver.go index 2edf1572..7f2e6dd7 100644 --- a/cmd/flux/suspend_receiver.go +++ b/cmd/flux/suspend_receiver.go @@ -19,15 +19,18 @@ package main import ( "github.com/spf13/cobra" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" ) var suspendReceiverCmd = &cobra.Command{ Use: "receiver [name]", Short: "Suspend reconciliation of Receiver", - Long: "The suspend command disables the reconciliation of a Receiver resource.", + Long: `The suspend command disables the reconciliation of a Receiver resource.`, Example: ` # Suspend reconciliation for an existing Receiver - flux suspend receiver main`, + flux suspend receiver main + + # Suspend reconciliation for multiple Receivers + flux suspend receiver main-1 main-2`, ValidArgsFunction: resourceNamesCompletionFunc(notificationv1.GroupVersion.WithKind(notificationv1.ReceiverKind)), RunE: suspendCommand{ apiType: receiverType, diff --git a/cmd/flux/suspend_source.go b/cmd/flux/suspend_source.go index c255afbf..4542e815 100644 --- a/cmd/flux/suspend_source.go +++ b/cmd/flux/suspend_source.go @@ -23,7 +23,7 @@ import ( var suspendSourceCmd = &cobra.Command{ Use: "source", Short: "Suspend sources", - Long: "The suspend sub-commands suspend the reconciliation of a source.", + Long: `The suspend sub-commands suspend the reconciliation of a source.`, } func init() { diff --git a/cmd/flux/suspend_source_bucket.go b/cmd/flux/suspend_source_bucket.go index e452b9dc..7fdcfced 100644 --- a/cmd/flux/suspend_source_bucket.go +++ b/cmd/flux/suspend_source_bucket.go @@ -19,15 +19,18 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" ) var suspendSourceBucketCmd = &cobra.Command{ Use: "bucket [name]", Short: "Suspend reconciliation of a Bucket", - Long: "The suspend command disables the reconciliation of a Bucket resource.", + Long: `The suspend command disables the reconciliation of a Bucket resource.`, Example: ` # Suspend reconciliation for an existing Bucket - flux suspend source bucket podinfo`, + flux suspend source bucket podinfo + + # Suspend reconciliation for multiple Buckets + flux suspend source bucket podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.BucketKind)), RunE: suspendCommand{ apiType: bucketType, diff --git a/cmd/flux/suspend_source_chart.go b/cmd/flux/suspend_source_chart.go index 750e65ff..6da1d8c0 100644 --- a/cmd/flux/suspend_source_chart.go +++ b/cmd/flux/suspend_source_chart.go @@ -19,15 +19,18 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var suspendSourceHelmChartCmd = &cobra.Command{ Use: "chart [name]", Short: "Suspend reconciliation of a HelmChart", - Long: "The suspend command disables the reconciliation of a HelmChart resource.", + Long: `The suspend command disables the reconciliation of a HelmChart resource.`, Example: ` # Suspend reconciliation for an existing HelmChart - flux suspend source chart podinfo`, + flux suspend source chart podinfo + + # Suspend reconciliation for multiple HelmCharts + flux suspend source chart podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmChartKind)), RunE: suspendCommand{ apiType: helmChartType, diff --git a/cmd/flux/suspend_source_git.go b/cmd/flux/suspend_source_git.go index 16ebd0a6..4decf796 100644 --- a/cmd/flux/suspend_source_git.go +++ b/cmd/flux/suspend_source_git.go @@ -19,15 +19,18 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var suspendSourceGitCmd = &cobra.Command{ Use: "git [name]", Short: "Suspend reconciliation of a GitRepository", - Long: "The suspend command disables the reconciliation of a GitRepository resource.", + Long: `The suspend command disables the reconciliation of a GitRepository resource.`, Example: ` # Suspend reconciliation for an existing GitRepository - flux suspend source git podinfo`, + flux suspend source git podinfo + + # Suspend reconciliation for multiple GitRepositories + flux suspend source git podinfo-1 podinfo-2`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.GitRepositoryKind)), RunE: suspendCommand{ apiType: gitRepositoryType, diff --git a/cmd/flux/suspend_source_helm.go b/cmd/flux/suspend_source_helm.go index 0b29a2dc..a59343bd 100644 --- a/cmd/flux/suspend_source_helm.go +++ b/cmd/flux/suspend_source_helm.go @@ -19,15 +19,18 @@ package main import ( "github.com/spf13/cobra" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) var suspendSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Suspend reconciliation of a HelmRepository", - Long: "The suspend command disables the reconciliation of a HelmRepository resource.", + Long: `The suspend command disables the reconciliation of a HelmRepository resource.`, Example: ` # Suspend reconciliation for an existing HelmRepository - flux suspend source helm bitnami`, + flux suspend source helm bitnami + + # Suspend reconciliation for multiple HelmRepositories + flux suspend source helm bitnami-1 bitnami-2 `, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), RunE: suspendCommand{ apiType: helmRepositoryType, diff --git a/cmd/flux/suspend_source_oci.go b/cmd/flux/suspend_source_oci.go new file mode 100644 index 00000000..3b8e36d6 --- /dev/null +++ b/cmd/flux/suspend_source_oci.go @@ -0,0 +1,56 @@ +/* +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 ( + "github.com/spf13/cobra" + + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" +) + +var suspendSourceOCIRepositoryCmd = &cobra.Command{ + Use: "oci [name]", + Short: "Suspend reconciliation of an OCIRepository", + Long: `The suspend command disables the reconciliation of an OCIRepository resource.`, + Example: ` # Suspend reconciliation for an existing OCIRepository + flux suspend source oci podinfo + + # Suspend reconciliation for multiple OCIRepositories + flux suspend source oci podinfo-1 podinfo-2`, + ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.OCIRepositoryKind)), + RunE: suspendCommand{ + apiType: ociRepositoryType, + object: ociRepositoryAdapter{&sourcev1.OCIRepository{}}, + list: ociRepositoryListAdapter{&sourcev1.OCIRepositoryList{}}, + }.run, +} + +func init() { + suspendSourceCmd.AddCommand(suspendSourceOCIRepositoryCmd) +} + +func (obj ociRepositoryAdapter) isSuspended() bool { + return obj.OCIRepository.Spec.Suspend +} + +func (obj ociRepositoryAdapter) setSuspended() { + obj.OCIRepository.Spec.Suspend = true +} + +func (a ociRepositoryListAdapter) item(i int) suspendable { + return &ociRepositoryAdapter{&a.OCIRepositoryList.Items[i]} +} diff --git a/cmd/flux/tag.go b/cmd/flux/tag.go new file mode 100644 index 00000000..176fc7a1 --- /dev/null +++ b/cmd/flux/tag.go @@ -0,0 +1,31 @@ +/* +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 ( + "github.com/spf13/cobra" +) + +var tagCmd = &cobra.Command{ + Use: "tag", + Short: "Tag artifacts", + Long: `The tag command is used to tag OCI artifacts.`, +} + +func init() { + rootCmd.AddCommand(tagCmd) +} diff --git a/cmd/flux/tag_artifact.go b/cmd/flux/tag_artifact.go new file mode 100644 index 00000000..c7b7ecea --- /dev/null +++ b/cmd/flux/tag_artifact.go @@ -0,0 +1,115 @@ +/* +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/spf13/cobra" + + oci "github.com/fluxcd/pkg/oci/client" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/flags" +) + +var tagArtifactCmd = &cobra.Command{ + Use: "artifact", + Short: "Tag artifact", + Long: withPreviewNote(`The tag artifact command creates tags for the given OCI artifact. +The command can read the credentials from '~/.docker/config.json' but they can also be passed with --creds. It can also login to a supported provider with the --provider flag.`), + Example: ` # Tag an artifact version as latest + flux tag artifact oci://ghcr.io/org/manifests/app:v0.0.1 --tag latest +`, + RunE: tagArtifactCmdRun, +} + +type tagArtifactFlags struct { + tags []string + creds string + provider flags.SourceOCIProvider +} + +var tagArtifactArgs = newTagArtifactFlags() + +func newTagArtifactFlags() tagArtifactFlags { + return tagArtifactFlags{ + provider: flags.SourceOCIProvider(sourcev1.GenericOCIProvider), + } +} + +func init() { + tagArtifactCmd.Flags().StringSliceVar(&tagArtifactArgs.tags, "tag", nil, "tag name") + tagArtifactCmd.Flags().StringVar(&tagArtifactArgs.creds, "creds", "", "credentials for OCI registry in the format [:] if --provider is generic") + tagArtifactCmd.Flags().Var(&tagArtifactArgs.provider, "provider", tagArtifactArgs.provider.Description()) + tagCmd.AddCommand(tagArtifactCmd) +} + +func tagArtifactCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("artifact name is required") + } + ociURL := args[0] + + if len(tagArtifactArgs.tags) < 1 { + return fmt.Errorf("--tag is required") + } + + url, err := oci.ParseArtifactURL(ociURL) + if err != nil { + return err + } + + ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) + defer cancel() + + ociClient := oci.NewClient(oci.DefaultOptions()) + + if tagArtifactArgs.provider.String() == sourcev1.GenericOCIProvider && tagArtifactArgs.creds != "" { + logger.Actionf("logging in to registry with credentials") + if err := ociClient.LoginWithCredentials(tagArtifactArgs.creds); err != nil { + return fmt.Errorf("could not login with credentials: %w", err) + } + } + + if tagArtifactArgs.provider.String() != sourcev1.GenericOCIProvider { + logger.Actionf("logging in to registry with provider credentials") + ociProvider, err := tagArtifactArgs.provider.ToOCIProvider() + if err != nil { + return fmt.Errorf("provider not supported: %w", err) + } + + if err := ociClient.LoginWithProvider(ctx, url, ociProvider); err != nil { + return fmt.Errorf("error during login with provider: %w", err) + } + } + + logger.Actionf("tagging artifact") + + for _, tag := range tagArtifactArgs.tags { + img, err := ociClient.Tag(ctx, url, tag) + if err != nil { + return fmt.Errorf("tagging artifact failed: %w", err) + } + + logger.Successf("artifact tagged as %s", img) + } + + return nil + +} diff --git a/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml index 6bd46a2c..263e9128 100644 --- a/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml +++ b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml @@ -1,4 +1,4 @@ -apiVersion: autoscaling/v2beta2 +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: podinfo diff --git a/cmd/flux/testdata/build-kustomization/ignore/.sourceignore b/cmd/flux/testdata/build-kustomization/ignore/.sourceignore new file mode 100644 index 00000000..12bc52ca --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/ignore/.sourceignore @@ -0,0 +1,2 @@ +# exclude all +/* diff --git a/cmd/flux/testdata/build-kustomization/ignore/configmap.yaml b/cmd/flux/testdata/build-kustomization/ignore/configmap.yaml new file mode 100644 index 00000000..7f58221a --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/ignore/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +data: + var: test +kind: ConfigMap +metadata: + name: configmap_ignore diff --git a/cmd/flux/testdata/build-kustomization/ignore/not_deployable/ignore_svc.yaml b/cmd/flux/testdata/build-kustomization/ignore/not_deployable/ignore_svc.yaml new file mode 100644 index 00000000..43fa3984 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/ignore/not_deployable/ignore_svc.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: do_not_deploy +spec: + type: ClusterIP + selector: + app: podinfo + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc diff --git a/cmd/flux/testdata/build-kustomization/ignore/secret.yaml b/cmd/flux/testdata/build-kustomization/ignore/secret.yaml new file mode 100644 index 00000000..65cc9d0c --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/ignore/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +data: + token: KipTT1BTKio= +kind: Secret +metadata: + name: secret_ignore +type: Opaque diff --git a/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml index 6d3eabed..9f5e3ef3 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml @@ -1,5 +1,5 @@ --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: podinfo @@ -13,3 +13,7 @@ spec: kind: GitRepository name: podinfo targetNamespace: default + postBuild: + substitute: + cluster_env: "prod" + cluster_region: "eu-central-1" diff --git a/cmd/flux/testdata/build-kustomization/podinfo-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-result.yaml index ce66ff0f..bf162251 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo-result.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo-result.yaml @@ -77,7 +77,7 @@ spec: cpu: 100m memory: 64Mi --- -apiVersion: autoscaling/v2beta2 +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: labels: @@ -123,6 +123,31 @@ spec: type: ClusterIP --- apiVersion: v1 +data: + .dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ== +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: docker-secret + namespace: default +type: kubernetes.io/dockerconfigjson +--- +apiVersion: v1 +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: secret-basic-auth-stringdata + namespace: default +stringData: + password: '**SOPS**' + username: '**SOPS**' +type: kubernetes.io/basic-auth +--- +apiVersion: v1 data: token: KipTT1BTKio= kind: Secret @@ -146,3 +171,4 @@ metadata: name: db-user-pass-bkbd782d2c namespace: default type: Opaque +--- diff --git a/cmd/flux/testdata/build-kustomization/podinfo-source.yaml b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml index 745dd650..409482dd 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo-source.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml @@ -4,7 +4,7 @@ kind: Namespace metadata: name: {{ .fluxns }} --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: podinfo diff --git a/cmd/flux/testdata/build-kustomization/podinfo-with-ignore-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-with-ignore-result.yaml new file mode 100644 index 00000000..671d7d20 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-with-ignore-result.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +data: + var: test +kind: ConfigMap +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: configmap_ignore + namespace: default +--- +apiVersion: v1 +data: + token: KipTT1BTKio= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: secret_ignore + namespace: default +type: Opaque +--- diff --git a/cmd/flux/testdata/build-kustomization/podinfo-with-var-substitution-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-with-var-substitution-result.yaml new file mode 100644 index 00000000..01686566 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-with-var-substitution-result.yaml @@ -0,0 +1,217 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + environment: prod + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + region: eu-central-1 + name: podinfo + namespace: default +spec: + minReadySeconds: 3 + progressDeadlineSeconds: 60 + revisionHistoryLimit: 5 + selector: + matchLabels: + app: podinfo + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + template: + metadata: + annotations: + prometheus.io/port: "9797" + prometheus.io/scrape: "true" + labels: + app: podinfo + spec: + containers: + - command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: '#34577c' + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + name: podinfod + ports: + - containerPort: 9898 + name: http + protocol: TCP + - containerPort: 9797 + name: http-metrics + protocol: TCP + - containerPort: 9999 + name: grpc + protocol: TCP + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi +--- +apiVersion: v1 +data: + cluster.json: | + { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "iteration": 1636369574387, + "links": [], + "panels": [ + { + "datasource": "${DS_PROMETHEUS}", + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "red", + "value": 100 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 24, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "7.5.5", + "targets": [ + { + "exemplar": true, + "expr": "count(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"True\",kind=~\"Kustomization|HelmRelease\"})\n-\nsum(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"Deleted\",kind=~\"Kustomization|HelmRelease\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Cluster Reconcilers", + "type": "stat" + }, + { + "collapsed": false, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 15, + "panels": [], + "title": "Status", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 27, + "style": "light", + "tags": [ + "flux" + ], + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Flux Cluster Stats", + "uid": "flux-cluster", + "version": 1 + } +kind: ConfigMap +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + kustomize.toolkit.fluxcd.io/substitute: disabled + name: flux-grafana-dashboards-kt8md725kf + namespace: default +--- diff --git a/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml index 943381c8..647ce020 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml @@ -77,7 +77,7 @@ spec: cpu: 100m memory: 64Mi --- -apiVersion: autoscaling/v2beta2 +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: labels: @@ -99,3 +99,4 @@ spec: apiVersion: apps/v1 kind: Deployment name: podinfo +--- diff --git a/cmd/flux/testdata/build-kustomization/podinfo/dockerconfigjson-sops-secret.yaml b/cmd/flux/testdata/build-kustomization/podinfo/dockerconfigjson-sops-secret.yaml new file mode 100644 index 00000000..c05731bc --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/dockerconfigjson-sops-secret.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +data: + .dockerconfigjson: ENC[AES256_GCM,data:KHCFH3hNnc+PMfWLFEPjebf3W4z4WXbGFAANRZyZC+07z7wlrTALJM6rn8YslW4tMAWCoAYxblC5WRCszTy0h9rw0U/RGOv5H0qCgnNg/FILFUqhwo9pNfrUH+MEP4M9qxxbLKZwObpHUE7DUsKx1JYAxsI=,iv:q48lqUbUQD+0cbYcjNMZMJLRdGHi78ZmDhNAT2th9tg=,tag:QRI2SZZXQrAcdql3R5AH2g==,type:str] +kind: Secret +metadata: + name: docker-secret +type: kubernetes.io/dockerconfigjson +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3eU1CTEJhVXZ4eEVYYkVV + OU90TEcrR2pYckttN0pBanJoSUZWSW1RQXlRCkUydFJ3V1NZUTBuVFF0aC9GUEcw + bUdhNjJWTkoyL1FUVi9Dc1dxUDBkM0UKLS0tIE1sQXkwcWdGaEFuY0RHQTVXM0J6 + dWpJcThEbW15V3dXYXpPZklBdW1Hd1kKoIAdmGNPrEctV8h1w8KuvQ5S+BGmgqN9 + MgpNmUhJjWhgcQpb5BRYpQesBOgU5TBGK7j58A6DMDKlSiYZsdQchQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2022-02-03T16:03:17Z" + mac: ENC[AES256_GCM,data:AHdYSawajwgAFwlmDN1IPNmT9vWaYKzyVIra2d6sPcjTbZ8/p+VRSRpVm4XZFFsaNnW5AUJaouwXnKYDTmJDXKlr/rQcu9kXqsssQgdzcXaA6l5uJlgsnml8ba7J3OK+iEKMax23mwQEx2EUskCd9ENOwFDkunP02sxqDNOz20k=,iv:8F5OamHt3fAVorf6p+SoIrWoqkcATSGWVoM0EK87S4M=,tag:E1mxXnc7wWkEX5BxhpLtng==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.7.1 diff --git a/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml index 6bd46a2c..263e9128 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml @@ -1,4 +1,4 @@ -apiVersion: autoscaling/v2beta2 +apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: podinfo diff --git a/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml b/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml index 0ba07668..435c23c6 100644 --- a/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml +++ b/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml @@ -4,6 +4,8 @@ resources: - ./deployment.yaml - ./hpa.yaml - ./service.yaml +- ./dockerconfigjson-sops-secret.yaml +- ./stringdata-secret.yaml secretGenerator: - files: - token=token.encrypted diff --git a/cmd/flux/testdata/build-kustomization/podinfo/stringdata-secret.yaml b/cmd/flux/testdata/build-kustomization/podinfo/stringdata-secret.yaml new file mode 100644 index 00000000..c9bb980d --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/stringdata-secret.yaml @@ -0,0 +1,28 @@ +apiVersion: v1 +kind: Secret +metadata: + name: secret-basic-auth-stringdata +type: kubernetes.io/basic-auth +stringData: + username: ENC[AES256_GCM,data:uKiQR48=,iv:jh2lgyAVu7igJAgoJsnOGhjxFyvUAa9lvT21u3hhqpU=,tag:zXM2JEpk3ZEH7WfkcWXXkw==,type:str] + password: ENC[AES256_GCM,data:PyhZmNhy929JGQ==,iv:PBqPaJmSw21+kn4gIlg5VdjLNZyf613z5RUTCesBoVw=,tag:Hjc7DsuUrtsz7PYPdNkL3g==,type:str] +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBJd0xxbDZhYjVoZzY4YWhK + d2NvMVgrSGRVUGhHRGg3R1FpVURnbmh1TDBzCjcwby85M3JaK09QVk0yZFNMb2NL + c2NQZW5hS1FhYlBHU0VoUzBVYzZYUUUKLS0tIEdaNEw2Y0VjVHpZc3pyYUtLVmJk + NmN3K2VLU0NiZ1d0VHBYbGlCM1lrNmMKeWz3yfFbMNE+ly21oLfc1XnDSPRmnlPP + wIs8lk/qrzVZ45C9GdWnnPeGZZiia46Yop9TxseUS8gCjJ6KCxJCAg== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2022-02-06T12:51:07Z" + mac: ENC[AES256_GCM,data:jtdzwj19uxdxvnmXg1HkAkDA6XlKMJOYFy7uLI5t/t11LwGop5Yeo7a4nQEEELehRx9J7B6U6NiySxAxBxWx5uW5vI5c8+069VV6dkiCIefnYSzuoIhQafjlFl1/KvH7VEjIWfHYuXF09v9PEKXkxEHUYDpS3QqQ3ymHRRI08pU=, iv:xX3E7F+AM29Pm8G5oqxRfYu9E7tEBGIaHeCJYgrtFmc=,tag:MJPGusNvu05z939jg8PAwQ==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.7.1 diff --git a/cmd/flux/testdata/build-kustomization/var-substitution/cluster.json b/cmd/flux/testdata/build-kustomization/var-substitution/cluster.json new file mode 100644 index 00000000..8184714a --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/var-substitution/cluster.json @@ -0,0 +1,124 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "iteration": 1636369574387, + "links": [], + "panels": [ + { + "datasource": "${DS_PROMETHEUS}", + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "red", + "value": 100 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 6, + "x": 0, + "y": 0 + }, + "id": 24, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "last" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "value" + }, + "pluginVersion": "7.5.5", + "targets": [ + { + "exemplar": true, + "expr": "count(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"True\",kind=~\"Kustomization|HelmRelease\"})\n-\nsum(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"Deleted\",kind=~\"Kustomization|HelmRelease\"})", + "interval": "", + "legendFormat": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Cluster Reconcilers", + "type": "stat" + }, + { + "collapsed": false, + "datasource": "${DS_PROMETHEUS}", + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 9 + }, + "id": 15, + "panels": [], + "title": "Status", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 27, + "style": "light", + "tags": [ + "flux" + ], + "time": { + "from": "now-15m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Flux Cluster Stats", + "uid": "flux-cluster", + "version": 1 +} diff --git a/cmd/flux/testdata/build-kustomization/var-substitution/deployment.yaml b/cmd/flux/testdata/build-kustomization/var-substitution/deployment.yaml new file mode 100644 index 00000000..9b5f2c35 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/var-substitution/deployment.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + environment: ${cluster_env:=dev} + region: ${cluster_region} + name: podinfo +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/cmd/flux/testdata/build-kustomization/var-substitution/kustomization.yaml b/cmd/flux/testdata/build-kustomization/var-substitution/kustomization.yaml new file mode 100644 index 00000000..8edecffd --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/var-substitution/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./deployment.yaml +generatorOptions: + labels: + kustomize.toolkit.fluxcd.io/substitute: disabled +configMapGenerator: + - name: flux-grafana-dashboards + files: + - cluster.json diff --git a/cmd/flux/testdata/check/check_pre.golden b/cmd/flux/testdata/check/check_pre.golden index 02ba7892..312f6861 100644 --- a/cmd/flux/testdata/check/check_pre.golden +++ b/cmd/flux/testdata/check/check_pre.golden @@ -1,3 +1,3 @@ ► checking prerequisites -✔ Kubernetes {{ .serverVersion }} >=1.19.0-0 +✔ Kubernetes {{ .serverVersion }} >=1.28.0-0 ✔ prerequisites checks passed diff --git a/cmd/flux/testdata/cluster_info/gitrepositories.yaml b/cmd/flux/testdata/cluster_info/gitrepositories.yaml new file mode 100644 index 00000000..3e4ba5c7 --- /dev/null +++ b/cmd/flux/testdata/cluster_info/gitrepositories.yaml @@ -0,0 +1,424 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.12.0 + name: gitrepositories.source.toolkit.fluxcd.io +spec: + group: source.toolkit.fluxcd.io + names: + kind: GitRepository + listKind: GitRepositoryList + plural: gitrepositories + shortNames: + - gitrepo + singular: gitrepository + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.url + name: URL + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1 + schema: + openAPIV3Schema: + description: GitRepository is the Schema for the gitrepositories API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: GitRepositorySpec specifies the required configuration to + produce an Artifact for a Git repository. + properties: + ignore: + description: Ignore overrides the set of excluded patterns in the + .sourceignore format (which is the same as .gitignore). If not provided, + a default will be used, consult the documentation for your version + to find out what those are. + type: string + include: + description: Include specifies a list of GitRepository resources which + Artifacts should be included in the Artifact produced for this GitRepository. + items: + description: GitRepositoryInclude specifies a local reference to + a GitRepository which Artifact (sub-)contents must be included, + and where they should be placed. + properties: + fromPath: + description: FromPath specifies the path to copy contents from, + defaults to the root of the Artifact. + type: string + repository: + description: GitRepositoryRef specifies the GitRepository which + Artifact contents must be included. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + toPath: + description: ToPath specifies the path to copy contents to, + defaults to the name of the GitRepositoryRef. + type: string + required: + - repository + type: object + type: array + interval: + description: Interval at which the GitRepository URL is checked for + updates. This interval is approximate and may be subject to jitter + to ensure efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + proxySecretRef: + description: ProxySecretRef specifies the Secret containing the proxy + configuration to use while communicating with the Git server. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + recurseSubmodules: + description: RecurseSubmodules enables the initialization of all submodules + within the GitRepository as cloned from the URL, using their default + settings. + type: boolean + ref: + description: Reference specifies the Git reference to resolve and + monitor for changes, defaults to the 'master' branch. + properties: + branch: + description: Branch to check out, defaults to 'master' if no other + field is defined. + type: string + commit: + description: "Commit SHA to check out, takes precedence over all + reference fields. \n This can be combined with Branch to shallow + clone the branch, in which the commit is expected to exist." + type: string + name: + description: "Name of the reference to check out; takes precedence + over Branch, Tag and SemVer. \n It must be a valid Git reference: + https://git-scm.com/docs/git-check-ref-format#_description Examples: + \"refs/heads/main\", \"refs/tags/v0.1.0\", \"refs/pull/420/head\", + \"refs/merge-requests/1/head\"" + type: string + semver: + description: SemVer tag expression to check out, takes precedence + over Tag. + type: string + tag: + description: Tag to check out, takes precedence over Branch. + type: string + type: object + secretRef: + description: SecretRef specifies the Secret containing authentication + credentials for the GitRepository. For HTTPS repositories the Secret + must contain 'username' and 'password' fields for basic auth or + 'bearerToken' field for token auth. For SSH repositories the Secret + must contain 'identity' and 'known_hosts' fields. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + suspend: + description: Suspend tells the controller to suspend the reconciliation + of this GitRepository. + type: boolean + timeout: + default: 60s + description: Timeout for Git operations like cloning, defaults to + 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + url: + description: URL specifies the Git repository URL, it can be an HTTP/S + or SSH address. + pattern: ^(http|https|ssh)://.*$ + type: string + verify: + description: Verification specifies the configuration to verify the + Git commit signature(s). + properties: + mode: + default: HEAD + description: "Mode specifies which Git object(s) should be verified. + \n The variants \"head\" and \"HEAD\" both imply the same thing, + i.e. verify the commit that the HEAD of the Git repository points + to. The variant \"head\" solely exists to ensure backwards compatibility." + enum: + - head + - HEAD + - Tag + - TagAndHEAD + type: string + secretRef: + description: SecretRef specifies the Secret containing the public + keys of trusted Git authors. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + required: + - interval + - url + type: object + status: + default: + observedGeneration: -1 + description: GitRepositoryStatus records the observed state of a Git repository. + properties: + artifact: + description: Artifact represents the last successful GitRepository + reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: LastUpdateTime is the timestamp corresponding to + the last update of the Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: Path is the relative file path of the Artifact. It + can be used to locate the file in the root of the Artifact storage + on the local file system of the controller managing the Source. + type: string + revision: + description: Revision is a human-readable identifier traceable + in the origin source system. It can be a Git commit SHA, Git + tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: URL is the HTTP address of the Artifact as exposed + by the controller managing the Source. It can be used to retrieve + the Artifact for consumption, e.g. by another controller applying + the Artifact contents. + type: string + required: + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the GitRepository. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + includedArtifacts: + description: IncludedArtifacts contains a list of the last successfully + included Artifacts as instructed by GitRepositorySpec.Include. + items: + description: Artifact represents the output of a Source reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of + ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: LastUpdateTime is the timestamp corresponding to + the last update of the Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI + annotations. + type: object + path: + description: Path is the relative file path of the Artifact. + It can be used to locate the file in the root of the Artifact + storage on the local file system of the controller managing + the Source. + type: string + revision: + description: Revision is a human-readable identifier traceable + in the origin source system. It can be a Git commit SHA, Git + tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: URL is the HTTP address of the Artifact as exposed + by the controller managing the Source. It can be used to retrieve + the Artifact for consumption, e.g. by another controller applying + the Artifact contents. + type: string + required: + - lastUpdateTime + - path + - revision + - url + type: object + type: array + lastHandledReconcileAt: + description: LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value can + be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last observed generation of + the GitRepository object. + format: int64 + type: integer + observedIgnore: + description: ObservedIgnore is the observed exclusion patterns used + for constructing the source artifact. + type: string + observedInclude: + description: ObservedInclude is the observed list of GitRepository + resources used to produce the current Artifact. + items: + description: GitRepositoryInclude specifies a local reference to + a GitRepository which Artifact (sub-)contents must be included, + and where they should be placed. + properties: + fromPath: + description: FromPath specifies the path to copy contents from, + defaults to the root of the Artifact. + type: string + repository: + description: GitRepositoryRef specifies the GitRepository which + Artifact contents must be included. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + toPath: + description: ToPath specifies the path to copy contents to, + defaults to the name of the GitRepositoryRef. + type: string + required: + - repository + type: object + type: array + observedRecurseSubmodules: + description: ObservedRecurseSubmodules is the observed resource submodules + configuration used to produce the current Artifact. + type: boolean + sourceVerificationMode: + description: SourceVerificationMode is the last used verification + mode indicating which Git object(s) have been verified. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/cmd/flux/testdata/create_hr/basic.yaml b/cmd/flux/testdata/create_hr/basic.yaml new file mode 100644 index 00000000..066b1610 --- /dev/null +++ b/cmd/flux/testdata/create_hr/basic.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + chart: + spec: + chart: podinfo + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo + interval: 1m0s diff --git a/cmd/flux/testdata/create_hr/hc_basic.yaml b/cmd/flux/testdata/create_hr/hc_basic.yaml new file mode 100644 index 00000000..6e99f5d0 --- /dev/null +++ b/cmd/flux/testdata/create_hr/hc_basic.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + chartRef: + kind: HelmChart + name: podinfo + interval: 1m0s diff --git a/cmd/flux/testdata/create_hr/or_basic.yaml b/cmd/flux/testdata/create_hr/or_basic.yaml new file mode 100644 index 00000000..368027a2 --- /dev/null +++ b/cmd/flux/testdata/create_hr/or_basic.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + chartRef: + kind: OCIRepository + name: podinfo + interval: 1m0s diff --git a/cmd/flux/testdata/create_hr/setup-source.yaml b/cmd/flux/testdata/create_hr/setup-source.yaml new file mode 100644 index 00000000..254024a0 --- /dev/null +++ b/cmd/flux/testdata/create_hr/setup-source.yaml @@ -0,0 +1,39 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 1m0s + provider: generic + type: oci + url: oci://ghcr.io/stefanprodan/charts +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmChart +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 1m0s + chart: podinfo + sourceRef: + kind: HelmRepository + name: podinfo +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 10m + url: oci://ghcr.io/stefanprodan/manifests/podinfo + ref: + tag: latest diff --git a/cmd/flux/testdata/create_secret/git/git-bearer-token.yaml b/cmd/flux/testdata/create_secret/git/git-bearer-token.yaml new file mode 100644 index 00000000..7dcfde2d --- /dev/null +++ b/cmd/flux/testdata/create_secret/git/git-bearer-token.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: bearer-token-auth + namespace: my-namespace +stringData: + bearerToken: ghp_baR2qnFF0O41WlucePL3udt2N9vVZS4R0hAS + diff --git a/cmd/flux/testdata/create_secret/git/secret-ca-crt.yaml b/cmd/flux/testdata/create_secret/git/secret-ca-crt.yaml new file mode 100644 index 00000000..958bd45f --- /dev/null +++ b/cmd/flux/testdata/create_secret/git/secret-ca-crt.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: ca-crt + namespace: my-namespace +stringData: + ca.crt: ca-data + password: my-password + username: my-username + diff --git a/cmd/flux/testdata/create_secret/notation/invalid-trust-policy.json b/cmd/flux/testdata/create_secret/notation/invalid-trust-policy.json new file mode 100644 index 00000000..257b772f --- /dev/null +++ b/cmd/flux/testdata/create_secret/notation/invalid-trust-policy.json @@ -0,0 +1,4 @@ +{ + "version": "1.0", + "trustPolicies": [{}] +} diff --git a/cmd/flux/testdata/create_secret/notation/invalid.json b/cmd/flux/testdata/create_secret/notation/invalid.json new file mode 100644 index 00000000..e16c76df --- /dev/null +++ b/cmd/flux/testdata/create_secret/notation/invalid.json @@ -0,0 +1 @@ +"" diff --git a/cmd/flux/testdata/create_secret/notation/secret-ca-crt.yaml b/cmd/flux/testdata/create_secret/notation/secret-ca-crt.yaml new file mode 100644 index 00000000..28f37c08 --- /dev/null +++ b/cmd/flux/testdata/create_secret/notation/secret-ca-crt.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: notation-config + namespace: my-namespace +stringData: + ca.crt: ca-data-crt + trustpolicy.json: | + { + "version": "1.0", + "trustPolicies": [ + { + "name": "fluxcd.io", + "registryScopes": [ + "*" + ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": [ "ca:fluxcd.io" ], + "trustedIdentities": [ + "*" + ] + } + ] + } + diff --git a/cmd/flux/testdata/create_secret/notation/secret-ca-multi.yaml b/cmd/flux/testdata/create_secret/notation/secret-ca-multi.yaml new file mode 100644 index 00000000..80652de7 --- /dev/null +++ b/cmd/flux/testdata/create_secret/notation/secret-ca-multi.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: notation-config + namespace: my-namespace +stringData: + ca.crt: ca-data-crt + ca.pem: ca-data-pem + trustpolicy.json: | + { + "version": "1.0", + "trustPolicies": [ + { + "name": "fluxcd.io", + "registryScopes": [ + "*" + ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": [ "ca:fluxcd.io" ], + "trustedIdentities": [ + "*" + ] + } + ] + } + diff --git a/cmd/flux/testdata/create_secret/notation/secret-ca-pem.yaml b/cmd/flux/testdata/create_secret/notation/secret-ca-pem.yaml new file mode 100644 index 00000000..e206809b --- /dev/null +++ b/cmd/flux/testdata/create_secret/notation/secret-ca-pem.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: notation-config + namespace: my-namespace +stringData: + ca.pem: ca-data-pem + trustpolicy.json: | + { + "version": "1.0", + "trustPolicies": [ + { + "name": "fluxcd.io", + "registryScopes": [ + "*" + ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": [ "ca:fluxcd.io" ], + "trustedIdentities": [ + "*" + ] + } + ] + } + diff --git a/cmd/flux/testdata/create_secret/notation/test-ca.crt b/cmd/flux/testdata/create_secret/notation/test-ca.crt new file mode 100644 index 00000000..e1ad1da4 --- /dev/null +++ b/cmd/flux/testdata/create_secret/notation/test-ca.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbDCCAlSgAwIBAgIUP7zhmTw5XTWLcgBGkBEsErMOkz4wDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCUk8xCzAJBgNVBAgMAkJVMRIwEAYDVQQHDAlCdWNoYXJl +c3QxDzANBgNVBAoMBk5vdGFyeTEZMBcGA1UEAwwQc3RlZmFucHJvZGFuLmNvbTAe +Fw0yNDAyMjUxMDAyMzZaFw0yOTAyMjQxMDAyMzZaMFoxCzAJBgNVBAYTAlJPMQsw +CQYDVQQIDAJCVTESMBAGA1UEBwwJQnVjaGFyZXN0MQ8wDQYDVQQKDAZOb3Rhcnkx +GTAXBgNVBAMMEHN0ZWZhbnByb2Rhbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDtH4oPi3SyX/DGv6NdjIvmApvD9eeSgsmHdwpAly8T9D2me+fx +Z+wRNJmq4aq/A1anX+Sg28iwHzV+1WKpsHnjYzDAJSEYP2S8A5H1nGRKUoibdijw +C3QBh5C75rjF/tmZVSX/Vgbf3HJJEsF4WUxWabLxoV2QLo7UlEsQd9+bSeKNMncx +1+E6FdbRCrYo90iobvZJ8K/S2zCWq/JTeHfTnmSEDhx6nMJcaSjvMPn3zyauWcQw +dDpkcaGiJ64fEJRT2OFxXv9u+vDmIMKzo/Wjbd+IzFj6YY4VisK88aU7tmDelnk5 +gQB9eu62PFoaVsYJp4VOhblFKvGJpQwbWB9BAgMBAAGjKjAoMA4GA1UdDwEB/wQE +AwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEA +6x+C6hAIbLwMvkNx4K5p7Qe/pLQR0VwQFAw10yr/5KSN+YKFpon6pQ0TebL7qll+ +uBGZvtQhN6v+DlnVqB7lvJKd+89isgirkkews5KwuXg7Gv5UPIugH0dXISZU8DMJ +7J4oKREv5HzdFmfsUfNlQcfyVTjKL6UINXfKGdqNNxXxR9b4a1TY2JcmEhzBTHaq +ZqX6HK784a0dB7aHgeFrFwPCCP4M684Hs7CFbk3jo2Ef4ljnB5AyWpe8pwCLMdRt +UjSjL5xJWVQvRU+STQsPr6SvpokPCG4rLQyjgeYYk4CCj5piSxbSUZFavq8v1y7Y +m91USVqfeUX7ZzjDxPHE2A== +-----END CERTIFICATE----- diff --git a/cmd/flux/testdata/create_secret/notation/test-ca2.crt b/cmd/flux/testdata/create_secret/notation/test-ca2.crt new file mode 100644 index 00000000..e1ad1da4 --- /dev/null +++ b/cmd/flux/testdata/create_secret/notation/test-ca2.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbDCCAlSgAwIBAgIUP7zhmTw5XTWLcgBGkBEsErMOkz4wDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCUk8xCzAJBgNVBAgMAkJVMRIwEAYDVQQHDAlCdWNoYXJl +c3QxDzANBgNVBAoMBk5vdGFyeTEZMBcGA1UEAwwQc3RlZmFucHJvZGFuLmNvbTAe +Fw0yNDAyMjUxMDAyMzZaFw0yOTAyMjQxMDAyMzZaMFoxCzAJBgNVBAYTAlJPMQsw +CQYDVQQIDAJCVTESMBAGA1UEBwwJQnVjaGFyZXN0MQ8wDQYDVQQKDAZOb3Rhcnkx +GTAXBgNVBAMMEHN0ZWZhbnByb2Rhbi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDtH4oPi3SyX/DGv6NdjIvmApvD9eeSgsmHdwpAly8T9D2me+fx +Z+wRNJmq4aq/A1anX+Sg28iwHzV+1WKpsHnjYzDAJSEYP2S8A5H1nGRKUoibdijw +C3QBh5C75rjF/tmZVSX/Vgbf3HJJEsF4WUxWabLxoV2QLo7UlEsQd9+bSeKNMncx +1+E6FdbRCrYo90iobvZJ8K/S2zCWq/JTeHfTnmSEDhx6nMJcaSjvMPn3zyauWcQw +dDpkcaGiJ64fEJRT2OFxXv9u+vDmIMKzo/Wjbd+IzFj6YY4VisK88aU7tmDelnk5 +gQB9eu62PFoaVsYJp4VOhblFKvGJpQwbWB9BAgMBAAGjKjAoMA4GA1UdDwEB/wQE +AwIHgDAWBgNVHSUBAf8EDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEA +6x+C6hAIbLwMvkNx4K5p7Qe/pLQR0VwQFAw10yr/5KSN+YKFpon6pQ0TebL7qll+ +uBGZvtQhN6v+DlnVqB7lvJKd+89isgirkkews5KwuXg7Gv5UPIugH0dXISZU8DMJ +7J4oKREv5HzdFmfsUfNlQcfyVTjKL6UINXfKGdqNNxXxR9b4a1TY2JcmEhzBTHaq +ZqX6HK784a0dB7aHgeFrFwPCCP4M684Hs7CFbk3jo2Ef4ljnB5AyWpe8pwCLMdRt +UjSjL5xJWVQvRU+STQsPr6SvpokPCG4rLQyjgeYYk4CCj5piSxbSUZFavq8v1y7Y +m91USVqfeUX7ZzjDxPHE2A== +-----END CERTIFICATE----- diff --git a/cmd/flux/testdata/create_secret/notation/test-trust-policy.json b/cmd/flux/testdata/create_secret/notation/test-trust-policy.json new file mode 100644 index 00000000..998c6dce --- /dev/null +++ b/cmd/flux/testdata/create_secret/notation/test-trust-policy.json @@ -0,0 +1,18 @@ +{ + "version": "1.0", + "trustPolicies": [ + { + "name": "fluxcd.io", + "registryScopes": [ + "*" + ], + "signatureVerification": { + "level" : "strict" + }, + "trustStores": [ "ca:fluxcd.io" ], + "trustedIdentities": [ + "*" + ] + } + ] +} diff --git a/cmd/flux/testdata/create_secret/oci/create-secret.yaml b/cmd/flux/testdata/create_secret/oci/create-secret.yaml new file mode 100644 index 00000000..b023bbe2 --- /dev/null +++ b/cmd/flux/testdata/create_secret/oci/create-secret.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: ghcr + namespace: my-namespace +stringData: + .dockerconfigjson: '{"auths":{"ghcr.io":{"username":"stefanprodan","password":"password","auth":"c3RlZmFucHJvZGFuOnBhc3N3b3Jk"}}}' +type: kubernetes.io/dockerconfigjson + diff --git a/cmd/flux/testdata/create_secret/tls/deprecated-secret-tls.yaml b/cmd/flux/testdata/create_secret/tls/deprecated-secret-tls.yaml new file mode 100644 index 00000000..04e01d63 --- /dev/null +++ b/cmd/flux/testdata/create_secret/tls/deprecated-secret-tls.yaml @@ -0,0 +1,107 @@ +Flag --cert-file has been deprecated, please use --tls-crt-file instead +Flag --key-file has been deprecated, please use --tls-key-file instead +Flag --ca-file has been deprecated, please use --ca-crt-file instead +--- +apiVersion: v1 +kind: Secret +metadata: + name: certs + namespace: my-namespace +stringData: + caFile: | + -----BEGIN CERTIFICATE----- + MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw + GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw + NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49 + AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr + RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE + AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ + 4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O + 1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb + OD8EjjCMY69RMO0= + -----END CERTIFICATE----- + certFile: | + -----BEGIN CERTIFICATE----- + MIIFazCCA1OgAwIBAgIUT84jeO/ncOrqI+FY05Fzbg8Ed7MwDQYJKoZIhvcNAQEL + BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM + GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA4MDgxNDQyMzVaFw0yMjA4 + MDgxNDQyMzVaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw + HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB + AQUAA4ICDwAwggIKAoICAQDn/rPsZ74oypiwCzLlx57zplTiCi/WLSF+MmLGuTvM + EQnV+OND2zFgvDIV/vFs3brkd6rLVI4NcdgSj4YKULCMwwOl45hQPdCTEPJvUhCm + M+FuQ0czmEEJSjZtdLFz1B7QB/JemNnbfigxM9mlg58AlBhVJqn8q64wd/kC/W/K + JTLJuBiVf12ZiPoPfO4WSxAqD3opZ8gdbmK0KYQAhKjEto6ZrYGisfwU1gt3l8M7 + sCJSpEkOkpuQgJ8D+xzJS36VXBJQMMP9nAPps+x/rGFplsPMsXEFFiwvR1+FJZwz + lg2sJ91bLGZQ7vn74MfsGrxpiJwllRThJyT7C9V0sjb5trT2lEqZlP2dRSJYt7aJ + 1crEcdGSl6RIKgxSV6Hk8dh/ZaTjrTwaKxVkPo2IeEXy5xrR7DyonOQ6Yes0KOCm + JB5yHkFlIVEnLm/HZXEtm3bPHsFgTZuInyBCOMXpUESuVZIw8YK+Vd6AExGPPwZ4 + n5I/sCDxWII9owIj3LeLzdUG6JoroahhGmo8rgpbJpPnS+VgryQ/raUQjqDzDCuE + 9vKXKBlSUqK6H9A+NMc0mme7M8/GX7T7ewFGUB/xsdrcO4yXjqHnAe0yLf8epDjC + hh76bYqwwinVrmfcNcRxFVJZW2z0gGdgkOkOLaVVb9ggPV2SNAHbN4A+St/iRYR5 + awIDAQABo1MwUTAdBgNVHQ4EFgQUzMaCqVM30EZFfTeNUIJ5fNPAhaQwHwYDVR0j + BBgwFoAUzMaCqVM30EZFfTeNUIJ5fNPAhaQwDwYDVR0TAQH/BAUwAwEB/zANBgkq + hkiG9w0BAQsFAAOCAgEAVmk1rXtVkYR1Vs2Va/xrUaGXlFznhPU/Fft44kiEkkLp + mLVelWyAqvXYioqssZwuZnTjGz0DQPqzJjqwuGy4CHwPLmhCtfHplrbWo8a0ivYC + cL20KfZsG941siUh7LGBjTsq6mWBf2ytlFmg/fg93SgmqcEUAUcdps0JpZD8lgWB + ZMstfr6E3jaEus3OsvDD6hJNYZ5clJ5+ynLoWZ99A9JC0U46hmIZpRjbdSvasKpD + XrXTdpzyL/Do3znXE/yfoHv4//Rj2CpPHJLYRCIzvuf1mo1fWd53FjHvrbUvaHFz + CGuZROd4dC4Rx5nZw2ogIYvJ8m6HpIDkL3pBNSQJtIsvAYEQcotJoa5D/e9fu2Wr + +og37oCY4OXzViEBQvyxKD4cajNco1fgGKEaFROADwr3JceGI7Anq5W+xdUvAGNM + QuGeCueqNyrJ0CbQ1zEhwgpk/VYfB0u9m0bjMellRlKMdojby+FDCJtAJesx9no4 + SQXyx+aNHhj3qReysjGNwZvBk1IHL04HAT+ogNiYhTl1J/YON4MB5UN6Y2PxP6uG + KvJGPigx4fAwfR/d78o5ngwoH9m+8FUg8+qllJ8XgIbl/VXKTk3G4ceOm4eBmrel + DwWuBhELSjtXWPWhMlkiebgejDbAear53Lia2Cc43zx/KuhMHBTlKY/vY4F2YiI= + -----END CERTIFICATE----- + keyFile: | + -----BEGIN PRIVATE KEY----- + MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDn/rPsZ74oypiw + CzLlx57zplTiCi/WLSF+MmLGuTvMEQnV+OND2zFgvDIV/vFs3brkd6rLVI4NcdgS + j4YKULCMwwOl45hQPdCTEPJvUhCmM+FuQ0czmEEJSjZtdLFz1B7QB/JemNnbfigx + M9mlg58AlBhVJqn8q64wd/kC/W/KJTLJuBiVf12ZiPoPfO4WSxAqD3opZ8gdbmK0 + KYQAhKjEto6ZrYGisfwU1gt3l8M7sCJSpEkOkpuQgJ8D+xzJS36VXBJQMMP9nAPp + s+x/rGFplsPMsXEFFiwvR1+FJZwzlg2sJ91bLGZQ7vn74MfsGrxpiJwllRThJyT7 + C9V0sjb5trT2lEqZlP2dRSJYt7aJ1crEcdGSl6RIKgxSV6Hk8dh/ZaTjrTwaKxVk + Po2IeEXy5xrR7DyonOQ6Yes0KOCmJB5yHkFlIVEnLm/HZXEtm3bPHsFgTZuInyBC + OMXpUESuVZIw8YK+Vd6AExGPPwZ4n5I/sCDxWII9owIj3LeLzdUG6JoroahhGmo8 + rgpbJpPnS+VgryQ/raUQjqDzDCuE9vKXKBlSUqK6H9A+NMc0mme7M8/GX7T7ewFG + UB/xsdrcO4yXjqHnAe0yLf8epDjChh76bYqwwinVrmfcNcRxFVJZW2z0gGdgkOkO + LaVVb9ggPV2SNAHbN4A+St/iRYR5awIDAQABAoICAQCTxuixQ/wbW8IbEWcgeyHD + LkaPndGO6jyVeF73GvL+MDRFuj558NvpNLfqzvTWVf9AnQGMd5Xs9oGegRHu7Csp + 3ucp+moBYv7DT14+jtXQKOgGJpDqSqfS1RUKb/TBRXNDLGy02UScziWoAdE33zmf + UraVNwW8z1crxKA3yVw2Na++UqhGQlVLAbfXucqnJLVtNWKpkVQlezUgcfmFovsm + Iut+9MjI6/sZAqdXTLKuCKo0XjWzNKwnRecE0CYsCwzc80MvFYEiwQi1C0kwoouC + iOi8MKM/jDok+5/a3nQ7X+/ho5sbApNCJpfSXAK9YOJ3ju93+RjNuvORfp4/sW3W + OGXw6X30Ym7WS/7oYuwEILyqdyNOvKU7a+17d/W/YA60NOdA4iJI3aTfYFMD3l14 + Da+D/wkTlEN3Ye7GN21A9AsZwWWiT9G5FOxWWVv7nTPG+Ix5ewehQWt/3DxhSizR + inMBizL5xpwx9LRWHnXX277lChYmPFAAMXINl1hnX6s0EY9pSDHN0IddibJkNKBD + m1CN37rqxoXQz4zoAyJGfQVkakqe16ayqI9yuQwO6AUkZcD5DYQdz9QYOTnYrQc6 + 6haC3D0Fmqg1s4v+6gpxZA/qTri0gVl/v/NN4Mk2/qWtK33imOedgD+5LXhZdBgJ + Mqn53AErG/AT622jvSb5UQKCAQEA/DTGLh0Ct97PCm+c+PxRFyieaHNJLWENKyxp + HoWGHfp2Bvt2Vphoi7GpRCM/yta4vCZgZmeWTQ0yBg6iPVPRA6Ho5hqh9OkUYVoh + prL3JsIU20jTutYjo2aefO4qXnJfkkXxNO2FElUHDTwtWdlGJQKvlUJwTv6xO19v + bQQkhZSpri6gIpi5Nkm2SGEtDofRJ+F6ThbQibEatL6DR00dh39MYQz+tZP5olzn + kX5bHEBWB7gy+YxTGF8FdlCSQTBBtNSKsAv3Cxj4qEHm+fu09vnH6fOZKenT2nXD + 5QE/RpgQzLV1TumCjqLzqwp7bbzH+4mjsXpF3KHBZwnhMnDIRwKCAQEA63wYzjBy + no0GBBz0hOWrOwQ/AjUHfi47o3Xvl4RBjZclM171HKH7oMCnQvVKTNq8jvakCZjc + UI6i+H4R6aokiFS2xGbC2H3ZlSMFNwhb2xUs/C4Nr7JSOWZBtDy5QBspUsp26f7m + 9VNVRzCmnxWV9be/1TxHDzDhslNlL5TMejbMorWnrtNG41KWwGtwvv2gApr3894j + eJNOh0WGfsMkXUM6+4v4WcCGrdV8Cr6Nvu96ZZe2PWu2dANtAfnxqogXXCoFE6r1 + vie7hFSfJ2QR/vEbanED4pYGTtGYP1oseScx0u0hLhGLGccVBUNZlRbox4rIOELI + v9MLuiOL4YX7vQKCAQAGzMl3HtMe8AP3DRFXaT4qeK7ktA8KCS7YtibTatg14LXj + 9E25gfx3n7+nlae3qVhrwkEhIbPcuflaTnSzYJonFet4oMkzGEGzakG0A+lEA0Ga + s/j5daKaWj71sVo1F7JZ+EbLnYfT+bTp93BllsUcZFkllhf/GUDgD++qKc1uSJbW + mm044ZNE0nH2u6ACX0kVYS/yAQ14WO0WaHiTqJGeQKFnkHkhni7B4O1hb923AkkP + hjjhn5Xx90Xnbb6zwUBURtLCcmAjzXWO29AFd3Lmoc9xEF9V0PckUb6JYyI4ngr9 + 6fqSuRsLC3u0ZeD0EX322zwtodVWYIodZBfNS1srAoIBAQCjTUPGeUKDQTjS0WGg + Z8T/AErRtQSlNFqXWMn2QPlUv2RE460HVi2xpOhZPtFvyqDIY7IOFbtzAfdya7rw + V9VN1bGJMdodV+jzy31qVJmerGit2SIUnYz30TnvS80L78oQZ+dfDi4MIuYYoFxs + JgQAipS1wz9kAXoCuGKLRJ0og6gVjfPjARE/w55XgiqFyEyWgfFBZOMkUsM6e7Rx + Y9Jr+puEpeRsGV9MXafPq6WQq3It0a/HmFLG0TlfDX3RzN6mQ12R7hTM8bDQa/6S + yorQSVPB1O3kzDVDo4X5KQd+XPfoVhmUYQYdsjmZlMMi6Og0uMFwgp/Epw6S3uO6 + WbfhAoIBAQCOp4iIc87GyxWL8u6HrJaqmFlqkfou0hI+y9h6FfzsBYU6y3+gRYdF + wr2S9EUAb80kEQ1v0pt9417NOGc1pmYjKCZmDZ7qeGCGk2PR0U59+xJetXBWWhbq + 5JxcwdRYoHyrmC/LINxzzqYOQbQevbW0zcEskeKfJsOtj9WJt6U9B1YZbE8pu2QV + xjvb+YekD2R+n/umV6eiaGfDau+EWudYVTqY0mR7y9hTiFR/KnqSsy2BUjljpacS + XBQO4ig7vY8+1+L3w2xpTN95/rXAvB4BbO/DLea9ArikePoSJ+bVTj0YwrKBghep + kOvbvVANrpsunlSAcpXm1qkV+G+xPnyJ + -----END PRIVATE KEY----- diff --git a/cmd/flux/testdata/create_secret/tls/secret-tls.yaml b/cmd/flux/testdata/create_secret/tls/secret-tls.yaml index 359af2b7..047c9221 100644 --- a/cmd/flux/testdata/create_secret/tls/secret-tls.yaml +++ b/cmd/flux/testdata/create_secret/tls/secret-tls.yaml @@ -5,7 +5,19 @@ metadata: name: certs namespace: my-namespace stringData: - certFile: | + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw + GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw + NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49 + AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr + RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE + AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ + 4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O + 1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb + OD8EjjCMY69RMO0= + -----END CERTIFICATE----- + tls.crt: | -----BEGIN CERTIFICATE----- MIIFazCCA1OgAwIBAgIUT84jeO/ncOrqI+FY05Fzbg8Ed7MwDQYJKoZIhvcNAQEL BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM @@ -37,7 +49,7 @@ stringData: KvJGPigx4fAwfR/d78o5ngwoH9m+8FUg8+qllJ8XgIbl/VXKTk3G4ceOm4eBmrel DwWuBhELSjtXWPWhMlkiebgejDbAear53Lia2Cc43zx/KuhMHBTlKY/vY4F2YiI= -----END CERTIFICATE----- - keyFile: | + tls.key: | -----BEGIN PRIVATE KEY----- MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDn/rPsZ74oypiw CzLlx57zplTiCi/WLSF+MmLGuTvMEQnV+OND2zFgvDIV/vFs3brkd6rLVI4NcdgS @@ -90,3 +102,4 @@ stringData: XBQO4ig7vY8+1+L3w2xpTN95/rXAvB4BbO/DLea9ArikePoSJ+bVTj0YwrKBghep kOvbvVANrpsunlSAcpXm1qkV+G+xPnyJ -----END PRIVATE KEY----- +type: kubernetes.io/tls diff --git a/cmd/flux/testdata/create_secret/tls/test-ca.pem b/cmd/flux/testdata/create_secret/tls/test-ca.pem new file mode 100644 index 00000000..080bd24e --- /dev/null +++ b/cmd/flux/testdata/create_secret/tls/test-ca.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBhzCCAS2gAwIBAgIUdsAtiX3gN0uk7ddxASWYE/tdv0wwCgYIKoZIzj0EAwIw +GTEXMBUGA1UEAxMOZXhhbXBsZS5jb20gQ0EwHhcNMjAwNDE3MDgxODAwWhcNMjUw +NDE2MDgxODAwWjAZMRcwFQYDVQQDEw5leGFtcGxlLmNvbSBDQTBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABK7h/5D8bV93MmEdhu02JsS6ugB8s6PzRl3PV4xs3Sbr +RNkkM59+x3b0iWx/i76qPYpNLoiVUVXQmA9Y+4DbMxijUzBRMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQGyUiU1QEZiMAqjsnIYTwZ +4yp5wzAPBgNVHREECDAGhwR/AAABMAoGCCqGSM49BAMCA0gAMEUCIQDzdtvKdE8O +1+WRTZ9MuSiFYcrEz7Zne7VXouDEKqKEigIgM4WlbDeuNCKbqhqj+xZV0pa3rweb +OD8EjjCMY69RMO0= +-----END CERTIFICATE----- diff --git a/cmd/flux/testdata/create_source_chart/basic.yaml b/cmd/flux/testdata/create_source_chart/basic.yaml new file mode 100644 index 00000000..fc7dc51d --- /dev/null +++ b/cmd/flux/testdata/create_source_chart/basic.yaml @@ -0,0 +1,14 @@ +✚ generating HelmChart source +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmChart +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + chart: podinfo + interval: 0s + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo diff --git a/cmd/flux/testdata/create_source_chart/setup-source.yaml b/cmd/flux/testdata/create_source_chart/setup-source.yaml new file mode 100644 index 00000000..18787c72 --- /dev/null +++ b/cmd/flux/testdata/create_source_chart/setup-source.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 1m0s + provider: generic + type: oci + url: oci://ghcr.io/stefanprodan/charts diff --git a/cmd/flux/testdata/create_source_chart/verify_basic.yaml b/cmd/flux/testdata/create_source_chart/verify_basic.yaml new file mode 100644 index 00000000..46ce5ded --- /dev/null +++ b/cmd/flux/testdata/create_source_chart/verify_basic.yaml @@ -0,0 +1,16 @@ +✚ generating HelmChart source +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmChart +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + chart: podinfo + interval: 0s + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo + verify: + provider: cosign diff --git a/cmd/flux/testdata/create_source_chart/verify_complete.yaml b/cmd/flux/testdata/create_source_chart/verify_complete.yaml new file mode 100644 index 00000000..73b32b02 --- /dev/null +++ b/cmd/flux/testdata/create_source_chart/verify_complete.yaml @@ -0,0 +1,19 @@ +✚ generating HelmChart source +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmChart +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + chart: podinfo + interval: 0s + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo + verify: + matchOIDCIdentity: + - issuer: foo + subject: bar + provider: cosign diff --git a/cmd/flux/testdata/create_source_git/export.golden b/cmd/flux/testdata/create_source_git/export.golden new file mode 100644 index 00000000..b23aaaab --- /dev/null +++ b/cmd/flux/testdata/create_source_git/export.golden @@ -0,0 +1,14 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: default +spec: + ignore: |- + .cosign + non-existent-dir/ + interval: 1m0s + ref: + branch: master + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/create_source_git/source-git-branch-commit.yaml b/cmd/flux/testdata/create_source_git/source-git-branch-commit.yaml new file mode 100644 index 00000000..d8ce277e --- /dev/null +++ b/cmd/flux/testdata/create_source_git/source-git-branch-commit.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + ref: + branch: main + commit: c88a2f41 + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/create_source_git/source-git-branch.yaml b/cmd/flux/testdata/create_source_git/source-git-branch.yaml new file mode 100644 index 00000000..ecf12ef2 --- /dev/null +++ b/cmd/flux/testdata/create_source_git/source-git-branch.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + ref: + branch: test + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/create_source_git/source-git-commit.yaml b/cmd/flux/testdata/create_source_git/source-git-commit.yaml new file mode 100644 index 00000000..87e0a733 --- /dev/null +++ b/cmd/flux/testdata/create_source_git/source-git-commit.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + ref: + commit: c88a2f41 + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/create_source_git/source-git-refname.yaml b/cmd/flux/testdata/create_source_git/source-git-refname.yaml new file mode 100644 index 00000000..0009c36b --- /dev/null +++ b/cmd/flux/testdata/create_source_git/source-git-refname.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + ref: + name: refs/heads/main + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/create_source_git/source-git-semver.yaml b/cmd/flux/testdata/create_source_git/source-git-semver.yaml new file mode 100644 index 00000000..3fdbb7e4 --- /dev/null +++ b/cmd/flux/testdata/create_source_git/source-git-semver.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + ref: + semver: v1.01 + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/create_source_git/source-git-tag.yaml b/cmd/flux/testdata/create_source_git/source-git-tag.yaml new file mode 100644 index 00000000..6ea46324 --- /dev/null +++ b/cmd/flux/testdata/create_source_git/source-git-tag.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + ref: + tag: test + url: https://github.com/stefanprodan/podinfo diff --git a/cmd/flux/testdata/create_source_helm/https.golden b/cmd/flux/testdata/create_source_helm/https.golden new file mode 100644 index 00000000..02da00a5 --- /dev/null +++ b/cmd/flux/testdata/create_source_helm/https.golden @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 5m0s + url: https://stefanprodan.github.io/charts/podinfo diff --git a/cmd/flux/testdata/create_source_helm/oci-with-secret.golden b/cmd/flux/testdata/create_source_helm/oci-with-secret.golden new file mode 100644 index 00000000..d01f6f72 --- /dev/null +++ b/cmd/flux/testdata/create_source_helm/oci-with-secret.golden @@ -0,0 +1,12 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 5m0s + secretRef: + name: creds + type: oci + url: oci://ghcr.io/stefanprodan/charts/podinfo diff --git a/cmd/flux/testdata/create_source_helm/oci.golden b/cmd/flux/testdata/create_source_helm/oci.golden new file mode 100644 index 00000000..92f09558 --- /dev/null +++ b/cmd/flux/testdata/create_source_helm/oci.golden @@ -0,0 +1,10 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmRepository +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 5m0s + type: oci + url: oci://ghcr.io/stefanprodan/charts/podinfo diff --git a/cmd/flux/testdata/diff-artifact/deployment-diff.yaml b/cmd/flux/testdata/diff-artifact/deployment-diff.yaml new file mode 100644 index 00000000..350d4c1b --- /dev/null +++ b/cmd/flux/testdata/diff-artifact/deployment-diff.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-diff + namespace: default +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/cmd/flux/testdata/diff-artifact/deployment.yaml b/cmd/flux/testdata/diff-artifact/deployment.yaml new file mode 100644 index 00000000..3910da6a --- /dev/null +++ b/cmd/flux/testdata/diff-artifact/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + minReadySeconds: 3 + revisionHistoryLimit: 5 + progressDeadlineSeconds: 60 + strategy: + rollingUpdate: + maxUnavailable: 0 + type: RollingUpdate + selector: + matchLabels: + app: podinfo + template: + metadata: + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9797" + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: ghcr.io/stefanprodan/podinfo:6.0.10 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 9898 + protocol: TCP + - name: http-metrics + containerPort: 9797 + protocol: TCP + - name: grpc + containerPort: 9999 + protocol: TCP + command: + - ./podinfo + - --port=9898 + - --port-metrics=9797 + - --grpc-port=9999 + - --grpc-service-name=podinfo + - --level=info + - --random-delay=false + - --random-error=false + env: + - name: PODINFO_UI_COLOR + value: "#34577c" + livenessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/healthz + initialDelaySeconds: 5 + timeoutSeconds: 5 + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 + resources: + limits: + cpu: 2000m + memory: 512Mi + requests: + cpu: 100m + memory: 64Mi diff --git a/cmd/flux/testdata/diff-artifact/success.golden b/cmd/flux/testdata/diff-artifact/success.golden new file mode 100644 index 00000000..3fbafa68 --- /dev/null +++ b/cmd/flux/testdata/diff-artifact/success.golden @@ -0,0 +1 @@ +✔ no changes detected diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden b/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden index 098497fc..7856dd58 100644 --- a/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden +++ b/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden @@ -1,4 +1,6 @@ ► HorizontalPodAutoscaler/default/podinfo created ► Service/default/podinfo created +► Secret/default/docker-secret created +► Secret/default/secret-basic-auth-stringdata created ► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-dockerconfigjson-sops-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-dockerconfigjson-sops-secret.golden new file mode 100644 index 00000000..17972fff --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-dockerconfigjson-sops-secret.golden @@ -0,0 +1,6 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/secret-basic-auth-stringdata created +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden index 38269544..5608c24e 100644 --- a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-sops-secret.golden @@ -1,6 +1,8 @@ ► Deployment/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created ► Service/default/podinfo created +► Secret/default/docker-secret created +► Secret/default/secret-basic-auth-stringdata created ► Secret/default/podinfo-token-77t89m9b67 drifted data diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden index ac76978f..bfce21b5 100644 --- a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden @@ -1,11 +1,13 @@ ► Deployment/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created ► Service/default/podinfo created +► Secret/default/docker-secret created +► Secret/default/secret-basic-auth-stringdata created ► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/db-user-pass-bkbd782d2c drifted data.password ± value change - - ****** - + ***** + - *** (before) + + *** (after) diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden index d65e5968..a9fe09f2 100644 --- a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden @@ -2,15 +2,12 @@ ► HorizontalPodAutoscaler/default/podinfo created ► Service/default/podinfo drifted -spec.ports - ⇆ order changed - - http, grpc - + grpc, http - spec.ports.http.port ± value change - 9899 + 9898 +► Secret/default/docker-secret created +► Secret/default/secret-basic-auth-stringdata created ► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-stringdata-sops-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-stringdata-sops-secret.golden new file mode 100644 index 00000000..6c79b906 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-stringdata-sops-secret.golden @@ -0,0 +1,12 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo created +► Secret/default/docker-secret created +► Secret/default/secret-basic-auth-stringdata drifted + +data + - one map entry removed: + one map entry added: + username1: "*****" username: "*****" + +► Secret/default/podinfo-token-77t89m9b67 created +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden index 033db67e..6c502226 100644 --- a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden @@ -1,4 +1,6 @@ ► Deployment/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created ► Service/default/podinfo created +► Secret/default/docker-secret created +► Secret/default/secret-basic-auth-stringdata created ► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/dockerconfigjson-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/dockerconfigjson-sops-secret.yaml new file mode 100644 index 00000000..1a689e92 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/dockerconfigjson-sops-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + .dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ== +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: docker-secret + namespace: default +type: kubernetes.io/dockerconfigjson diff --git a/cmd/flux/testdata/diff-kustomization/flux-kustomization-multiobj.yaml b/cmd/flux/testdata/diff-kustomization/flux-kustomization-multiobj.yaml new file mode 100644 index 00000000..53b03336 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/flux-kustomization-multiobj.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: podinfo + +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: podinfo + targetNamespace: default diff --git a/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden b/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden index da1c23da..5c320a1b 100644 --- a/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden +++ b/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden @@ -1,5 +1,7 @@ ► Deployment/default/podinfo created ► HorizontalPodAutoscaler/default/podinfo created ► Service/default/podinfo created +► Secret/default/docker-secret created +► Secret/default/secret-basic-auth-stringdata created ► Secret/default/podinfo-token-77t89m9b67 created ► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/stringdata-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/stringdata-sops-secret.yaml new file mode 100644 index 00000000..8fa3459d --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/stringdata-sops-secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: secret-basic-auth-stringdata + namespace: default +stringData: + password: KipTT1BTKio= + username1: KipTT1BTKio= +type: kubernetes.io/basic-auth diff --git a/cmd/flux/testdata/envsubst/file.gold b/cmd/flux/testdata/envsubst/file.gold new file mode 100644 index 00000000..58ac924e --- /dev/null +++ b/cmd/flux/testdata/envsubst/file.gold @@ -0,0 +1,10 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: test + namespace: flux-system +spec: + ref: + branch: main + interval: 5m + url: ssh://git@github.com/example/test diff --git a/cmd/flux/testdata/envsubst/file.yaml b/cmd/flux/testdata/envsubst/file.yaml new file mode 100644 index 00000000..e54aa24f --- /dev/null +++ b/cmd/flux/testdata/envsubst/file.yaml @@ -0,0 +1,10 @@ +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: ${REPO_NAME} + namespace: ${REPO_NAMESPACE:=flux-system} +spec: + ref: + branch: main + interval: 5m + url: ssh://git@github.com/example/${REPO_NAME} diff --git a/cmd/flux/testdata/export/alert.yaml b/cmd/flux/testdata/export/alert.yaml index a8a7883a..e5ffba02 100644 --- a/cmd/flux/testdata/export/alert.yaml +++ b/cmd/flux/testdata/export/alert.yaml @@ -1,5 +1,5 @@ --- -apiVersion: notification.toolkit.fluxcd.io/v1beta1 +apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Alert metadata: name: flux-system @@ -14,4 +14,3 @@ spec: providerRef: name: slack summary: Slacktest Notification - diff --git a/cmd/flux/testdata/export/bucket.yaml b/cmd/flux/testdata/export/bucket.yaml index c998daf4..5547ee94 100644 --- a/cmd/flux/testdata/export/bucket.yaml +++ b/cmd/flux/testdata/export/bucket.yaml @@ -1,5 +1,5 @@ --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: Bucket metadata: name: flux-system @@ -11,4 +11,3 @@ spec: provider: aws region: us-east-1 timeout: 30s - diff --git a/cmd/flux/testdata/export/git-repo.yaml b/cmd/flux/testdata/export/git-repo.yaml index ba7c3b6c..58743c74 100644 --- a/cmd/flux/testdata/export/git-repo.yaml +++ b/cmd/flux/testdata/export/git-repo.yaml @@ -1,16 +1,14 @@ --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: - gitImplementation: go-git interval: 5m0s ref: branch: main secretRef: name: flux-system - timeout: 20s + timeout: 1m0s url: ssh://git@github.com/example/repo - diff --git a/cmd/flux/testdata/export/helm-chart.yaml b/cmd/flux/testdata/export/helm-chart.yaml new file mode 100644 index 00000000..7300917c --- /dev/null +++ b/cmd/flux/testdata/export/helm-chart.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmChart +metadata: + name: flux-system + namespace: {{ .fluxns }} +spec: + chart: podinfo + interval: 1m0s + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo + version: '*' diff --git a/cmd/flux/testdata/export/helm-release.yaml b/cmd/flux/testdata/export/helm-release.yaml index 8f861110..cabd8db5 100644 --- a/cmd/flux/testdata/export/helm-release.yaml +++ b/cmd/flux/testdata/export/helm-release.yaml @@ -1,5 +1,5 @@ --- -apiVersion: helm.toolkit.fluxcd.io/v2beta1 +apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: flux-system @@ -15,4 +15,3 @@ spec: namespace: {{ .fluxns }} version: '*' interval: 5m0s - diff --git a/cmd/flux/testdata/export/helm-repo.yaml b/cmd/flux/testdata/export/helm-repo.yaml index 1503ebf8..685b9079 100644 --- a/cmd/flux/testdata/export/helm-repo.yaml +++ b/cmd/flux/testdata/export/helm-repo.yaml @@ -1,11 +1,11 @@ --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: interval: 5m0s + provider: generic timeout: 1m0s url: https://stefanprodan.github.io/podinfo - diff --git a/cmd/flux/testdata/export/image-policy.yaml b/cmd/flux/testdata/export/image-policy.yaml index 4a1bcbca..8feff350 100644 --- a/cmd/flux/testdata/export/image-policy.yaml +++ b/cmd/flux/testdata/export/image-policy.yaml @@ -1,5 +1,5 @@ --- -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImagePolicy metadata: name: flux-system @@ -10,4 +10,3 @@ spec: policy: semver: range: 5.0.x - diff --git a/cmd/flux/testdata/export/image-repo.yaml b/cmd/flux/testdata/export/image-repo.yaml index 959126be..5263245c 100644 --- a/cmd/flux/testdata/export/image-repo.yaml +++ b/cmd/flux/testdata/export/image-repo.yaml @@ -1,10 +1,12 @@ --- -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageRepository metadata: name: flux-system namespace: {{ .fluxns }} spec: + exclusionList: + - ^.*\.sig$ image: ghcr.io/test/podinfo interval: 1m0s - + provider: generic diff --git a/cmd/flux/testdata/export/image-update.yaml b/cmd/flux/testdata/export/image-update.yaml index 35566278..c78757aa 100644 --- a/cmd/flux/testdata/export/image-update.yaml +++ b/cmd/flux/testdata/export/image-update.yaml @@ -1,5 +1,5 @@ --- -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageUpdateAutomation metadata: name: flux-system @@ -17,4 +17,3 @@ spec: update: path: ./clusters/my-cluster strategy: Setters - diff --git a/cmd/flux/testdata/export/ks.yaml b/cmd/flux/testdata/export/ks.yaml index cd6085d2..5130ab8f 100644 --- a/cmd/flux/testdata/export/ks.yaml +++ b/cmd/flux/testdata/export/ks.yaml @@ -1,5 +1,5 @@ --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: flux-system @@ -11,4 +11,3 @@ spec: sourceRef: kind: GitRepository name: flux-system - diff --git a/cmd/flux/testdata/export/objects.yaml b/cmd/flux/testdata/export/objects.yaml index 6d46e187..a512e62c 100644 --- a/cmd/flux/testdata/export/objects.yaml +++ b/cmd/flux/testdata/export/objects.yaml @@ -4,7 +4,7 @@ kind: Namespace metadata: name: {{ .fluxns }} --- -apiVersion: notification.toolkit.fluxcd.io/v1beta1 +apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Provider metadata: name: slack @@ -14,7 +14,7 @@ spec: channel: 'A channel with spacess' address: https://hooks.slack.com/services/mock --- -apiVersion: notification.toolkit.fluxcd.io/v1beta1 +apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Alert metadata: name: flux-system @@ -30,7 +30,7 @@ spec: - kind: "Kustomization" name: "*" --- -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageRepository metadata: name: flux-system @@ -39,7 +39,7 @@ spec: image: ghcr.io/test/podinfo interval: 1m0s --- -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImagePolicy metadata: name: flux-system @@ -51,7 +51,7 @@ spec: semver: range: 5.0.x --- -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageUpdateAutomation metadata: name: flux-system @@ -71,7 +71,7 @@ spec: path: ./clusters/my-cluster strategy: Setters --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: name: flux-system @@ -84,7 +84,7 @@ spec: interval: 5m url: ssh://git@github.com/example/repo --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: flux-system @@ -97,7 +97,7 @@ spec: interval: 5m prune: true --- -apiVersion: notification.toolkit.fluxcd.io/v1beta1 +apiVersion: notification.toolkit.fluxcd.io/v1 kind: Receiver metadata: name: flux-system @@ -114,7 +114,7 @@ spec: name: flux-system namespace: flux-system --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: name: flux-system @@ -124,7 +124,21 @@ spec: timeout: 1m0s url: https://stefanprodan.github.io/podinfo --- -apiVersion: helm.toolkit.fluxcd.io/v2beta1 +apiVersion: source.toolkit.fluxcd.io/v1 +kind: HelmChart +metadata: + name: flux-system + namespace: {{ .fluxns }} +spec: + chart: podinfo + interval: 1m0s + reconcileStrategy: ChartVersion + sourceRef: + kind: HelmRepository + name: podinfo + version: '*' +--- +apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: flux-system @@ -139,7 +153,7 @@ spec: name: flux-systen namespace: {{ .fluxns }} --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: Bucket metadata: name: flux-system diff --git a/cmd/flux/testdata/export/provider.yaml b/cmd/flux/testdata/export/provider.yaml index ce91902f..c3ca08e3 100644 --- a/cmd/flux/testdata/export/provider.yaml +++ b/cmd/flux/testdata/export/provider.yaml @@ -1,5 +1,5 @@ --- -apiVersion: notification.toolkit.fluxcd.io/v1beta1 +apiVersion: notification.toolkit.fluxcd.io/v1beta3 kind: Provider metadata: name: slack @@ -8,4 +8,3 @@ spec: address: https://hooks.slack.com/services/mock channel: A channel with spacess type: slack - diff --git a/cmd/flux/testdata/export/receiver.yaml b/cmd/flux/testdata/export/receiver.yaml index e4fefadb..2f221390 100644 --- a/cmd/flux/testdata/export/receiver.yaml +++ b/cmd/flux/testdata/export/receiver.yaml @@ -1,5 +1,5 @@ --- -apiVersion: notification.toolkit.fluxcd.io/v1beta1 +apiVersion: notification.toolkit.fluxcd.io/v1 kind: Receiver metadata: name: flux-system @@ -8,6 +8,7 @@ spec: events: - ping - push + interval: 10m0s resources: - kind: GitRepository name: flux-system @@ -15,4 +16,3 @@ spec: secretRef: name: webhook-token type: github - diff --git a/cmd/flux/testdata/get/get.golden b/cmd/flux/testdata/get/get.golden new file mode 100644 index 00000000..5d741ccc --- /dev/null +++ b/cmd/flux/testdata/get/get.golden @@ -0,0 +1,4 @@ +NAME REVISION SUSPENDED READY MESSAGE +podinfo main@sha1:696f056d False True Fetched revision: main@sha1:696f056d +podinfo-shard1 main@sha1:696f056d False True Fetched revision: main@sha1:696f056d +podinfo-shard2 main@sha1:696f056d False True Fetched revision: main@sha1:696f056d diff --git a/cmd/flux/testdata/get/get_label_one.golden b/cmd/flux/testdata/get/get_label_one.golden new file mode 100644 index 00000000..1adcb4ae --- /dev/null +++ b/cmd/flux/testdata/get/get_label_one.golden @@ -0,0 +1,2 @@ +NAME REVISION SUSPENDED READY MESSAGE +podinfo-shard1 main@sha1:696f056d False True Fetched revision: main@sha1:696f056d diff --git a/cmd/flux/testdata/get/get_label_two.golden b/cmd/flux/testdata/get/get_label_two.golden new file mode 100644 index 00000000..c001e63f --- /dev/null +++ b/cmd/flux/testdata/get/get_label_two.golden @@ -0,0 +1,2 @@ +NAME REVISION SUSPENDED READY MESSAGE +podinfo main@sha1:696f056d False True Fetched revision: main@sha1:696f056d diff --git a/cmd/flux/testdata/get/objects.yaml b/cmd/flux/testdata/get/objects.yaml new file mode 100644 index 00000000..42913c20 --- /dev/null +++ b/cmd/flux/testdata/get/objects.yaml @@ -0,0 +1,91 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: {{ .fluxns }} +spec: + ref: + branch: main + secretRef: + name: flux-system + url: ssh://git@github.com/example/repo + interval: 5m +status: + artifact: + lastUpdateTime: "2021-08-01T04:28:42Z" + revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f + path: "example" + url: "example" + conditions: + - lastTransitionTime: "2021-07-20T00:48:16Z" + message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' + reason: GitOperationSucceed + status: "True" + type: Ready +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + sharding.fluxcd.io/key: shard1 + name: podinfo-shard1 + namespace: {{ .fluxns }} +spec: + ref: + branch: main + secretRef: + name: flux-system + url: ssh://git@github.com/example/repo + interval: 5m +status: + artifact: + lastUpdateTime: "2021-08-01T04:28:42Z" + revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f + path: "example" + url: "example" + conditions: + - lastTransitionTime: "2021-07-20T00:48:16Z" + message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' + reason: GitOperationSucceed + status: "True" + type: Ready +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + sharding.fluxcd.io/key: shard2 + name: podinfo-shard2 + namespace: {{ .fluxns }} +spec: + ref: + branch: main + secretRef: + name: flux-system + url: ssh://git@github.com/example/repo + interval: 5m +status: + artifact: + lastUpdateTime: "2021-08-01T04:28:42Z" + revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f + path: "example" + url: "example" + conditions: + - lastTransitionTime: "2021-07-20T00:48:16Z" + message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' + reason: GitOperationSucceed + status: "True" + type: Ready diff --git a/cmd/flux/testdata/helmrelease/create_helmrelease_from_git.golden b/cmd/flux/testdata/helmrelease/create_helmrelease_from_git.golden index d5f35515..3b241421 100644 --- a/cmd/flux/testdata/helmrelease/create_helmrelease_from_git.golden +++ b/cmd/flux/testdata/helmrelease/create_helmrelease_from_git.golden @@ -3,4 +3,4 @@ ✔ HelmRelease created ◎ waiting for HelmRelease reconciliation ✔ HelmRelease thrfg is ready -✔ applied revision 6.0.0 +✔ applied revision 6.3.5 diff --git a/cmd/flux/testdata/helmrelease/create_source_git.golden b/cmd/flux/testdata/helmrelease/create_source_git.golden index e7db754b..7db60b29 100644 --- a/cmd/flux/testdata/helmrelease/create_source_git.golden +++ b/cmd/flux/testdata/helmrelease/create_source_git.golden @@ -3,4 +3,4 @@ ✔ GitRepository source created ◎ waiting for GitRepository source reconciliation ✔ GitRepository source reconciliation completed -✔ fetched revision: 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951 +✔ fetched revision: 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 diff --git a/cmd/flux/testdata/helmrelease/delete_helmrelease_from_git.golden b/cmd/flux/testdata/helmrelease/delete_helmrelease_from_git.golden index 8ef80f6e..f481ce67 100644 --- a/cmd/flux/testdata/helmrelease/delete_helmrelease_from_git.golden +++ b/cmd/flux/testdata/helmrelease/delete_helmrelease_from_git.golden @@ -1,2 +1,2 @@ -► deleting helmreleases thrfg in {{ .ns }} namespace -✔ helmreleases deleted +► deleting helmrelease thrfg in {{ .ns }} namespace +✔ helmrelease deleted diff --git a/cmd/flux/testdata/helmrelease/get_helmrelease_from_git.golden b/cmd/flux/testdata/helmrelease/get_helmrelease_from_git.golden index 1cfeacc3..8bd236dd 100644 --- a/cmd/flux/testdata/helmrelease/get_helmrelease_from_git.golden +++ b/cmd/flux/testdata/helmrelease/get_helmrelease_from_git.golden @@ -1,2 +1,2 @@ -NAME READY MESSAGE REVISION SUSPENDED -thrfg True Release reconciliation succeeded 6.0.0 False +NAME REVISION SUSPENDED READY MESSAGE +thrfg 6.3.5 False True Helm install succeeded for release thrfg-1/thrfg.v1 with chart podinfo@6.3.5 diff --git a/cmd/flux/testdata/helmrelease/reconcile_helmrelease_from_git.golden b/cmd/flux/testdata/helmrelease/reconcile_helmrelease_from_git.golden index a1cf24b4..2c2bca89 100644 --- a/cmd/flux/testdata/helmrelease/reconcile_helmrelease_from_git.golden +++ b/cmd/flux/testdata/helmrelease/reconcile_helmrelease_from_git.golden @@ -1,8 +1,12 @@ ► annotating GitRepository thrfg in {{ .ns }} namespace ✔ GitRepository annotated ◎ waiting for GitRepository reconciliation -✔ fetched revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951 +✔ fetched revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 +► annotating HelmChart {{ .ns }}-thrfg in {{ .ns }} namespace +✔ HelmChart annotated +◎ waiting for HelmChart reconciliation +✔ fetched revision 6.3.5 ► annotating HelmRelease thrfg in {{ .ns }} namespace ✔ HelmRelease annotated ◎ waiting for HelmRelease reconciliation -✔ applied revision 6.0.0 +✔ applied revision 6.3.5 diff --git a/cmd/flux/testdata/helmrelease/resume_helmrelease_from_git.golden b/cmd/flux/testdata/helmrelease/resume_helmrelease_from_git.golden index ad9727fe..0ceaf0d4 100644 --- a/cmd/flux/testdata/helmrelease/resume_helmrelease_from_git.golden +++ b/cmd/flux/testdata/helmrelease/resume_helmrelease_from_git.golden @@ -1,5 +1,5 @@ -► resuming helmreleases thrfg in {{ .ns }} namespace -✔ helmreleases resumed +► resuming helmrelease thrfg in {{ .ns }} namespace +✔ helmrelease resumed ◎ waiting for HelmRelease reconciliation -✔ HelmRelease reconciliation completed -✔ applied revision 6.0.0 +✔ HelmRelease thrfg reconciliation completed +✔ applied revision 6.3.5 diff --git a/cmd/flux/testdata/helmrelease/suspend_helmrelease_from_git.golden b/cmd/flux/testdata/helmrelease/suspend_helmrelease_from_git.golden index d2f36d24..894a1ad2 100644 --- a/cmd/flux/testdata/helmrelease/suspend_helmrelease_from_git.golden +++ b/cmd/flux/testdata/helmrelease/suspend_helmrelease_from_git.golden @@ -1,2 +1,2 @@ -► suspending helmreleases thrfg in {{ .ns }} namespace -✔ helmreleases suspended +► suspending helmrelease thrfg in {{ .ns }} namespace +✔ helmrelease suspended diff --git a/cmd/flux/testdata/image/get_image_policy_regex.golden b/cmd/flux/testdata/image/get_image_policy_regex.golden index 8486fb1f..9f654d4e 100644 --- a/cmd/flux/testdata/image/get_image_policy_regex.golden +++ b/cmd/flux/testdata/image/get_image_policy_regex.golden @@ -1,2 +1,2 @@ -NAME READY MESSAGE LATEST IMAGE -podinfo-regex True Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to: 5.0.0 ghcr.io/stefanprodan/podinfo:5.0.0 +NAME LATEST IMAGE READY MESSAGE +podinfo-regex ghcr.io/stefanprodan/podinfo:5.0.0 True Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to 5.0.0 diff --git a/cmd/flux/testdata/image/get_image_policy_semver.golden b/cmd/flux/testdata/image/get_image_policy_semver.golden index 6f8a17dd..7100445e 100644 --- a/cmd/flux/testdata/image/get_image_policy_semver.golden +++ b/cmd/flux/testdata/image/get_image_policy_semver.golden @@ -1,2 +1,2 @@ -NAME READY MESSAGE LATEST IMAGE -podinfo-semver True Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to: 5.0.3 ghcr.io/stefanprodan/podinfo:5.0.3 +NAME LATEST IMAGE READY MESSAGE +podinfo-semver ghcr.io/stefanprodan/podinfo:5.0.3 True Latest image tag for 'ghcr.io/stefanprodan/podinfo' resolved to 5.0.3 diff --git a/cmd/flux/testdata/kustomization/create_kustomization_from_git.golden b/cmd/flux/testdata/kustomization/create_kustomization_from_git.golden index 82fa5938..a328a1c3 100644 --- a/cmd/flux/testdata/kustomization/create_kustomization_from_git.golden +++ b/cmd/flux/testdata/kustomization/create_kustomization_from_git.golden @@ -3,4 +3,4 @@ ✔ Kustomization created ◎ waiting for Kustomization reconciliation ✔ Kustomization tkfg is ready -✔ applied revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951 +✔ applied revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 diff --git a/cmd/flux/testdata/kustomization/create_source_git.golden b/cmd/flux/testdata/kustomization/create_source_git.golden index e7db754b..7db60b29 100644 --- a/cmd/flux/testdata/kustomization/create_source_git.golden +++ b/cmd/flux/testdata/kustomization/create_source_git.golden @@ -3,4 +3,4 @@ ✔ GitRepository source created ◎ waiting for GitRepository source reconciliation ✔ GitRepository source reconciliation completed -✔ fetched revision: 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951 +✔ fetched revision: 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 diff --git a/cmd/flux/testdata/kustomization/delete_kustomization_from_git.golden b/cmd/flux/testdata/kustomization/delete_kustomization_from_git.golden index 2142286d..d12a671f 100644 --- a/cmd/flux/testdata/kustomization/delete_kustomization_from_git.golden +++ b/cmd/flux/testdata/kustomization/delete_kustomization_from_git.golden @@ -1,2 +1,2 @@ -► deleting kustomizations tkfg in {{ .ns }} namespace -✔ kustomizations deleted +► deleting kustomization tkfg in {{ .ns }} namespace +✔ kustomization deleted diff --git a/cmd/flux/testdata/kustomization/get_kustomization_from_git.golden b/cmd/flux/testdata/kustomization/get_kustomization_from_git.golden index a613aa34..99c0d2bc 100644 --- a/cmd/flux/testdata/kustomization/get_kustomization_from_git.golden +++ b/cmd/flux/testdata/kustomization/get_kustomization_from_git.golden @@ -1,2 +1,2 @@ -NAME READY MESSAGE REVISION SUSPENDED -tkfg True Applied revision: 6.0.0/627d5c4 6.0.0/627d5c4 False +NAME REVISION SUSPENDED READY MESSAGE +tkfg 6.3.5@sha1:67e2c98a False True Applied revision: 6.3.5@sha1:67e2c98a diff --git a/cmd/flux/testdata/kustomization/reconcile_kustomization_from_git.golden b/cmd/flux/testdata/kustomization/reconcile_kustomization_from_git.golden index e87dc391..b781eca7 100644 --- a/cmd/flux/testdata/kustomization/reconcile_kustomization_from_git.golden +++ b/cmd/flux/testdata/kustomization/reconcile_kustomization_from_git.golden @@ -1,8 +1,8 @@ ► annotating GitRepository tkfg in {{ .ns }} namespace ✔ GitRepository annotated ◎ waiting for GitRepository reconciliation -✔ fetched revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951 +✔ fetched revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 ► annotating Kustomization tkfg in {{ .ns }} namespace ✔ Kustomization annotated ◎ waiting for Kustomization reconciliation -✔ applied revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951 +✔ applied revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 diff --git a/cmd/flux/testdata/kustomization/resume_kustomization_from_git.golden b/cmd/flux/testdata/kustomization/resume_kustomization_from_git.golden index e24cd125..f8492110 100644 --- a/cmd/flux/testdata/kustomization/resume_kustomization_from_git.golden +++ b/cmd/flux/testdata/kustomization/resume_kustomization_from_git.golden @@ -1,5 +1,5 @@ -► resuming kustomizations tkfg in {{ .ns }} namespace -✔ kustomizations resumed +► resuming kustomization tkfg in {{ .ns }} namespace +✔ kustomization resumed ◎ waiting for Kustomization reconciliation -✔ Kustomization reconciliation completed -✔ applied revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951 +✔ Kustomization tkfg reconciliation completed +✔ applied revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 diff --git a/cmd/flux/testdata/kustomization/resume_kustomization_from_git_multiple_args.golden b/cmd/flux/testdata/kustomization/resume_kustomization_from_git_multiple_args.golden new file mode 100644 index 00000000..318f29f2 --- /dev/null +++ b/cmd/flux/testdata/kustomization/resume_kustomization_from_git_multiple_args.golden @@ -0,0 +1,2 @@ +► resuming kustomization tkfg in {{ .ns }} namespace +✔ kustomization resumed diff --git a/cmd/flux/testdata/kustomization/resume_kustomization_from_git_multiple_args_wait.golden b/cmd/flux/testdata/kustomization/resume_kustomization_from_git_multiple_args_wait.golden new file mode 100644 index 00000000..e0775140 --- /dev/null +++ b/cmd/flux/testdata/kustomization/resume_kustomization_from_git_multiple_args_wait.golden @@ -0,0 +1,6 @@ +► resuming kustomization tkfg in {{ .ns }} namespace +✔ kustomization resumed +✗ no Kustomization objects found in {{ .ns }} namespace +◎ waiting for Kustomization reconciliation +✔ Kustomization tkfg reconciliation completed +✔ applied revision 6.3.5@sha1:67e2c98a60dc92283531412a9e604dd4bae005a9 diff --git a/cmd/flux/testdata/kustomization/suspend_kustomization_from_git.golden b/cmd/flux/testdata/kustomization/suspend_kustomization_from_git.golden index 8a30f2a4..f82cd8ac 100644 --- a/cmd/flux/testdata/kustomization/suspend_kustomization_from_git.golden +++ b/cmd/flux/testdata/kustomization/suspend_kustomization_from_git.golden @@ -1,2 +1,2 @@ -► suspending kustomizations tkfg in {{ .ns }} namespace -✔ kustomizations suspended +► suspending kustomization tkfg in {{ .ns }} namespace +✔ kustomization suspended diff --git a/cmd/flux/testdata/kustomization/suspend_kustomization_from_git_multiple_args.golden b/cmd/flux/testdata/kustomization/suspend_kustomization_from_git_multiple_args.golden new file mode 100644 index 00000000..42abd122 --- /dev/null +++ b/cmd/flux/testdata/kustomization/suspend_kustomization_from_git_multiple_args.golden @@ -0,0 +1,4 @@ +► suspending kustomization tkfg in {{ .ns }} namespace +✔ kustomization suspended +✗ Kustomization foo not found in {{ .ns }} namespace +✗ Kustomization bar not found in {{ .ns }} namespace diff --git a/cmd/flux/testdata/logs/all-logs.txt b/cmd/flux/testdata/logs/all-logs.txt new file mode 100644 index 00000000..ca4d5f0f --- /dev/null +++ b/cmd/flux/testdata/logs/all-logs.txt @@ -0,0 +1,6 @@ +2022-08-02T12:55:34.419Z info GitRepository/podinfo.default - no changes since last reconcilation: observed revision +2022-08-02T12:56:04.679Z error GitRepository/flux-system.flux-system - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z error Kustomization/flux-system.flux-system - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z info Kustomization/podinfo.default - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z info GitRepository/podinfo.default - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z error Kustomization/podinfo.flux-system - no changes since last reconcilation: observed revision diff --git a/cmd/flux/testdata/logs/kind.txt b/cmd/flux/testdata/logs/kind.txt new file mode 100644 index 00000000..77c3cdd6 --- /dev/null +++ b/cmd/flux/testdata/logs/kind.txt @@ -0,0 +1,2 @@ +2022-08-02T12:56:34.961Z error Kustomization/flux-system.flux-system - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z error Kustomization/podinfo.flux-system - no changes since last reconcilation: observed revision diff --git a/cmd/flux/testdata/logs/log-level.txt b/cmd/flux/testdata/logs/log-level.txt new file mode 100644 index 00000000..070867ae --- /dev/null +++ b/cmd/flux/testdata/logs/log-level.txt @@ -0,0 +1,3 @@ +2022-08-02T12:56:04.679Z error GitRepository/flux-system.flux-system - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z error Kustomization/flux-system.flux-system - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z error Kustomization/podinfo.flux-system - no changes since last reconcilation: observed revision diff --git a/cmd/flux/testdata/logs/multiple-filters.txt b/cmd/flux/testdata/logs/multiple-filters.txt new file mode 100644 index 00000000..c82cf09c --- /dev/null +++ b/cmd/flux/testdata/logs/multiple-filters.txt @@ -0,0 +1 @@ +2022-08-02T12:56:34.961Z error Kustomization/podinfo.flux-system - no changes since last reconcilation: observed revision diff --git a/cmd/flux/testdata/logs/namespace.txt b/cmd/flux/testdata/logs/namespace.txt new file mode 100644 index 00000000..e5c4450f --- /dev/null +++ b/cmd/flux/testdata/logs/namespace.txt @@ -0,0 +1,3 @@ +2022-08-02T12:55:34.419Z info GitRepository/podinfo.default - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z info Kustomization/podinfo.default - no changes since last reconcilation: observed revision +2022-08-02T12:56:34.961Z info GitRepository/podinfo.default - no changes since last reconcilation: observed revision diff --git a/cmd/flux/testdata/oci/create_source_oci.golden b/cmd/flux/testdata/oci/create_source_oci.golden new file mode 100644 index 00000000..68b64df3 --- /dev/null +++ b/cmd/flux/testdata/oci/create_source_oci.golden @@ -0,0 +1,5 @@ +► applying OCIRepository +✔ OCIRepository created +◎ waiting for OCIRepository reconciliation +✔ OCIRepository reconciliation completed +✔ fetched revision: 6.3.5@sha256:6c959c51ccbb952e5fe4737563338a0aaf975675dcf812912cf09e5463181871 diff --git a/cmd/flux/testdata/oci/delete_oci.golden b/cmd/flux/testdata/oci/delete_oci.golden new file mode 100644 index 00000000..8e8599fa --- /dev/null +++ b/cmd/flux/testdata/oci/delete_oci.golden @@ -0,0 +1,2 @@ +► deleting source oci thrfg in {{ .ns }} namespace +✔ source oci deleted diff --git a/cmd/flux/testdata/oci/export.golden b/cmd/flux/testdata/oci/export.golden new file mode 100644 index 00000000..7a043b64 --- /dev/null +++ b/cmd/flux/testdata/oci/export.golden @@ -0,0 +1,11 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 10m0s + ref: + tag: 6.3.5 + url: oci://ghcr.io/stefanprodan/manifests/podinfo diff --git a/cmd/flux/testdata/oci/export_with_complete_verification.golden b/cmd/flux/testdata/oci/export_with_complete_verification.golden new file mode 100644 index 00000000..8b862a83 --- /dev/null +++ b/cmd/flux/testdata/oci/export_with_complete_verification.golden @@ -0,0 +1,16 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 0s + ref: + tag: 6.3.5 + url: oci://ghcr.io/stefanprodan/manifests/podinfo + verify: + matchOIDCIdentity: + - issuer: github + subject: stefanprodan + provider: cosign diff --git a/cmd/flux/testdata/oci/export_with_issuer.golden b/cmd/flux/testdata/oci/export_with_issuer.golden new file mode 100644 index 00000000..3abaf36a --- /dev/null +++ b/cmd/flux/testdata/oci/export_with_issuer.golden @@ -0,0 +1,16 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 0s + ref: + tag: 6.3.5 + url: oci://ghcr.io/stefanprodan/manifests/podinfo + verify: + matchOIDCIdentity: + - issuer: github + subject: "" + provider: cosign diff --git a/cmd/flux/testdata/oci/export_with_secret.golden b/cmd/flux/testdata/oci/export_with_secret.golden new file mode 100644 index 00000000..0b41116f --- /dev/null +++ b/cmd/flux/testdata/oci/export_with_secret.golden @@ -0,0 +1,13 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 10m0s + ref: + tag: 6.3.5 + secretRef: + name: creds + url: oci://ghcr.io/stefanprodan/manifests/podinfo diff --git a/cmd/flux/testdata/oci/export_with_subject.golden b/cmd/flux/testdata/oci/export_with_subject.golden new file mode 100644 index 00000000..93eca53b --- /dev/null +++ b/cmd/flux/testdata/oci/export_with_subject.golden @@ -0,0 +1,16 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 0s + ref: + tag: 6.3.5 + url: oci://ghcr.io/stefanprodan/manifests/podinfo + verify: + matchOIDCIdentity: + - issuer: "" + subject: stefanprodan + provider: cosign diff --git a/cmd/flux/testdata/oci/export_with_verify_secret.golden b/cmd/flux/testdata/oci/export_with_verify_secret.golden new file mode 100644 index 00000000..d7e94aad --- /dev/null +++ b/cmd/flux/testdata/oci/export_with_verify_secret.golden @@ -0,0 +1,15 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 10m0s + ref: + tag: 6.3.5 + url: oci://ghcr.io/stefanprodan/manifests/podinfo + verify: + provider: cosign + secretRef: + name: cosign-pub diff --git a/cmd/flux/testdata/oci/get_oci.golden b/cmd/flux/testdata/oci/get_oci.golden new file mode 100644 index 00000000..fb1db40b --- /dev/null +++ b/cmd/flux/testdata/oci/get_oci.golden @@ -0,0 +1,2 @@ +NAME REVISION SUSPENDED READY MESSAGE +thrfg 6.3.5@sha256:6c959c51 False True stored artifact for digest '6.3.5@sha256:6c959c51' diff --git a/cmd/flux/testdata/oci/reconcile_oci.golden b/cmd/flux/testdata/oci/reconcile_oci.golden new file mode 100644 index 00000000..ad42fed9 --- /dev/null +++ b/cmd/flux/testdata/oci/reconcile_oci.golden @@ -0,0 +1,4 @@ +► annotating OCIRepository thrfg in {{ .ns }} namespace +✔ OCIRepository annotated +◎ waiting for OCIRepository reconciliation +✔ fetched revision 6.3.5@sha256:6c959c51ccbb952e5fe4737563338a0aaf975675dcf812912cf09e5463181871 diff --git a/cmd/flux/testdata/oci/resume_oci.golden b/cmd/flux/testdata/oci/resume_oci.golden new file mode 100644 index 00000000..34a0deba --- /dev/null +++ b/cmd/flux/testdata/oci/resume_oci.golden @@ -0,0 +1,5 @@ +► resuming source oci thrfg in {{ .ns }} namespace +✔ source oci resumed +◎ waiting for OCIRepository reconciliation +✔ OCIRepository thrfg reconciliation completed +✔ fetched revision 6.3.5@sha256:6c959c51ccbb952e5fe4737563338a0aaf975675dcf812912cf09e5463181871 diff --git a/cmd/flux/testdata/oci/suspend_oci.golden b/cmd/flux/testdata/oci/suspend_oci.golden new file mode 100644 index 00000000..40a0420e --- /dev/null +++ b/cmd/flux/testdata/oci/suspend_oci.golden @@ -0,0 +1,2 @@ +► suspending source oci thrfg in {{ .ns }} namespace +✔ source oci suspended diff --git a/cmd/flux/testdata/trace/deployment.golden b/cmd/flux/testdata/trace/deployment.golden index 8412a50d..6702a0d6 100644 --- a/cmd/flux/testdata/trace/deployment.golden +++ b/cmd/flux/testdata/trace/deployment.golden @@ -5,21 +5,21 @@ Status: Managed by Flux --- HelmRelease: podinfo Namespace: {{ .ns }} -Revision: 6.0.0 -Status: Last reconciled at 2021-07-16 15:42:20 +0000 UTC +Revision: 6.3.5 +Status: Last reconciled at {{ .helmReleaseLastReconcile }} Message: Release reconciliation succeeded --- HelmChart: podinfo-podinfo Namespace: {{ .fluxns }} Chart: podinfo -Version: 6.0.0 -Revision: 6.0.0 -Status: Last reconciled at 2021-07-16 15:32:09 +0000 UTC -Message: Fetched revision: 6.0.0 +Version: 6.3.5 +Revision: 6.3.5 +Status: Last reconciled at {{ .helmChartLastReconcile }} +Message: Fetched revision: 6.3.5 --- HelmRepository: podinfo Namespace: {{ .fluxns }} URL: https://stefanprodan.github.io/podinfo -Revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56 -Status: Last reconciled at 2021-07-11 00:25:46 +0000 UTC -Message: Fetched revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56 +Revision: sha1:8411f23d07d3701f0e96e7d9e503b7936d7e1d56 +Status: Last reconciled at {{ .helmRepositoryLastReconcile }} +Message: Fetched revision: main@sha1:8411f23d07d3701f0e96e7d9e503b7936d7e1d56 diff --git a/cmd/flux/testdata/trace/deployment.yaml b/cmd/flux/testdata/trace/deployment.yaml index 4ea81141..4e28b8e9 100644 --- a/cmd/flux/testdata/trace/deployment.yaml +++ b/cmd/flux/testdata/trace/deployment.yaml @@ -34,7 +34,7 @@ spec: command: [ "echo hello world" ] image: busybox --- -apiVersion: helm.toolkit.fluxcd.io/v2beta1 +apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: labels: @@ -59,11 +59,9 @@ status: status: "True" type: Ready helmChart: {{ .fluxns }}/podinfo-podinfo - lastAppliedRevision: 6.0.0 - lastAttemptedRevision: 6.0.0 - lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531 + lastAttemptedRevision: 6.3.5 --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmChart metadata: name: podinfo-podinfo @@ -73,24 +71,24 @@ spec: sourceRef: kind: HelmRepository name: podinfo - version: 6.0.0 + version: 6.3.5 interval: 5m status: artifact: checksum: cf13ba96773d9a879cd052c86e73199b3f96c854 lastUpdateTime: "2021-08-01T04:42:55Z" - revision: 6.0.0 + revision: 6.3.5 path: "example" url: "example" conditions: - lastTransitionTime: "2021-07-16T15:32:09Z" - message: 'Fetched revision: 6.0.0' + message: 'Fetched revision: 6.3.5' reason: ChartPullSucceeded status: "True" type: Ready --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: HelmRepository metadata: labels: @@ -106,17 +104,17 @@ status: artifact: checksum: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56 lastUpdateTime: "2021-07-11T00:25:46Z" - revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56 + revision: sha1:8411f23d07d3701f0e96e7d9e503b7936d7e1d56 path: "example" url: "example" conditions: - lastTransitionTime: "2021-07-11T00:25:46Z" - message: 'Fetched revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56' + message: 'Fetched revision: main@sha1:8411f23d07d3701f0e96e7d9e503b7936d7e1d56' reason: IndexationSucceed status: "True" type: Ready --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infrastructure @@ -126,18 +124,17 @@ spec: sourceRef: kind: GitRepository name: flux-system - validation: client interval: 5m prune: true status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" - message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f' + message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: labels: @@ -146,7 +143,6 @@ metadata: name: flux-system namespace: {{ .fluxns }} spec: - gitImplementation: go-git ref: branch: main secretRef: diff --git a/cmd/flux/testdata/trace/helmrelease-oci.golden b/cmd/flux/testdata/trace/helmrelease-oci.golden new file mode 100644 index 00000000..01b314da --- /dev/null +++ b/cmd/flux/testdata/trace/helmrelease-oci.golden @@ -0,0 +1,21 @@ + +Object: HelmRelease/podinfo +Namespace: {{ .ns }} +Status: Managed by Flux +--- +Kustomization: infrastructure +Namespace: {{ .fluxns }} +Path: ./infrastructure +Revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f +Status: Last reconciled at {{ .kustomizationLastReconcile }} +Message: Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f +--- +OCIRepository: flux-system +Namespace: {{ .fluxns }} +URL: oci://ghcr.io/example/repo +Tag: 1.2.3 +Revision: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3 +Origin Revision: 6.1.6@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 +Origin Source: https://github.com/stefanprodan/podinfo.git +Status: Last reconciled at {{ .ociRepositoryLastReconcile }} +Message: stored artifact for digest 'sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3' diff --git a/cmd/flux/testdata/trace/helmrelease-oci.yaml b/cmd/flux/testdata/trace/helmrelease-oci.yaml new file mode 100644 index 00000000..d4ac8f72 --- /dev/null +++ b/cmd/flux/testdata/trace/helmrelease-oci.yaml @@ -0,0 +1,91 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .ns }} +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: infrastructure + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: {{ .ns }} +spec: + chart: + spec: + chart: podinfo + sourceRef: + kind: HelmRepository + name: podinfo + namespace: {{ .fluxns }} + interval: 5m +status: + conditions: + - lastTransitionTime: "2021-07-16T15:42:20Z" + message: Release reconciliation succeeded + reason: ReconciliationSucceeded + status: "True" + type: Ready + helmChart: {{ .fluxns }}/podinfo-podinfo + lastAppliedRevision: 6.3.5 + lastAttemptedRevision: 6.3.5 + lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531 +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: infrastructure + namespace: {{ .fluxns }} +spec: + path: ./infrastructure + sourceRef: + kind: OCIRepository + name: flux-system + interval: 5m + prune: false +status: + conditions: + - lastTransitionTime: "2021-08-01T04:52:56Z" + message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' + reason: ReconciliationSucceeded + status: "True" + type: Ready + lastAppliedRevision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: flux-system + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: flux-system + namespace: {{ .fluxns }} +spec: + interval: 10m0s + provider: generic + ref: + tag: 1.2.3 + timeout: 60s + url: oci://ghcr.io/example/repo +status: + artifact: + lastUpdateTime: "2022-08-10T10:07:59Z" + metadata: + org.opencontainers.image.revision: 6.1.6@sha1:450796ddb2ab6724ee1cc32a4be56da032d1cca0 + org.opencontainers.image.source: https://github.com/stefanprodan/podinfo.git + path: "example" + revision: sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3 + url: "example" + conditions: + - lastTransitionTime: "2021-07-20T00:48:16Z" + message: "stored artifact for digest 'sha256:dbdb109711ffb3be77504d2670dbe13c24dd63d8d7f1fb489d350e5bfe930dd3'" + reason: Succeed + status: "True" + type: Ready diff --git a/cmd/flux/testdata/trace/helmrelease.golden b/cmd/flux/testdata/trace/helmrelease.golden index 29de7a72..384e2181 100644 --- a/cmd/flux/testdata/trace/helmrelease.golden +++ b/cmd/flux/testdata/trace/helmrelease.golden @@ -1,19 +1,19 @@ -Object: HelmRelease/podinfo -Namespace: {{ .ns }} -Status: Managed by Flux +Object: HelmRelease/podinfo +Namespace: {{ .ns }} +Status: Managed by Flux --- -Kustomization: infrastructure -Namespace: {{ .fluxns }} -Path: ./infrastructure -Revision: main/696f056df216eea4f9401adbee0ff744d4df390f -Status: Last reconciled at 2021-08-01 04:52:56 +0000 UTC -Message: Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f +Kustomization: infrastructure +Namespace: {{ .fluxns }} +Path: ./infrastructure +Revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f +Status: Last reconciled at {{ .kustomizationLastReconcile }} +Message: Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f --- -GitRepository: flux-system -Namespace: {{ .fluxns }} -URL: ssh://git@github.com/example/repo -Branch: main -Revision: main/696f056df216eea4f9401adbee0ff744d4df390f -Status: Last reconciled at 2021-07-20 00:48:16 +0000 UTC -Message: Fetched revision: main/696f056df216eea4f9401adbee0ff744d4df390f +GitRepository: flux-system +Namespace: {{ .fluxns }} +URL: ssh://git@github.com/example/repo +Branch: main +Revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f +Status: Last reconciled at {{ .gitRepositoryLastReconcile }} +Message: Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f diff --git a/cmd/flux/testdata/trace/helmrelease.yaml b/cmd/flux/testdata/trace/helmrelease.yaml index 80a8b8f9..d82a8c03 100644 --- a/cmd/flux/testdata/trace/helmrelease.yaml +++ b/cmd/flux/testdata/trace/helmrelease.yaml @@ -9,7 +9,7 @@ kind: Namespace metadata: name: {{ .ns }} --- -apiVersion: helm.toolkit.fluxcd.io/v2beta1 +apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: labels: @@ -34,11 +34,11 @@ status: status: "True" type: Ready helmChart: {{ .fluxns }}/podinfo-podinfo - lastAppliedRevision: 6.0.0 - lastAttemptedRevision: 6.0.0 + lastAppliedRevision: 6.3.5 + lastAttemptedRevision: 6.3.5 lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531 --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta1 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infrastructure @@ -48,19 +48,18 @@ spec: sourceRef: kind: GitRepository name: flux-system - validation: client interval: 5m prune: false status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" - message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f' + message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready - lastAppliedRevision: main/696f056df216eea4f9401adbee0ff744d4df390f + lastAppliedRevision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: labels: @@ -69,7 +68,6 @@ metadata: name: flux-system namespace: {{ .fluxns }} spec: - gitImplementation: go-git ref: branch: main secretRef: @@ -79,12 +77,12 @@ spec: status: artifact: lastUpdateTime: "2021-08-01T04:28:42Z" - revision: main/696f056df216eea4f9401adbee0ff744d4df390f + revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f path: "example" url: "example" conditions: - lastTransitionTime: "2021-07-20T00:48:16Z" - message: 'Fetched revision: main/696f056df216eea4f9401adbee0ff744d4df390f' + message: 'Fetched revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: GitOperationSucceed status: "True" type: Ready diff --git a/cmd/flux/testdata/tree/kustomizations.yaml b/cmd/flux/testdata/tree/kustomizations.yaml index 45055da2..98a619e9 100644 --- a/cmd/flux/testdata/tree/kustomizations.yaml +++ b/cmd/flux/testdata/tree/kustomizations.yaml @@ -4,7 +4,7 @@ kind: Namespace metadata: name: {{ .fluxns }} --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: flux-system @@ -19,7 +19,7 @@ spec: status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" - message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f' + message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready @@ -40,7 +40,7 @@ status: - id: {{ .fluxns }}_flux-system_source.toolkit.fluxcd.io_GitRepository v: v1beta1 --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: infrastructure @@ -55,7 +55,7 @@ spec: status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" - message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f' + message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready @@ -66,7 +66,7 @@ status: - id: cert-manager_cert-manager_source.toolkit.fluxcd.io_HelmRepository v: v1beta1 --- -apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: empty @@ -81,7 +81,7 @@ spec: status: conditions: - lastTransitionTime: "2021-08-01T04:52:56Z" - message: 'Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f' + message: 'Applied revision: main@sha1:696f056df216eea4f9401adbee0ff744d4df390f' reason: ReconciliationSucceeded status: "True" type: Ready diff --git a/cmd/flux/trace.go b/cmd/flux/trace.go index b5805ffd..a62e9a52 100644 --- a/cmd/flux/trace.go +++ b/cmd/flux/trace.go @@ -33,20 +33,23 @@ import ( "k8s.io/cli-runtime/pkg/resource" "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/fluxcd/flux2/internal/utils" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + helmv2 "github.com/fluxcd/helm-controller/api/v2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" fluxmeta "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "github.com/fluxcd/pkg/oci" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var traceCmd = &cobra.Command{ Use: "trace [ ...]", Short: "Trace in-cluster objects throughout the GitOps delivery pipeline", - Long: `The trace command shows how one or more objects are managed by Flux, + Long: withPreviewNote(`The trace command shows how one or more objects are managed by Flux, from which source and revision they come, and what the latest reconciliation status is. -You can also trace multiple objects with different resource kinds using / multiple times.`, +You can also trace multiple objects with different resource kinds using / multiple times.`), Example: ` # Trace a Kubernetes Deployment flux trace -n apps deployment my-app @@ -61,7 +64,7 @@ You can also trace multiple objects with different resource kinds using 0 && hr.Spec.Install.CRDs != helmv2.Skip) || + (hr.Spec.Upgrade != nil && len(hr.Spec.Upgrade.CRDs) > 0 && hr.Spec.Upgrade.CRDs != helmv2.Skip) { + selector := client.MatchingLabels{ + fmt.Sprintf("%s/name", helmv2.GroupVersion.Group): hr.GetName(), + fmt.Sprintf("%s/namespace", helmv2.GroupVersion.Group): hr.GetNamespace(), + } + crdKind := "CustomResourceDefinition" + var list apiextensionsv1.CustomResourceDefinitionList + if err := kubeClient.List(ctx, &list, selector); err == nil { + for _, crd := range list.Items { + found := false + for _, r := range result { + if r.Name == crd.GetName() && r.GroupKind.Kind == crdKind { + found = true + break + } + } + + if !found { + result = append(result, object.ObjMetadata{ + Name: crd.GetName(), + GroupKind: schema.GroupKind{ + Group: apiextensionsv1.GroupName, + Kind: crdKind, + }, + }) + } + } + } + } + + return result, nil } diff --git a/cmd/flux/uninstall.go b/cmd/flux/uninstall.go index 15d271b8..d82077ea 100644 --- a/cmd/flux/uninstall.go +++ b/cmd/flux/uninstall.go @@ -22,25 +22,17 @@ import ( "github.com/manifoldco/promptui" "github.com/spf13/cobra" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - networkingv1 "k8s.io/api/networking/v1" - rbacv1 "k8s.io/api/rbac/v1" - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/apimachinery/pkg/api/errors" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/uninstall" ) var uninstallCmd = &cobra.Command{ Use: "uninstall", + Args: cobra.NoArgs, Short: "Uninstall Flux and its custom resource definitions", - Long: "The uninstall command removes the Flux components and the toolkit.fluxcd.io resources from the cluster.", + Long: `The uninstall command removes the Flux components and the toolkit.fluxcd.io resources from the cluster.`, Example: ` # Uninstall Flux components, its custom resources and namespace flux uninstall --namespace=flux-system @@ -69,245 +61,48 @@ func init() { } func uninstallCmdRun(cmd *cobra.Command, args []string) error { - if !uninstallArgs.dryRun && !uninstallArgs.silent { - prompt := promptui.Prompt{ - Label: "Are you sure you want to delete Flux and its custom resource definitions", - IsConfirm: true, - } - if _, err := prompt.Run(); err != nil { - return fmt.Errorf("aborting") - } - } - ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } - logger.Actionf("deleting components in %s namespace", *kubeconfigArgs.Namespace) - uninstallComponents(ctx, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun) - - logger.Actionf("deleting toolkit.fluxcd.io finalizers in all namespaces") - uninstallFinalizers(ctx, kubeClient, uninstallArgs.dryRun) - - logger.Actionf("deleting toolkit.fluxcd.io custom resource definitions") - uninstallCustomResourceDefinitions(ctx, kubeClient, uninstallArgs.dryRun) - - if !uninstallArgs.keepNamespace { - uninstallNamespace(ctx, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun) - } - - logger.Successf("uninstall finished") - return nil -} - -func uninstallComponents(ctx context.Context, kubeClient client.Client, namespace string, dryRun bool) { - opts, dryRunStr := getDeleteOptions(dryRun) - selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} - { - var list appsv1.DeploymentList - if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { - for _, r := range list.Items { - if err := kubeClient.Delete(ctx, &r, opts); err != nil { - logger.Failuref("Deployment/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("Deployment/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) - } - } - } - } - { - var list corev1.ServiceList - if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { - for _, r := range list.Items { - if err := kubeClient.Delete(ctx, &r, opts); err != nil { - logger.Failuref("Service/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("Service/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) - } - } - } - } - { - var list networkingv1.NetworkPolicyList - if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { - for _, r := range list.Items { - if err := kubeClient.Delete(ctx, &r, opts); err != nil { - logger.Failuref("NetworkPolicy/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("NetworkPolicy/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) - } - } - } - } - { - var list corev1.ServiceAccountList - if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { - for _, r := range list.Items { - if err := kubeClient.Delete(ctx, &r, opts); err != nil { - logger.Failuref("ServiceAccount/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("ServiceAccount/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) - } - } - } - } - { - var list rbacv1.ClusterRoleList - if err := kubeClient.List(ctx, &list, selector); err == nil { - for _, r := range list.Items { - if err := kubeClient.Delete(ctx, &r, opts); err != nil { - logger.Failuref("ClusterRole/%s deletion failed: %s", r.Name, err.Error()) - } else { - logger.Successf("ClusterRole/%s deleted %s", r.Name, dryRunStr) - } - } - } - } - { - var list rbacv1.ClusterRoleBindingList - if err := kubeClient.List(ctx, &list, selector); err == nil { - for _, r := range list.Items { - if err := kubeClient.Delete(ctx, &r, opts); err != nil { - logger.Failuref("ClusterRoleBinding/%s deletion failed: %s", r.Name, err.Error()) - } else { - logger.Successf("ClusterRoleBinding/%s deleted %s", r.Name, dryRunStr) - } + if !uninstallArgs.dryRun && !uninstallArgs.silent { + info, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("cluster info unavailable: %w", err) } } - } -} -func uninstallFinalizers(ctx context.Context, kubeClient client.Client, dryRun bool) { - opts, dryRunStr := getUpdateOptions(dryRun) - { - var list sourcev1.GitRepositoryList - if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { - for _, r := range list.Items { - r.Finalizers = []string{} - if err := kubeClient.Update(ctx, &r, opts); err != nil { - logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) - } - } - } - } - { - var list sourcev1.HelmRepositoryList - if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { - for _, r := range list.Items { - r.Finalizers = []string{} - if err := kubeClient.Update(ctx, &r, opts); err != nil { - logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) - } - } - } - } - { - var list sourcev1.HelmChartList - if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { - for _, r := range list.Items { - r.Finalizers = []string{} - if err := kubeClient.Update(ctx, &r, opts); err != nil { - logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) - } - } + promptLabel := "Are you sure you want to delete Flux and its custom resource definitions" + if !installManagedByFlux(info.managedBy) { + promptLabel = fmt.Sprintf("Flux is managed by %s! Are you sure you want to delete Flux and its CRDs using Flux CLI", info.managedBy) } - } - { - var list sourcev1.BucketList - if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { - for _, r := range list.Items { - r.Finalizers = []string{} - if err := kubeClient.Update(ctx, &r, opts); err != nil { - logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) - } - } - } - } - { - var list kustomizev1.KustomizationList - if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { - for _, r := range list.Items { - r.Finalizers = []string{} - if err := kubeClient.Update(ctx, &r, opts); err != nil { - logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) - } - } - } - } - { - var list helmv2.HelmReleaseList - if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { - for _, r := range list.Items { - r.Finalizers = []string{} - if err := kubeClient.Update(ctx, &r, opts); err != nil { - logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) - } else { - logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) - } - } + prompt := promptui.Prompt{ + Label: promptLabel, + IsConfirm: true, } - } -} - -func uninstallCustomResourceDefinitions(ctx context.Context, kubeClient client.Client, dryRun bool) { - opts, dryRunStr := getDeleteOptions(dryRun) - selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} - { - var list apiextensionsv1.CustomResourceDefinitionList - if err := kubeClient.List(ctx, &list, selector); err == nil { - for _, r := range list.Items { - if err := kubeClient.Delete(ctx, &r, opts); err != nil { - logger.Failuref("CustomResourceDefinition/%s deletion failed: %s", r.Name, err.Error()) - } else { - logger.Successf("CustomResourceDefinition/%s deleted %s", r.Name, dryRunStr) - } - } + if _, err := prompt.Run(); err != nil { + return fmt.Errorf("aborting") } } -} -func uninstallNamespace(ctx context.Context, kubeClient client.Client, namespace string, dryRun bool) { - opts, dryRunStr := getDeleteOptions(dryRun) - ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} - if err := kubeClient.Delete(ctx, &ns, opts); err != nil { - logger.Failuref("Namespace/%s deletion failed: %s", namespace, err.Error()) - } else { - logger.Successf("Namespace/%s deleted %s", namespace, dryRunStr) - } -} + logger.Actionf("deleting components in %s namespace", *kubeconfigArgs.Namespace) + uninstall.Components(ctx, logger, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun) -func getDeleteOptions(dryRun bool) (*client.DeleteOptions, string) { - opts := &client.DeleteOptions{} - var dryRunStr string - if dryRun { - client.DryRunAll.ApplyToDelete(opts) - dryRunStr = "(dry run)" - } + logger.Actionf("deleting toolkit.fluxcd.io finalizers in all namespaces") + uninstall.Finalizers(ctx, logger, kubeClient, uninstallArgs.dryRun) - return opts, dryRunStr -} + logger.Actionf("deleting toolkit.fluxcd.io custom resource definitions") + uninstall.CustomResourceDefinitions(ctx, logger, kubeClient, uninstallArgs.dryRun) -func getUpdateOptions(dryRun bool) (*client.UpdateOptions, string) { - opts := &client.UpdateOptions{} - var dryRunStr string - if dryRun { - client.DryRunAll.ApplyToUpdate(opts) - dryRunStr = "(dry run)" + if !uninstallArgs.keepNamespace { + uninstall.Namespace(ctx, logger, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun) } - return opts, dryRunStr + logger.Successf("uninstall finished") + return nil } diff --git a/cmd/flux/version.go b/cmd/flux/version.go index f5da9e29..bddbfd4d 100644 --- a/cmd/flux/version.go +++ b/cmd/flux/version.go @@ -22,19 +22,21 @@ import ( "fmt" "strings" + "github.com/google/go-containerregistry/pkg/name" "github.com/spf13/cobra" v1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/yaml" + "sigs.k8s.io/yaml/goyaml.v2" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) var versionCmd = &cobra.Command{ Use: "version", Short: "Print the client and server-side components version information.", - Long: "Print the client and server-side components version information for the current context.", + Long: `Print the client and server-side components version information for the current context.`, Example: `# Print client and server-side version flux version @@ -54,6 +56,12 @@ type versionFlags struct { var versionArgs versionFlags +type versionInfo struct { + Flux string `yaml:"flux"` + Distribution string `yaml:"distribution,omitempty"` + Controller map[string]string `yaml:"controller,inline"` +} + func init() { versionCmd.Flags().BoolVar(&versionArgs.client, "client", false, "print only client version") @@ -70,15 +78,29 @@ func versionCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - info := map[string]string{} - info["flux"] = rootArgs.defaults.Version + // versionInfo struct and goyaml is used because we care about the order. + // Without this `distribution` is printed before `flux` when the struct is marshalled. + info := &versionInfo{ + Controller: map[string]string{}, + } + info.Flux = rootArgs.defaults.Version if !versionArgs.client { - kubeClient, err := utils.KubeClient(kubeconfigArgs) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } + clusterInfo, err := getFluxClusterInfo(ctx, kubeClient) + // ignoring not found errors because it means that the GitRepository CRD isn't installed but a user might + // have other controllers(e.g notification-controller), and we want to still return information for them. + if err != nil && !errors.IsNotFound(err) { + return err + } + if clusterInfo.distribution() != "" { + info.Distribution = clusterInfo.distribution() + } + selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} var list v1.DeploymentList if err := kubeClient.List(ctx, &list, client.InNamespace(*kubeconfigArgs.Namespace), selector); err != nil { @@ -95,7 +117,7 @@ func versionCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } - info[name] = tag + info.Controller[name] = tag } } } @@ -104,7 +126,7 @@ func versionCmdRun(cmd *cobra.Command, args []string) error { var err error if versionArgs.output == "json" { - marshalled, err = json.MarshalIndent(&info, "", " ") + marshalled, err = info.toJSON() marshalled = append(marshalled, "\n"...) } else { marshalled, err = yaml.Marshal(&info) @@ -118,13 +140,34 @@ func versionCmdRun(cmd *cobra.Command, args []string) error { return nil } +func (info versionInfo) toJSON() ([]byte, error) { + mapInfo := map[string]string{ + "flux": info.Flux, + } + + if info.Distribution != "" { + mapInfo["distribution"] = info.Distribution + } + for k, v := range info.Controller { + mapInfo[k] = v + } + return json.MarshalIndent(&mapInfo, "", " ") +} + func splitImageStr(image string) (string, string, error) { - imageArr := strings.Split(image, ":") - if len(imageArr) < 2 { + ref, err := name.ParseReference(image) + if err != nil { + return "", "", fmt.Errorf("parsing image '%s' failed: %w", image, err) + } + + reg := ref.Context().RegistryStr() + repo := strings.TrimPrefix(image, reg) + parts := strings.Split(repo, ":") + if len(parts) < 2 { return "", "", fmt.Errorf("missing image tag in image %s", image) } - name, tag := imageArr[0], imageArr[1] - nameArr := strings.Split(name, "/") - return nameArr[len(nameArr)-1], tag, nil + n, t := parts[0], strings.TrimPrefix(repo, parts[0]+":") + nameArr := strings.Split(n, "/") + return nameArr[len(nameArr)-1], t, nil } diff --git a/cmd/flux/version_test.go b/cmd/flux/version_test.go new file mode 100644 index 00000000..c209a32b --- /dev/null +++ b/cmd/flux/version_test.go @@ -0,0 +1,60 @@ +/* +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 ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestSplitImageStr(t *testing.T) { + tests := []struct { + url string + expectedName string + expectedTag string + }{ + { + url: "fluxcd/notification-controller:v1.0.0", + expectedName: "notification-controller", + expectedTag: "v1.0.0", + }, + { + url: "ghcr.io/fluxcd/kustomize-controller:v1.0.0", + expectedName: "kustomize-controller", + expectedTag: "v1.0.0", + }, + { + url: "reg.internal:8080/fluxcd/source-controller:v1.0.0", + expectedName: "source-controller", + expectedTag: "v1.0.0", + }, + { + url: "fluxcd/source-controller:v1.0.1@sha256:49921d1c7b100650dd654a32df1f6e626b54dfe9707d7bb7bdf43fb7c81f1baf", + expectedName: "source-controller", + expectedTag: "v1.0.1@sha256:49921d1c7b100650dd654a32df1f6e626b54dfe9707d7bb7bdf43fb7c81f1baf", + }, + } + + for _, tt := range tests { + g := NewWithT(t) + n, t, err := splitImageStr(tt.url) + g.Expect(err).To(Not(HaveOccurred())) + g.Expect(n).To(BeEquivalentTo(tt.expectedName)) + g.Expect(t).To(BeEquivalentTo(tt.expectedTag)) + } +} diff --git a/cmd/flux/version_utils.go b/cmd/flux/version_utils.go index 8f64ebf3..5bf8326e 100644 --- a/cmd/flux/version_utils.go +++ b/cmd/flux/version_utils.go @@ -18,9 +18,10 @@ package main import ( "fmt" + "strings" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/manifestgen/install" + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" ) func getVersion(input string) (string, error) { @@ -28,6 +29,10 @@ func getVersion(input string) (string, error) { return rootArgs.defaults.Version, nil } + if input != install.MakeDefaultOptions().Version && !strings.HasPrefix(input, "v") { + return "", fmt.Errorf("targeted version '%s' must be prefixed with 'v'", input) + } + if isEmbeddedVersion(input) { return input, nil } diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 5b1ab70e..00000000 --- a/docs/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Flux v2 Documentation - -The documentation for `flux2` has moved to this repository: . - -[The Website's README](https://github.com/fluxcd/website/#readme) has information on how to - -- modify and extend documentation -- run the site locally -- publish changes - -and where all the individual pieces of content come from. - -It will be easier for us to maintain a coherent web presences (and merge all of Flux documentation) in one central repository. This was partly discussed in . - -## toolkit.fluxcd.io - -For historical reasons we are keeping a `_redirects` file in this directory. It defines how redirects from the old site `toolkit.fluxcd.io` to our new website work. Changes to this file need to be merged and a manual build triggered in the Netlify App. diff --git a/docs/_files/gitops-toolkit.png b/docs/_files/gitops-toolkit.png deleted file mode 100644 index cc5447b6..00000000 Binary files a/docs/_files/gitops-toolkit.png and /dev/null differ diff --git a/docs/_redirects b/docs/_redirects deleted file mode 100644 index 4b23c408..00000000 --- a/docs/_redirects +++ /dev/null @@ -1,18 +0,0 @@ -# individual rules -/core-concepts https://fluxcd.io/docs/concepts 301! -/contributing https://fluxcd.io/contributing 301! -/install.sh https://fluxcd.io/install.sh 301! - -# refer to https://github.com/fluxcd/flux2/discussions/367 -/dev-guides/* https://fluxcd.io/docs/gitops-toolkit/:splat 301! - - -# this is how things looked in the navbar anyway..? -/guides/faq-migration https://fluxcd.io/docs/migration/faq-migration 301! -/guides/flux-v1-automation-migration https://fluxcd.io/docs/migration/flux-v1-automation-migration 301! -/guides/flux-v1-migration https://fluxcd.io/docs/migration/flux-v1-migration 301! -/guides/helm-operator-migration https://fluxcd.io/docs/migration/helm-operator-migration 301! - - -# catch all -/* https://fluxcd.io/docs/:splat 301! diff --git a/docs/diagrams/fluxcd-controllers.png b/docs/diagrams/fluxcd-controllers.png new file mode 100644 index 00000000..dd641260 Binary files /dev/null and b/docs/diagrams/fluxcd-controllers.png differ diff --git a/docs/internal/release.md b/docs/internal/release.md deleted file mode 100644 index 6fa08f03..00000000 --- a/docs/internal/release.md +++ /dev/null @@ -1,139 +0,0 @@ -# Flux release procedure - -The Flux Go modules and the GitOps Toolkit controllers are released by following the [semver](https://semver.org) conventions. - -Repositories subject to semver releases: - -1. [fluxcd/pkg](https://github.com/fluxcd/pkg) - - modules: `apis/meta`, `runtime`, various utilities - - dependencies: `k8s.io/*`, `sigs.k8s.io/controller-runtime` -1. [fluxcd/source-controller](https://github.com/fluxcd/source-controller) - - modules: `api` - - dependencies: `github.com/fluxcd/pkg/*` -1. [fluxcd/kustomize-controller](https://github.com/fluxcd/kustomize-controller) - - modules: `api` - - dependencies: `github.com/fluxcd/source-controller/api`, `github.com/fluxcd/pkg/*` -1. [fluxcd/helm-controller](https://github.com/fluxcd/helm-controller) - - modules: `api` - - dependencies: `github.com/fluxcd/source-controller/api`, `github.com/fluxcd/pkg/*` -1. [fluxcd/image-reflector-controller](https://github.com/fluxcd/image-reflector-controller) - - modules: `api` - - dependencies: `github.com/fluxcd/pkg/*` -1. [fluxcd/image-automation-controller](https://github.com/fluxcd/image-automation-controller) - - modules: `api` - - dependencies: `github.com/fluxcd/source-controller/api`, `github.com/fluxcd/image-reflector-controller/api`, `github.com/fluxcd/pkg/*` -1. [fluxcd/notification-controller](https://github.com/fluxcd/notification-controller) - - modules: `api` - - dependencies: `github.com/fluxcd/pkg/*` -1. [fluxcd/flux2](https://github.com/fluxcd/flux2) - - modules: `manifestgen` - - dependencies: `github.com/fluxcd/source-controller/api`, `github.com/fluxcd/kustomize-controller/api`, `github.com/fluxcd/helm-controller/api`, `github.com/fluxcd/image-reflector-controller/api`, `github.com/fluxcd/image-automation-controller/api`, `github.com/fluxcd/notification-controller/api`, `github.com/fluxcd/pkg/*` -1. [fluxcd/terraform-provider-flux](https://github.com/fluxcd/terraform-provider-flux) - - dependencies: `github.com/fluxcd/flux2/pkg/manifestgen` - -## Release procedure - -### Go packages - -The Go packages in [fluxcd/pkg](https://github.com/fluxcd/pkg) are dedicated modules, -each module has its own set of dependencies and release cycle. - -Release procedure for a package: - -1. Checkout the `main` branch and pull changes from remote. -1. Run `make release- VER=`. - -### Controllers - -A toolkit controller has a dedicated module for its API, the API module -has its own set of dependencies. - -Release procedure for a controller and its API: - -1. Checkout the `main` branch and pull changes from remote. -1. Create a `api/` tag and push it to remote. -1. Create a new branch from `main` i.e. `release-`. This - will function as your release preparation branch. -1. Update the `github.com/fluxcd/-controller/api` version in `go.mod` -1. Add an entry to the `CHANGELOG.md` for the new release and change the - `newTag` value in ` config/manager/kustomization.yaml` to that of the - semver release you are going to make. Commit and push your changes. -1. Create a PR for your release branch and get it merged into `main`. -1. Create a `` tag for the merge commit in `main` and - push it to remote. -1. Confirm CI builds and releases the newly tagged version. - -### Flux - -Release procedure for Flux: - -1. Checkout the `main` branch and pull changes from remote. -1. Create a `` tag form `main` and push it to remote. -1. Confirm CI builds and releases the newly tagged version. - -## Upgrade Kubernetes modules - -Flux has the following Kubernetes dependencies: - -- `k8s.io/api` -- `k8s.io/apiextensions-apiserver` -- `k8s.io/apimachinery` -- `k8s.io/cli-runtime` -- `k8s.io/client-go` -- `sigs.k8s.io/controller-runtime` - -**Note** that all `k8s.io/*` packages must have the same version in `go.mod` e.g.: - -``` - k8s.io/api v0.20.2 - k8s.io/apiextensions-apiserver v0.20.2 - k8s.io/apimachinery v0.20.2 - k8s.io/cli-runtime v0.20.2 - k8s.io/client-go v0.20.2 -``` - -The specialised reconcilers depend on: - -- kustomize-controller: `sigs.k8s.io/kustomize/api` -- image-automation-controller: `sigs.k8s.io/kustomize/kyaml` -- helm-controller: `helm.sh/helm/v3` - -**Note** that the `k8s.io/*` version must be compatible with both `kustomize/api` and `helm/v3`. -If there is a breaking change in `client-go` we have to wait for Kustomize and Helm to upgrade first. - -### Upgrade procedure: - -`fluxcd/pkg`: - -1. Update the `k8s.io/*` version in `pkg/apis/meta/go.mod` -1. Release the `apis/meta` package -1. Update `apis/meta` version in `pkg/runtime/go.mod` -1. Update the `k8s.io/*` version in `pkg/runtime/go.mod` -1. Update `sigs.k8s.io/controller-runtime` version in `pkg/runtime/go.mod` -1. Release the `runtime` package - -`fluxcd/source-controller`: - -1. Update the `github.com/fluxcd/pkg/apis/meta` version in `source-controller/api/go.mod` and `source-controller/go.mod` -1. Update the `k8s.io/*` version in `source-controller/api/go.mod` and `source-controller/go.mod` -1. Update the `sigs.k8s.io/controller-runtime` version in `source-controller/api/go.mod` and `source-controller/go.mod` -1. Update the `github.com/fluxcd/pkg/runtime` version in `source-controller/go.mod` -1. Release the `api` package - -`fluxcd/-controller`: - -1. Update the `github.com/fluxcd/source-controller/api` version in `-controller/api/go.mod` and `-controller/go.mod` -1. Update the `github.com/fluxcd/pkg/apis/meta` version in `-controller/api/go.mod` and `-controller/go.mod` -1. Update the `k8s.io/*` version in `-controller/api/go.mod` and `-controller/go.mod` -1. Update the `github.com/fluxcd/pkg/runtime` version in `-controller/go.mod` -1. Release the `api` package - -`fluxcd/flux2`: - -1. Update the `github.com/fluxcd/*-controller/api` version in `flux2/go.mod` (automated with [GitHub Actions](../../.github/workflows/update.yaml)) -1. Update the `github.com/fluxcd/pkg/*` version in `flux2/go.mod` -1. Update the `k8s.io/*` and `github.com/fluxcd/pkg/runtime` version in `flux2/go.mod` - -`fluxcd/terraform-provider-flux`: - -1. Update the `github.com/fluxcd/flux2` version in `terraform-provider-flux/go.mod` (automated with [GitHub Actions](https://github.com/fluxcd/terraform-provider-flux/blob/main/.github/workflows/update.yaml)) diff --git a/docs/release/README.md b/docs/release/README.md new file mode 100644 index 00000000..0faa5ad2 --- /dev/null +++ b/docs/release/README.md @@ -0,0 +1,9 @@ +# Flux Dev Documentation + +## Release specifications + +- [Flux distribution](https://fluxcd.io/flux/releases/) +- [Flux APIs and controllers](https://fluxcd.io/flux/releases/controllers/) +- [Flux shared packages](https://fluxcd.io/flux/releases/packages/) +- [Flux release procedures](https://fluxcd.io/flux/releases/procedure/) +- [Flux release notes template](release-notes-template.md) diff --git a/docs/release/release-notes-template.md b/docs/release/release-notes-template.md new file mode 100644 index 00000000..0b3a41de --- /dev/null +++ b/docs/release/release-notes-template.md @@ -0,0 +1,72 @@ +# Flux release note template + +This is a template for release notes. It is intended to be used as a +starting point for writing release notes for a new release. It should be copied +to a temporary file, and then edited to reflect the changes in the release. + +Once the release notes are complete, you can tag the release and push it to +GitHub. + +After the release is tagged, the CI will build the release artifacts and upload +them to the GitHub release page. The release notes can then be copied from the +temporary file to the GitHub release page. + +The release notes should be formatted using [Markdown](https://guides.github.com/features/mastering-markdown/), +and not make use of line breaks unless they function as paragraph breaks. + +For examples of release notes, including language and formatting of the release +highlights, see the [Flux release notes](https://github.com/fluxcd/flux2/releases). + +## GitHub release template + +The following template can be used for the GitHub release page: + +```markdown +## Highlights + + + +### Fixes and improvements + + + +## New documentation + + + +## Components changelog + +- -controller [v](https://github.com/fluxcd/-controller/blob//CHANGELOG.md + +## CLI changelog + + +``` + +In some scenarios, you may want to include specific information about API +changes and/or upgrade procedures. Consult [the formatting of +`v2.0.0-rc.1`](https://github.com/fluxcd/flux2/releases/tag/v2.0.0-rc.1) for +such an example. + +## Slack message template + +The following template can be used for the Slack release message: + +```markdown +:sparkles: *We are pleased to announce the release of Flux [](https://github.com/fluxcd/flux2/releases/tag//)!* + + + +:hammer_and_pick: *Fixes and improvements* + + + +:books: Documentation + + + +:heart: Big thanks to all the Flux contributors that helped us with this release! +``` + +For more concrete examples, see the pinned messages in the [Flux Slack +channel](https://cloud-native.slack.com/archives/CLAJ40HV3). \ No newline at end of file diff --git a/go.mod b/go.mod index ed41fed9..7eeeae09 100644 --- a/go.mod +++ b/go.mod @@ -1,161 +1,253 @@ -module github.com/fluxcd/flux2 +module github.com/fluxcd/flux2/v2 -go 1.17 +go 1.22.0 + +// Fix CVE-2022-28948. +replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 require ( - github.com/Masterminds/semver/v3 v3.1.0 - github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab - github.com/cyphar/filepath-securejoin v0.2.2 - github.com/fluxcd/go-git-providers v0.5.3 - github.com/fluxcd/helm-controller/api v0.15.0 - github.com/fluxcd/image-automation-controller/api v0.19.0 - github.com/fluxcd/image-reflector-controller/api v0.15.0 - github.com/fluxcd/kustomize-controller/api v0.19.1 - github.com/fluxcd/notification-controller/api v0.20.1 - github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect - github.com/fluxcd/pkg/apis/meta v0.10.2 - github.com/fluxcd/pkg/runtime v0.12.3 - github.com/fluxcd/pkg/ssa v0.11.0 - github.com/fluxcd/pkg/ssh v0.3.1 - github.com/fluxcd/pkg/untar v0.0.5 - github.com/fluxcd/pkg/version v0.0.1 - github.com/fluxcd/source-controller/api v0.20.1 - github.com/go-git/go-git/v5 v5.4.2 - github.com/gonvenience/bunt v1.3.2 - github.com/gonvenience/ytbx v1.4.2 - github.com/google/go-cmp v0.5.6 - github.com/google/go-containerregistry v0.2.0 - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/hashicorp/go-retryablehttp v0.7.0 // indirect - github.com/homeport/dyff v1.4.6 + github.com/Masterminds/semver/v3 v3.2.1 + github.com/ProtonMail/go-crypto v1.0.0 + github.com/cyphar/filepath-securejoin v0.2.5 + github.com/distribution/distribution/v3 v3.0.0-alpha.1 + github.com/fluxcd/cli-utils v0.36.0-flux.7 + github.com/fluxcd/go-git-providers v0.20.1 + github.com/fluxcd/helm-controller/api v1.0.1 + github.com/fluxcd/image-automation-controller/api v0.38.0 + github.com/fluxcd/image-reflector-controller/api v0.32.0 + github.com/fluxcd/kustomize-controller/api v1.3.0 + github.com/fluxcd/notification-controller/api v1.3.0 + github.com/fluxcd/pkg/apis/event v0.9.0 + github.com/fluxcd/pkg/apis/meta v1.5.0 + github.com/fluxcd/pkg/envsubst v1.1.0 + github.com/fluxcd/pkg/git v0.19.0 + github.com/fluxcd/pkg/git/gogit v0.19.0 + github.com/fluxcd/pkg/kustomize v1.11.0 + github.com/fluxcd/pkg/oci v0.37.1 + github.com/fluxcd/pkg/runtime v0.47.1 + github.com/fluxcd/pkg/sourceignore v0.7.0 + github.com/fluxcd/pkg/ssa v0.39.1 + github.com/fluxcd/pkg/ssh v0.13.0 + github.com/fluxcd/pkg/tar v0.7.0 + github.com/fluxcd/pkg/version v0.4.0 + github.com/fluxcd/source-controller/api v1.3.0 + github.com/go-git/go-git/v5 v5.12.0 + github.com/go-logr/logr v1.4.1 + github.com/gonvenience/bunt v1.3.5 + github.com/gonvenience/ytbx v1.4.4 + github.com/google/go-cmp v0.6.0 + github.com/google/go-containerregistry v0.19.1 + github.com/hashicorp/go-cleanhttp v0.5.2 + github.com/homeport/dyff v1.7.1 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-shellwords v1.0.12 - github.com/olekukonko/tablewriter v0.0.4 - github.com/spf13/cobra v1.2.1 + github.com/notaryproject/notation-go v1.1.0 + github.com/olekukonko/tablewriter v0.0.5 + github.com/onsi/gomega v1.33.1 + github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 - golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 - k8s.io/api v0.23.1 - k8s.io/apiextensions-apiserver v0.23.1 - k8s.io/apimachinery v0.23.1 - k8s.io/cli-runtime v0.23.1 - k8s.io/client-go v0.23.1 - k8s.io/kubectl v0.23.1 - sigs.k8s.io/cli-utils v0.27.0 - sigs.k8s.io/controller-runtime v0.11.0 - sigs.k8s.io/kustomize/api v0.10.1 - sigs.k8s.io/yaml v1.3.0 + github.com/theckman/yacspin v0.13.12 + golang.org/x/crypto v0.22.0 + golang.org/x/term v0.19.0 + golang.org/x/text v0.14.0 + k8s.io/api v0.30.0 + k8s.io/apiextensions-apiserver v0.30.0 + k8s.io/apimachinery v0.30.0 + k8s.io/cli-runtime v0.30.0 + k8s.io/client-go v0.30.0 + k8s.io/kubectl v0.30.0 + sigs.k8s.io/controller-runtime v0.18.1 + sigs.k8s.io/kustomize/api v0.17.1 + sigs.k8s.io/kustomize/kyaml v0.17.0 + sigs.k8s.io/yaml v1.4.0 ) require ( - github.com/fluxcd/pkg/kustomize v0.0.2 - sigs.k8s.io/kustomize/kyaml v0.13.0 -) - -require ( - cloud.google.com/go v0.81.0 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.18 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/BurntSushi/toml v0.4.1 // indirect - github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect - github.com/Microsoft/go-winio v0.4.16 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/acomagu/bufpipe v1.0.3 // indirect - github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect - github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 // indirect - github.com/emirpasic/gods v1.12.0 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect - github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect - github.com/fluxcd/pkg/apis/acl v0.0.3 // indirect - github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect - github.com/fvbommel/sortorder v1.0.1 // indirect - github.com/go-errors/errors v1.0.1 // indirect - github.com/go-git/gcfg v1.5.0 // indirect - github.com/go-git/go-billy/v5 v5.3.1 // indirect - github.com/go-logr/logr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.19.5 // indirect - github.com/go-openapi/swag v0.19.14 // indirect + code.gitea.io/sdk/gitea v0.17.1 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.27.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect + github.com/aws/smithy-go v1.20.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/chzyer/readline v1.5.1 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.5.0 // indirect + github.com/docker/cli v24.0.9+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.9+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect + github.com/docker/go-metrics v0.0.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanphx/json-patch v5.7.0+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/color v1.13.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fluxcd/pkg/apis/acl v0.3.0 // indirect + github.com/fluxcd/pkg/apis/kustomize v1.5.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-ldap/ldap/v3 v3.4.6 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/gonvenience/neat v1.3.7 // indirect - github.com/gonvenience/term v1.0.1 // indirect - github.com/gonvenience/text v1.0.6 // indirect - github.com/gonvenience/wrap v1.1.0 // indirect - github.com/google/btree v1.0.1 // indirect - github.com/google/go-github/v41 v41.0.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/gonvenience/neat v1.3.13 // indirect + github.com/gonvenience/term v1.0.2 // indirect + github.com/gonvenience/text v1.0.7 // indirect + github.com/gonvenience/wrap v1.2.0 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-github/v61 v61.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/handlers v1.5.1 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/hashicorp/errwrap v1.0.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.1 // indirect - github.com/imdario/mergo v0.3.12 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.5 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.5 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.7 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect - github.com/mitchellh/go-wordwrap v1.0.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/hashstructure v1.1.0 // indirect github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 // indirect + github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/notaryproject/notation-core-go v1.0.2 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/russross/blackfriday v1.5.2 // indirect - github.com/russross/blackfriday/v2 v2.0.1 // indirect - github.com/sergi/go-diff v1.2.0 // indirect - github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect - github.com/stretchr/testify v1.7.0 // indirect + github.com/prometheus/client_golang v1.19.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.53.0 // indirect + github.com/prometheus/procfs v0.14.0 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect + github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect + github.com/redis/go-redis/v9 v9.1.0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 // indirect - github.com/xanzy/go-gitlab v0.54.3 // indirect - github.com/xanzy/ssh-agent v0.3.0 // indirect - github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect - go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect - golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.27.1 // indirect + github.com/xanzy/go-gitlab v0.101.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.44.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/sdk v1.21.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.20.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/component-base v0.23.1 // indirect - k8s.io/klog/v2 v2.30.0 // indirect - k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect - sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/component-base v0.30.0 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect + k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) - -// Fix for CVE-2020-29652: https://github.com/golang/crypto/commit/8b5274cf687fd9316b4108863654cc57385531e8 -// Fix for CVE-2021-43565: https://github.com/golang/crypto/commit/5770296d904e90f15f38f77dfc2e43fdf5efc083 -replace golang.org/x/crypto => golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 diff --git a/go.sum b/go.sum index 600c5da3..a3350677 100644 --- a/go.sum +++ b/go.sum @@ -1,1433 +1,742 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v42.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0= -github.com/Azure/go-autorest/autorest v0.10.2/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630= -github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= -github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= -github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= -github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= -github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= -github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= -github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= -github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= -github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd h1:sjQovDkwrZp8u+gxLtPgKGjk5hCxuy2hrRejBTA9xFU= -github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= -github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk= -github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab h1:5FiL/TCaiKCss/BLMIACDxxadYrx767l9kh0qYX+sLQ= -github.com/ProtonMail/go-crypto v0.0.0-20211221144345-a4f6767435ab/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= -github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +code.gitea.io/sdk/gitea v0.17.1 h1:3jCPOG2ojbl8AcfaUCRYLT5MUcBMFwS0OSK2mA5Zok8= +code.gitea.io/sdk/gitea v0.17.1/go.mod h1:aCnBqhHpoEWA180gMbaCtdX9Pl6BWBAuuP2miadoTNM= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 h1:E+OJmp2tPvt1W+amx48v1eqbjDYsgN+RzP4q16yV5eM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 h1:FDif4R1+UUR+00q6wquyX90K7A8dN+R5E8GEadoP7sU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2/go.mod h1:aiYBYui4BJ/BJCAIKs92XiPyQfTaBWqvHujDwKb6CBU= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aMclParm9/5Vgp+TY51uBQ= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.28.2/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.31.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= +github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA= +github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/ecr v1.27.4 h1:Qr9W21mzWT3RhfYn9iAux7CeRIdbnTAqmiOlASqQgZI= +github.com/aws/aws-sdk-go-v2/service/ecr v1.27.4/go.mod h1:if7ybzzjOmDB8pat9FE35AHTY6ZxlYSy3YviSmFZv8c= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= -github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0= +github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= -github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= -github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/daviddengcn/go-colortext v0.0.0-20160507010035-511bcaf42ccd/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46 h1:7QPwrLT79GlD5sizHf27aoY2RTvw62mO6x7mxkScNk0= -github.com/drone/envsubst/v2 v2.0.0-20210730161058-179042472c46/go.mod h1:esf2rsHFNlZlxsqsZDojNBcnNs5REqIvRrWRHqX0vEU= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153 h1:yUdfgN0XgIJw7foRItutHYUIhlcKzcSf5vDpdhQAKTc= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v0.0.0-20200808040245-162e5629780b/go.mod h1:NAJj0yf/KaRKURN6nyi7A9IZydMivZEm9oQLWNjfKDc= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= -github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= -github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0-alpha.1 h1:jn7I1gvjOvmLztH1+1cLiUFud7aeJCIQcgzugtwjyJo= +github.com/distribution/distribution/v3 v3.0.0-alpha.1/go.mod h1:LCp4JZp1ZalYg0W/TN05jarCQu+h4w7xc7ZfQF4Y/cY= +github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= +github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50= +github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= +github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= +github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/go-git-providers v0.5.3 h1:Sg5XH+aXb6mYwITtdE4yc4yLoyFW3aVZx33qWuQb9/Q= -github.com/fluxcd/go-git-providers v0.5.3/go.mod h1:4jTHTmSx3rFGnG78KUVgFYeG6vWFnKwUSr2mi31tvp8= -github.com/fluxcd/helm-controller/api v0.15.0 h1:1uei4JWf5cOEbixj8d5mZ3EMruuarR8yCSiPIsaotKo= -github.com/fluxcd/helm-controller/api v0.15.0/go.mod h1:/OeNzk18BVa7UmhGphENJdD/2GerKpMeDSxY8QYlO0o= -github.com/fluxcd/image-automation-controller/api v0.19.0 h1:XR2yBR3RxB6i1mS6ZpqgbEnuV23s9q4JfkyKLyOTViU= -github.com/fluxcd/image-automation-controller/api v0.19.0/go.mod h1:e9hAvFZT5y1X6NaSNUHXkabpMkPA3Z1bDr3yea8gMzE= -github.com/fluxcd/image-reflector-controller/api v0.15.0 h1:2XUKXLhWjbS7X8k1Ur/LJaIv2C8kbpErB46yw4Xmf4U= -github.com/fluxcd/image-reflector-controller/api v0.15.0/go.mod h1:SPUqO4kodOglDFpZ+GhW/XBhKo71mWIqFRc+oT0jCfc= -github.com/fluxcd/kustomize-controller/api v0.19.1 h1:71E9/7WNQN7aNVhTvfweyOEPwOLVnohAhcR0qMnA67g= -github.com/fluxcd/kustomize-controller/api v0.19.1/go.mod h1:q0AA6fxVlm8fvXZEaqSMMw8ANPharpywBve7dlcARhk= -github.com/fluxcd/notification-controller/api v0.20.1 h1:iK1+icG0ESuSUki6O9tL/4uxPx6eymYwaFxGprlKSQA= -github.com/fluxcd/notification-controller/api v0.20.1/go.mod h1:YFW/YQ6kScEzpnuKgvOJWak+9zGyF3FJ73kKsSQ4LDk= -github.com/fluxcd/pkg/apis/acl v0.0.3 h1:Lw0ZHdpnO4G7Zy9KjrzwwBmDZQuy4qEjaU/RvA6k1lc= -github.com/fluxcd/pkg/apis/acl v0.0.3/go.mod h1:XPts6lRJ9C9fIF9xVWofmQwftvhY25n1ps7W9xw0XLU= -github.com/fluxcd/pkg/apis/kustomize v0.3.1 h1:wmb5D9e1+Rr3/5O3235ERuj+h2VKUArVfYYk68QKP+w= -github.com/fluxcd/pkg/apis/kustomize v0.3.1/go.mod h1:k2HSRd68UwgNmOYBPOd6WbX6a2MH2X/Jeh7e3s3PFPc= -github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI= -github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I= -github.com/fluxcd/pkg/kustomize v0.0.2 h1:ipvQrxSeuGZDsPZrVUL6tYMlTR5xqYTZp6G0Tdy2hVs= -github.com/fluxcd/pkg/kustomize v0.0.2/go.mod h1:AFwnf3OqQmpTCuwCARTGpPRMBf0ZFJNGCvW63KbgK04= -github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0= -github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM= -github.com/fluxcd/pkg/ssa v0.11.0 h1:ejEMlHPsbXMP8BJQx3+0PwoBgJur0mHiPcMNKcFwoOE= -github.com/fluxcd/pkg/ssa v0.11.0/go.mod h1:S+qig7BTOxop0c134y8Yv8/iQST4Kt7S2xXiFkP4VMA= -github.com/fluxcd/pkg/ssh v0.3.1 h1:iQw07bkX2rScactX8WYv+uMDsufFOlg8M3fV2TNs244= -github.com/fluxcd/pkg/ssh v0.3.1/go.mod h1:Sebfv4Um51PvomuYdMvKRncQW5dtKhZ5J5TA+wiHNSQ= -github.com/fluxcd/pkg/untar v0.0.5 h1:UGI3Ch1UIEIaqQvMicmImL1s9npQa64DJ/ozqHKB7gk= -github.com/fluxcd/pkg/untar v0.0.5/go.mod h1:O6V9+rtl8c1mHBafgqFlJN6zkF1HS5SSYn7RpQJ/nfw= -github.com/fluxcd/pkg/version v0.0.1 h1:/8asQoDXSThz3csiwi4Qo8Zb6blAxLXbtxNgeMJ9bCg= -github.com/fluxcd/pkg/version v0.0.1/go.mod h1:WAF4FEEA9xyhngF8TDxg3UPu5fA1qhEYV8Pmi2Il01Q= -github.com/fluxcd/source-controller/api v0.20.1 h1:BfYw1gNHykiCVFNtDz3atcf3Vph+arfuveKmouI98wE= -github.com/fluxcd/source-controller/api v0.20.1/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= -github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= -github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= -github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= -github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= -github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= -github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= -github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/cli-utils v0.36.0-flux.7 h1:81zEo/LNmIRWMgtsZy/8L13TMUZHmmJib4gHRvKwVE8= +github.com/fluxcd/cli-utils v0.36.0-flux.7/go.mod h1:TcfLhvBjtQnqxYMsHQUAEB2c5WJRVuibtas2Izz5ZTs= +github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= +github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo= +github.com/fluxcd/go-git-providers v0.20.1 h1:ER10UUup3y/lAyANvMjgaYI/9av/upetF2PTi3aCqvs= +github.com/fluxcd/go-git-providers v0.20.1/go.mod h1:FhBThaf3/kyKCBg4v0mKcQqQB2rPDv/L8baH3+nFtHc= +github.com/fluxcd/helm-controller/api v1.0.1 h1:Gn9qEVuif6D5+gHmVwTEZkR4+nmLOcOhKx4Sw2gL2EA= +github.com/fluxcd/helm-controller/api v1.0.1/go.mod h1:/6AD5a2qjo/ttxVM8GR33syLZwqigta60DCLdy8GrME= +github.com/fluxcd/image-automation-controller/api v0.38.0 h1:+phX67uf0INGDC4sghsPPNUiE8taVp7AcWgJH8LkiUk= +github.com/fluxcd/image-automation-controller/api v0.38.0/go.mod h1:FfWWRxG03514+MUNJ+uN6fXzjwdbqsJqCggukIZ1tx8= +github.com/fluxcd/image-reflector-controller/api v0.32.0 h1:mb/v9JzRHcjLcnGqmgsq0+yCcoOyae/TrOWae9T87PE= +github.com/fluxcd/image-reflector-controller/api v0.32.0/go.mod h1:Ap3/KK8MfQAdmuhakg9CweEa3Xwwmvausbqrgd3HBWY= +github.com/fluxcd/kustomize-controller/api v1.3.0 h1:IwXkU48lQ/YhU6XULlPXDgQlnpNyQdCNbUvhLdWVIbE= +github.com/fluxcd/kustomize-controller/api v1.3.0/go.mod h1:kg/WM9Uye5NOqGVW/F3jnkjrlgFZHHa84+4lnzOV8fI= +github.com/fluxcd/notification-controller/api v1.3.0 h1:e3Plvo44XIKP2pUjwx8U4/fMpPwVM3EvJrYLIIqcVrI= +github.com/fluxcd/notification-controller/api v1.3.0/go.mod h1:KUaWXACNwWpAYo/Q4mzBjGbsYlUzXdq654jc1XpgMQw= +github.com/fluxcd/pkg/apis/acl v0.3.0 h1:UOrKkBTOJK+OlZX7n8rWt2rdBmDCoTK+f5TY2LcZi8A= +github.com/fluxcd/pkg/apis/acl v0.3.0/go.mod h1:WVF9XjSMVBZuU+HTTiSebGAWMgM7IYexFLyVWbK9bNY= +github.com/fluxcd/pkg/apis/event v0.9.0 h1:iKxU+3v/3bAuC1C1iXg1mjbIiaEQet7WETh8lsfdcpY= +github.com/fluxcd/pkg/apis/event v0.9.0/go.mod h1:5LjcTeppPMEyOgtTbIP7q2GbVwIRUfujIxynIjHBV/k= +github.com/fluxcd/pkg/apis/kustomize v1.5.0 h1:ah4sfqccnio+/5Edz/tVz6LetFhiBoDzXAElj6fFCzU= +github.com/fluxcd/pkg/apis/kustomize v1.5.0/go.mod h1:nEzhnhHafhWOUUV8VMFLojUOH+HHDEsL75y54mt/c30= +github.com/fluxcd/pkg/apis/meta v1.5.0 h1:/G82d2Az5D9op3F+wJUpD8jw/eTV0suM6P7+cSURoUM= +github.com/fluxcd/pkg/apis/meta v1.5.0/go.mod h1:Y3u7JomuuKtr5fvP1Iji2/50FdRe5GcBug2jawNVkdM= +github.com/fluxcd/pkg/envsubst v1.1.0 h1:b0a9QsG36btk3MIWf7yM9FhVPhyXh6lLJu8eZk4Fyow= +github.com/fluxcd/pkg/envsubst v1.1.0/go.mod h1:4Uca9c2Bhu4+65sa6NbChEA3zZKhqyAjtgDEi+Zq9Y8= +github.com/fluxcd/pkg/git v0.19.0 h1:zIv+GAT0ieIUpnGBVi3Bhax/qq4Rr28BW7Jv4DTt6zE= +github.com/fluxcd/pkg/git v0.19.0/go.mod h1:wkqUOSrTjtsVVk/gC6/7RxVpi9GcqAA+7O5HVJF5S14= +github.com/fluxcd/pkg/git/gogit v0.19.0 h1:SdoNAmC/HTPXniQjp609X59rCsBiA+Sdq1Hv8SnYC6I= +github.com/fluxcd/pkg/git/gogit v0.19.0/go.mod h1:8kOmrNMjq8daQTVLhp6klhuoY8+s81gydM0MozDjaHM= +github.com/fluxcd/pkg/gittestserver v0.12.0 h1:QGbIVyje9U6urSAeDw3diKb/5wdA+Cnw1YJN+3Zflaw= +github.com/fluxcd/pkg/gittestserver v0.12.0/go.mod h1:Eh82e+kzKdhpafnUwR5oCBmxqAqhF5QuCn290AFntPM= +github.com/fluxcd/pkg/kustomize v1.11.0 h1:8YV4i6VCCxpXGlK+NzfNKbuhuSlK6Bfdr/Qv5jJgEtQ= +github.com/fluxcd/pkg/kustomize v1.11.0/go.mod h1:SfkN+DKgf8aLNoQtNuHBUEeB/uyC4nGzbbF+Ld0TmPU= +github.com/fluxcd/pkg/oci v0.37.1 h1:p4rfCHZlBWL+Q5Xey51iiBRmoje0IevCBT0/r8iae3M= +github.com/fluxcd/pkg/oci v0.37.1/go.mod h1:LrVuX6VACenJ5ycQJxec+I7YJegCsE4nzRUV+6RuxcY= +github.com/fluxcd/pkg/runtime v0.47.1 h1:Q1tAFsp92uurWyoEe52AmMC4k+6DYTPBrUQDs+nz/9c= +github.com/fluxcd/pkg/runtime v0.47.1/go.mod h1:97a+PqpWMgQsoqh91uH3EQz+/DC7Uxc8xcu/rDHFC5c= +github.com/fluxcd/pkg/sourceignore v0.7.0 h1:qQrB2o543wA1o4vgR62ufwkAaDp8+f8Wdj1HKDlmDrU= +github.com/fluxcd/pkg/sourceignore v0.7.0/go.mod h1:A4GuZt2seJJkBm3kMiIx9nheoYZs98KTMr/A6/2fIro= +github.com/fluxcd/pkg/ssa v0.39.1 h1:xPYRKqgqB5p+5jgz2xBkXCE/7i1FIOa+nA3Wr7Gu2Ek= +github.com/fluxcd/pkg/ssa v0.39.1/go.mod h1:AkhMoFxipMf3WoO3lkXjj2nHNIz6sA5yQ50aBodtxnk= +github.com/fluxcd/pkg/ssh v0.13.0 h1:lPU1Gst8XIz7AU2dhdqVFaaOWd54/O1LZu62vH4JB/s= +github.com/fluxcd/pkg/ssh v0.13.0/go.mod h1:J9eyirMd4s++tWG4euRRhmcthKX203GPHpzFpH++TP8= +github.com/fluxcd/pkg/tar v0.7.0 h1:xdg95f4DlzMgd4m+xPRXrX4NLb8P8b5SAqB19sDOLIs= +github.com/fluxcd/pkg/tar v0.7.0/go.mod h1:KLg1zMZF7sEncGA9LEsfkskbCMyLSEgrjBRXqFK++VE= +github.com/fluxcd/pkg/version v0.4.0 h1:3F6oeIZ+ug/f7pALIBhcUhfURel37EPPOn7nsGfsnOg= +github.com/fluxcd/pkg/version v0.4.0/go.mod h1:izVsSDxac81qWRmpOL9qcxZYx+zAN1ajoP5SidGP6PA= +github.com/fluxcd/source-controller/api v1.3.0 h1:Z5Lq0aJY87yg0cQDEuwGLKS60GhdErCHtsi546HUt10= +github.com/fluxcd/source-controller/api v1.3.0/go.mod h1:+tfd0vltjcVs/bbnq9AlYR9AAHSVfM/Z4v4TpQmdJf4= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= +github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= -github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= -github.com/go-logr/zapr v1.2.2 h1:5YNlIL6oZLydaV4dOFjL8YpgXF/tPeTbnpatnu3cq6o= -github.com/go-logr/zapr v1.2.2/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= -github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.0.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= -github.com/gonvenience/bunt v1.1.3/go.mod h1:hZ898ZprNWgaVlq4s1ivsJu3AY+3wrlJadF5Gp7Yq2c= -github.com/gonvenience/bunt v1.3.1/go.mod h1:G+d3dJBxxOqV2oca96psgAnPABVC9QptEifjVqPu+mo= -github.com/gonvenience/bunt v1.3.2 h1:gDiyTDfPf87fCtIbFzvENrmlnDZYbENdhhRW2kun/tw= -github.com/gonvenience/bunt v1.3.2/go.mod h1:oTOZqb1TVL1KqZm57zUCJCiwUliNfY8+d3QndOVqxpg= -github.com/gonvenience/neat v1.3.6/go.mod h1:wv0eXmvwFfpuS5bpf2fIofXQvf8W7HTdSzKfGCYbIe8= -github.com/gonvenience/neat v1.3.7 h1:k4shy3sgSBfUk9LTN51naxVIkB6BlGaxH0ReERO92zw= -github.com/gonvenience/neat v1.3.7/go.mod h1:Y4eeQH3GEBvjmLoMiu4oWGyOopGDaI2/y2jwcVfvlvs= -github.com/gonvenience/term v1.0.0/go.mod h1:wohD4Iqso9Eol7qc2VnNhSFFhZxok5PvO7pZhdrAn4E= -github.com/gonvenience/term v1.0.1 h1:8bg2O0ox0Ss64nnUxW5AXlSHhllc8dTOSKuKu6uoGpw= -github.com/gonvenience/term v1.0.1/go.mod h1:TrQEhxBNE/ng5kTV+S0OvQowTDJSfhlBeZbcOmTR6qI= -github.com/gonvenience/text v1.0.6 h1:9JH9fz0BL0NX4uKmjLuVcsBKiniPa+XLpf8KH9so44U= -github.com/gonvenience/text v1.0.6/go.mod h1:9U5WbkT/5wR5+aNMR4HucamY+HgVMEn+UbF78XHmUio= -github.com/gonvenience/wrap v1.1.0 h1:d8gEZrXS/zg4BC1q0U4nHpPIh5k6muKpQ1+rQFBwpYc= -github.com/gonvenience/wrap v1.1.0/go.mod h1:L47Cm1sK1G8QmFAYQfkHcF/sQ1IBJUa0u4sjqiLqPdM= -github.com/gonvenience/ytbx v1.4.2 h1:fpgOpsQ+gwTPqiatki0aY7q3BEjt7EcwiI5b+D0Qjvg= -github.com/gonvenience/ytbx v1.4.2/go.mod h1:GkUMPGH5qZSg1S8L6u9XNI9hJ4L1yKSQFIA4J8vaPdY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= -github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/gonvenience/bunt v1.3.5 h1:wSQquifvwEWtzn27k1ngLfeLaStyt0k1b/K6TrlCNAs= +github.com/gonvenience/bunt v1.3.5/go.mod h1:7ApqkVBEWvX04oJ28Q2WeI/BvJM6VtukaJAU/q/pTs8= +github.com/gonvenience/neat v1.3.13 h1:wRp1k0GX5EOpelNH3GyLaFy4SvnJ6k1U5SenmEWkXko= +github.com/gonvenience/neat v1.3.13/go.mod h1:aE3+z4XlTJ+RzlZxdFiAIIJc1ikYLALAWtX9LqjQ87Q= +github.com/gonvenience/term v1.0.2 h1:qKa2RydbWIrabGjR/fegJwpW5m+JvUwFL8mLhHzDXn0= +github.com/gonvenience/term v1.0.2/go.mod h1:wThTR+3MzWtWn7XGVW6qQ65uaVf8GHED98KmwpuEQeo= +github.com/gonvenience/text v1.0.7 h1:YmIqmgTwxnACYCG59DykgMbomwteYyNhAmEUEJtPl14= +github.com/gonvenience/text v1.0.7/go.mod h1:OAjH+mohRszffLY6OjgQcUXiSkbrIavooFpfIt1ZwAs= +github.com/gonvenience/wrap v1.2.0 h1:CwAoa60QIBVmQn/aUregAbk9FstEr17k9vCYpKF972c= +github.com/gonvenience/wrap v1.2.0/go.mod h1:iNijaTmFD8+ORmNp9iS+dSBcCJrmIwwyoYLUngToGdk= +github.com/gonvenience/ytbx v1.4.4 h1:jQopwyaLsVGuwdxSiN4WkXjsEaFNPJ3V4lUj7eyEpzo= +github.com/gonvenience/ytbx v1.4.4/go.mod h1:w37+MKCPcCMY/jpPNmEklD4xKqrOAVBO6kIWW2+uI6M= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-containerregistry v0.2.0 h1:cWFYx+kOkKdyOET0pcp7GMCmxj7da40StvluSuSXWCg= -github.com/google/go-containerregistry v0.2.0/go.mod h1:Ts3Wioz1r5ayWx8sS6vLcWltWcM1aqFjd/eVrkFhrWM= -github.com/google/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg= -github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= +github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= +github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.0 h1:eu1EI/mbirUgP5C8hVsTNaGZreBDlYiwC1FZWkvQPQ4= -github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/homeport/dyff v1.4.6 h1:ReC8Hi1I6SkmPmNOqGl9EUlZctx+6AloCzSulqwHge8= -github.com/homeport/dyff v1.4.6/go.mod h1:DBCaTwJUIQLNQxNOKTce/OgRxCwwa8erBdN88bBfb9Y= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/homeport/dyff v1.7.1 h1:B3KJUtnU53H2UryxGcfYKQPrde8VjjbwlHZbczH3giQ= +github.com/homeport/dyff v1.7.1/go.mod h1:iLe5b3ymc9xmHZNuJlNVKERE8L2isQMBLxFiTXcwZY0= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/k0kubun/pp v2.3.0+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ktrysmt/go-bitbucket v0.9.34/go.mod h1:FWxy2UK7GlK5b0NSJGc5hPqnssVlkNnsChvyuOf/Xno= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= -github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 h1:BXxTozrOU8zgC5dkpn3J6NTRdoP+hjok/e+ACr4Hibk= github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3/go.mod h1:x1uk6vxTiVuNt6S5R2UYgdhpj3oKojXvOXauHZ7dEnI= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= github.com/mitchellh/hashstructure v1.1.0 h1:P6P1hdjqAAknpY/M1CGipelZgp+4y9ja9kmUZPXP+H0= github.com/mitchellh/hashstructure v1.1.0/go.mod h1:xUDAozZz0Wmdiufv0uyhnHkUTN6/6d8ulp4AwfLKrmA= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297 h1:yH0SvLzcbZxcJXho2yh7CqdENGMQe73Cw3woZBpPli0= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/notaryproject/notation-core-go v1.0.2 h1:VEt+mbsgdANd9b4jqgmx2C7U0DmwynOuD2Nhxh3bANw= +github.com/notaryproject/notation-core-go v1.0.2/go.mod h1:2HkQzUwg08B3x9oVIztHsEh7Vil2Rj+tYgxH+JObLX4= +github.com/notaryproject/notation-go v1.1.0 h1:7WBeH8FGoA+GkeUwmBIBnlJc/PpdYaUKfiXu6ZZeEeg= +github.com/notaryproject/notation-go v1.1.0/go.mod h1:ZSk34URQar5fnWflaFByzpDvuefgZKm/mp8Q2tQpBaw= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= -github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= -github.com/onsi/ginkgo v1.15.1/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= -github.com/onsi/ginkgo v1.15.2/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= -github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= -github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= -github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= -github.com/onsi/gomega v1.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= +github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= +github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= +github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.28.0 h1:vGVfV9KrDTvWt5boZO0I19g2E3CsWfpPPKZM9dt3mEw= -github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE= +github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s= +github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= +github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.2.1 h1:+KmjbUw1hriSNMF55oPrkZcb27aECyrj8V2ytv7kWDw= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/spyzhov/ajson v0.4.2/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzyqMuVA= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/texttheater/golang-levenshtein v1.0.1 h1:+cRNoVrfiwufQPhoMzB6N0Yf/Mqajr6t1lOv8GyGE2U= github.com/texttheater/golang-levenshtein v1.0.1/go.mod h1:PYAKrbF5sAiq9wd+H82hs7gNaen0CplQ9uvm6+enD/8= -github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/vdemeester/k8s-pkg-credentialprovider v1.18.1-0.20201019120933-f1d16962a4db/go.mod h1:grWy0bkr1XO6hqbaaCKaPXqkBVlMGHYG6PGykktwbJc= +github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4= +github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74 h1:JwtAtbp7r/7QSyGz8mKUbYJBg2+6Cd7OjM8o/GNOcVo= github.com/virtuald/go-ordered-json v0.0.0-20170621173500-b18e6e673d74/go.mod h1:RmMWU37GKR2s6pgrIEB4ixgpVCt/cf7dnJv3fuH1J1c= -github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/xanzy/go-gitlab v0.54.3 h1:fPfZ3Jcu5dPc3xyIYtAALZsEgoyKNFNuULD+TdJ7Zvk= -github.com/xanzy/go-gitlab v0.54.3/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM= -github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= -github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca h1:1CFlNzQhALwjS9mBAUkycX616GzgsuYUOCHA5+HSlXI= -github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/xanzy/go-gitlab v0.101.0 h1:qRgvX8DNE19zRugB6rnnZMZ5ubhITSKPLNWEyc6UIPg= +github.com/xanzy/go-gitlab v0.101.0/go.mod h1:ETg8tcj4OhrB84UEgeE8dSuV/0h4BBL1uOV/qK0vlyI= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= -go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= -go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= -go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= -go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= -go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= -go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= -go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= -go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw= +go.opentelemetry.io/contrib/exporters/autoexport v0.46.1/go.mod h1:ha0aiYm+DOPsLHjh0zoQ8W8sLT+LJ58J3j47lGpSLrU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= +go.opentelemetry.io/otel/exporters/prometheus v0.44.0 h1:08qeJgaPC0YEBu2PQMbqU3rogTlyzpjhCI2b58Yn00w= +go.opentelemetry.io/otel/exporters/prometheus v0.44.0/go.mod h1:ERL2uIeBtg4TxZdojHUwzZfIFlUIjZtxubT5p4h1Gjg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= +go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.starlark.net v0.0.0-20231121155337-90ade8b19d09 h1:hzy3LFnSN8kuQK8h9tHl4ndF6UruMj47OqwqsS+/Ai4= +go.starlark.net v0.0.0-20231121155337-90ade8b19d09/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210917163549-3c21e5b27794/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180224232135-f6cff0780e54/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk= -golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= -gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= -gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= -gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/appengine v1.0.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY= -k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= -k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= -k8s.io/api v0.23.1 h1:ncu/qfBfUoClqwkTGbeRqqOqBCRoUAflMuOaOD7J0c8= -k8s.io/api v0.23.1/go.mod h1:WfXnOnwSqNtG62Y1CdjoMxh7r7u9QXGCkA1u0na2jgo= -k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA= -k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= -k8s.io/apiextensions-apiserver v0.23.1 h1:xxE0q1vLOVZiWORu1KwNRQFsGWtImueOrqSl13sS5EU= -k8s.io/apiextensions-apiserver v0.23.1/go.mod h1:0qz4fPaHHsVhRApbtk3MGXNn2Q9M/cVWWhfHdY2SxiM= -k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig= -k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= -k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= -k8s.io/apimachinery v0.23.1 h1:sfBjlDFwj2onG0Ijx5C+SrAoeUscPrmghm7wHP+uXlo= -k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno= -k8s.io/apiserver v0.18.8/go.mod h1:12u5FuGql8Cc497ORNj79rhPdiXQC4bf53X/skR/1YM= -k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= -k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= -k8s.io/apiserver v0.23.1/go.mod h1:Bqt0gWbeM2NefS8CjWswwd2VNAKN6lUKR85Ft4gippY= -k8s.io/cli-runtime v0.22.2/go.mod h1:tkm2YeORFpbgQHEK/igqttvPTRIHFRz5kATlw53zlMI= -k8s.io/cli-runtime v0.23.0/go.mod h1:B5N3YH0KP1iKr6gEuJ/RRmGjO0mJQ/f/JrsmEiPQAlU= -k8s.io/cli-runtime v0.23.1 h1:vHUZrq1Oejs0WaJnxs09mLHKScvIIl2hMSthhS8o8Yo= -k8s.io/cli-runtime v0.23.1/go.mod h1:r9r8H/qfXo9w+69vwUL7LokKlLRKW5D6A8vUKCx+YL0= -k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU= -k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= -k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= -k8s.io/client-go v0.23.1 h1:Ma4Fhf/p07Nmj9yAB1H7UwbFHEBrSPg8lviR24U2GiQ= -k8s.io/client-go v0.23.1/go.mod h1:6QSI8fEuqD4zgFK0xbdwfB/PthBsIxCJMa3s17WlcO0= -k8s.io/cloud-provider v0.18.8/go.mod h1:cn9AlzMPVIXA4HHLVbgGUigaQlZyHSZ7WAwDEFNrQSs= -k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= -k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= -k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= -k8s.io/code-generator v0.23.1/go.mod h1:V7yn6VNTCWW8GqodYCESVo95fuiEg713S8B7WacWZDA= -k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU= -k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllunGug= -k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= -k8s.io/component-base v0.23.1 h1:j/BqdZUWeWKCy2v/jcgnOJAzpRYWSbGcjGVYICko8Uc= -k8s.io/component-base v0.23.1/go.mod h1:6llmap8QtJIXGDd4uIWJhAq0Op8AtQo6bDW2RrNMTeo= -k8s.io/component-helpers v0.22.2/go.mod h1:+N61JAR9aKYSWbnLA88YcFr9K/6ISYvRNybX7QW7Rs8= -k8s.io/component-helpers v0.23.1/go.mod h1:ZK24U+2oXnBPcas2KolLigVVN9g5zOzaHLkHiQMFGr0= -k8s.io/csi-translation-lib v0.18.8/go.mod h1:6cA6Btlzxy9s3QrS4BCZzQqclIWnTLr6Jx3H2ctAzY4= -k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= -k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= -k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.10.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/kubectl v0.22.2/go.mod h1:BApg2j0edxLArCOfO0ievI27EeTQqBDMNU9VQH734iQ= -k8s.io/kubectl v0.23.1 h1:gmscOiV4Y4XIRIn14gQBBADoyyVrDZPbxRCTDga4RSA= -k8s.io/kubectl v0.23.1/go.mod h1:Ui7dJKdUludF8yWAOSN7JZEkOuYixX5yF6E6NjoukKE= -k8s.io/legacy-cloud-providers v0.18.8/go.mod h1:tgp4xYf6lvjrWnjQwTOPvWQE9IVqSBGPF4on0IyICQE= -k8s.io/metrics v0.22.2/go.mod h1:GUcsBtpsqQD1tKFS/2wCKu4ZBowwRncLOJH1rgWs3uw= -k8s.io/metrics v0.23.1/go.mod h1:qXvsM1KANrc+ZZeFwj6Phvf0NLiC+d3RwcsLcdGc+xs= -k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210820185131-d34e5cb4466e/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 h1:ZKMMxTvduyf5WUtREOqg5LiXaN1KO/+0oOQPRFrClpo= -k8s.io/utils v0.0.0-20211208161948-7d6a63dca704/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= -modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= -modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= -modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= -modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I= -sigs.k8s.io/cli-utils v0.27.0 h1:BxI7lPNn0fBZa5g4UwR+ShJyL4CCxELA6tLHbr2YrpU= -sigs.k8s.io/cli-utils v0.27.0/go.mod h1:8ll2fyx+bzjbwmwUnKBQU+2LDbMDsxy44DiDZ+drALg= -sigs.k8s.io/controller-runtime v0.10.1/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= -sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0CwgqMpZWQ= -sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= -sigs.k8s.io/kustomize/api v0.8.11/go.mod h1:a77Ls36JdfCWojpUqR6m60pdGY1AYFix4AH83nJtY1g= -sigs.k8s.io/kustomize/api v0.10.1 h1:KgU7hfYoscuqag84kxtzKdEC3mKMb99DPI3a0eaV1d0= -sigs.k8s.io/kustomize/api v0.10.1/go.mod h1:2FigT1QN6xKdcnGS2Ppp1uIWrtWN28Ms8A3OZUZhwr8= -sigs.k8s.io/kustomize/cmd/config v0.9.13/go.mod h1:7547FLF8W/lTaDf0BDqFTbZxM9zqwEJqCKN9sSR0xSs= -sigs.k8s.io/kustomize/cmd/config v0.10.2/go.mod h1:K2aW7nXJ0AaT+VA/eO0/dzFLxmpFcTzudmAgDwPY1HQ= -sigs.k8s.io/kustomize/kustomize/v4 v4.2.0/go.mod h1:MOkR6fmhwG7hEDRXBYELTi5GSFcLwfqwzTRHW3kv5go= -sigs.k8s.io/kustomize/kustomize/v4 v4.4.1/go.mod h1:qOKJMMz2mBP+vcS7vK+mNz4HBLjaQSWRY22EF6Tb7Io= -sigs.k8s.io/kustomize/kyaml v0.11.0/go.mod h1:GNMwjim4Ypgp/MueD3zXHLRJEjz7RvtPae0AwlvEMFM= -sigs.k8s.io/kustomize/kyaml v0.12.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= -sigs.k8s.io/kustomize/kyaml v0.13.0 h1:9c+ETyNfSrVhxvphs+K2dzT3dh5oVPPEqPOE/cUpScY= -sigs.k8s.io/kustomize/kyaml v0.13.0/go.mod h1:FTJxEZ86ScK184NpGSAQcfEqee0nul8oLCK30D47m4E= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ= -sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/structured-merge-diff/v4 v4.2.0 h1:kDvPBbnPk+qYmkHmSo8vKGp438IASWofnbbUKDE/bv0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs= +k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/cli-runtime v0.30.0 h1:0vn6/XhOvn1RJ2KJOC6IRR2CGqrpT6QQF4+8pYpWQ48= +k8s.io/cli-runtime v0.30.0/go.mod h1:vATpDMATVTMA79sZ0YUCzlMelf6rUjoBzlp+RnoM+cg= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/component-base v0.30.0 h1:cj6bp38g0ainlfYtaOQuRELh5KSYjhKxM+io7AUIk4o= +k8s.io/component-base v0.30.0/go.mod h1:V9x/0ePFNaKeKYA3bOvIbrNoluTSG+fSJKjLdjOoeXQ= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 h1:SbdLaI6mM6ffDSJCadEaD4IkuPzepLDGlkd2xV0t1uA= +k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kubectl v0.30.0 h1:xbPvzagbJ6RNYVMVuiHArC1grrV5vSmmIcSZuCdzRyk= +k8s.io/kubectl v0.30.0/go.mod h1:zgolRw2MQXLPwmic2l/+iHs239L49fhSeICuMhQQXTI= +k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= +k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.18.1 h1:RpWbigmuiylbxOCLy0tGnq1cU1qWPwNIQzoJk+QeJx4= +sigs.k8s.io/controller-runtime v0.18.1/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/kustomize/api v0.17.1 h1:MYJBOP/yQ3/5tp4/sf6HiiMfNNyO97LmtnirH9SLNr4= +sigs.k8s.io/kustomize/api v0.17.1/go.mod h1:ffn5491s2EiNrJSmgqcWGzQUVhc/pB0OKNI0HsT/0tA= +sigs.k8s.io/kustomize/kyaml v0.17.0 h1:G2bWs03V9Ur2PinHLzTUJ8Ded+30SzXZKiO92SRDs3c= +sigs.k8s.io/kustomize/kyaml v0.17.0/go.mod h1:6lxkYF1Cv9Ic8g/N7I86cvxNc5iinUo/P2vKsHNmpyE= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/install/README.md b/install/README.md index 99bb0487..05c0a488 100644 --- a/install/README.md +++ b/install/README.md @@ -9,6 +9,9 @@ To install the latest release run: curl -s https://raw.githubusercontent.com/fluxcd/flux2/main/install/flux.sh | sudo bash ``` +**Note**: You may want to export the `GITHUB_TOKEN` environment variable using a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) +to avoid GitHub API rate-limiting errors if executing the install script repeatedly during a short time frame. + The install script does the following: * attempts to detect your OS * downloads and unpacks the release tar file in a temporary directory diff --git a/install/flux.sh b/install/flux.sh index 496f2158..34e6a980 100755 --- a/install/flux.sh +++ b/install/flux.sh @@ -112,10 +112,10 @@ download() { case $DOWNLOADER in curl) - curl -o "$1" -sfL "$2" + curl -u user:$GITHUB_TOKEN -o "$1" -sfL "$2" ;; wget) - wget -qO "$1" "$2" + wget --auth-no-challenge --user=user --password=$GITHUB_TOKEN -qO "$1" "$2" ;; *) fatal "Incorrect executable '${DOWNLOADER}'" diff --git a/internal/bootstrap/bootstrap_plain_git.go b/internal/bootstrap/bootstrap_plain_git.go deleted file mode 100644 index f661e936..00000000 --- a/internal/bootstrap/bootstrap_plain_git.go +++ /dev/null @@ -1,363 +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 bootstrap - -import ( - "context" - "fmt" - "os" - "path/filepath" - "strings" - "time" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/cli-runtime/pkg/genericclioptions" - "sigs.k8s.io/cli-utils/pkg/object" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/kustomize/api/filesys" - "sigs.k8s.io/kustomize/api/konfig" - "sigs.k8s.io/yaml" - - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - - "github.com/fluxcd/flux2/internal/bootstrap/git" - "github.com/fluxcd/flux2/internal/utils" - "github.com/fluxcd/flux2/pkg/log" - "github.com/fluxcd/flux2/pkg/manifestgen/install" - "github.com/fluxcd/flux2/pkg/manifestgen/kustomization" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" - "github.com/fluxcd/flux2/pkg/manifestgen/sync" - "github.com/fluxcd/flux2/pkg/status" -) - -type PlainGitBootstrapper struct { - url string - branch string - caBundle []byte - - author git.Author - commitMessageAppendix string - - gpgKeyRingPath string - gpgPassphrase string - gpgKeyID string - - restClientGetter genericclioptions.RESTClientGetter - - postGenerateSecret []PostGenerateSecretFunc - - git git.Git - kube client.Client - logger log.Logger -} - -type GitOption interface { - applyGit(b *PlainGitBootstrapper) -} - -func WithRepositoryURL(url string) GitOption { - return repositoryURLOption(url) -} - -type repositoryURLOption string - -func (o repositoryURLOption) applyGit(b *PlainGitBootstrapper) { - b.url = string(o) -} - -func WithPostGenerateSecretFunc(callback PostGenerateSecretFunc) GitOption { - return postGenerateSecret(callback) -} - -type postGenerateSecret PostGenerateSecretFunc - -func (o postGenerateSecret) applyGit(b *PlainGitBootstrapper) { - b.postGenerateSecret = append(b.postGenerateSecret, PostGenerateSecretFunc(o)) -} - -func NewPlainGitProvider(git git.Git, kube client.Client, opts ...GitOption) (*PlainGitBootstrapper, error) { - b := &PlainGitBootstrapper{ - git: git, - kube: kube, - } - for _, opt := range opts { - opt.applyGit(b) - } - return b, nil -} - -func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifestsBase string, options install.Options, secretOpts sourcesecret.Options) error { - // Clone if not already - if _, err := b.git.Status(); err != nil { - if err != git.ErrNoGitRepository { - return err - } - - b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url) - var cloned bool - if err = retry(1, 2*time.Second, func() (err error) { - cloned, err = b.git.Clone(ctx, b.url, b.branch, b.caBundle) - return - }); err != nil { - return fmt.Errorf("failed to clone repository: %w", err) - } - if cloned { - b.logger.Successf("cloned repository") - } - } - - // Generate component manifests - b.logger.Actionf("generating component manifests") - manifests, err := install.Generate(options, manifestsBase) - if err != nil { - return fmt.Errorf("component manifest generation failed: %w", err) - } - b.logger.Successf("generated component manifests") - - // Write manifest to Git repository - if err = b.git.Write(manifests.Path, strings.NewReader(manifests.Content)); err != nil { - return fmt.Errorf("failed to write manifest %q: %w", manifests.Path, err) - } - - // Git commit generated - gpgOpts := git.WithGpgSigningOption(b.gpgKeyRingPath, b.gpgPassphrase, b.gpgKeyID) - commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version) - if b.commitMessageAppendix != "" { - commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix - } - commit, err := b.git.Commit(git.Commit{ - Author: b.author, - Message: commitMsg, - }, gpgOpts) - if err != nil && err != git.ErrNoStagedFiles { - return fmt.Errorf("failed to commit sync manifests: %w", err) - } - if err == nil { - b.logger.Successf("committed sync manifests to %q (%q)", b.branch, commit) - b.logger.Actionf("pushing component manifests to %q", b.url) - if err = b.git.Push(ctx, b.caBundle); err != nil { - return fmt.Errorf("failed to push manifests: %w", err) - } - } else { - b.logger.Successf("component manifests are up to date") - } - - // Conditionally install manifests - if mustInstallManifests(ctx, b.kube, options.Namespace) { - componentsYAML := filepath.Join(b.git.Path(), manifests.Path) - - // Apply components using any existing customisations - kfile := filepath.Join(filepath.Dir(componentsYAML), konfig.DefaultKustomizationFileName()) - if _, err := os.Stat(kfile); err == nil { - // Apply the components and their patches - b.logger.Actionf("installing components in %q namespace", options.Namespace) - if _, err := utils.Apply(ctx, b.restClientGetter, kfile); err != nil { - return err - } - } else { - // Apply the CRDs and controllers - if _, err := utils.Apply(ctx, b.restClientGetter, componentsYAML); err != nil { - return err - } - } - b.logger.Successf("installed components") - } - - b.logger.Successf("reconciled components") - return nil -} - -func (b *PlainGitBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error { - // Determine if there is an existing secret - secretKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace} - b.logger.Actionf("determining if source secret %q exists", secretKey) - ok, err := secretExists(ctx, b.kube, secretKey) - if err != nil { - return fmt.Errorf("failed to determine if deploy key secret exists: %w", err) - } - - // Return early if exists and no custom config is passed - if ok && len(options.CAFilePath+options.PrivateKeyPath+options.Username+options.Password) == 0 { - b.logger.Successf("source secret up to date") - return nil - } - - // Generate source secret - b.logger.Actionf("generating source secret") - manifest, err := sourcesecret.Generate(options) - if err != nil { - return err - } - var secret corev1.Secret - if err := yaml.Unmarshal([]byte(manifest.Content), &secret); err != nil { - return fmt.Errorf("failed to unmarshal generated source secret manifest: %w", err) - } - - for _, callback := range b.postGenerateSecret { - if err = callback(ctx, secret, options); err != nil { - return err - } - } - - // Apply source secret - b.logger.Actionf("applying source secret %q", secretKey) - if err = reconcileSecret(ctx, b.kube, secret); err != nil { - return err - } - b.logger.Successf("reconciled source secret") - - return nil -} - -func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error { - // Confirm that sync configuration does not overwrite existing config - if curPath, err := kustomizationPathDiffers(ctx, b.kube, client.ObjectKey{Name: options.Name, Namespace: options.Namespace}, options.TargetPath); err != nil { - return fmt.Errorf("failed to determine if sync configuration would overwrite existing Kustomization: %w", err) - } else if curPath != "" { - return fmt.Errorf("sync path configuration (%q) would overwrite path (%q) of existing Kustomization", options.TargetPath, curPath) - } - - // Clone if not already - if _, err := b.git.Status(); err != nil { - if err == git.ErrNoGitRepository { - b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url) - var cloned bool - if err = retry(1, 2*time.Second, func() (err error) { - cloned, err = b.git.Clone(ctx, b.url, b.branch, b.caBundle) - return - }); err != nil { - return fmt.Errorf("failed to clone repository: %w", err) - } - if cloned { - b.logger.Successf("cloned repository") - } - } - return err - } - - // Generate sync manifests and write to Git repository - b.logger.Actionf("generating sync manifests") - manifests, err := sync.Generate(options) - if err != nil { - return fmt.Errorf("sync manifests generation failed: %w", err) - } - if err = b.git.Write(manifests.Path, strings.NewReader(manifests.Content)); err != nil { - return fmt.Errorf("failed to write manifest %q: %w", manifests.Path, err) - } - kusManifests, err := kustomization.Generate(kustomization.Options{ - FileSystem: filesys.MakeFsOnDisk(), - BaseDir: b.git.Path(), - TargetPath: filepath.Dir(manifests.Path), - }) - if err != nil { - return fmt.Errorf("kustomization.yaml generation failed: %w", err) - } - if err = b.git.Write(kusManifests.Path, strings.NewReader(kusManifests.Content)); err != nil { - return fmt.Errorf("failed to write manifest %q: %w", kusManifests.Path, err) - } - b.logger.Successf("generated sync manifests") - - // Git commit generated - gpgOpts := git.WithGpgSigningOption(b.gpgKeyRingPath, b.gpgPassphrase, b.gpgKeyID) - commitMsg := fmt.Sprintf("Add Flux sync manifests") - if b.commitMessageAppendix != "" { - commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix - } - commit, err := b.git.Commit(git.Commit{ - Author: b.author, - Message: commitMsg, - }, gpgOpts) - - if err != nil && err != git.ErrNoStagedFiles { - return fmt.Errorf("failed to commit sync manifests: %w", err) - } - if err == nil { - b.logger.Successf("committed sync manifests to %q (%q)", b.branch, commit) - b.logger.Actionf("pushing sync manifests to %q", b.url) - if err = b.git.Push(ctx, b.caBundle); err != nil { - return fmt.Errorf("failed to push sync manifests: %w", err) - } - } else { - b.logger.Successf("sync manifests are up to date") - } - - // Apply to cluster - b.logger.Actionf("applying sync manifests") - if _, err := utils.Apply(ctx, b.restClientGetter, filepath.Join(b.git.Path(), kusManifests.Path)); err != nil { - return err - } - - b.logger.Successf("reconciled sync configuration") - - return nil -} - -func (b *PlainGitBootstrapper) ReportKustomizationHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error { - head, err := b.git.Head() - if err != nil { - return err - } - - objKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace} - - b.logger.Waitingf("waiting for Kustomization %q to be reconciled", objKey.String()) - - expectRevision := fmt.Sprintf("%s/%s", options.Branch, head) - var k kustomizev1.Kustomization - if err := wait.PollImmediate(pollInterval, timeout, kustomizationReconciled( - ctx, b.kube, objKey, &k, expectRevision), - ); err != nil { - b.logger.Failuref(err.Error()) - return err - } - - b.logger.Successf("Kustomization reconciled successfully") - return nil -} - -func (b *PlainGitBootstrapper) ReportComponentsHealth(ctx context.Context, install install.Options, timeout time.Duration) error { - cfg, err := utils.KubeConfig(b.restClientGetter) - if err != nil { - return err - } - - checker, err := status.NewStatusChecker(cfg, 5*time.Second, timeout, b.logger) - if err != nil { - return err - } - - var components = install.Components - components = append(components, install.ComponentsExtra...) - - var identifiers []object.ObjMetadata - for _, component := range components { - identifiers = append(identifiers, object.ObjMetadata{ - Namespace: install.Namespace, - Name: component, - GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, - }) - } - - b.logger.Actionf("confirming components are healthy") - if err := checker.Assess(identifiers...); err != nil { - return err - } - b.logger.Successf("all components are healthy") - return nil -} diff --git a/internal/bootstrap/git/commit_options.go b/internal/bootstrap/git/commit_options.go deleted file mode 100644 index e39614d2..00000000 --- a/internal/bootstrap/git/commit_options.go +++ /dev/null @@ -1,42 +0,0 @@ -package git - -// Option is a some configuration that modifies options for a commit. -type Option interface { - // ApplyToCommit applies this configuration to a given commit option. - ApplyToCommit(*CommitOptions) -} - -// CommitOptions contains options for making a commit. -type CommitOptions struct { - *GPGSigningInfo -} - -// GPGSigningInfo contains information for signing a commit. -type GPGSigningInfo struct { - KeyRingPath string - Passphrase string - KeyID string -} - -type GpgSigningOption struct { - *GPGSigningInfo -} - -func (w GpgSigningOption) ApplyToCommit(in *CommitOptions) { - in.GPGSigningInfo = w.GPGSigningInfo -} - -func WithGpgSigningOption(path, passphrase, keyID string) Option { - // Return nil if no path is set, even if other options are configured. - if path == "" { - return GpgSigningOption{} - } - - return GpgSigningOption{ - GPGSigningInfo: &GPGSigningInfo{ - KeyRingPath: path, - Passphrase: passphrase, - KeyID: keyID, - }, - } -} diff --git a/internal/bootstrap/git/git.go b/internal/bootstrap/git/git.go deleted file mode 100644 index 1a0f6d86..00000000 --- a/internal/bootstrap/git/git.go +++ /dev/null @@ -1,52 +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 git - -import ( - "context" - "errors" - "io" -) - -var ( - ErrNoGitRepository = errors.New("no git repository") - ErrNoStagedFiles = errors.New("no staged files") -) - -type Author struct { - Name string - Email string -} - -type Commit struct { - Author - Hash string - Message string -} - -// Git is an interface for basic Git operations on a single branch of a -// remote repository. -type Git interface { - Init(url, branch string) (bool, error) - Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error) - Write(path string, reader io.Reader) error - Commit(message Commit, options ...Option) (string, error) - Push(ctx context.Context, caBundle []byte) error - Status() (bool, error) - Head() (string, error) - Path() string -} diff --git a/internal/bootstrap/git/gogit/gogit.go b/internal/bootstrap/git/gogit/gogit.go deleted file mode 100644 index 5525aeff..00000000 --- a/internal/bootstrap/git/gogit/gogit.go +++ /dev/null @@ -1,296 +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 gogit - -import ( - "context" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "time" - - "github.com/ProtonMail/go-crypto/openpgp" - gogit "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/config" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/transport" - - "github.com/fluxcd/flux2/internal/bootstrap/git" -) - -type GoGit struct { - path string - auth transport.AuthMethod - repository *gogit.Repository -} - -type CommitOptions struct { - GpgKeyPath string - GpgKeyPassphrase string - KeyID string -} - -func New(path string, auth transport.AuthMethod) *GoGit { - return &GoGit{ - path: path, - auth: auth, - } -} - -func (g *GoGit) Init(url, branch string) (bool, error) { - if g.repository != nil { - return false, nil - } - - r, err := gogit.PlainInit(g.path, false) - if err != nil { - return false, err - } - if _, err = r.CreateRemote(&config.RemoteConfig{ - Name: gogit.DefaultRemoteName, - URLs: []string{url}, - }); err != nil { - return false, err - } - branchRef := plumbing.NewBranchReferenceName(branch) - if err = r.CreateBranch(&config.Branch{ - Name: branch, - Remote: gogit.DefaultRemoteName, - Merge: branchRef, - }); err != nil { - return false, err - } - // PlainInit assumes the initial branch to always be master, we can - // overwrite this by setting the reference of the Storer to a new - // symbolic reference (as there are no commits yet) that points - // the HEAD to our new branch. - if err = r.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, branchRef)); err != nil { - return false, err - } - - g.repository = r - return true, nil -} - -func (g *GoGit) Clone(ctx context.Context, url, branch string, caBundle []byte) (bool, error) { - branchRef := plumbing.NewBranchReferenceName(branch) - r, err := gogit.PlainCloneContext(ctx, g.path, false, &gogit.CloneOptions{ - URL: url, - Auth: g.auth, - RemoteName: gogit.DefaultRemoteName, - ReferenceName: branchRef, - SingleBranch: true, - - NoCheckout: false, - Progress: nil, - Tags: gogit.NoTags, - CABundle: caBundle, - }) - if err != nil { - if err == transport.ErrEmptyRemoteRepository || isRemoteBranchNotFoundErr(err, branchRef.String()) { - return g.Init(url, branch) - } - return false, err - } - - g.repository = r - return true, nil -} - -func (g *GoGit) Write(path string, reader io.Reader) error { - if g.repository == nil { - return git.ErrNoGitRepository - } - - wt, err := g.repository.Worktree() - if err != nil { - return err - } - - f, err := wt.Filesystem.Create(path) - if err != nil { - return err - } - defer f.Close() - - _, err = io.Copy(f, reader) - return err -} - -func (g *GoGit) Commit(message git.Commit, opts ...git.Option) (string, error) { - if g.repository == nil { - return "", git.ErrNoGitRepository - } - - wt, err := g.repository.Worktree() - if err != nil { - return "", err - } - - status, err := wt.Status() - if err != nil { - return "", err - } - - // apply the options - options := &git.CommitOptions{} - for _, opt := range opts { - opt.ApplyToCommit(options) - } - - // go-git has [a bug](https://github.com/go-git/go-git/issues/253) - // whereby it thinks broken symlinks to absolute paths are - // modified. There's no circumstance in which we want to commit a - // change to a broken symlink: so, detect and skip those. - var changed bool - for file, _ := range status { - abspath := filepath.Join(g.path, file) - info, err := os.Lstat(abspath) - if err != nil { - return "", fmt.Errorf("checking if %s is a symlink: %w", file, err) - } - if info.Mode()&os.ModeSymlink > 0 { - // symlinks are OK; broken symlinks are probably a result - // of the bug mentioned above, but not of interest in any - // case. - if _, err := os.Stat(abspath); os.IsNotExist(err) { - continue - } - } - _, _ = wt.Add(file) - changed = true - } - - if !changed { - head, err := g.repository.Head() - if err != nil { - return "", err - } - return head.Hash().String(), git.ErrNoStagedFiles - } - - commitOpts := &gogit.CommitOptions{ - Author: &object.Signature{ - Name: message.Name, - Email: message.Email, - When: time.Now(), - }, - } - - if options.GPGSigningInfo != nil { - entity, err := getOpenPgpEntity(*options.GPGSigningInfo) - if err != nil { - return "", err - } - - commitOpts.SignKey = entity - } - - commit, err := wt.Commit(message.Message, commitOpts) - if err != nil { - return "", err - } - return commit.String(), nil -} - -func (g *GoGit) Push(ctx context.Context, caBundle []byte) error { - if g.repository == nil { - return git.ErrNoGitRepository - } - - return g.repository.PushContext(ctx, &gogit.PushOptions{ - RemoteName: gogit.DefaultRemoteName, - Auth: g.auth, - Progress: nil, - CABundle: caBundle, - }) -} - -func (g *GoGit) Status() (bool, error) { - if g.repository == nil { - return false, git.ErrNoGitRepository - } - wt, err := g.repository.Worktree() - if err != nil { - return false, err - } - status, err := wt.Status() - if err != nil { - return false, err - } - return status.IsClean(), nil -} - -func (g *GoGit) Head() (string, error) { - if g.repository == nil { - return "", git.ErrNoGitRepository - } - head, err := g.repository.Head() - if err != nil { - return "", err - } - return head.Hash().String(), nil -} - -func (g *GoGit) Path() string { - return g.path -} - -func isRemoteBranchNotFoundErr(err error, ref string) bool { - return strings.Contains(err.Error(), fmt.Sprintf("couldn't find remote ref %q", ref)) -} - -func getOpenPgpEntity(info git.GPGSigningInfo) (*openpgp.Entity, error) { - r, err := os.Open(info.KeyRingPath) - if err != nil { - return nil, fmt.Errorf("unable to open GPG key ring: %w", err) - } - - entityList, err := openpgp.ReadKeyRing(r) - if err != nil { - return nil, err - } - - if len(entityList) == 0 { - return nil, fmt.Errorf("empty GPG key ring") - } - - var entity *openpgp.Entity - if info.KeyID != "" { - for _, ent := range entityList { - if ent.PrimaryKey.KeyIdString() == info.KeyID { - entity = ent - } - } - - if entity == nil { - return nil, fmt.Errorf("no GPG private key matching key id '%s' found", info.KeyID) - } - } else { - entity = entityList[0] - } - - err = entity.PrivateKey.Decrypt([]byte(info.Passphrase)) - if err != nil { - return nil, fmt.Errorf("unable to decrypt GPG private key: %w", err) - } - - return entity, nil -} diff --git a/internal/bootstrap/git/gogit/gogit_test.go b/internal/bootstrap/git/gogit/gogit_test.go deleted file mode 100644 index 416898d1..00000000 --- a/internal/bootstrap/git/gogit/gogit_test.go +++ /dev/null @@ -1,67 +0,0 @@ -//go:build unit -// +build unit - -package gogit - -import ( - "testing" - - "github.com/fluxcd/flux2/internal/bootstrap/git" -) - -func TestGetOpenPgpEntity(t *testing.T) { - tests := []struct { - name string - keyPath string - passphrase string - id string - expectErr bool - }{ - { - name: "no default key id given", - keyPath: "testdata/private.key", - passphrase: "flux", - id: "", - expectErr: false, - }, - { - name: "key id given", - keyPath: "testdata/private.key", - passphrase: "flux", - id: "0619327DBD777415", - expectErr: false, - }, - { - name: "wrong key id", - keyPath: "testdata/private.key", - passphrase: "flux", - id: "0619327DBD777416", - expectErr: true, - }, - { - name: "wrong password", - keyPath: "testdata/private.key", - passphrase: "fluxe", - id: "0619327DBD777415", - expectErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - gpgInfo := git.GPGSigningInfo{ - KeyRingPath: tt.keyPath, - Passphrase: tt.passphrase, - KeyID: tt.id, - } - - _, err := getOpenPgpEntity(gpgInfo) - if err != nil && !tt.expectErr { - t.Errorf("unexpected error: %s", err) - } - if err == nil && tt.expectErr { - t.Errorf("expected error when %s", tt.name) - } - }) - } -} diff --git a/internal/bootstrap/git/gogit/testdata/private.key b/internal/bootstrap/git/gogit/testdata/private.key deleted file mode 100644 index 9e56432c..00000000 Binary files a/internal/bootstrap/git/gogit/testdata/private.key and /dev/null differ diff --git a/internal/build/build.go b/internal/build/build.go index 416c2b0b..d8a8c381 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -20,28 +20,44 @@ import ( "bytes" "context" "encoding/base64" + "encoding/json" + "errors" "fmt" + "io" + "os" + "strings" "sync" "time" - "github.com/fluxcd/flux2/internal/utils" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - "github.com/fluxcd/pkg/kustomize" + "github.com/theckman/yacspin" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + k8syaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resource" + "sigs.k8s.io/kustomize/kyaml/yaml" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/pkg/kustomize" + runclient "github.com/fluxcd/pkg/runtime/client" + ssautil "github.com/fluxcd/pkg/ssa/utils" "sigs.k8s.io/kustomize/kyaml/filesys" + + "github.com/fluxcd/flux2/v2/internal/utils" ) const ( - controllerName = "kustomize-controller" - controllerGroup = "kustomize.toolkit.fluxcd.io" - mask = "**SOPS**" + controllerName = "kustomize-controller" + controllerGroup = "kustomize.toolkit.fluxcd.io" + mask = "**SOPS**" + dockercfgSecretType = "kubernetes.io/dockerconfigjson" + typeField = "type" + dataField = "data" + stringDataField = "stringData" ) var defaultTimeout = 80 * time.Second @@ -50,20 +66,35 @@ var defaultTimeout = 80 * time.Second // It retrieves the kustomization object from the k8s cluster // and overlays the manifests with the resources specified in the resourcesPath type Builder struct { - client client.WithWatch - restMapper meta.RESTMapper - name string - namespace string - resourcesPath string + client client.WithWatch + restMapper meta.RESTMapper + name string + namespace string + resourcesPath string + kustomizationFile string + ignore []string // mu is used to synchronize access to the kustomization file mu sync.Mutex action kustomize.Action kustomization *kustomizev1.Kustomization timeout time.Duration + spinner *yacspin.Spinner + dryRun bool + strictSubst bool } +// BuilderOptionFunc is a function that configures a Builder type BuilderOptionFunc func(b *Builder) error +// WithKustomizationFile sets the kustomization file +func WithKustomizationFile(file string) BuilderOptionFunc { + return func(b *Builder) error { + b.kustomizationFile = file + return nil + } +} + +// WithTimeout sets the timeout for the builder func WithTimeout(timeout time.Duration) BuilderOptionFunc { return func(b *Builder) error { b.timeout = timeout @@ -71,24 +102,93 @@ func WithTimeout(timeout time.Duration) BuilderOptionFunc { } } -// NewBuilder returns a new Builder -// to dp : create functional options -func NewBuilder(rcg *genericclioptions.ConfigFlags, name, resources string, opts ...BuilderOptionFunc) (*Builder, error) { - kubeClient, err := utils.KubeClient(rcg) - if err != nil { - return nil, err +func WithProgressBar() BuilderOptionFunc { + return func(b *Builder) error { + // Add a spinner + cfg := yacspin.Config{ + Frequency: 100 * time.Millisecond, + CharSet: yacspin.CharSets[59], + Suffix: "Kustomization diffing...", + SuffixAutoColon: true, + Message: "running dry-run", + StopCharacter: "✓", + StopColors: []string{"fgGreen"}, + } + spinner, err := yacspin.New(cfg) + if err != nil { + return fmt.Errorf("failed to create spinner: %w", err) + } + b.spinner = spinner + + return nil } +} - restMapper, err := rcg.ToRESTMapper() - if err != nil { - return nil, err +// WithClientConfig sets the client configuration +func WithClientConfig(rcg *genericclioptions.ConfigFlags, clientOpts *runclient.Options) BuilderOptionFunc { + return func(b *Builder) error { + kubeClient, err := utils.KubeClient(rcg, clientOpts) + if err != nil { + return err + } + + restMapper, err := rcg.ToRESTMapper() + if err != nil { + return err + } + b.client = kubeClient + b.restMapper = restMapper + b.namespace = *rcg.Namespace + return nil + } +} + +// WithNamespace sets the namespace +func WithNamespace(namespace string) BuilderOptionFunc { + return func(b *Builder) error { + b.namespace = namespace + return nil + } +} + +// WithDryRun sets the dry-run flag +func WithDryRun(dryRun bool) BuilderOptionFunc { + return func(b *Builder) error { + b.dryRun = dryRun + return nil + } +} + +// WithStrictSubstitute sets the strict substitute flag +func WithStrictSubstitute(strictSubstitute bool) BuilderOptionFunc { + return func(b *Builder) error { + b.strictSubst = strictSubstitute + return nil + } +} + +// WithIgnore sets ignore field +func WithIgnore(ignore []string) BuilderOptionFunc { + return func(b *Builder) error { + b.ignore = ignore + return nil } +} +// NewBuilder returns a new Builder +// It takes a kustomization name and a path to the resources +// It also takes a list of BuilderOptionFunc to configure the builder +// One of the options is WithClientConfig, that must be provided for the builder to work +// with the k8s cluster +// One other option is WithKustomizationFile, that must be provided for the builder to work +// with a local kustomization file. If the kustomization file is not provided, the builder +// will try to retrieve the kustomization object from the k8s cluster. +// WithDryRun sets the dry-run flag, and needs to be provided if the builder is used for +// a dry-run. This flag works in conjunction with WithKustomizationFile, because the +// kustomization object is not retrieved from the k8s cluster when the dry-run flag is set. +func NewBuilder(name, resources string, opts ...BuilderOptionFunc) (*Builder, error) { b := &Builder{ - client: kubeClient, - restMapper: restMapper, name: name, - namespace: *rcg.Namespace, resourcesPath: resources, } @@ -102,29 +202,54 @@ func NewBuilder(rcg *genericclioptions.ConfigFlags, name, resources string, opts b.timeout = defaultTimeout } + if b.dryRun && b.kustomizationFile == "" { + return nil, fmt.Errorf("kustomization file is required for dry-run") + } + + if !b.dryRun && b.client == nil { + return nil, fmt.Errorf("client is required for live run") + } + return b, nil } +func (b *Builder) resolveKustomization(liveKus *kustomizev1.Kustomization) (k *kustomizev1.Kustomization, err error) { + // local kustomization file takes precedence over live kustomization + if b.kustomizationFile != "" { + k, err = b.unMarshallKustomization() + if err != nil { + return + } + if !b.dryRun && liveKus != nil && liveKus.Status.Inventory != nil { + // merge the live kustomization status with the local kustomization in order to get the + // live resources status + k.Status = *liveKus.Status.DeepCopy() + } + } else { + k = liveKus + } + return +} + func (b *Builder) getKustomization(ctx context.Context) (*kustomizev1.Kustomization, error) { + liveKus := &kustomizev1.Kustomization{} namespacedName := types.NamespacedName{ Namespace: b.namespace, Name: b.name, } - - k := &kustomizev1.Kustomization{} - err := b.client.Get(ctx, namespacedName, k) + err := b.client.Get(ctx, namespacedName, liveKus) if err != nil { return nil, err } - return k, nil + return liveKus, nil } // Build builds the yaml manifests from the kustomization object // and overlays the manifests with the resources specified in the resourcesPath // It expects a kustomization.yaml file in the resourcesPath, and it will // generate a kustomization.yaml file if it doesn't exist -func (b *Builder) Build() ([]byte, error) { +func (b *Builder) Build() ([]*unstructured.Unstructured, error) { m, err := b.build() if err != nil { return nil, err @@ -135,7 +260,16 @@ func (b *Builder) Build() ([]byte, error) { return nil, fmt.Errorf("kustomize build failed: %w", err) } - return resources, nil + objects, err := ssautil.ReadObjects(bytes.NewReader(resources)) + if err != nil { + return nil, fmt.Errorf("kustomize build failed: %w", err) + } + + if m := b.kustomization.Spec.CommonMetadata; m != nil { + ssautil.SetCommonMetadata(objects, m.Labels, m.Annotations) + } + + return objects, nil } func (b *Builder) build() (m resmap.ResMap, err error) { @@ -143,8 +277,16 @@ func (b *Builder) build() (m resmap.ResMap, err error) { defer cancel() // Get the kustomization object - k, err := b.getKustomization(ctx) + liveKus := &kustomizev1.Kustomization{} + if !b.dryRun { + liveKus, err = b.getKustomization(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get kustomization object: %w", err) + } + } + k, err := b.resolveKustomization(liveKus) if err != nil { + err = fmt.Errorf("failed to get kustomization object: %w", err) return } @@ -182,7 +324,7 @@ func (b *Builder) build() (m resmap.ResMap, err error) { } // make sure secrets are masked - err = trimSopsData(res) + err = maskSopsData(res) if err != nil { return } @@ -192,14 +334,47 @@ func (b *Builder) build() (m resmap.ResMap, err error) { } +func (b *Builder) unMarshallKustomization() (*kustomizev1.Kustomization, error) { + data, err := os.ReadFile(b.kustomizationFile) + if err != nil { + return nil, fmt.Errorf("failed to read kustomization file %s: %w", b.kustomizationFile, err) + } + k := &kustomizev1.Kustomization{} + decoder := k8syaml.NewYAMLOrJSONDecoder(bytes.NewBuffer(data), len(data)) + // check for kustomization in yaml with the same name and namespace + for { + err = decoder.Decode(k) + if err != nil { + if err == io.EOF { + return nil, fmt.Errorf("failed find kustomization with name '%s' and namespace '%s' in file '%s'", + b.name, b.namespace, b.kustomizationFile) + } else { + return nil, fmt.Errorf("failed to unmarshall kustomization file %s: %w", b.kustomizationFile, err) + } + } + + if strings.HasPrefix(k.APIVersion, kustomizev1.GroupVersion.Group+"/") && + k.Kind == kustomizev1.KustomizationKind && + k.Name == b.name && + (k.Namespace == b.namespace || k.Namespace == "") { + break + } + } + return k, nil +} + func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath string) (kustomize.Action, error) { data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) if err != nil { return "", err } - gen := kustomize.NewGenerator(unstructured.Unstructured{Object: data}) - // acuire the lock + // a scanner will be used down the line to parse the list + // so we have to make sure to include newlines + ignoreList := strings.Join(b.ignore, "\n") + gen := kustomize.NewGeneratorWithIgnore("", ignoreList, unstructured.Unstructured{Object: data}) + + // acquire the lock b.mu.Lock() defer b.mu.Unlock() @@ -209,11 +384,11 @@ func (b *Builder) generate(kustomization kustomizev1.Kustomization, dirPath stri func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) { fs := filesys.MakeFsOnDisk() - // acuire the lock + // acquire the lock b.mu.Lock() defer b.mu.Unlock() - m, err := kustomize.BuildKustomization(fs, dirPath) + m, err := kustomize.Build(fs, dirPath) if err != nil { return nil, fmt.Errorf("kustomize build failed: %w", err) } @@ -225,7 +400,13 @@ func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomizatio if err != nil { return nil, err } - outRes, err := kustomize.SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res) + outRes, err := kustomize.SubstituteVariables(ctx, + b.client, + unstructured.Unstructured{Object: data}, + res, + kustomize.SubstituteWithDryRun(b.dryRun), + kustomize.SubstituteWithStrict(b.strictSubst), + ) if err != nil { return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err) } @@ -256,35 +437,149 @@ func (b *Builder) setOwnerLabels(res *resource.Resource) error { return nil } -func trimSopsData(res *resource.Resource) error { +func maskSopsData(res *resource.Resource) error { // sopsMess is the base64 encoded mask sopsMess := base64.StdEncoding.EncodeToString([]byte(mask)) if res.GetKind() == "Secret" { + // get both data and stringdata maps as a secret can have both dataMap := res.GetDataMap() - for k, v := range dataMap { - data, err := base64.StdEncoding.DecodeString(v) - if err != nil { - if _, ok := err.(base64.CorruptInputError); ok { - return fmt.Errorf("failed to decode secret data: %w", err) + stringDataMap := getStringDataMap(res) + asYaml, err := res.AsYAML() + if err != nil { + return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) + } + + // delete any sops data as we don't want to expose it + // assume that both data and stringdata are encrypted + if bytes.Contains(asYaml, []byte("sops:")) && bytes.Contains(asYaml, []byte("mac: ENC[")) { + // delete the sops object + res.PipeE(yaml.FieldClearer{Name: "sops"}) + + secretType, err := res.GetFieldValue(typeField) + // If the intended type is Opaque, then it can be omitted from the manifest, since it's the default + // Ref: https://kubernetes.io/docs/concepts/configuration/secret/#opaque-secrets + if errors.As(err, &yaml.NoFieldError{}) { + secretType = "Opaque" + } else if err != nil { + return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) + } + + if v, ok := secretType.(string); ok && v == dockercfgSecretType { + // if the secret is a json docker config secret, we need to mask the data with a json object + err := maskDockerconfigjsonSopsData(dataMap, true) + if err != nil { + return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) + } + + err = maskDockerconfigjsonSopsData(stringDataMap, false) + if err != nil { + return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) + } + + } else { + for k := range dataMap { + dataMap[k] = sopsMess + } + + for k := range stringDataMap { + stringDataMap[k] = mask } } + } else { + err := maskBase64EncryptedSopsData(dataMap, sopsMess) + if err != nil { + return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) + } - if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) { - dataMap[k] = sopsMess + err = maskSopsDataInStringDataSecret(stringDataMap, sopsMess) + if err != nil { + return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) } } + // set the data and stringdata maps res.SetDataMap(dataMap) + + if len(stringDataMap) > 0 { + err = res.SetMapField(yaml.NewMapRNode(&stringDataMap), stringDataField) + if err != nil { + return fmt.Errorf("failed to mask secret %s sops data: %w", res.GetName(), err) + } + } + } + + return nil +} + +func getStringDataMap(rn *resource.Resource) map[string]string { + n, err := rn.Pipe(yaml.Lookup(stringDataField)) + if err != nil { + return nil + } + result := map[string]string{} + _ = n.VisitFields(func(node *yaml.MapNode) error { + result[yaml.GetValue(node.Key)] = yaml.GetValue(node.Value) + return nil + }) + return result +} + +func maskDockerconfigjsonSopsData(dataMap map[string]string, encode bool) error { + sopsMess := struct { + Mask string `json:"mask"` + }{ + Mask: mask, + } + + maskJson, err := json.Marshal(sopsMess) + if err != nil { + return err + } + + if encode { + for k := range dataMap { + dataMap[k] = base64.StdEncoding.EncodeToString(maskJson) + } + return nil + } + + for k := range dataMap { + dataMap[k] = string(maskJson) + } + + return nil +} + +func maskBase64EncryptedSopsData(dataMap map[string]string, mask string) error { + for k, v := range dataMap { + data, err := base64.StdEncoding.DecodeString(v) + if corruptErr := base64.CorruptInputError(0); errors.As(err, &corruptErr) { + return corruptErr + } + + if bytes.Contains(data, []byte("sops")) && bytes.Contains(data, []byte("ENC[")) { + dataMap[k] = mask + } + } + + return nil +} + +func maskSopsDataInStringDataSecret(stringDataMap map[string]string, mask string) error { + for k, v := range stringDataMap { + if bytes.Contains([]byte(v), []byte("sops")) && bytes.Contains([]byte(v), []byte("ENC[")) { + stringDataMap[k] = mask + } } return nil } // Cancel cancels the build -// It restores a clean reprository +// It restores a clean repository func (b *Builder) Cancel() error { - // acuire the lock + // acquire the lock b.mu.Lock() defer b.mu.Unlock() diff --git a/internal/build/build_test.go b/internal/build/build_test.go index ffda319e..be62abc5 100644 --- a/internal/build/build_test.go +++ b/internal/build/build_test.go @@ -17,9 +17,15 @@ limitations under the License. package build import ( + "fmt" + "strings" "testing" + "time" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/pkg/apis/meta" "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -91,6 +97,45 @@ kind: Secret metadata: name: secret-basic-auth type: kubernetes.io/basic-auth +`, + }, + { + name: "secret sops secret", + yamlStr: `apiVersion: v1 +data: + .dockerconfigjson: ENC[AES256_GCM,data:KHCFH3hNnc+PMfWLFEPjebf3W4z4WXbGFAANRZyZC+07z7wlrTALJM6rn8YslW4tMAWCoAYxblC5WRCszTy0h9rw0U/RGOv5H0qCgnNg/FILFUqhwo9pNfrUH+MEP4M9qxxbLKZwObpHUE7DUsKx1JYAxsI=,iv:q48lqUbUQD+0cbYcjNMZMJLRdGHi78ZmDhNAT2th9tg=,tag:QRI2SZZXQrAcdql3R5AH2g==,type:str] +kind: Secret +metadata: + name: secret +type: kubernetes.io/dockerconfigjson +sops: + kms: [] + gcp_kms: [] + azure_kv: [] + hc_vault: [] + age: + - recipient: age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce + enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3eU1CTEJhVXZ4eEVYYkVV + OU90TEcrR2pYckttN0pBanJoSUZWSW1RQXlRCkUydFJ3V1NZUTBuVFF0aC9GUEcw + bUdhNjJWTkoyL1FUVi9Dc1dxUDBkM0UKLS0tIE1sQXkwcWdGaEFuY0RHQTVXM0J6 + dWpJcThEbW15V3dXYXpPZklBdW1Hd1kKoIAdmGNPrEctV8h1w8KuvQ5S+BGmgqN9 + MgpNmUhJjWhgcQpb5BRYpQesBOgU5TBGK7j58A6DMDKlSiYZsdQchQ== + -----END AGE ENCRYPTED FILE----- + lastmodified: "2022-02-03T16:03:17Z" + mac: ENC[AES256_GCM,data:AHdYSawajwgAFwlmDN1IPNmT9vWaYKzyVIra2d6sPcjTbZ8/p+VRSRpVm4XZFFsaNnW5AUJaouwXnKYDTmJDXKlr/rQcu9kXqsssQgdzcXaA6l5uJlgsnml8ba7J3OK+iEKMax23mwQEx2EUskCd9ENOwFDkunP02sxqDNOz20k=,iv:8F5OamHt3fAVorf6p+SoIrWoqkcATSGWVoM0EK87S4M=,tag:E1mxXnc7wWkEX5BxhpLtng==,type:str] + pgp: [] + encrypted_regex: ^(data|stringData)$ + version: 3.7.1 +`, + expected: `apiVersion: v1 +data: + .dockerconfigjson: eyJtYXNrIjoiKipTT1BTKioifQ== +kind: Secret +metadata: + name: secret +type: kubernetes.io/dockerconfigjson `, }, } @@ -103,7 +148,7 @@ type: kubernetes.io/basic-auth } resource := &resource.Resource{RNode: *r} - err = trimSopsData(resource) + err = maskSopsData(resource) if err != nil { t.Fatalf("unable to trim sops data: %v", err) } @@ -118,3 +163,201 @@ type: kubernetes.io/basic-auth }) } } + +func Test_unMarshallKustomization(t *testing.T) { + tests := []struct { + name string + localKsFile string + wantErr bool + errString string + }{ + { + name: "valid kustomization", + localKsFile: "testdata/local-kustomization/valid.yaml", + }, + { + name: "Multi-doc yaml containing kustomization and other resources", + localKsFile: "testdata/local-kustomization/multi-doc-valid.yaml", + }, + { + name: "no namespace", + localKsFile: "testdata/local-kustomization/no-ns.yaml", + }, + { + name: "kustomization with a different name", + localKsFile: "testdata/local-kustomization/different-name.yaml", + wantErr: true, + errString: "failed find kustomization with name", + }, + { + name: "yaml containing other resource with same name as kustomization", + localKsFile: "testdata/local-kustomization/invalid-resource.yaml", + wantErr: true, + errString: "failed find kustomization with name", + }, + } + + b := &Builder{ + name: "podinfo", + namespace: "flux-system", + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b.kustomizationFile = tt.localKsFile + ks, err := b.unMarshallKustomization() + if !tt.wantErr { + if err != nil { + t.Fatalf("unexpected err '%s'", err) + } + + if ks.Name != b.name && ks.Namespace != b.namespace { + t.Errorf("expected kustomization '%s/%s' to match '%s/%s'", + ks.Name, ks.Namespace, b.name, b.namespace) + } + } else { + if err == nil { + t.Fatal("expected error but got nil") + } + + if !strings.Contains(err.Error(), tt.errString) { + t.Errorf("expected error '%s' to contain string '%s'", err.Error(), tt.errString) + } + } + }) + } +} + +func Test_ResolveKustomization(t *testing.T) { + tests := []struct { + name string + localKsFile string + liveKustomization *kustomizev1.Kustomization + dryrun bool + }{ + { + name: "valid kustomization", + localKsFile: "testdata/local-kustomization/valid.yaml", + }, + { + name: "local and live kustomization", + localKsFile: "testdata/local-kustomization/valid.yaml", + liveKustomization: &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: "flux-system", + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: time.Minute * 5}, + Path: "./testdata/local-kustomization/valid.yaml", + }, + Status: kustomizev1.KustomizationStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + }, + }, + Inventory: &kustomizev1.ResourceInventory{ + Entries: []kustomizev1.ResourceRef{ + { + ID: "flux-system_podinfo_v1_service_podinfo", + Version: "v1", + }, + }, + }, + }, + }, + }, + { + name: "local and live kustomization with dryrun", + localKsFile: "testdata/local-kustomization/valid.yaml", + liveKustomization: &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: "flux-system", + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: time.Minute * 5}, + Path: "./testdata/local-kustomization/valid.yaml", + }, + Status: kustomizev1.KustomizationStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + }, + }, + Inventory: &kustomizev1.ResourceInventory{ + Entries: []kustomizev1.ResourceRef{ + { + ID: "flux-system_podinfo_v1_service_podinfo", + Version: "v1", + }, + }, + }, + }, + }, + dryrun: true, + }, + { + name: "live kustomization", + liveKustomization: &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: "flux-system", + }, + Spec: kustomizev1.KustomizationSpec{ + Interval: metav1.Duration{Duration: time.Minute * 5}, + Path: "./testdata/local-kustomization/valid.yaml", + }, + Status: kustomizev1.KustomizationStatus{ + Conditions: []metav1.Condition{ + { + Type: meta.ReadyCondition, + Status: metav1.ConditionTrue, + }, + }, + Inventory: &kustomizev1.ResourceInventory{ + Entries: []kustomizev1.ResourceRef{ + { + ID: "flux-system_podinfo_v1_service_podinfo", + Version: "v1", + }, + }, + }, + }, + }, + }, + } + + b := &Builder{ + name: "podinfo", + namespace: "flux-system", + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b.kustomizationFile = tt.localKsFile + b.dryRun = tt.dryrun + ks, err := b.resolveKustomization(tt.liveKustomization) + if err != nil { + t.Errorf("unexpected err '%s'", err) + } + if !tt.dryrun { + if b.kustomizationFile == "" { + if cmp.Diff(ks, tt.liveKustomization) != "" { + t.Errorf("expected kustomization to match live kustomization") + } + } else { + if tt.liveKustomization != nil && cmp.Diff(ks.Status, tt.liveKustomization.Status) != "" { + t.Errorf("expected kustomization status to match live kustomization status") + } + } + } else { + if ks.Status.Inventory != nil { + fmt.Println(ks.Status.Inventory) + t.Errorf("expected kustomization status to be nil") + } + } + }) + } +} diff --git a/internal/build/diff.go b/internal/build/diff.go index 43b85aae..3aad9d10 100644 --- a/internal/build/diff.go +++ b/internal/build/diff.go @@ -27,8 +27,6 @@ import ( "sort" "strings" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - "github.com/fluxcd/pkg/ssa" "github.com/gonvenience/bunt" "github.com/gonvenience/ytbx" "github.com/google/go-cmp/cmp" @@ -36,13 +34,20 @@ import ( "github.com/lucasb-eyer/go-colorful" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling" - "sigs.k8s.io/cli-utils/pkg/object" + "k8s.io/apimachinery/pkg/util/errors" "sigs.k8s.io/yaml" + + "github.com/fluxcd/cli-utils/pkg/kstatus/polling" + "github.com/fluxcd/cli-utils/pkg/object" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/pkg/ssa" + ssautil "github.com/fluxcd/pkg/ssa/utils" + + "github.com/fluxcd/flux2/v2/pkg/printers" ) func (b *Builder) Manager() (*ssa.ResourceManager, error) { - statusPoller := polling.NewStatusPoller(b.client, b.restMapper, nil) + statusPoller := polling.NewStatusPoller(b.client, b.restMapper, polling.Options{}) owner := ssa.Owner{ Field: controllerName, Group: controllerGroup, @@ -51,30 +56,35 @@ func (b *Builder) Manager() (*ssa.ResourceManager, error) { return ssa.NewResourceManager(b.client, statusPoller, owner), nil } -func (b *Builder) Diff() (string, error) { +func (b *Builder) Diff() (string, bool, error) { output := strings.Builder{} - res, err := b.Build() + createdOrDrifted := false + objects, err := b.Build() if err != nil { - return "", err + return "", createdOrDrifted, err } - // convert the build result into Kubernetes unstructured objects - objects, err := ssa.ReadObjects(bytes.NewReader(res)) + + err = ssa.SetNativeKindsDefaults(objects) if err != nil { - return "", err + return "", createdOrDrifted, err } resourceManager, err := b.Manager() if err != nil { - return "", err + return "", createdOrDrifted, err } ctx, cancel := context.WithTimeout(context.Background(), b.timeout) defer cancel() - if err := ssa.SetNativeKindsDefaults(objects); err != nil { - return "", err + if b.spinner != nil { + err = b.spinner.Start() + if err != nil { + return "", false, fmt.Errorf("failed to start spinner: %w", err) + } } + var diffErrs []error // create an inventory of objects to be reconciled newInventory := newInventory() for _, obj := range objects { @@ -85,55 +95,70 @@ func (b *Builder) Diff() (string, error) { } change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj, diffOptions) if err != nil { - if b.kustomization.Spec.Force && ssa.IsImmutableError(err) { - output.WriteString(writeString(fmt.Sprintf("► %s created\n", obj.GetName()), bunt.Green)) - } else { - output.WriteString(writeString(fmt.Sprintf("✗ %v\n", err), bunt.Red)) - } + // gather errors and continue, as we want to see all the diffs + diffErrs = append(diffErrs, err) continue } // if the object is a sops secret, we need to // make sure we diff only if the keys are different - if obj.GetKind() == "Secret" && change.Action == string(ssa.ConfiguredAction) { + if obj.GetKind() == "Secret" && change.Action == ssa.ConfiguredAction { diffSopsSecret(obj, liveObject, mergedObject, change) } - if change.Action == string(ssa.CreatedAction) { + if change.Action == ssa.CreatedAction { output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green)) + createdOrDrifted = true } - if change.Action == string(ssa.ConfiguredAction) { - output.WriteString(writeString(fmt.Sprintf("► %s drifted\n", change.Subject), bunt.WhiteSmoke)) + if change.Action == ssa.ConfiguredAction { + output.WriteString(bunt.Sprint(fmt.Sprintf("► %s drifted\n", change.Subject))) liveFile, mergedFile, tmpDir, err := writeYamls(liveObject, mergedObject) if err != nil { - return "", err + return "", createdOrDrifted, err } - defer cleanupDir(tmpDir) err = diff(liveFile, mergedFile, &output) if err != nil { - return "", err + cleanupDir(tmpDir) + return "", createdOrDrifted, err } + + cleanupDir(tmpDir) + createdOrDrifted = true } addObjectsToInventory(newInventory, change) } - if b.kustomization.Spec.Prune { + if b.spinner != nil { + b.spinner.Message("processing inventory") + } + + if b.kustomization.Spec.Prune && len(diffErrs) == 0 { oldStatus := b.kustomization.Status.DeepCopy() if oldStatus.Inventory != nil { - diffObjects, err := diffInventory(oldStatus.Inventory, newInventory) + staleObjects, err := diffInventory(oldStatus.Inventory, newInventory) if err != nil { - return "", err + return "", createdOrDrifted, err + } + if len(staleObjects) > 0 { + createdOrDrifted = true } - for _, object := range diffObjects { - output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed)) + for _, object := range staleObjects { + output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssautil.FmtUnstructured(object)), bunt.OrangeRed)) } } } - return output.String(), nil + if b.spinner != nil { + err = b.spinner.Stop() + if err != nil { + return "", createdOrDrifted, fmt.Errorf("failed to stop spinner: %w", err) + } + } + + return output.String(), createdOrDrifted, errors.Reduce(errors.Flatten(errors.NewAggregate(diffErrs))) } func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, string, string, error) { @@ -144,13 +169,13 @@ func writeYamls(liveObject, mergedObject *unstructured.Unstructured) (string, st liveYAML, _ := yaml.Marshal(liveObject) liveFile := filepath.Join(tmpDir, "live.yaml") - if err := os.WriteFile(liveFile, liveYAML, 0644); err != nil { + if err := os.WriteFile(liveFile, liveYAML, 0o600); err != nil { return "", "", "", err } mergedYAML, _ := yaml.Marshal(mergedObject) mergedFile := filepath.Join(tmpDir, "merged.yaml") - if err := os.WriteFile(mergedFile, mergedYAML, 0644); err != nil { + if err := os.WriteFile(mergedFile, mergedYAML, 0o600); err != nil { return "", "", "", err } @@ -183,45 +208,56 @@ func diff(liveFile, mergedFile string, output io.Writer) error { return fmt.Errorf("failed to compare input files: %w", err) } - reportWriter := &dyff.HumanReport{ - Report: report, - OmitHeader: true, - } + printer := printers.NewDyffPrinter() - if err := reportWriter.WriteReport(output); err != nil { - return fmt.Errorf("failed to print report: %w", err) - } + printer.Print(output, report) return nil } func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) { - data := obj.Object["data"] - for _, v := range data.(map[string]interface{}) { + // get both data and stringdata maps + data := obj.Object[dataField] + + if m, ok := data.(map[string]interface{}); ok && m != nil { + applySopsDiff(m, liveObject, mergedObject, change) + } +} + +func applySopsDiff(data map[string]interface{}, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) { + for _, v := range data { v, err := base64.StdEncoding.DecodeString(v.(string)) if err != nil { fmt.Println(err) } + if bytes.Contains(v, []byte(mask)) { if liveObject != nil && mergedObject != nil { - change.Action = string(ssa.UnchangedAction) - dataLive := liveObject.Object["data"].(map[string]interface{}) - dataMerged := mergedObject.Object["data"].(map[string]interface{}) - if cmp.Diff(keys(dataLive), keys(dataMerged)) != "" { - change.Action = string(ssa.ConfiguredAction) + change.Action = ssa.UnchangedAction + liveKeys, mergedKeys := sopsComparableByKeys(liveObject), sopsComparableByKeys(mergedObject) + if cmp.Diff(liveKeys, mergedKeys) != "" { + change.Action = ssa.ConfiguredAction } } } } } -func keys(m map[string]interface{}) []string { +func sopsComparableByKeys(object *unstructured.Unstructured) []string { + m := object.Object[dataField].(map[string]interface{}) keys := make([]string, len(m)) i := 0 for k := range m { + // make sure we can compare only on keys + m[k] = "*****" keys[i] = k i++ } + + object.Object[dataField] = m + + sort.Strings(keys) + return keys } diff --git a/internal/build/testdata/local-kustomization/different-name.yaml b/internal/build/testdata/local-kustomization/different-name.yaml new file mode 100644 index 00000000..03ea661f --- /dev/null +++ b/internal/build/testdata/local-kustomization/different-name.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: infra + namespace: flux-system +spec: + path: "./clusters/test-build" diff --git a/manifests/monitoring/kube-prometheus-stack/namespace.yaml b/internal/build/testdata/local-kustomization/invalid-resource.yaml similarity index 68% rename from manifests/monitoring/kube-prometheus-stack/namespace.yaml rename to internal/build/testdata/local-kustomization/invalid-resource.yaml index d3252360..51287766 100644 --- a/manifests/monitoring/kube-prometheus-stack/namespace.yaml +++ b/internal/build/testdata/local-kustomization/invalid-resource.yaml @@ -1,4 +1,4 @@ apiVersion: v1 kind: Namespace metadata: - name: monitoring + name: podinfo diff --git a/internal/build/testdata/local-kustomization/multi-doc-valid.yaml b/internal/build/testdata/local-kustomization/multi-doc-valid.yaml new file mode 100644 index 00000000..0fb89db9 --- /dev/null +++ b/internal/build/testdata/local-kustomization/multi-doc-valid.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: infra-namespace + namespace: flux-system + labels: + component.kutara.io/part-of: definitions +spec: + path: "./k8s/base/infra" + prune: true +--- +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 30s + ref: + branch: master + url: https://github.com/stefanprodan/podinfo +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: flux-system +spec: + path: "./clusters/test-build" +--- diff --git a/internal/build/testdata/local-kustomization/no-ns.yaml b/internal/build/testdata/local-kustomization/no-ns.yaml new file mode 100644 index 00000000..0253fdd6 --- /dev/null +++ b/internal/build/testdata/local-kustomization/no-ns.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo +spec: + path: "./clusters/test-build" diff --git a/internal/build/testdata/local-kustomization/valid.yaml b/internal/build/testdata/local-kustomization/valid.yaml new file mode 100644 index 00000000..48cddf4d --- /dev/null +++ b/internal/build/testdata/local-kustomization/valid.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: flux-system +spec: + path: "./clusters/test-build" diff --git a/internal/flags/arch.go b/internal/flags/arch.go deleted file mode 100644 index 7d39a4ea..00000000 --- a/internal/flags/arch.go +++ /dev/null @@ -1,54 +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 flags - -import ( - "fmt" - "strings" - - "github.com/fluxcd/flux2/internal/utils" -) - -var supportedArchs = []string{"amd64", "arm", "arm64"} - -type Arch string - -func (a *Arch) String() string { - return string(*a) -} - -func (a *Arch) Set(str string) error { - if strings.TrimSpace(str) == "" { - return fmt.Errorf("no arch given, must be one of: %s", - strings.Join(supportedArchs, ", ")) - } - if !utils.ContainsItemString(supportedArchs, str) { - return fmt.Errorf("unsupported arch '%s', must be one of: %s", - str, strings.Join(supportedArchs, ", ")) - - } - *a = Arch(str) - return nil -} - -func (a *Arch) Type() string { - return "arch" -} - -func (a *Arch) Description() string { - return fmt.Sprintf("cluster architecture, available options are: (%s)", strings.Join(supportedArchs, ", ")) -} diff --git a/internal/flags/crds.go b/internal/flags/crds.go index 47e05e4f..81f6d604 100644 --- a/internal/flags/crds.go +++ b/internal/flags/crds.go @@ -20,9 +20,9 @@ import ( "fmt" "strings" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedCRDsPolicies = []string{ diff --git a/internal/flags/decryption_provider.go b/internal/flags/decryption_provider.go index 9a416467..085340ab 100644 --- a/internal/flags/decryption_provider.go +++ b/internal/flags/decryption_provider.go @@ -20,7 +20,7 @@ import ( "fmt" "strings" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedDecryptionProviders = []string{"sops"} diff --git a/internal/flags/git_implementation.go b/internal/flags/git_implementation.go deleted file mode 100644 index b6896303..00000000 --- a/internal/flags/git_implementation.go +++ /dev/null @@ -1,55 +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 flags - -import ( - "fmt" - "strings" - - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" - - "github.com/fluxcd/flux2/internal/utils" -) - -var supportedGitImplementations = []string{sourcev1.GoGitImplementation, sourcev1.LibGit2Implementation} - -type GitImplementation string - -func (i *GitImplementation) String() string { - return string(*i) -} - -func (i *GitImplementation) Set(str string) error { - if str == "" { - return nil - } - if !utils.ContainsItemString(supportedGitImplementations, str) { - return fmt.Errorf("unsupported Git implementation '%s', must be one of: %s", - str, strings.Join(supportedGitImplementations, ", ")) - - } - *i = GitImplementation(str) - return nil -} - -func (i *GitImplementation) Type() string { - return "gitImplementation" -} - -func (i *GitImplementation) Description() string { - return fmt.Sprintf("the Git implementation to use, available options are: (%s)", strings.Join(supportedGitImplementations, ", ")) -} diff --git a/internal/flags/git_implementation_test.go b/internal/flags/git_implementation_test.go deleted file mode 100644 index 22048f2e..00000000 --- a/internal/flags/git_implementation_test.go +++ /dev/null @@ -1,50 +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 flags - -import ( - "testing" - - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" -) - -func TestGitImplementation_Set(t *testing.T) { - tests := []struct { - name string - str string - expect string - expectErr bool - }{ - {"supported", sourcev1.GoGitImplementation, sourcev1.GoGitImplementation, false}, - {"unsupported", "unsupported", "", true}, - {"empty", "", "", false}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var i GitImplementation - if err := i.Set(tt.str); (err != nil) != tt.expectErr { - t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) - } - if str := i.String(); str != tt.expect { - t.Errorf("Set() = %v, expect %v", str, tt.expect) - } - }) - } -} diff --git a/internal/flags/helm_chart_source.go b/internal/flags/helm_chart_source.go index c085d21f..d55f1914 100644 --- a/internal/flags/helm_chart_source.go +++ b/internal/flags/helm_chart_source.go @@ -20,12 +20,13 @@ import ( "fmt" "strings" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) -var supportedHelmChartSourceKinds = []string{sourcev1.HelmRepositoryKind, sourcev1.GitRepositoryKind, sourcev1.BucketKind} +var supportedHelmChartSourceKinds = []string{sourcev1.HelmRepositoryKind, sourcev1.GitRepositoryKind, sourcev1b2.BucketKind} type HelmChartSource struct { Kind string diff --git a/internal/flags/helm_chart_source_test.go b/internal/flags/helm_chart_source_test.go index 726581ed..0f4500ae 100644 --- a/internal/flags/helm_chart_source_test.go +++ b/internal/flags/helm_chart_source_test.go @@ -23,7 +23,7 @@ import ( "fmt" "testing" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestHelmChartSource_Set(t *testing.T) { diff --git a/internal/flags/helm_release_values.go b/internal/flags/helm_release_values.go deleted file mode 100644 index 62035be8..00000000 --- a/internal/flags/helm_release_values.go +++ /dev/null @@ -1,72 +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 flags - -import ( - "fmt" - "strings" - - "github.com/fluxcd/flux2/internal/utils" -) - -var supportedHelmReleaseValuesFromKinds = []string{"Secret", "ConfigMap"} - -type HelmReleaseValuesFrom struct { - Kind string - Name string -} - -func (v *HelmReleaseValuesFrom) String() string { - if v.Name == "" { - return "" - } - return fmt.Sprintf("%s/%s", v.Kind, v.Name) -} - -func (v *HelmReleaseValuesFrom) Set(str string) error { - if strings.TrimSpace(str) == "" { - return fmt.Errorf("no values given, please specify %s", - v.Description()) - } - - sourceKind, sourceName := utils.ParseObjectKindName(str) - if sourceKind == "" { - return fmt.Errorf("invalid Kubernetes object reference '%s', must be in format /", str) - } - 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, ", ")) - } - - v.Name = sourceName - v.Kind = cleanSourceKind - - return nil -} - -func (v *HelmReleaseValuesFrom) Type() string { - return "helmReleaseValuesFrom" -} - -func (v *HelmReleaseValuesFrom) Description() string { - return fmt.Sprintf( - "Kubernetes object reference that contains the values.yaml data key in the format '/', "+ - "where kind must be one of: (%s)", - strings.Join(supportedHelmReleaseValuesFromKinds, ", "), - ) -} diff --git a/internal/flags/helm_release_values_test.go b/internal/flags/helm_release_values_test.go deleted file mode 100644 index 369f64bb..00000000 --- a/internal/flags/helm_release_values_test.go +++ /dev/null @@ -1,50 +0,0 @@ -//go:build !e2e -// +build !e2e - -/* -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 flags - -import ( - "testing" -) - -func TestHelmReleaseValuesFrom_Set(t *testing.T) { - tests := []struct { - name string - str string - expect string - expectErr bool - }{ - {"supported", "Secret/foo", "Secret/foo", false}, - {"lower case kind", "secret/foo", "Secret/foo", false}, - {"unsupported", "Unsupported/kind", "", true}, - {"invalid format", "Secret", "", true}, - {"empty", "", "", true}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var h HelmReleaseValuesFrom - if err := h.Set(tt.str); (err != nil) != tt.expectErr { - t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) - } - if str := h.String(); str != tt.expect { - t.Errorf("Set() = %v, expect %v", str, tt.expect) - } - }) - } -} diff --git a/internal/flags/kustomization_source.go b/internal/flags/kustomization_source.go index ca0b9f8e..322285e1 100644 --- a/internal/flags/kustomization_source.go +++ b/internal/flags/kustomization_source.go @@ -20,12 +20,13 @@ import ( "fmt" "strings" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) -var supportedKustomizationSourceKinds = []string{sourcev1.GitRepositoryKind, sourcev1.BucketKind} +var supportedKustomizationSourceKinds = []string{sourcev1b2.OCIRepositoryKind, sourcev1.GitRepositoryKind, sourcev1b2.BucketKind} type KustomizationSource struct { Kind string diff --git a/internal/flags/kustomization_source_test.go b/internal/flags/kustomization_source_test.go index 2d1ba7e0..68b81f54 100644 --- a/internal/flags/kustomization_source_test.go +++ b/internal/flags/kustomization_source_test.go @@ -23,7 +23,7 @@ import ( "fmt" "testing" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestKustomizationSource_Set(t *testing.T) { diff --git a/internal/flags/local_helm_chart_source.go b/internal/flags/local_helm_chart_source.go new file mode 100644 index 00000000..19d2e530 --- /dev/null +++ b/internal/flags/local_helm_chart_source.go @@ -0,0 +1,70 @@ +/* +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 flags + +import ( + "fmt" + "strings" + + "github.com/fluxcd/flux2/v2/internal/utils" +) + +type LocalHelmChartSource struct { + Kind string + Name string +} + +func (s *LocalHelmChartSource) String() string { + if s.Name == "" { + return "" + } + return fmt.Sprintf("%s/%s", s.Kind, s.Name) +} + +func (s *LocalHelmChartSource) Set(str string) error { + if strings.TrimSpace(str) == "" { + return fmt.Errorf("no helm chart source given, please specify %s", + s.Description()) + } + + sourceKind, sourceName := utils.ParseObjectKindName(str) + if sourceKind == "" || sourceName == "" { + return fmt.Errorf("invalid helm chart source '%s', must be in format /", str) + } + cleanSourceKind, ok := utils.ContainsEqualFoldItemString(supportedHelmChartSourceKinds, sourceKind) + if !ok { + return fmt.Errorf("source kind '%s' is not supported, must be one of: %s", + sourceKind, strings.Join(supportedHelmChartSourceKinds, ", ")) + } + + s.Kind = cleanSourceKind + s.Name = sourceName + + return nil +} + +func (s *LocalHelmChartSource) Type() string { + return "helmChartSource" +} + +func (s *LocalHelmChartSource) Description() string { + return fmt.Sprintf( + "source that contains the chart in the format '/', "+ + "where kind must be one of: (%s)", + strings.Join(supportedHelmChartSourceKinds, ", "), + ) +} diff --git a/internal/flags/log_level.go b/internal/flags/log_level.go index bd880fff..4101a902 100644 --- a/internal/flags/log_level.go +++ b/internal/flags/log_level.go @@ -20,7 +20,7 @@ import ( "fmt" "strings" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedLogLevels = []string{"debug", "info", "error"} diff --git a/internal/flags/rsa_key_bits.go b/internal/flags/rsa_key_bits.go index e214e1bf..716a4617 100644 --- a/internal/flags/rsa_key_bits.go +++ b/internal/flags/rsa_key_bits.go @@ -39,6 +39,9 @@ func (b *RSAKeyBits) Set(str string) error { if err != nil { return err } + if bits < 1024 { + return fmt.Errorf("RSA key bit size must be at least 1024") + } if bits == 0 || bits%8 != 0 { return fmt.Errorf("RSA key bit size must be a multiples of 8") } @@ -51,5 +54,5 @@ func (b *RSAKeyBits) Type() string { } func (b *RSAKeyBits) Description() string { - return "SSH RSA public key bit size (multiplies of 8)" + return "SSH RSA public key bit size (multiplies of 8, min 1024)" } diff --git a/internal/flags/rsa_key_bits_test.go b/internal/flags/rsa_key_bits_test.go index d4bd2965..7199be73 100644 --- a/internal/flags/rsa_key_bits_test.go +++ b/internal/flags/rsa_key_bits_test.go @@ -32,8 +32,8 @@ func TestRSAKeyBits_Set(t *testing.T) { }{ {"supported", "4096", "4096", false}, {"empty (default)", "", "2048", false}, - {"unsupported", "0", "0", true}, - {"unsupported", "123", "0", true}, + {"unsupported", "512", "0", true}, + {"unsupported", "1025", "0", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/flags/safe_relative_path.go b/internal/flags/safe_relative_path.go index 4a5f78df..a58086e5 100644 --- a/internal/flags/safe_relative_path.go +++ b/internal/flags/safe_relative_path.go @@ -41,7 +41,10 @@ func (p *SafeRelativePath) Set(str string) error { return fmt.Errorf("invalid relative path '%s': %w", cleanP, err) } // NB: required, as a secure join of "./" will result in "." - cleanP = fmt.Sprintf("./%s", strings.TrimPrefix(cleanP, ".")) + if cleanP == "." { + cleanP = "" + } + cleanP = fmt.Sprintf("./%s", cleanP) *p = SafeRelativePath(cleanP) return nil } @@ -51,5 +54,5 @@ func (p *SafeRelativePath) Type() string { } func (p *SafeRelativePath) Description() string { - return fmt.Sprintf("secure relative path") + return "secure relative path" } diff --git a/internal/flags/safe_relative_path_test.go b/internal/flags/safe_relative_path_test.go index 325bd658..a9d5f3fe 100644 --- a/internal/flags/safe_relative_path_test.go +++ b/internal/flags/safe_relative_path_test.go @@ -37,6 +37,13 @@ func TestRelativePath_Set(t *testing.T) { {"traversing absolute path", "/foo/../bar", "./bar", false}, {"traversing overflowing absolute path", "/foo/../../../bar", "./bar", false}, {"empty", "", "./", false}, + {"relative empty path", "./", "./", false}, + {"double relative empty path", "././", "./", false}, + {"dot path", ".foo", "./.foo", false}, + {"relative dot path", "./.foo", "./.foo", false}, + {"current directory", ".", "./", false}, + {"parent directory", "..", "./", false}, + {"parent directory more qualified", "./..", "./", false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/flags/source_bucket_provider.go b/internal/flags/source_bucket_provider.go index 87e26977..5ab08ecb 100644 --- a/internal/flags/source_bucket_provider.go +++ b/internal/flags/source_bucket_provider.go @@ -20,11 +20,16 @@ import ( "fmt" "strings" - "github.com/fluxcd/flux2/internal/utils" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + "github.com/fluxcd/flux2/v2/internal/utils" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" ) -var supportedSourceBucketProviders = []string{sourcev1.GenericBucketProvider, sourcev1.AmazonBucketProvider} +var supportedSourceBucketProviders = []string{ + sourcev1.GenericBucketProvider, + sourcev1.AmazonBucketProvider, + sourcev1.AzureBucketProvider, + sourcev1.GoogleBucketProvider, +} type SourceBucketProvider string diff --git a/internal/flags/source_bucket_provider_test.go b/internal/flags/source_bucket_provider_test.go index 244a7ac6..2f3192fd 100644 --- a/internal/flags/source_bucket_provider_test.go +++ b/internal/flags/source_bucket_provider_test.go @@ -22,7 +22,7 @@ package flags import ( "testing" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" ) func TestSourceBucketProvider_Set(t *testing.T) { diff --git a/internal/flags/source_oci_provider.go b/internal/flags/source_oci_provider.go new file mode 100644 index 00000000..77ff0441 --- /dev/null +++ b/internal/flags/source_oci_provider.go @@ -0,0 +1,79 @@ +/* +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 flags + +import ( + "fmt" + "strings" + + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/pkg/oci" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" +) + +var supportedSourceOCIProviders = []string{ + sourcev1.GenericOCIProvider, + sourcev1.AmazonOCIProvider, + sourcev1.AzureOCIProvider, + sourcev1.GoogleOCIProvider, +} + +var sourceOCIProvidersToOCIProvider = map[string]oci.Provider{ + sourcev1.GenericOCIProvider: oci.ProviderGeneric, + sourcev1.AmazonOCIProvider: oci.ProviderAWS, + sourcev1.AzureOCIProvider: oci.ProviderAzure, + sourcev1.GoogleOCIProvider: oci.ProviderGCP, +} + +type SourceOCIProvider string + +func (p *SourceOCIProvider) String() string { + return string(*p) +} + +func (p *SourceOCIProvider) Set(str string) error { + if strings.TrimSpace(str) == "" { + return fmt.Errorf("no source OCI provider given, please specify %s", + p.Description()) + } + if !utils.ContainsItemString(supportedSourceOCIProviders, str) { + return fmt.Errorf("source OCI provider '%s' is not supported, must be one of: %v", + str, strings.Join(supportedSourceOCIProviders, ", ")) + } + *p = SourceOCIProvider(str) + return nil +} + +func (p *SourceOCIProvider) Type() string { + return "sourceOCIProvider" +} + +func (p *SourceOCIProvider) Description() string { + return fmt.Sprintf( + "the OCI provider name, available options are: (%s)", + strings.Join(supportedSourceOCIProviders, ", "), + ) +} + +func (p *SourceOCIProvider) ToOCIProvider() (oci.Provider, error) { + value, ok := sourceOCIProvidersToOCIProvider[p.String()] + if !ok { + return 0, fmt.Errorf("no mapping between source OCI provider %s and OCI provider", p.String()) + } + + return value, nil +} diff --git a/internal/flags/source_oci_verify_provider.go b/internal/flags/source_oci_verify_provider.go new file mode 100644 index 00000000..acd57a9d --- /dev/null +++ b/internal/flags/source_oci_verify_provider.go @@ -0,0 +1,58 @@ +/* +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 flags + +import ( + "fmt" + "strings" + + "github.com/fluxcd/flux2/v2/internal/utils" +) + +var supportedSourceOCIVerifyProviders = []string{ + "cosign", +} + +type SourceOCIVerifyProvider string + +func (p *SourceOCIVerifyProvider) String() string { + return string(*p) +} + +func (p *SourceOCIVerifyProvider) Set(str string) error { + if strings.TrimSpace(str) == "" { + return fmt.Errorf("no source OCI verify provider given, please specify %s", + p.Description()) + } + if !utils.ContainsItemString(supportedSourceOCIVerifyProviders, str) { + return fmt.Errorf("source OCI verify provider '%s' is not supported, must be one of: %v", + str, strings.Join(supportedSourceOCIVerifyProviders, ", ")) + } + *p = SourceOCIVerifyProvider(str) + return nil +} + +func (p *SourceOCIVerifyProvider) Type() string { + return "sourceOCIVerifyProvider" +} + +func (p *SourceOCIVerifyProvider) Description() string { + return fmt.Sprintf( + "the OCI verify provider name to use for signature verification, available options are: (%s)", + strings.Join(supportedSourceOCIVerifyProviders, ", "), + ) +} diff --git a/internal/flags/arch_test.go b/internal/flags/source_oci_verify_provider_test.go similarity index 76% rename from internal/flags/arch_test.go rename to internal/flags/source_oci_verify_provider_test.go index 9902fff4..e8966521 100644 --- a/internal/flags/arch_test.go +++ b/internal/flags/source_oci_verify_provider_test.go @@ -1,5 +1,8 @@ +//go:build !e2e +// +build !e2e + /* -Copyright 2020 The Flux authors +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. @@ -20,24 +23,24 @@ import ( "testing" ) -func TestArch_Set(t *testing.T) { +func TestSourceOCIVerifyProvider_Set(t *testing.T) { tests := []struct { name string str string expect string expectErr bool }{ - {"supported", "amd64", "amd64", false}, + {"supported", "cosign", "cosign", false}, {"unsupported", "unsupported", "", true}, {"empty", "", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var a Arch - if err := a.Set(tt.str); (err != nil) != tt.expectErr { + var s SourceOCIVerifyProvider + if err := s.Set(tt.str); (err != nil) != tt.expectErr { t.Errorf("Set() error = %v, expectErr %v", err, tt.expectErr) } - if str := a.String(); str != tt.expect { + if str := s.String(); str != tt.expect { t.Errorf("Set() = %v, expect %v", str, tt.expect) } }) diff --git a/internal/tree/tree.go b/internal/tree/tree.go index b651a19a..75c7ffd1 100644 --- a/internal/tree/tree.go +++ b/internal/tree/tree.go @@ -22,8 +22,8 @@ package tree import ( "strings" - "github.com/fluxcd/pkg/ssa" - "sigs.k8s.io/cli-utils/pkg/object" + "github.com/fluxcd/cli-utils/pkg/object" + ssautil "github.com/fluxcd/pkg/ssa/utils" ) const ( @@ -74,7 +74,7 @@ func (t *objMetadataTree) AddTree(tree ObjMetadataTree) { } func (t *objMetadataTree) Text() string { - return ssa.FmtObjMetadata(t.Resource) + return ssautil.FmtObjMetadata(t.Resource) } func (t *objMetadataTree) Items() []ObjMetadataTree { diff --git a/internal/utils/apply.go b/internal/utils/apply.go index e2360adb..5867287c 100644 --- a/internal/utils/apply.go +++ b/internal/utils/apply.go @@ -23,71 +23,93 @@ import ( "fmt" "os" "path/filepath" + "time" - "github.com/fluxcd/pkg/ssa" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/cli-runtime/pkg/genericclioptions" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/kustomize/api/konfig" - "github.com/fluxcd/flux2/pkg/manifestgen/kustomization" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling" + runclient "github.com/fluxcd/pkg/runtime/client" + "github.com/fluxcd/pkg/ssa" + ssautil "github.com/fluxcd/pkg/ssa/utils" + + "github.com/fluxcd/flux2/v2/pkg/manifestgen/kustomization" ) // Apply is the equivalent of 'kubectl apply --server-side -f'. // If the given manifest is a kustomization.yaml, then apply performs the equivalent of 'kubectl apply --server-side -k'. -func Apply(ctx context.Context, rcg genericclioptions.RESTClientGetter, manifestPath string) (string, error) { - cfg, err := KubeConfig(rcg) +func Apply(ctx context.Context, rcg genericclioptions.RESTClientGetter, opts *runclient.Options, root, manifestPath string) (string, error) { + objs, err := readObjects(root, manifestPath) if err != nil { return "", err } - restMapper, err := rcg.ToRESTMapper() - if err != nil { - return "", err + + if len(objs) == 0 { + return "", fmt.Errorf("no Kubernetes objects found at: %s", manifestPath) } - kubeClient, err := client.New(cfg, client.Options{Mapper: restMapper}) - if err != nil { + + if err := ssa.SetNativeKindsDefaults(objs); err != nil { return "", err } - kubePoller := polling.NewStatusPoller(kubeClient, restMapper, nil) - resourceManager := ssa.NewResourceManager(kubeClient, kubePoller, ssa.Owner{ - Field: "flux", - Group: "fluxcd.io", - }) + changeSet := ssa.NewChangeSet() - objs, err := readObjects(manifestPath) - if err != nil { - return "", err + // contains only CRDs and Namespaces + var stageOne []*unstructured.Unstructured + + // contains all objects except for CRDs and Namespaces + var stageTwo []*unstructured.Unstructured + + for _, u := range objs { + if ssautil.IsClusterDefinition(u) { + stageOne = append(stageOne, u) + } else { + stageTwo = append(stageTwo, u) + } } - if len(objs) < 1 { - return "", fmt.Errorf("no Kubernetes objects found at: %s", manifestPath) + if len(stageOne) > 0 { + cs, err := applySet(ctx, rcg, opts, stageOne) + if err != nil { + return "", err + } + changeSet.Append(cs.Entries) } - if err := ssa.SetNativeKindsDefaults(objs); err != nil { - return "", err + if len(changeSet.Entries) > 0 { + if err := waitForSet(rcg, opts, changeSet); err != nil { + return "", err + } } - changeSet, err := resourceManager.ApplyAllStaged(ctx, objs, ssa.DefaultApplyOptions()) - if err != nil { - return "", err + if len(stageTwo) > 0 { + cs, err := applySet(ctx, rcg, opts, stageTwo) + if err != nil { + return "", err + } + changeSet.Append(cs.Entries) } return changeSet.String(), nil } -func readObjects(manifestPath string) ([]*unstructured.Unstructured, error) { - if _, err := os.Stat(manifestPath); err != nil { +func readObjects(root, manifestPath string) ([]*unstructured.Unstructured, error) { + fi, err := os.Lstat(manifestPath) + if err != nil { return nil, err } + if fi.IsDir() || !fi.Mode().IsRegular() { + return nil, fmt.Errorf("expected %q to be a file", manifestPath) + } - if filepath.Base(manifestPath) == konfig.DefaultKustomizationFileName() { - resources, err := kustomization.Build(filepath.Dir(manifestPath)) + if isRecognizedKustomizationFile(manifestPath) { + resources, err := kustomization.BuildWithRoot(root, filepath.Dir(manifestPath)) if err != nil { return nil, err } - return ssa.ReadObjects(bytes.NewReader(resources)) + return ssautil.ReadObjects(bytes.NewReader(resources)) } ms, err := os.Open(manifestPath) @@ -96,5 +118,54 @@ func readObjects(manifestPath string) ([]*unstructured.Unstructured, error) { } defer ms.Close() - return ssa.ReadObjects(bufio.NewReader(ms)) + return ssautil.ReadObjects(bufio.NewReader(ms)) +} + +func newManager(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) (*ssa.ResourceManager, error) { + cfg, err := KubeConfig(rcg, opts) + if err != nil { + return nil, err + } + restMapper, err := rcg.ToRESTMapper() + if err != nil { + return nil, err + } + kubeClient, err := client.New(cfg, client.Options{Mapper: restMapper, Scheme: NewScheme()}) + if err != nil { + return nil, err + } + kubePoller := polling.NewStatusPoller(kubeClient, restMapper, polling.Options{}) + + return ssa.NewResourceManager(kubeClient, kubePoller, ssa.Owner{ + Field: "flux", + Group: "fluxcd.io", + }), nil + +} + +func applySet(ctx context.Context, rcg genericclioptions.RESTClientGetter, opts *runclient.Options, objects []*unstructured.Unstructured) (*ssa.ChangeSet, error) { + man, err := newManager(rcg, opts) + if err != nil { + return nil, err + } + + return man.ApplyAll(ctx, objects, ssa.DefaultApplyOptions()) +} + +func waitForSet(rcg genericclioptions.RESTClientGetter, opts *runclient.Options, changeSet *ssa.ChangeSet) error { + man, err := newManager(rcg, opts) + if err != nil { + return err + } + return man.WaitForSet(changeSet.ToObjMetadataSet(), ssa.WaitOptions{Interval: 2 * time.Second, Timeout: time.Minute}) +} + +func isRecognizedKustomizationFile(path string) bool { + base := filepath.Base(path) + for _, v := range konfig.RecognizedKustomizationFileNames() { + if base == v { + return true + } + } + return false } diff --git a/internal/utils/hex.go b/internal/utils/hex.go new file mode 100644 index 00000000..310e7432 --- /dev/null +++ b/internal/utils/hex.go @@ -0,0 +1,39 @@ +/* +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 utils + +import ( + "regexp" + "strings" +) + +// hexRegexp matches any hexadecimal notation between 40 and 128 characters. +var hexRegexp = regexp.MustCompile(`\b[a-f0-9]{40,128}\b`) + +// TruncateHex will replace any hexadecimal notation between 40 and 128 +// characters (SHA-1 up to SHA-512) within the given string with a truncated +// version of 8 characters. +func TruncateHex(str string) string { + if str == "" { + return "" + } + hits := hexRegexp.FindAllString(str, -1) + for _, v := range hits { + str = strings.Replace(str, v, string([]rune(v)[:8]), -1) + } + return str +} diff --git a/internal/utils/hex_test.go b/internal/utils/hex_test.go new file mode 100644 index 00000000..5cbc9d3c --- /dev/null +++ b/internal/utils/hex_test.go @@ -0,0 +1,104 @@ +/* +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 utils + +import ( + "testing" + + . "github.com/onsi/gomega" +) + +func TestTruncateHex(t *testing.T) { + tests := []struct { + name string + str string + want string + }{ + { + name: "SHA1 hash", + str: "16cfcc0b9066b3234dda29927ac1c19860d9663f", + want: "16cfcc0b", + }, + { + name: "SHA256 hash", + str: "c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0", + want: "c2448c95", + }, + { + name: "BLAKE3 hash", + str: "d7b332559f4e57c01dcbe24e53346b4e47696fd2a07f39639a4017a8c3a1d045", + want: "d7b33255", + }, + { + name: "SHA512 hash", + str: "dd81564b7e1e1d5986b166c21963d602f47f8610bf2a6ebbfd2f9c1e5ef05ef134f07e587383cbc049325c43e0e6817b5a282a74c0d569a5e057118484989781", + want: "dd81564b", + }, + { + name: "part of digest", + str: "sha256:c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0", + want: "sha256:c2448c95", + }, + { + name: "part of revision with digest", + str: "tag@sha256:c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0", + want: "tag@sha256:c2448c95", + }, + { + name: "legacy revision with hash", + str: "HEAD/16cfcc0b9066b3234dda29927ac1c19860d9663f", + want: "HEAD/16cfcc0b", + }, + { + name: "hex exceeding max length", + str: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + want: "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + }, + { + name: "hex under min length", + str: "ffffffffffffff", + want: "ffffffffffffff", + }, + { + name: "within string", + str: "this is a lengthy string with a hash 16cfcc0b9066b3234dda29927ac1c19860d9663f in it", + want: "this is a lengthy string with a hash 16cfcc0b in it", + }, + { + name: "within string (quoted)", + str: "this is a lengthy string with a hash \"c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0\" in it", + want: "this is a lengthy string with a hash \"c2448c95\" in it", + }, + { + name: "within string (single quoted)", + str: "this is a lengthy string with a hash 'sha256:c2448c95e262b10f9c1137bf1472f51c04dffca76966ff15eff409d0b300c0b0' in it", + want: "this is a lengthy string with a hash 'sha256:c2448c95' in it", + }, + { + name: "arbitrary string", + str: "which should not be modified", + want: "which should not be modified", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + + g.Expect(TruncateHex(tt.str)).To(Equal(tt.want)) + }) + } +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 361b9448..4a4787c2 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -27,7 +27,6 @@ import ( "runtime" "strings" - "github.com/olekukonko/tablewriter" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" @@ -42,21 +41,21 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/yaml" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" - imageautov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" - imagereflectv1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - notificationv1 "github.com/fluxcd/notification-controller/api/v1beta1" - "github.com/fluxcd/pkg/runtime/dependency" + helmv2 "github.com/fluxcd/helm-controller/api/v2" + imageautov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" + imagereflectv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" + "github.com/fluxcd/pkg/apis/meta" + runclient "github.com/fluxcd/pkg/runtime/client" "github.com/fluxcd/pkg/version" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" - "github.com/fluxcd/flux2/pkg/manifestgen/install" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" ) -type Utils struct { -} - type ExecMode string const ( @@ -107,15 +106,15 @@ func ExecKubectlCommand(ctx context.Context, mode ExecMode, kubeConfigPath strin return "", nil } -func KubeConfig(rcg genericclioptions.RESTClientGetter) (*rest.Config, error) { +func KubeConfig(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) (*rest.Config, error) { cfg, err := rcg.ToRESTConfig() if err != nil { return nil, fmt.Errorf("kubernetes configuration load failed: %w", err) } // avoid throttling request when some Flux CRDs are not registered - cfg.QPS = 50 - cfg.Burst = 100 + cfg.QPS = opts.QPS + cfg.Burst = opts.Burst return cfg, nil } @@ -129,21 +128,26 @@ func NewScheme() *apiruntime.Scheme { _ = rbacv1.AddToScheme(scheme) _ = appsv1.AddToScheme(scheme) _ = networkingv1.AddToScheme(scheme) + _ = sourcev1b2.AddToScheme(scheme) _ = sourcev1.AddToScheme(scheme) _ = kustomizev1.AddToScheme(scheme) _ = helmv2.AddToScheme(scheme) _ = notificationv1.AddToScheme(scheme) + _ = notificationv1b3.AddToScheme(scheme) _ = imagereflectv1.AddToScheme(scheme) _ = imageautov1.AddToScheme(scheme) return scheme } -func KubeClient(rcg genericclioptions.RESTClientGetter) (client.WithWatch, error) { +func KubeClient(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) (client.WithWatch, error) { cfg, err := rcg.ToRESTConfig() if err != nil { return nil, err } + cfg.QPS = opts.QPS + cfg.Burst = opts.Burst + scheme := NewScheme() kubeClient, err := client.NewWithWatch(cfg, client.Options{ Scheme: scheme, @@ -227,8 +231,8 @@ func ParseObjectKindNameNamespace(input string) (kind, name, namespace string) { return kind, name, namespace } -func MakeDependsOn(deps []string) []dependency.CrossNamespaceDependencyReference { - refs := []dependency.CrossNamespaceDependencyReference{} +func MakeDependsOn(deps []string) []meta.NamespacedObjectReference { + refs := []meta.NamespacedObjectReference{} for _, dep := range deps { parts := strings.Split(dep, "/") depNamespace := "" @@ -239,7 +243,7 @@ func MakeDependsOn(deps []string) []dependency.CrossNamespaceDependencyReference } else { depName = parts[0] } - refs = append(refs, dependency.CrossNamespaceDependencyReference{ + refs = append(refs, meta.NamespacedObjectReference{ Namespace: depNamespace, Name: depName, }) @@ -247,24 +251,6 @@ func MakeDependsOn(deps []string) []dependency.CrossNamespaceDependencyReference return refs } -func PrintTable(writer io.Writer, header []string, rows [][]string) { - table := tablewriter.NewWriter(writer) - table.SetHeader(header) - table.SetAutoWrapText(false) - table.SetAutoFormatHeaders(true) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetAlignment(tablewriter.ALIGN_LEFT) - table.SetCenterSeparator("") - table.SetColumnSeparator("") - table.SetRowSeparator("") - table.SetHeaderLine(false) - table.SetBorder(false) - table.SetTablePadding("\t") - table.SetNoWhiteSpace(true) - table.AppendBulk(rows) - table.Render() -} - func ValidateComponents(components []string) error { defaults := install.MakeDefaultOptions() bootstrapAllComponents := append(defaults.Components, defaults.ComponentsExtra...) diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index 516423e6..2cec902c 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -20,12 +20,11 @@ limitations under the License. package utils import ( - "os" "path/filepath" "reflect" "testing" - "github.com/fluxcd/pkg/runtime/dependency" + "github.com/fluxcd/pkg/apis/meta" ) func TestCompatibleVersion(t *testing.T) { @@ -88,7 +87,7 @@ func TestMakeDependsOn(t *testing.T) { "someNSD/", "", } - want := []dependency.CrossNamespaceDependencyReference{ + want := []meta.NamespacedObjectReference{ {Namespace: "someNSA", Name: "someNameA"}, {Namespace: "someNSB", Name: "someNameB"}, {Namespace: "", Name: "someNameC"}, @@ -135,16 +134,9 @@ func TestExtractCRDs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create temporary directory to write the result in. - dir, err := os.MkdirTemp("", "flux-TestExtractCRDs") - if err != nil { - t.Fatalf("failed to create temporary directory: %v", err) - } - defer os.RemoveAll(dir) - - outManifestPath := filepath.Join(dir, "crds.yaml") + outManifestPath := filepath.Join(t.TempDir(), "crds.yaml") inManifestPath := filepath.Join("testdata", tt.inManifestFile) - if err = ExtractCRDs(inManifestPath, outManifestPath); (err != nil) != tt.expectErr { + if err := ExtractCRDs(inManifestPath, outManifestPath); (err != nil) != tt.expectErr { t.Errorf("ExtractCRDs() error = %v, expectErr %v", err, tt.expectErr) } }) diff --git a/manifests/bases/helm-controller/kustomization.yaml b/manifests/bases/helm-controller/kustomization.yaml index 99911dcf..5f08e847 100644 --- a/manifests/bases/helm-controller/kustomization.yaml +++ b/manifests/bases/helm-controller/kustomization.yaml @@ -1,10 +1,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/helm-controller/releases/download/v0.15.0/helm-controller.crds.yaml -- https://github.com/fluxcd/helm-controller/releases/download/v0.15.0/helm-controller.deployment.yaml +- https://github.com/fluxcd/helm-controller/releases/download/v1.0.1/helm-controller.crds.yaml +- https://github.com/fluxcd/helm-controller/releases/download/v1.0.1/helm-controller.deployment.yaml - account.yaml -patchesJson6902: +transformers: +- labels.yaml +patches: - target: group: apps version: v1 diff --git a/manifests/bases/helm-controller/labels.yaml b/manifests/bases/helm-controller/labels.yaml new file mode 100644 index 00000000..29c577f0 --- /dev/null +++ b/manifests/bases/helm-controller/labels.yaml @@ -0,0 +1,9 @@ +apiVersion: builtin +kind: LabelTransformer +metadata: + name: labels +labels: + app.kubernetes.io/component: helm-controller +fieldSpecs: + - path: metadata/labels + create: true diff --git a/manifests/bases/helm-controller/patch.yaml b/manifests/bases/helm-controller/patch.yaml index 59ea939b..17f163d2 100644 --- a/manifests/bases/helm-controller/patch.yaml +++ b/manifests/bases/helm-controller/patch.yaml @@ -1,6 +1,25 @@ - op: add path: /spec/template/spec/containers/0/args/0 - value: --events-addr=http://notification-controller/ + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: helm-controller +- op: add + path: /spec/template/spec/priorityClassName + value: system-cluster-critical +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMAXPROCS + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.cpu +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMEMLIMIT + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.memory diff --git a/manifests/bases/image-automation-controller/kustomization.yaml b/manifests/bases/image-automation-controller/kustomization.yaml index c0eaf567..e5b6e3ed 100644 --- a/manifests/bases/image-automation-controller/kustomization.yaml +++ b/manifests/bases/image-automation-controller/kustomization.yaml @@ -1,10 +1,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/image-automation-controller/releases/download/v0.19.0/image-automation-controller.crds.yaml -- https://github.com/fluxcd/image-automation-controller/releases/download/v0.19.0/image-automation-controller.deployment.yaml +- https://github.com/fluxcd/image-automation-controller/releases/download/v0.38.0/image-automation-controller.crds.yaml +- https://github.com/fluxcd/image-automation-controller/releases/download/v0.38.0/image-automation-controller.deployment.yaml - account.yaml -patchesJson6902: +transformers: +- labels.yaml +patches: - target: group: apps version: v1 diff --git a/manifests/bases/image-automation-controller/labels.yaml b/manifests/bases/image-automation-controller/labels.yaml new file mode 100644 index 00000000..88ddf5a5 --- /dev/null +++ b/manifests/bases/image-automation-controller/labels.yaml @@ -0,0 +1,9 @@ +apiVersion: builtin +kind: LabelTransformer +metadata: + name: labels +labels: + app.kubernetes.io/component: image-automation-controller +fieldSpecs: + - path: metadata/labels + create: true diff --git a/manifests/bases/image-automation-controller/patch.yaml b/manifests/bases/image-automation-controller/patch.yaml index 4015f0f8..73d0d8c7 100644 --- a/manifests/bases/image-automation-controller/patch.yaml +++ b/manifests/bases/image-automation-controller/patch.yaml @@ -1,6 +1,22 @@ - op: add path: /spec/template/spec/containers/0/args/0 - value: --events-addr=http://notification-controller/ + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: image-automation-controller +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMAXPROCS + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.cpu +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMEMLIMIT + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.memory diff --git a/manifests/bases/image-reflector-controller/kustomization.yaml b/manifests/bases/image-reflector-controller/kustomization.yaml index eb6b1fe9..2cd1f1ea 100644 --- a/manifests/bases/image-reflector-controller/kustomization.yaml +++ b/manifests/bases/image-reflector-controller/kustomization.yaml @@ -1,10 +1,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.15.0/image-reflector-controller.crds.yaml -- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.15.0/image-reflector-controller.deployment.yaml +- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.32.0/image-reflector-controller.crds.yaml +- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.32.0/image-reflector-controller.deployment.yaml - account.yaml -patchesJson6902: +transformers: +- labels.yaml +patches: - target: group: apps version: v1 diff --git a/manifests/bases/image-reflector-controller/labels.yaml b/manifests/bases/image-reflector-controller/labels.yaml new file mode 100644 index 00000000..ee3a2f4f --- /dev/null +++ b/manifests/bases/image-reflector-controller/labels.yaml @@ -0,0 +1,9 @@ +apiVersion: builtin +kind: LabelTransformer +metadata: + name: labels +labels: + app.kubernetes.io/component: image-reflector-controller +fieldSpecs: + - path: metadata/labels + create: true diff --git a/manifests/bases/image-reflector-controller/patch.yaml b/manifests/bases/image-reflector-controller/patch.yaml index 39814a23..4f3e4a7f 100644 --- a/manifests/bases/image-reflector-controller/patch.yaml +++ b/manifests/bases/image-reflector-controller/patch.yaml @@ -1,6 +1,22 @@ - op: add path: /spec/template/spec/containers/0/args/0 - value: --events-addr=http://notification-controller/ + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: image-reflector-controller +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMAXPROCS + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.cpu +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMEMLIMIT + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.memory diff --git a/manifests/bases/kustomize-controller/kustomization.yaml b/manifests/bases/kustomize-controller/kustomization.yaml index 65244f60..8104b199 100644 --- a/manifests/bases/kustomize-controller/kustomization.yaml +++ b/manifests/bases/kustomize-controller/kustomization.yaml @@ -1,10 +1,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/kustomize-controller/releases/download/v0.19.1/kustomize-controller.crds.yaml -- https://github.com/fluxcd/kustomize-controller/releases/download/v0.19.1/kustomize-controller.deployment.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.3.0/kustomize-controller.crds.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.3.0/kustomize-controller.deployment.yaml - account.yaml -patchesJson6902: +transformers: +- labels.yaml +patches: - target: group: apps version: v1 diff --git a/manifests/bases/kustomize-controller/labels.yaml b/manifests/bases/kustomize-controller/labels.yaml new file mode 100644 index 00000000..86e93ee2 --- /dev/null +++ b/manifests/bases/kustomize-controller/labels.yaml @@ -0,0 +1,9 @@ +apiVersion: builtin +kind: LabelTransformer +metadata: + name: labels +labels: + app.kubernetes.io/component: kustomize-controller +fieldSpecs: + - path: metadata/labels + create: true diff --git a/manifests/bases/kustomize-controller/patch.yaml b/manifests/bases/kustomize-controller/patch.yaml index 380cdfbd..7645b684 100644 --- a/manifests/bases/kustomize-controller/patch.yaml +++ b/manifests/bases/kustomize-controller/patch.yaml @@ -1,6 +1,25 @@ - op: add path: /spec/template/spec/containers/0/args/0 - value: --events-addr=http://notification-controller/ + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: kustomize-controller +- op: add + path: /spec/template/spec/priorityClassName + value: system-cluster-critical +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMAXPROCS + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.cpu +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMEMLIMIT + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.memory diff --git a/manifests/bases/notification-controller/kustomization.yaml b/manifests/bases/notification-controller/kustomization.yaml index 9c01b456..28481fdc 100644 --- a/manifests/bases/notification-controller/kustomization.yaml +++ b/manifests/bases/notification-controller/kustomization.yaml @@ -1,10 +1,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/notification-controller/releases/download/v0.20.1/notification-controller.crds.yaml -- https://github.com/fluxcd/notification-controller/releases/download/v0.20.1/notification-controller.deployment.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.3.0/notification-controller.crds.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.3.0/notification-controller.deployment.yaml - account.yaml -patchesJson6902: +transformers: +- labels.yaml +patches: - target: group: apps version: v1 diff --git a/manifests/bases/notification-controller/labels.yaml b/manifests/bases/notification-controller/labels.yaml new file mode 100644 index 00000000..4f6fdf07 --- /dev/null +++ b/manifests/bases/notification-controller/labels.yaml @@ -0,0 +1,9 @@ +apiVersion: builtin +kind: LabelTransformer +metadata: + name: labels +labels: + app.kubernetes.io/component: notification-controller +fieldSpecs: + - path: metadata/labels + create: true diff --git a/manifests/bases/notification-controller/patch.yaml b/manifests/bases/notification-controller/patch.yaml index 9c36ccdd..fe3be228 100644 --- a/manifests/bases/notification-controller/patch.yaml +++ b/manifests/bases/notification-controller/patch.yaml @@ -1,3 +1,19 @@ - op: add path: /spec/template/spec/serviceAccountName value: notification-controller +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMAXPROCS + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.cpu +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMEMLIMIT + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.memory diff --git a/manifests/bases/source-controller/kustomization.yaml b/manifests/bases/source-controller/kustomization.yaml index 9998c78b..040e367a 100644 --- a/manifests/bases/source-controller/kustomization.yaml +++ b/manifests/bases/source-controller/kustomization.yaml @@ -1,10 +1,12 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/source-controller/releases/download/v0.20.1/source-controller.crds.yaml -- https://github.com/fluxcd/source-controller/releases/download/v0.20.1/source-controller.deployment.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.3.0/source-controller.crds.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.3.0/source-controller.deployment.yaml - account.yaml -patchesJson6902: +transformers: +- labels.yaml +patches: - target: group: apps version: v1 diff --git a/manifests/bases/source-controller/labels.yaml b/manifests/bases/source-controller/labels.yaml new file mode 100644 index 00000000..a7d9e0ad --- /dev/null +++ b/manifests/bases/source-controller/labels.yaml @@ -0,0 +1,9 @@ +apiVersion: builtin +kind: LabelTransformer +metadata: + name: labels +labels: + app.kubernetes.io/component: source-controller +fieldSpecs: + - path: metadata/labels + create: true diff --git a/manifests/bases/source-controller/patch.yaml b/manifests/bases/source-controller/patch.yaml index 15c1d28e..1420c8e1 100644 --- a/manifests/bases/source-controller/patch.yaml +++ b/manifests/bases/source-controller/patch.yaml @@ -1,6 +1,25 @@ - op: add path: /spec/template/spec/containers/0/args/0 - value: --events-addr=http://notification-controller/ + value: --events-addr=http://notification-controller.flux-system.svc.cluster.local./ - op: add path: /spec/template/spec/serviceAccountName value: source-controller +- op: add + path: /spec/template/spec/priorityClassName + value: system-cluster-critical +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMAXPROCS + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.cpu +- op: add + path: /spec/template/spec/containers/0/env/- + value: + name: GOMEMLIMIT + valueFrom: + resourceFieldRef: + containerName: manager + resource: limits.memory diff --git a/manifests/crds/kustomization.yaml b/manifests/crds/kustomization.yaml index 7c6dfde4..93b24833 100644 --- a/manifests/crds/kustomization.yaml +++ b/manifests/crds/kustomization.yaml @@ -1,9 +1,9 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- https://github.com/fluxcd/source-controller/releases/download/v0.20.1/source-controller.crds.yaml -- https://github.com/fluxcd/kustomize-controller/releases/download/v0.19.1/kustomize-controller.crds.yaml -- https://github.com/fluxcd/helm-controller/releases/download/v0.15.0/helm-controller.crds.yaml -- https://github.com/fluxcd/notification-controller/releases/download/v0.20.1/notification-controller.crds.yaml -- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.15.0/image-reflector-controller.crds.yaml -- https://github.com/fluxcd/image-automation-controller/releases/download/v0.19.0/image-automation-controller.crds.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.3.0/source-controller.crds.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.3.0/kustomize-controller.crds.yaml +- https://github.com/fluxcd/helm-controller/releases/download/v1.0.1/helm-controller.crds.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.3.0/notification-controller.crds.yaml +- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.32.0/image-reflector-controller.crds.yaml +- https://github.com/fluxcd/image-automation-controller/releases/download/v0.38.0/image-automation-controller.crds.yaml diff --git a/manifests/install/kustomization.yaml b/manifests/install/kustomization.yaml index 712102e4..edce8ca8 100644 --- a/manifests/install/kustomization.yaml +++ b/manifests/install/kustomization.yaml @@ -13,3 +13,16 @@ resources: - ../policies transformers: - labels.yaml +images: + - name: fluxcd/source-controller + newName: ghcr.io/fluxcd/source-controller + - name: fluxcd/kustomize-controller + newName: ghcr.io/fluxcd/kustomize-controller + - name: fluxcd/helm-controller + newName: ghcr.io/fluxcd/helm-controller + - name: fluxcd/notification-controller + newName: ghcr.io/fluxcd/notification-controller + - name: fluxcd/image-reflector-controller + newName: ghcr.io/fluxcd/image-reflector-controller + - name: fluxcd/image-automation-controller + newName: ghcr.io/fluxcd/image-automation-controller diff --git a/manifests/integrations/eventhub-credentials-sync/_cronjobs/generic/secret-azure-credentials.yaml b/manifests/integrations/eventhub-credentials-sync/_cronjobs/generic/secret-azure-credentials.yaml index 8a6d8a2c..af2da5b3 100644 --- a/manifests/integrations/eventhub-credentials-sync/_cronjobs/generic/secret-azure-credentials.yaml +++ b/manifests/integrations/eventhub-credentials-sync/_cronjobs/generic/secret-azure-credentials.yaml @@ -10,5 +10,5 @@ metadata: type: Opaque # This is just a example secret, you should never store secrets in git. # One way forward can be to use sealed-secrets or SOPS -# https://fluxcd.io/docs/guides/sealed-secrets/ -# https://fluxcd.io/docs/guides/mozilla-sops/ +# https://fluxcd.io/flux/guides/sealed-secrets/ +# https://fluxcd.io/flux/guides/mozilla-sops/ diff --git a/manifests/integrations/registry-credentials-sync/aws/config-patches.yaml b/manifests/integrations/registry-credentials-sync/aws/config-patches.yaml index b99001a6..f57ccf79 100644 --- a/manifests/integrations/registry-credentials-sync/aws/config-patches.yaml +++ b/manifests/integrations/registry-credentials-sync/aws/config-patches.yaml @@ -24,8 +24,8 @@ metadata: ## If not using IRSA, set the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables ## Store these values in a Secret and load them in the container using envFrom. ## For managing this secret via GitOps, consider using SOPS or SealedSecrets and add that manifest in a resource file for this kustomize build. -## https://fluxcd.io/docs/guides/mozilla-sops/ -## https://fluxcd.io/docs/guides/sealed-secrets/ +## https://fluxcd.io/flux/guides/mozilla-sops/ +## https://fluxcd.io/flux/guides/sealed-secrets/ # --- # apiVersion: apps/v1 # kind: Deployment diff --git a/manifests/monitoring/README.md b/manifests/monitoring/README.md new file mode 100644 index 00000000..af5d720b --- /dev/null +++ b/manifests/monitoring/README.md @@ -0,0 +1,22 @@ +# :warning: Removal Notice + +Starting Flux v2.1.0, released August 24, 2023, the Flux monitoring +configurations in this repository were marked as deprecated. The new monitoring +docs are available at [Flux monitoring](https://fluxcd.io/flux/monitoring/) +docs with new example configurations in +[fluxcd/flux2-monitoring-example](https://github.com/fluxcd/flux2-monitoring-example/). + +The deprecated configurations were removed in Flux v2.2 on December 13, 2023. All +users of these configurations are advised to use the new monitoring setup, +following the [docs](https://fluxcd.io/flux/monitoring/) and the +[examples](https://github.com/fluxcd/flux2-monitoring-example/). + +After collecting a lot of user feedback about our monitoring recommendation, in +order to serve most of the needs of the users, we decided to create a new +monitoring setup leveraging more of the kube-prometheus-stack, specifically +kube-state-metrics, to enable configuring Flux custom metrics, see the [Flux +custom Prometheus metrics](https://fluxcd.io/flux/monitoring/custom-metrics/) +docs to learn more about it. Please refer to +[fluxcd/flux2/4128](https://github.com/fluxcd/flux2/issues/4128) for a detailed +explanation about this change and the new capabilities offered by the new +monitoring setup. diff --git a/manifests/monitoring/grafana/dashboards/cluster.json b/manifests/monitoring/grafana/dashboards/cluster.json deleted file mode 100644 index 21a46bb9..00000000 --- a/manifests/monitoring/grafana/dashboards/cluster.json +++ /dev/null @@ -1,966 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "iteration": 1636369574387, - "links": [], - "panels": [ - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "red", - "value": 100 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 0, - "y": 0 - }, - "id": 24, - "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "last" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "value" - }, - "pluginVersion": "7.5.5", - "targets": [ - { - "exemplar": true, - "expr": "count(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"True\",kind=~\"Kustomization|HelmRelease\"})\n-\nsum(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"Deleted\",kind=~\"Kustomization|HelmRelease\"})", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Cluster Reconcilers", - "type": "stat" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 6, - "y": 0 - }, - "id": 28, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "last" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "value" - }, - "pluginVersion": "7.5.5", - "targets": [ - { - "exemplar": true, - "expr": "sum(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"False\",kind=~\"Kustomization|HelmRelease\"})", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Failing Reconcilers", - "type": "stat" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "red", - "value": 100 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 12, - "y": 0 - }, - "id": 29, - "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "last" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "value" - }, - "pluginVersion": "7.5.5", - "targets": [ - { - "exemplar": true, - "expr": "count(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"True\",kind=~\"GitRepository|HelmRepository|Bucket\"})\n-\nsum(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"Deleted\",kind=~\"GitRepository|HelmRepository|Bucket\"})", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Kubernetes Manifests Sources", - "type": "stat" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 18, - "y": 0 - }, - "id": 30, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "last" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "value" - }, - "pluginVersion": "7.5.5", - "targets": [ - { - "exemplar": true, - "expr": "sum(gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"False\",kind=~\"GitRepository|HelmRepository|Bucket\"})", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Failing Sources", - "type": "stat" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "#EAB839", - "value": 1 - }, - { - "color": "red", - "value": 61 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 4, - "w": 12, - "x": 0, - "y": 5 - }, - "id": 8, - "options": { - "displayMode": "gradient", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showUnfilled": true, - "text": {} - }, - "pluginVersion": "7.5.5", - "targets": [ - { - "exemplar": true, - "expr": " sum(rate(gotk_reconcile_duration_seconds_sum{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",kind=~\"Kustomization|HelmRelease\"}[5m])) by (kind)\n/\n sum(rate(gotk_reconcile_duration_seconds_count{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",kind=~\"Kustomization|HelmRelease\"}[5m])) by (kind)", - "interval": "", - "legendFormat": "{{kind}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Reconciler ops avg. duration", - "type": "bargauge" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "#EAB839", - "value": 1 - }, - { - "color": "red", - "value": 61 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 4, - "w": 12, - "x": 12, - "y": 5 - }, - "id": 31, - "options": { - "displayMode": "gradient", - "orientation": "horizontal", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "showUnfilled": true, - "text": {} - }, - "pluginVersion": "7.5.5", - "targets": [ - { - "exemplar": true, - "expr": " sum(rate(gotk_reconcile_duration_seconds_sum{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",kind=~\"GitRepository|HelmRepository|Bucket\"}[5m])) by (kind)\n/\n sum(rate(gotk_reconcile_duration_seconds_count{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",kind=~\"GitRepository|HelmRepository|Bucket\"}[5m])) by (kind)", - "interval": "", - "legendFormat": "{{kind}}", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Source ops avg. duration", - "type": "bargauge" - }, - { - "collapsed": false, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 9 - }, - "id": 15, - "panels": [], - "title": "Status", - "type": "row" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "custom": { - "align": null, - "filterable": true - }, - "mappings": [ - { - "from": "", - "id": 1, - "text": "Ready", - "to": "", - "type": 1, - "value": "0" - }, - { - "from": "", - "id": 2, - "text": "Not Ready", - "to": "", - "type": 1, - "value": "1" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "blue", - "value": 0 - }, - { - "color": "red", - "value": 1 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Status" - }, - "properties": [ - { - "id": "custom.displayMode", - "value": "color-background" - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 10 - }, - "id": 33, - "options": { - "showHeader": true - }, - "pluginVersion": "7.5.5", - "targets": [ - { - "exemplar": true, - "expr": "gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"False\",kind=~\"Kustomization|HelmRelease\"}", - "format": "table", - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Cluster reconciliation readiness ", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true, - "__name__": true, - "app": true, - "container": true, - "endpoint": true, - "exported_namespace": true, - "instance": true, - "job": true, - "kubernetes_namespace": true, - "kubernetes_pod_name": true, - "pod": true, - "pod_template_hash": true, - "status": true, - "type": true - }, - "indexByName": {}, - "renameByName": { - "Value": "Status", - "kind": "Kind", - "name": "Name", - "namespace": "Namespace" - } - } - } - ], - "type": "table" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "custom": { - "align": null, - "filterable": true - }, - "mappings": [ - { - "from": "", - "id": 1, - "text": "Ready", - "to": "", - "type": 1, - "value": "0" - }, - { - "from": "", - "id": 2, - "text": "Not Ready", - "to": "", - "type": 1, - "value": "1" - } - ], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "blue", - "value": 0 - }, - { - "color": "red", - "value": 1 - } - ] - } - }, - "overrides": [ - { - "matcher": { - "id": "byName", - "options": "Status" - }, - "properties": [ - { - "id": "custom.displayMode", - "value": "color-background" - } - ] - } - ] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 10 - }, - "id": 34, - "options": { - "showHeader": true - }, - "pluginVersion": "7.5.5", - "targets": [ - { - "exemplar": true, - "expr": "gotk_reconcile_condition{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",type=\"Ready\",status=\"False\",kind=~\"GitRepository|HelmRepository|Bucket\"}", - "format": "table", - "instant": true, - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Source acquisition readiness ", - "transformations": [ - { - "id": "organize", - "options": { - "excludeByName": { - "Time": true, - "__name__": true, - "app": true, - "container": true, - "endpoint": true, - "exported_namespace": true, - "instance": true, - "job": true, - "kubernetes_namespace": true, - "kubernetes_pod_name": true, - "pod": true, - "pod_template_hash": true, - "status": true, - "type": true - }, - "indexByName": {}, - "renameByName": { - "Value": "Status", - "kind": "Kind", - "name": "Name", - "namespace": "Namespace" - } - } - } - ], - "type": "table" - }, - { - "collapsed": false, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 18 - }, - "id": 17, - "panels": [], - "title": "Timing", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": {}, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 19 - }, - "hiddenSeries": false, - "id": 27, - "legend": { - "alignAsTable": true, - "avg": true, - "current": false, - "hideEmpty": true, - "hideZero": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.5.5", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "exemplar": true, - "expr": " sum(rate(gotk_reconcile_duration_seconds_sum{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",kind=~\"Kustomization|HelmRelease\"}[5m])) by (kind, name)\n/\n sum(rate(gotk_reconcile_duration_seconds_count{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",kind=~\"Kustomization|HelmRelease\"}[5m])) by (kind, name)", - "hide": false, - "interval": "", - "legendFormat": "{{kind}}/{{name}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Cluster reconciliation duration", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": {}, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 27 - }, - "hiddenSeries": false, - "id": 35, - "legend": { - "alignAsTable": true, - "avg": true, - "current": false, - "hideEmpty": true, - "hideZero": true, - "max": false, - "min": false, - "rightSide": true, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "7.5.5", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "exemplar": true, - "expr": " sum(rate(gotk_reconcile_duration_seconds_sum{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",kind=~\"GitRepository|HelmRepository|Bucket\"}[5m])) by (kind, name)\n/\n sum(rate(gotk_reconcile_duration_seconds_count{namespace=~\"$operator_namespace\",exported_namespace=~\"$namespace\",kind=~\"GitRepository|HelmRepository|Bucket\"}[5m])) by (kind, name)", - "hide": false, - "interval": "", - "legendFormat": "{{kind}}/{{name}}", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Source acquisition duration", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "refresh": "", - "schemaVersion": 27, - "style": "light", - "tags": [ - "flux" - ], - "templating": { - "list": [ - { - "current": { - "selected": false, - "text": "Prometheus", - "value": "Prometheus" - }, - "description": null, - "error": null, - "hide": 2, - "includeAll": false, - "label": null, - "multi": false, - "name": "DS_PROMETHEUS", - "options": [], - "query": "prometheus", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" - }, - { - "allValue": "", - "current": { - "selected": true, - "text": [ - "All" - ], - "value": [ - "$__all" - ] - }, - "datasource": "$DS_PROMETHEUS", - "definition": "label_values(gotk_reconcile_condition, namespace)", - "description": null, - "error": null, - "hide": 0, - "includeAll": true, - "label": null, - "multi": true, - "name": "operator_namespace", - "options": [], - "query": { - "query": "label_values(gotk_reconcile_condition, namespace)", - "refId": "StandardVariableQuery" - }, - "refresh": 2, - "regex": "", - "skipUrlSync": false, - "sort": 5, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - }, - { - "allValue": null, - "current": { - "selected": true, - "tags": [], - "text": [ - "All" - ], - "value": [ - "$__all" - ] - }, - "datasource": null, - "definition": "label_values(gotk_reconcile_condition, exported_namespace)", - "description": null, - "error": null, - "hide": 0, - "includeAll": true, - "label": null, - "multi": true, - "name": "namespace", - "options": [], - "query": { - "query": "label_values(gotk_reconcile_condition, exported_namespace)", - "refId": "StandardVariableQuery" - }, - "refresh": 2, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "tagValuesQuery": "", - "tags": [], - "tagsQuery": "", - "type": "query", - "useTags": false - } - ] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ] - }, - "timezone": "", - "title": "Flux Cluster Stats", - "uid": "flux-cluster", - "version": 1 -} diff --git a/manifests/monitoring/grafana/dashboards/control-plane.json b/manifests/monitoring/grafana/dashboards/control-plane.json deleted file mode 100644 index 3eff4d57..00000000 --- a/manifests/monitoring/grafana/dashboards/control-plane.json +++ /dev/null @@ -1,1302 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "target": { - "limit": 100, - "matchAny": false, - "tags": [], - "type": "dashboard" - }, - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "gnetId": null, - "graphTooltip": 0, - "id": 29, - "iteration": 1639041352219, - "links": [], - "liveNow": false, - "panels": [ - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "red", - "value": 100 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 0, - "y": 0 - }, - "id": 24, - "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "last" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "value" - }, - "pluginVersion": "8.2.3", - "targets": [ - { - "expr": "sum(go_info{namespace=\"$namespace\",pod=~\".*-controller-.*\"})", - "interval": "", - "legendFormat": "pods", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Controllers", - "type": "stat" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "#EAB839", - "value": 50 - }, - { - "color": "red", - "value": 100 - } - ] - }, - "unit": "s" - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 6, - "y": 0 - }, - "id": 23, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "8.2.3", - "targets": [ - { - "expr": "max(workqueue_longest_running_processor_seconds{namespace=\"$namespace\",pod=~\".*-controller-.*\"})", - "hide": false, - "interval": "", - "legendFormat": "seconds", - "refId": "B" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Max Work Queue", - "type": "stat" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "#EAB839", - "value": 500000000 - }, - { - "color": "red", - "value": 900000000 - } - ] - }, - "unit": "decbits" - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 12, - "y": 0 - }, - "id": 25, - "options": { - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showThresholdLabels": false, - "showThresholdMarkers": true, - "text": {} - }, - "pluginVersion": "8.2.3", - "targets": [ - { - "expr": "sum(go_memstats_alloc_bytes{namespace=\"$namespace\",pod=~\".*-controller-.*\"})", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Memory", - "type": "gauge" - }, - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "blue", - "value": null - }, - { - "color": "#EAB839", - "value": 50 - }, - { - "color": "red", - "value": 100 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 18, - "y": 0 - }, - "id": 26, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "mean" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "8.2.3", - "targets": [ - { - "expr": "sum(rate(rest_client_requests_total{namespace=\"$namespace\",pod=~\".*-controller-.*\"}[1m]))", - "interval": "", - "legendFormat": "requests", - "refId": "A" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "API Requests", - "type": "stat" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "decimals": null, - "description": "", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 5 - }, - "hiddenSeries": false, - "id": 21, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "sum(rate(rest_client_requests_total{namespace=\"$namespace\"}[1m]))", - "hide": false, - "interval": "", - "legendFormat": "total", - "refId": "A" - }, - { - "expr": "sum(rate(rest_client_requests_total{namespace=\"$namespace\",code!~\"2..\"}[1m]))", - "hide": false, - "interval": "", - "legendFormat": "errors", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Kubernetes API Requests", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:912", - "decimals": null, - "format": "short", - "label": "", - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:913", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "collapsed": false, - "datasource": "${DS_PROMETHEUS}", - "fieldConfig": { - "defaults": {}, - "overrides": [] - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 13 - }, - "id": 15, - "panels": [], - "title": "Resource Usage", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 11, - "w": 12, - "x": 0, - "y": 14 - }, - "hiddenSeries": false, - "id": 11, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": true, - "steppedLine": false, - "targets": [ - { - "expr": "rate(process_cpu_seconds_total{namespace=\"$namespace\",pod=~\".*-controller-.*\"}[1m])", - "interval": "", - "legendFormat": "{{pod}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "CPU Usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:93", - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:94", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 11, - "w": 12, - "x": 12, - "y": 14 - }, - "hiddenSeries": false, - "id": 13, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": true, - "steppedLine": false, - "targets": [ - { - "expr": "rate(go_memstats_alloc_bytes_total{namespace=\"$namespace\",pod=~\".*-controller-.*\"}[1m])", - "hide": false, - "interval": "", - "legendFormat": "{{pod}}", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Memory Usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:93", - "decimals": 0, - "format": "bytes", - "label": "", - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:94", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "collapsed": false, - "datasource": "${DS_PROMETHEUS}", - "fieldConfig": { - "defaults": {}, - "overrides": [] - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 25 - }, - "id": 17, - "panels": [], - "title": "Reconciliation Stats", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 26 - }, - "hiddenSeries": false, - "id": 27, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "workqueue_longest_running_processor_seconds{name=\"kustomization\"}", - "hide": false, - "interval": "", - "legendFormat": "kustomizations", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Cluster Reconciliation Duration", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:912", - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:913", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": true, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "decimals": 2, - "description": "", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 34 - }, - "hiddenSeries": false, - "id": 2, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": false, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": true, - "targets": [ - { - "expr": "sum(increase(controller_runtime_reconcile_total{controller=\"kustomization\",result!=\"error\"}[1m])) by (controller)", - "format": "time_series", - "interval": "", - "legendFormat": "successful reconciliations ", - "refId": "A" - }, - { - "expr": "sum(increase(controller_runtime_reconcile_total{controller=\"kustomization\",result=\"error\"}[1m])) by (controller)", - "format": "time_series", - "interval": "", - "legendFormat": "failed reconciliations ", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Cluster Reconciliations ops/min", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:1171", - "format": "opm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:1172", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": true, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "decimals": 2, - "description": "", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 34 - }, - "hiddenSeries": false, - "id": 4, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": false, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": true, - "targets": [ - { - "expr": "sum(increase(controller_runtime_reconcile_total{controller=\"gitrepository\",result!=\"error\"}[1m]))", - "format": "time_series", - "interval": "", - "legendFormat": "successful git pulls", - "refId": "A" - }, - { - "expr": "sum(increase(controller_runtime_reconcile_total{controller=\"gitrepository\",result=\"error\"}[1m]))", - "format": "time_series", - "interval": "", - "legendFormat": "failed git pulls", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Git Sources ops/min", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:285", - "format": "opm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:286", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "collapsed": false, - "datasource": "${DS_PROMETHEUS}", - "fieldConfig": { - "defaults": {}, - "overrides": [] - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 43 - }, - "id": 19, - "panels": [], - "title": "Helm Stats", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 24, - "x": 0, - "y": 44 - }, - "hiddenSeries": false, - "id": 9, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "rightSide": true, - "show": false, - "total": false, - "values": true - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null as zero", - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.50, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"helmrelease\"}[5m])) by (le))", - "hide": true, - "interval": "", - "legendFormat": "P50", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.90, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"helmrelease\"}[5m])) by (le))", - "hide": true, - "interval": "", - "legendFormat": "P90", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.99, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"helmrelease\"}[5m])) by (le))", - "hide": false, - "interval": "", - "legendFormat": "P99", - "refId": "C" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Helm Release Duration", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:1612", - "format": "s", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:1613", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": true, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "decimals": 2, - "description": "", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 52 - }, - "hiddenSeries": false, - "id": 5, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": false, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": true, - "targets": [ - { - "expr": "sum(increase(controller_runtime_reconcile_total{controller=\"helmrelease\",result!=\"error\"}[1m])) by (controller)", - "format": "time_series", - "interval": "", - "legendFormat": "successful reconciliations ", - "refId": "A" - }, - { - "expr": "sum(increase(controller_runtime_reconcile_total{controller=\"helmrelease\",result=\"error\"}[1m])) by (controller)", - "format": "time_series", - "interval": "", - "legendFormat": "failed reconciliations ", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Helm Releases ops/min", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:1102", - "format": "opm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:1103", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": true, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "decimals": 2, - "description": "", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 9, - "w": 12, - "x": 12, - "y": 52 - }, - "hiddenSeries": false, - "id": 6, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": true - }, - "lines": false, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pluginVersion": "8.2.3", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": true, - "targets": [ - { - "expr": "sum(increase(controller_runtime_reconcile_total{controller=\"helmchart\",result!=\"error\"}[1m])) by (controller)", - "format": "time_series", - "interval": "", - "legendFormat": "successful chart pulls", - "refId": "A" - }, - { - "expr": "sum(increase(controller_runtime_reconcile_total{controller=\"helmchart\",result=\"error\"}[1m])) by (controller)", - "format": "time_series", - "interval": "", - "legendFormat": "failed chart pulls", - "refId": "B" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Helm Charts ops/min", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "$$hashKey": "object:1892", - "format": "opm", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "$$hashKey": "object:1893", - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "refresh": "10s", - "schemaVersion": 31, - "style": "light", - "tags": [ - "flux" - ], - "templating": { - "list": [ - { - "current": { - "selected": false, - "text": "Prometheus", - "value": "Prometheus" - }, - "description": null, - "error": null, - "hide": 2, - "includeAll": false, - "label": null, - "multi": false, - "name": "DS_PROMETHEUS", - "options": [], - "query": "prometheus", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" - }, - { - "allValue": null, - "current": { - "selected": false, - "text": "flux-system", - "value": "flux-system" - }, - "datasource": "${DS_PROMETHEUS}", - "definition": "workqueue_work_duration_seconds_count", - "description": null, - "error": null, - "hide": 0, - "includeAll": false, - "label": null, - "multi": false, - "name": "namespace", - "options": [], - "query": { - "query": "workqueue_work_duration_seconds_count", - "refId": "Prometheus-namespace-Variable-Query" - }, - "refresh": 2, - "regex": "/.*namespace=\"([^\"]*).*/", - "skipUrlSync": false, - "sort": 0, - "tagValuesQuery": "", - "tagsQuery": "", - "type": "query", - "useTags": false - } - ] - }, - "time": { - "from": "now-15m", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ] - }, - "timezone": "", - "title": "Flux Control Plane", - "uid": "flux-control-plane", - "version": 2 -} \ No newline at end of file diff --git a/manifests/monitoring/grafana/datasources.yaml b/manifests/monitoring/grafana/datasources.yaml deleted file mode 100644 index f3961d19..00000000 --- a/manifests/monitoring/grafana/datasources.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-datasources - namespace: flux-system -data: - datasources.yaml: |- - apiVersion: 1 - deleteDatasources: - - name: prometheus - datasources: - - name: prometheus - type: prometheus - access: proxy - url: http://prometheus:9090 - isDefault: true - editable: true - version: 1 diff --git a/manifests/monitoring/grafana/deployment.yaml b/manifests/monitoring/grafana/deployment.yaml deleted file mode 100644 index d53d66b6..00000000 --- a/manifests/monitoring/grafana/deployment.yaml +++ /dev/null @@ -1,60 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: grafana - labels: - app: grafana -spec: - replicas: 1 - selector: - matchLabels: - app: grafana - template: - metadata: - labels: - app: grafana - annotations: - prometheus.io/scrape: 'false' - spec: - containers: - - name: grafana - image: "grafana/grafana:7.5.4" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 3000 - protocol: TCP - env: - - name: GF_PATHS_PROVISIONING - value: /etc/grafana/provisioning/ - - name: GF_AUTH_BASIC_ENABLED - value: "false" - - name: GF_AUTH_ANONYMOUS_ENABLED - value: "true" - - name: GF_AUTH_ANONYMOUS_ORG_ROLE - value: Admin - - name: GF_USERS_DEFAULT_THEME - value: "light" - volumeMounts: - - name: grafana - mountPath: /var/lib/grafana - - name: dashboards - mountPath: /etc/grafana/dashboards - - name: datasources - mountPath: /etc/grafana/provisioning/datasources - - name: providers - mountPath: /etc/grafana/provisioning/dashboards - resources: - {} - volumes: - - name: grafana - emptyDir: {} - - name: dashboards - configMap: - name: grafana-dashboards - - name: providers - configMap: - name: grafana-providers - - name: datasources - configMap: - name: grafana-datasources diff --git a/manifests/monitoring/grafana/kustomization.yaml b/manifests/monitoring/grafana/kustomization.yaml deleted file mode 100644 index 13e0356a..00000000 --- a/manifests/monitoring/grafana/kustomization.yaml +++ /dev/null @@ -1,14 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: flux-system -resources: - - service.yaml - - deployment.yaml - - providers.yaml - - datasources.yaml -configMapGenerator: - - name: grafana-dashboards - files: - - dashboards/control-plane.json - - dashboards/cluster.json - diff --git a/manifests/monitoring/grafana/providers.yaml b/manifests/monitoring/grafana/providers.yaml deleted file mode 100644 index 4f5e23c9..00000000 --- a/manifests/monitoring/grafana/providers.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-providers - namespace: flux-system -data: - providers.yaml: |+ - apiVersion: 1 - providers: - - name: 'default' - orgId: 1 - folder: '' - type: file - disableDeletion: false - editable: true - options: - path: /etc/grafana/dashboards diff --git a/manifests/monitoring/grafana/service.yaml b/manifests/monitoring/grafana/service.yaml deleted file mode 100644 index 87968d71..00000000 --- a/manifests/monitoring/grafana/service.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: grafana - namespace: flux-system - labels: - app: grafana -spec: - type: ClusterIP - ports: - - port: 3000 - targetPort: http - protocol: TCP - name: http - selector: - app: grafana diff --git a/manifests/monitoring/kube-prometheus-stack/kustomization.yaml b/manifests/monitoring/kube-prometheus-stack/kustomization.yaml deleted file mode 100644 index 920eff14..00000000 --- a/manifests/monitoring/kube-prometheus-stack/kustomization.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: monitoring -resources: - - namespace.yaml - - repository.yaml - - release.yaml diff --git a/manifests/monitoring/kube-prometheus-stack/release.yaml b/manifests/monitoring/kube-prometheus-stack/release.yaml deleted file mode 100644 index f4e6d60f..00000000 --- a/manifests/monitoring/kube-prometheus-stack/release.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: helm.toolkit.fluxcd.io/v2beta1 -kind: HelmRelease -metadata: - name: kube-prometheus-stack -spec: - interval: 5m - chart: - spec: - version: 23.2.0 - chart: kube-prometheus-stack - sourceRef: - kind: HelmRepository - name: prometheus-community - interval: 1m - install: - crds: Create - upgrade: - crds: CreateReplace - values: - alertmanager: - enabled: false - grafana: - sidecar: - dashboards: - searchNamespace: ALL - prometheus: - prometheusSpec: - podMonitorSelector: - matchLabels: - app.kubernetes.io/part-of: flux diff --git a/manifests/monitoring/kube-prometheus-stack/repository.yaml b/manifests/monitoring/kube-prometheus-stack/repository.yaml deleted file mode 100644 index 258f4087..00000000 --- a/manifests/monitoring/kube-prometheus-stack/repository.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: source.toolkit.fluxcd.io/v1beta1 -kind: HelmRepository -metadata: - name: prometheus-community -spec: - interval: 1m - url: https://prometheus-community.github.io/helm-charts diff --git a/manifests/monitoring/kustomization.yaml b/manifests/monitoring/kustomization.yaml deleted file mode 100644 index b93baa2d..00000000 --- a/manifests/monitoring/kustomization.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: flux-system -resources: -- prometheus -- grafana diff --git a/manifests/monitoring/monitoring-config/kustomization.yaml b/manifests/monitoring/monitoring-config/kustomization.yaml deleted file mode 100644 index 7b53fa1f..00000000 --- a/manifests/monitoring/monitoring-config/kustomization.yaml +++ /dev/null @@ -1,13 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: flux-system -resources: - - podmonitor.yaml -configMapGenerator: - - name: flux-grafana-dashboards - files: - - ../grafana/dashboards/control-plane.json - - ../grafana/dashboards/cluster.json - options: - labels: - grafana_dashboard: flux-system diff --git a/manifests/monitoring/monitoring-config/podmonitor.yaml b/manifests/monitoring/monitoring-config/podmonitor.yaml deleted file mode 100644 index 7838272a..00000000 --- a/manifests/monitoring/monitoring-config/podmonitor.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: monitoring.coreos.com/v1 -kind: PodMonitor -metadata: - name: flux-system - namespace: flux-system - labels: - app.kubernetes.io/part-of: flux -spec: - namespaceSelector: - matchNames: - - flux-system - selector: - matchExpressions: - - key: app - operator: In - values: - - helm-controller - - source-controller - - kustomize-controller - - notification-controller - - image-automation-controller - - image-reflector-controller - podMetricsEndpoints: - - port: http-prom diff --git a/manifests/monitoring/prometheus/account.yaml b/manifests/monitoring/prometheus/account.yaml deleted file mode 100644 index e475c196..00000000 --- a/manifests/monitoring/prometheus/account.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: prometheus - namespace: flux-system diff --git a/manifests/monitoring/prometheus/deployment.yaml b/manifests/monitoring/prometheus/deployment.yaml deleted file mode 100644 index 293a9353..00000000 --- a/manifests/monitoring/prometheus/deployment.yaml +++ /dev/null @@ -1,52 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: prometheus - namespace: flux-system -spec: - replicas: 1 - selector: - matchLabels: - app: prometheus - template: - metadata: - labels: - app: prometheus - annotations: - appmesh.k8s.aws/sidecarInjectorWebhook: disabled - sidecar.istio.io/inject: "false" - spec: - serviceAccountName: prometheus - containers: - - name: prometheus - image: prom/prometheus:v2.26.0 - imagePullPolicy: IfNotPresent - args: - - '--storage.tsdb.retention=2h' - - '--config.file=/etc/prometheus/prometheus.yml' - ports: - - containerPort: 9090 - name: http - livenessProbe: - httpGet: - path: /-/healthy - port: 9090 - readinessProbe: - httpGet: - path: /-/ready - port: 9090 - resources: - requests: - cpu: 10m - memory: 128Mi - volumeMounts: - - name: config-volume - mountPath: /etc/prometheus - - name: data-volume - mountPath: /prometheus/data - volumes: - - name: config-volume - configMap: - name: prometheus - - name: data-volume - emptyDir: {} \ No newline at end of file diff --git a/manifests/monitoring/prometheus/kustomization.yaml b/manifests/monitoring/prometheus/kustomization.yaml deleted file mode 100644 index b3377956..00000000 --- a/manifests/monitoring/prometheus/kustomization.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: flux-system -resources: - - account.yaml - - rbac.yaml - - service.yaml - - deployment.yaml -configMapGenerator: - - name: prometheus - files: - - prometheus.yml diff --git a/manifests/monitoring/prometheus/prometheus.yml b/manifests/monitoring/prometheus/prometheus.yml deleted file mode 100644 index 93f35c09..00000000 --- a/manifests/monitoring/prometheus/prometheus.yml +++ /dev/null @@ -1,52 +0,0 @@ -global: - scrape_interval: 10s -scrape_configs: - -# Kubernetes API -- job_name: kubernetes-apiserver - kubernetes_sd_configs: - - role: endpoints - namespaces: - names: - - default - scheme: https - tls_config: - ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt - insecure_skip_verify: true - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - relabel_configs: - - source_labels: [__meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name] - action: keep - regex: kubernetes;https - -# Kubernetes pods -- job_name: kubernetes-pods - kubernetes_sd_configs: - - role: pod - relabel_configs: - - action: keep - regex: true - source_labels: - - __meta_kubernetes_pod_annotation_prometheus_io_scrape - - action: replace - regex: (.+) - source_labels: - - __meta_kubernetes_pod_annotation_prometheus_io_path - target_label: __metrics_path__ - - action: replace - regex: ([^:]+)(?::\d+)?;(\d+) - replacement: $1:$2 - source_labels: - - __address__ - - __meta_kubernetes_pod_annotation_prometheus_io_port - target_label: __address__ - - action: labelmap - regex: __meta_kubernetes_pod_label_(.+) - - action: replace - source_labels: - - __meta_kubernetes_namespace - target_label: namespace - - action: replace - source_labels: - - __meta_kubernetes_pod_name - target_label: pod diff --git a/manifests/monitoring/prometheus/rbac.yaml b/manifests/monitoring/prometheus/rbac.yaml deleted file mode 100644 index 926a6d83..00000000 --- a/manifests/monitoring/prometheus/rbac.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: prometheus-flux-system -rules: - - apiGroups: [""] - resources: - - nodes - - services - - endpoints - - pods - - nodes/proxy - verbs: ["get", "list", "watch"] - - apiGroups: [""] - resources: - - configmaps - verbs: ["get"] - - nonResourceURLs: ["/metrics"] - verbs: ["get"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: prometheus-flux-system -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: prometheus-flux-system -subjects: -- kind: ServiceAccount - name: prometheus - namespace: flux-system diff --git a/manifests/monitoring/prometheus/service.yaml b/manifests/monitoring/prometheus/service.yaml deleted file mode 100644 index 143cd9b4..00000000 --- a/manifests/monitoring/prometheus/service.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: prometheus - namespace: flux-system -spec: - selector: - app: prometheus - ports: - - name: http - protocol: TCP - port: 9090 \ No newline at end of file diff --git a/manifests/openshift/kustomization.yaml b/manifests/openshift/kustomization.yaml new file mode 100644 index 00000000..a161f2b1 --- /dev/null +++ b/manifests/openshift/kustomization.yaml @@ -0,0 +1,48 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: flux-system +resources: + - namespace.yaml + - scc.yaml + - ../bases/source-controller + - ../bases/kustomize-controller + - ../bases/notification-controller + - ../bases/helm-controller + - ../bases/image-reflector-controller + - ../bases/image-automation-controller + - ../rbac + - ../policies +transformers: + - labels.yaml +images: + - name: fluxcd/source-controller + newName: ghcr.io/fluxcd/source-controller + - name: fluxcd/kustomize-controller + newName: ghcr.io/fluxcd/kustomize-controller + - name: fluxcd/helm-controller + newName: ghcr.io/fluxcd/helm-controller + - name: fluxcd/notification-controller + newName: ghcr.io/fluxcd/notification-controller + - name: fluxcd/image-reflector-controller + newName: ghcr.io/fluxcd/image-reflector-controller + - name: fluxcd/image-automation-controller + newName: ghcr.io/fluxcd/image-automation-controller +patches: + - patch: | + apiVersion: apps/v1 + kind: Deployment + metadata: + name: all + spec: + template: + spec: + securityContext: + $patch: delete + containers: + - name: manager + securityContext: + runAsUser: 65534 + seccompProfile: + $patch: delete + target: + kind: Deployment diff --git a/manifests/openshift/labels.yaml b/manifests/openshift/labels.yaml new file mode 100644 index 00000000..5a5d78b4 --- /dev/null +++ b/manifests/openshift/labels.yaml @@ -0,0 +1,10 @@ +apiVersion: builtin +kind: LabelTransformer +metadata: + name: labels +labels: + app.kubernetes.io/part-of: flux + app.kubernetes.io/instance: flux-system +fieldSpecs: + - path: metadata/labels + create: true diff --git a/manifests/openshift/namespace.yaml b/manifests/openshift/namespace.yaml new file mode 100644 index 00000000..c00a4321 --- /dev/null +++ b/manifests/openshift/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: flux-system diff --git a/manifests/openshift/scc.yaml b/manifests/openshift/scc.yaml new file mode 100644 index 00000000..6a25f70b --- /dev/null +++ b/manifests/openshift/scc.yaml @@ -0,0 +1,43 @@ +# Allow Flux controllers to run as non-root on OpenShift +# Docs: https://fluxcd.io/flux/installation/configuration/openshift/ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: flux-scc +rules: + - apiGroups: + - security.openshift.io + resources: + - securitycontextconstraints + resourceNames: + - nonroot + verbs: + - use +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: flux-scc +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: flux-scc +subjects: + - kind: ServiceAccount + name: source-controller + namespace: flux-system + - kind: ServiceAccount + name: kustomize-controller + namespace: flux-system + - kind: ServiceAccount + name: helm-controller + namespace: flux-system + - kind: ServiceAccount + name: notification-controller + namespace: flux-system + - kind: ServiceAccount + name: image-reflector-controller + namespace: flux-system + - kind: ServiceAccount + name: image-automation-controller + namespace: flux-system diff --git a/manifests/rbac/controller.yaml b/manifests/rbac/controller.yaml index 7fb181c5..b059891f 100644 --- a/manifests/rbac/controller.yaml +++ b/manifests/rbac/controller.yaml @@ -23,6 +23,8 @@ rules: resources: - namespaces - secrets + - configmaps + - serviceaccounts verbs: - get - list @@ -34,19 +36,27 @@ rules: verbs: - create - patch +# required by leader election - apiGroups: - - "" + - "" resources: - - configmaps - - configmaps/status + - configmaps verbs: - - get - - list - - watch - - create - - update - - patch - - delete + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch - apiGroups: - "coordination.k8s.io" resources: @@ -59,6 +69,11 @@ rules: - update - patch - delete +# required for flow control +- nonResourceURLs: + - /livez/ping + verbs: + - head --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/manifests/rbac/edit.yaml b/manifests/rbac/edit.yaml new file mode 100644 index 00000000..34569105 --- /dev/null +++ b/manifests/rbac/edit.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: flux-edit + labels: + rbac.authorization.k8s.io/aggregate-to-edit: "true" + rbac.authorization.k8s.io/aggregate-to-admin: "true" +rules: + - apiGroups: + - notification.toolkit.fluxcd.io + - source.toolkit.fluxcd.io + - helm.toolkit.fluxcd.io + - image.toolkit.fluxcd.io + - kustomize.toolkit.fluxcd.io + resources: ["*"] + verbs: + - create + - delete + - deletecollection + - patch + - update diff --git a/manifests/rbac/kustomization.yaml b/manifests/rbac/kustomization.yaml index 6a1d4a69..4e708ef2 100644 --- a/manifests/rbac/kustomization.yaml +++ b/manifests/rbac/kustomization.yaml @@ -3,3 +3,6 @@ kind: Kustomization resources: - controller.yaml - reconciler.yaml + - edit.yaml + - view.yaml + - resourcequota.yaml diff --git a/manifests/rbac/resourcequota.yaml b/manifests/rbac/resourcequota.yaml new file mode 100644 index 00000000..51739c70 --- /dev/null +++ b/manifests/rbac/resourcequota.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: ResourceQuota +metadata: + name: critical-pods +spec: + hard: + pods: "1000" + scopeSelector: + matchExpressions: + - operator: In + scopeName: PriorityClass + values: + - system-node-critical + - system-cluster-critical diff --git a/manifests/rbac/view.yaml b/manifests/rbac/view.yaml new file mode 100644 index 00000000..f0caf5be --- /dev/null +++ b/manifests/rbac/view.yaml @@ -0,0 +1,20 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: flux-view + labels: + rbac.authorization.k8s.io/aggregate-to-admin: "true" + rbac.authorization.k8s.io/aggregate-to-edit: "true" + rbac.authorization.k8s.io/aggregate-to-view: "true" +rules: + - apiGroups: + - notification.toolkit.fluxcd.io + - source.toolkit.fluxcd.io + - helm.toolkit.fluxcd.io + - image.toolkit.fluxcd.io + - kustomize.toolkit.fluxcd.io + resources: ["*"] + verbs: + - get + - list + - watch diff --git a/manifests/scripts/bundle.sh b/manifests/scripts/bundle.sh index c8f242c8..4259e026 100755 --- a/manifests/scripts/bundle.sh +++ b/manifests/scripts/bundle.sh @@ -42,7 +42,7 @@ rm -rf $OUT_PATH mkdir -p $OUT_PATH files="" -info using "$(kustomize version --short)" +info using kustomize "$(kustomize version)" # build controllers for controller in ${IN_PATH}/bases/*/; do diff --git a/internal/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go similarity index 56% rename from internal/bootstrap/bootstrap.go rename to pkg/bootstrap/bootstrap.go index d3b79ed6..3faa9214 100644 --- a/internal/bootstrap/bootstrap.go +++ b/pkg/bootstrap/bootstrap.go @@ -27,21 +27,32 @@ import ( apierr "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + apierrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" - "github.com/fluxcd/flux2/pkg/manifestgen/install" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" - "github.com/fluxcd/flux2/pkg/manifestgen/sync" + "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 ( ErrReconciledWithWarning = errors.New("reconciled with warning") ) +// Reconciler reconciles and reports the health of different +// components and kubernetes resources involved in the installation of Flux. +// +// It is recommended use the `ReconcilerWithSyncCheck` interface that also +// reports the health of the GitRepository. type Reconciler interface { // ReconcileComponents reconciles the components by generating the // manifests with the provided values, committing them to Git and @@ -74,6 +85,14 @@ type RepositoryReconciler interface { ReconcileRepository(ctx context.Context) error } +// ReconcilerWithSyncCheck extends the Reconciler interface to also report the health of the GitReposiotry +// that syncs Flux on the cluster +type ReconcilerWithSyncCheck interface { + Reconciler + // ReportGitRepoHealth reports about the health of the GitRepository synchronizing the components. + ReportGitRepoHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error +} + type PostGenerateSecretFunc func(ctx context.Context, secret corev1.Secret, options sourcesecret.Options) error func Run(ctx context.Context, reconciler Reconciler, manifestsBase string, @@ -97,18 +116,22 @@ func Run(ctx context.Context, reconciler Reconciler, manifestsBase string, return err } - var healthErrCount int + var errs []error + if r, ok := reconciler.(ReconcilerWithSyncCheck); ok { + if err := r.ReportGitRepoHealth(ctx, syncOpts, pollInterval, timeout); err != nil { + errs = append(errs, err) + } + } + if err := reconciler.ReportKustomizationHealth(ctx, syncOpts, pollInterval, timeout); err != nil { - healthErrCount++ + errs = append(errs, err) } + if err := reconciler.ReportComponentsHealth(ctx, installOpts, timeout); err != nil { - healthErrCount++ + errs = append(errs, err) } - if healthErrCount > 0 { - // Composing a "smart" error message here from the returned - // errors does not result in any useful information for the - // user, as both methods log the failures they run into. - err = fmt.Errorf("bootstrap failed with %d health check failure(s)", healthErrCount) + if len(errs) > 0 { + err = fmt.Errorf("bootstrap failed with %d health check failure(s): %w", len(errs), apierrors.NewAggregate(errs)) } return err @@ -150,6 +173,26 @@ func reconcileSecret(ctx context.Context, kube client.Client, secret corev1.Secr return kube.Update(ctx, &existing) } +func reconcileImagePullSecret(ctx context.Context, kube client.Client, installOpts install.Options) error { + credentials := strings.SplitN(installOpts.RegistryCredential, ":", 2) + dcj, err := sourcesecret.GenerateDockerConfigJson(installOpts.Registry, credentials[0], credentials[1]) + if err != nil { + return fmt.Errorf("failed to generate docker config json: %w", err) + } + + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: installOpts.Namespace, + Name: installOpts.ImagePullSecret, + }, + StringData: map[string]string{ + corev1.DockerConfigJsonKey: string(dcj), + }, + Type: corev1.SecretTypeDockerConfigJson, + } + return reconcileSecret(ctx, kube, secret) +} + func kustomizationPathDiffers(ctx context.Context, kube client.Client, objKey client.ObjectKey, path string) (string, error) { var k kustomizev1.Kustomization if err := kube.Get(ctx, objKey, &k); err != nil { @@ -171,34 +214,47 @@ func kustomizationPathDiffers(ctx context.Context, kube client.Client, objKey cl return k.Spec.Path, nil } -func kustomizationReconciled(ctx context.Context, kube client.Client, objKey client.ObjectKey, - kustomization *kustomizev1.Kustomization, expectRevision string) func() (bool, error) { +type objectWithConditions interface { + client.Object + GetConditions() []metav1.Condition +} - return func() (bool, error) { - if err := kube.Get(ctx, objKey, kustomization); err != nil { +func objectReconciled(kube client.Client, objKey client.ObjectKey, clientObject objectWithConditions, expectRevision string) wait.ConditionWithContextFunc { + return func(ctx context.Context) (bool, error) { + // for some reason, TypeMeta gets unset after kube.Get so we want to store the GVK and set it after + // ref https://github.com/kubernetes-sigs/controller-runtime/issues/1517#issuecomment-844703142 + gvk := clientObject.GetObjectKind().GroupVersionKind() + if err := kube.Get(ctx, objKey, clientObject); err != nil { return false, err } + clientObject.GetObjectKind().SetGroupVersionKind(gvk) - // Detect suspended Kustomization, as this would result in an endless wait - if kustomization.Spec.Suspend { - return false, fmt.Errorf("Kustomization is suspended") + kind := gvk.Kind + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(clientObject) + 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 + // Detect suspended object, as this would result in an endless wait + if suspended, ok, _ := unstructured.NestedBool(obj, "spec", "suspend"); ok && suspended { + return false, fmt.Errorf("%s '%s' is suspended", kind, objKey.String()) } - // Confirm the given revision has been attempted by the controller - if kustomization.Status.LastAttemptedRevision != expectRevision { + // Confirm the state we are observing is for the current generation + if generation, ok, _ := unstructured.NestedInt64(obj, "status", "observedGeneration"); ok && generation != clientObject.GetGeneration() { return false, nil } // Confirm the resource is healthy - if c := apimeta.FindStatusCondition(kustomization.Status.Conditions, meta.ReadyCondition); c != nil { + if c := apimeta.FindStatusCondition(clientObject.GetConditions(), meta.ReadyCondition); c != nil { switch c.Status { case metav1.ConditionTrue: - return true, nil + // Confirm the given revision has been attempted by the controller + hasRev, err := hasRevision(kind, obj, expectRevision) + if err != nil { + return false, err + } + return hasRev, nil case metav1.ConditionFalse: return false, fmt.Errorf(c.Message) } @@ -207,6 +263,21 @@ func kustomizationReconciled(ctx context.Context, kube client.Client, objKey cli } } +// hasRevision checks that the reconciled revision (for Kustomization this is `.status.lastAttemptedRevision` +// and for Source APIs, it is stored in `.status.artifact.revision`) is the same as the expectedRev +func hasRevision(kind string, obj map[string]interface{}, expectedRev string) (bool, error) { + var rev string + switch kind { + case sourcev1.GitRepositoryKind, sourcev1b2.OCIRepositoryKind, sourcev1b2.BucketKind, sourcev1.HelmChartKind: + rev, _, _ = unstructured.NestedString(obj, "status", "artifact", "revision") + case kustomizev1.KustomizationKind: + rev, _, _ = unstructured.NestedString(obj, "status", "lastAttemptedRevision") + default: + return false, fmt.Errorf("cannot get status revision for kind: '%s'", kind) + } + return sourcev1b2.TransformLegacyRevision(rev) == expectedRev, nil +} + func retry(retries int, wait time.Duration, fn func() error) (err error) { for i := 0; ; i++ { err = fn() diff --git a/pkg/bootstrap/bootstrap_plain_git.go b/pkg/bootstrap/bootstrap_plain_git.go new file mode 100644 index 00000000..78af0c88 --- /dev/null +++ b/pkg/bootstrap/bootstrap_plain_git.go @@ -0,0 +1,549 @@ +/* +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 bootstrap + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + "github.com/ProtonMail/go-crypto/openpgp" + gogit "github.com/go-git/go-git/v5" + corev1 "k8s.io/api/core/v1" + apimeta "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/cli-runtime/pkg/genericclioptions" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/kustomize/api/konfig" + "sigs.k8s.io/yaml" + + "github.com/fluxcd/cli-utils/pkg/object" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/repository" + "github.com/fluxcd/pkg/kustomize/filesys" + runclient "github.com/fluxcd/pkg/runtime/client" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/utils" + "github.com/fluxcd/flux2/v2/pkg/log" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/install" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/kustomization" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" + "github.com/fluxcd/flux2/v2/pkg/status" +) + +type PlainGitBootstrapper struct { + url string + branch string + + signature git.Signature + commitMessageAppendix string + + gpgKeyRing openpgp.EntityList + gpgPassphrase string + gpgKeyID string + + restClientGetter genericclioptions.RESTClientGetter + restClientOptions *runclient.Options + + postGenerateSecret []PostGenerateSecretFunc + + gitClient repository.Client + kube client.Client + logger log.Logger +} + +type GitOption interface { + applyGit(b *PlainGitBootstrapper) +} + +func WithRepositoryURL(url string) GitOption { + return repositoryURLOption(url) +} + +type repositoryURLOption string + +func (o repositoryURLOption) applyGit(b *PlainGitBootstrapper) { + b.url = string(o) +} + +func WithPostGenerateSecretFunc(callback PostGenerateSecretFunc) GitOption { + return postGenerateSecret(callback) +} + +type postGenerateSecret PostGenerateSecretFunc + +func (o postGenerateSecret) applyGit(b *PlainGitBootstrapper) { + b.postGenerateSecret = append(b.postGenerateSecret, PostGenerateSecretFunc(o)) +} + +func NewPlainGitProvider(git repository.Client, kube client.Client, opts ...GitOption) (*PlainGitBootstrapper, error) { + b := &PlainGitBootstrapper{ + gitClient: git, + kube: kube, + } + for _, opt := range opts { + opt.applyGit(b) + } + return b, nil +} + +func (b *PlainGitBootstrapper) ReconcileComponents(ctx context.Context, manifestsBase string, options install.Options, _ sourcesecret.Options) error { + // Clone if not already + if _, err := b.gitClient.Head(); err != nil { + if err != git.ErrNoGitRepository { + return err + } + + b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url) + var cloned bool + if err = retry(1, 2*time.Second, func() (err error) { + if err = b.cleanGitRepoDir(); err != nil { + b.logger.Warningf(" failed to clean directory for git repo: %w", err) + return + } + _, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{ + CheckoutStrategy: repository.CheckoutStrategy{ + Branch: b.branch, + }, + }) + if err != nil { + b.logger.Warningf(" clone failure: %s", err) + } + if err == nil { + cloned = true + } + return + }); err != nil { + return fmt.Errorf("failed to clone repository: %w", err) + } + if cloned { + b.logger.Successf("cloned repository") + } + } + + // Generate component manifests + b.logger.Actionf("generating component manifests") + manifests, err := install.Generate(options, manifestsBase) + if err != nil { + return fmt.Errorf("component manifest generation failed: %w", err) + } + b.logger.Successf("generated component manifests") + + // Write generated files and make a commit + var signer *openpgp.Entity + if b.gpgKeyRing != nil { + signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) + if err != nil { + return fmt.Errorf("failed to generate OpenPGP entity: %w", err) + } + } + commitMsg := fmt.Sprintf("Add Flux %s component manifests", options.Version) + if b.commitMessageAppendix != "" { + commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix + } + + commit, err := b.gitClient.Commit(git.Commit{ + Author: b.signature, + Message: commitMsg, + }, repository.WithFiles(map[string]io.Reader{ + manifests.Path: strings.NewReader(manifests.Content), + }), repository.WithSigner(signer)) + if err != nil && err != git.ErrNoStagedFiles { + return fmt.Errorf("failed to commit component manifests: %w", err) + } + + if err == nil { + b.logger.Successf("committed component manifests to %q (%q)", b.branch, commit) + b.logger.Actionf("pushing component manifests to %q", b.url) + if err = b.gitClient.Push(ctx, repository.PushConfig{}); err != nil { + return fmt.Errorf("failed to push manifests: %w", err) + } + } else { + b.logger.Successf("component manifests are up to date") + } + + // Conditionally install manifests + if mustInstallManifests(ctx, b.kube, options.Namespace) { + b.logger.Actionf("installing components in %q namespace", options.Namespace) + + componentsYAML := filepath.Join(b.gitClient.Path(), manifests.Path) + kfile := filepath.Join(filepath.Dir(componentsYAML), konfig.DefaultKustomizationFileName()) + if _, err := os.Stat(kfile); err == nil { + // Apply the components and their patches + if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.gitClient.Path(), kfile); err != nil { + return err + } + } else { + // Apply the CRDs and controllers + if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.gitClient.Path(), componentsYAML); err != nil { + return err + } + } + b.logger.Successf("installed components") + } + + // Reconcile image pull secret if needed + if options.ImagePullSecret != "" && options.RegistryCredential != "" { + if err := reconcileImagePullSecret(ctx, b.kube, options); err != nil { + return fmt.Errorf("failed to reconcile image pull secret: %w", err) + } + b.logger.Successf("reconciled image pull secret %s", options.ImagePullSecret) + } + + b.logger.Successf("reconciled components") + return nil +} + +func (b *PlainGitBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error { + // Determine if there is an existing secret + secretKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace} + b.logger.Actionf("determining if source secret %q exists", secretKey) + ok, err := secretExists(ctx, b.kube, secretKey) + if err != nil { + return fmt.Errorf("failed to determine if deploy key secret exists: %w", err) + } + + // Return early if exists and no custom config is passed + if ok && options.Keypair == nil && len(options.CAFile) == 0 && len(options.Username+options.Password) == 0 { + b.logger.Successf("source secret up to date") + return nil + } + + // Generate source secret + b.logger.Actionf("generating source secret") + manifest, err := sourcesecret.Generate(options) + if err != nil { + return err + } + var secret corev1.Secret + if err := yaml.Unmarshal([]byte(manifest.Content), &secret); err != nil { + return fmt.Errorf("failed to unmarshal generated source secret manifest: %w", err) + } + + for _, callback := range b.postGenerateSecret { + if err = callback(ctx, secret, options); err != nil { + return err + } + } + + // Apply source secret + b.logger.Actionf("applying source secret %q", secretKey) + if err = reconcileSecret(ctx, b.kube, secret); err != nil { + return err + } + b.logger.Successf("reconciled source secret") + + return nil +} + +func (b *PlainGitBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error { + // Confirm that sync configuration does not overwrite existing config + if curPath, err := kustomizationPathDiffers(ctx, b.kube, client.ObjectKey{Name: options.Name, Namespace: options.Namespace}, options.TargetPath); err != nil { + return fmt.Errorf("failed to determine if sync configuration would overwrite existing Kustomization: %w", err) + } else if curPath != "" { + return fmt.Errorf("sync path configuration (%q) would overwrite path (%q) of existing Kustomization", options.TargetPath, curPath) + } + + // Clone if not already + if _, err := b.gitClient.Head(); err != nil { + if err == git.ErrNoGitRepository { + b.logger.Actionf("cloning branch %q from Git repository %q", b.branch, b.url) + var cloned bool + if err = retry(1, 2*time.Second, func() (err error) { + if err = b.cleanGitRepoDir(); err != nil { + b.logger.Warningf(" failed to clean directory for git repo: %w", err) + return + } + _, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{ + CheckoutStrategy: repository.CheckoutStrategy{ + Branch: b.branch, + }, + }) + if err != nil { + b.logger.Warningf(" clone failure: %s", err) + } + if err == nil { + cloned = true + } + return + }); err != nil { + return fmt.Errorf("failed to clone repository: %w", err) + } + if cloned { + b.logger.Successf("cloned repository") + } + } + return err + } + + // Generate sync manifests and write to Git repository + b.logger.Actionf("generating sync manifests") + manifests, err := sync.Generate(options) + if err != nil { + return fmt.Errorf("sync manifests generation failed: %w", err) + } + + // Create secure Kustomize FS + fs, err := filesys.MakeFsOnDiskSecureBuild(b.gitClient.Path()) + if err != nil { + return fmt.Errorf("failed to initialize Kustomize file system: %w", err) + } + + if err = fs.WriteFile(filepath.Join(b.gitClient.Path(), manifests.Path), []byte(manifests.Content)); err != nil { + return err + } + + // Generate Kustomization + kusManifests, err := kustomization.Generate(kustomization.Options{ + FileSystem: fs, + BaseDir: b.gitClient.Path(), + TargetPath: filepath.Dir(manifests.Path), + }) + if err != nil { + return fmt.Errorf("%s generation failed: %w", konfig.DefaultKustomizationFileName(), err) + } + b.logger.Successf("generated sync manifests") + + // Write generated files and make a commit + var signer *openpgp.Entity + if b.gpgKeyRing != nil { + signer, err = getOpenPgpEntity(b.gpgKeyRing, b.gpgPassphrase, b.gpgKeyID) + if err != nil { + return fmt.Errorf("failed to generate OpenPGP entity: %w", err) + } + } + commitMsg := "Add Flux sync manifests" + if b.commitMessageAppendix != "" { + commitMsg = commitMsg + "\n\n" + b.commitMessageAppendix + } + + commit, err := b.gitClient.Commit(git.Commit{ + Author: b.signature, + Message: commitMsg, + }, repository.WithFiles(map[string]io.Reader{ + kusManifests.Path: strings.NewReader(kusManifests.Content), + }), repository.WithSigner(signer)) + if err != nil && err != git.ErrNoStagedFiles { + return fmt.Errorf("failed to commit sync manifests: %w", err) + } + + if err == nil { + b.logger.Successf("committed sync manifests to %q (%q)", b.branch, commit) + b.logger.Actionf("pushing sync manifests to %q", b.url) + err = b.gitClient.Push(ctx, repository.PushConfig{}) + if err != nil { + if strings.HasPrefix(err.Error(), gogit.ErrNonFastForwardUpdate.Error()) { + b.logger.Waitingf("git conflict detected, retrying with a fresh clone") + if err := os.RemoveAll(b.gitClient.Path()); err != nil { + return fmt.Errorf("failed to remove tmp dir: %w", err) + } + if err := os.Mkdir(b.gitClient.Path(), 0o700); err != nil { + return fmt.Errorf("failed to recreate tmp dir: %w", err) + } + if err = retry(1, 2*time.Second, func() (err error) { + if err = b.cleanGitRepoDir(); err != nil { + b.logger.Warningf(" failed to clean directory for git repo: %w", err) + return + } + _, err = b.gitClient.Clone(ctx, b.url, repository.CloneConfig{ + CheckoutStrategy: repository.CheckoutStrategy{ + Branch: b.branch, + }, + }) + if err != nil { + b.logger.Warningf(" clone failure: %s", err) + } + return + }); err != nil { + return fmt.Errorf("failed to clone repository: %w", err) + } + return b.ReconcileSyncConfig(ctx, options) + } + return fmt.Errorf("failed to push sync manifests: %w", err) + } + } else { + b.logger.Successf("sync manifests are up to date") + } + + // Apply to cluster + b.logger.Actionf("applying sync manifests") + if _, err := utils.Apply(ctx, b.restClientGetter, b.restClientOptions, b.gitClient.Path(), filepath.Join(b.gitClient.Path(), kusManifests.Path)); err != nil { + return err + } + + b.logger.Successf("reconciled sync configuration") + + return nil +} + +func (b *PlainGitBootstrapper) ReportKustomizationHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error { + head, err := b.gitClient.Head() + if err != nil { + return err + } + + objKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace} + + expectRevision := fmt.Sprintf("%s@%s", options.Branch, git.Hash(head).Digest()) + b.logger.Waitingf("waiting for Kustomization %q to be reconciled", objKey.String()) + k := &kustomizev1.Kustomization{ + TypeMeta: metav1.TypeMeta{ + Kind: kustomizev1.KustomizationKind, + }, + } + if err := wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true, + objectReconciled(b.kube, objKey, k, expectRevision)); err != nil { + // If the poll timed out, we want to log the ready condition message as + // that likely contains the reason + if errors.Is(err, context.DeadlineExceeded) { + readyCondition := apimeta.FindStatusCondition(k.Status.Conditions, meta.ReadyCondition) + if readyCondition != nil && readyCondition.Status != metav1.ConditionTrue { + err = fmt.Errorf("kustomization '%s' not ready: '%s'", objKey, readyCondition.Message) + } + } + b.logger.Failuref(err.Error()) + return fmt.Errorf("error while waiting for Kustomization to be ready: '%s'", err) + } + b.logger.Successf("Kustomization reconciled successfully") + return nil +} + +func (b *PlainGitBootstrapper) ReportGitRepoHealth(ctx context.Context, options sync.Options, pollInterval, timeout time.Duration) error { + head, err := b.gitClient.Head() + if err != nil { + return err + } + + objKey := client.ObjectKey{Name: options.Name, Namespace: options.Namespace} + + b.logger.Waitingf("waiting for GitRepository %q to be reconciled", objKey.String()) + expectRevision := fmt.Sprintf("%s@%s", options.Branch, git.Hash(head).Digest()) + g := &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + } + if err := wait.PollUntilContextTimeout(ctx, pollInterval, timeout, true, + objectReconciled(b.kube, objKey, g, expectRevision)); err != nil { + // If the poll timed out, we want to log the ready condition message as + // that likely contains the reason + if errors.Is(err, context.DeadlineExceeded) { + readyCondition := apimeta.FindStatusCondition(g.Status.Conditions, meta.ReadyCondition) + if readyCondition != nil && readyCondition.Status != metav1.ConditionTrue { + err = fmt.Errorf("gitrepository '%s' not ready: '%s'", objKey, readyCondition.Message) + } + } + b.logger.Failuref(err.Error()) + return fmt.Errorf("error while waiting for GitRepository to be ready: '%s'", err) + } + b.logger.Successf("GitRepository reconciled successfully") + return nil +} +func (b *PlainGitBootstrapper) ReportComponentsHealth(ctx context.Context, install install.Options, timeout time.Duration) error { + cfg, err := utils.KubeConfig(b.restClientGetter, b.restClientOptions) + if err != nil { + return err + } + + checker, err := status.NewStatusChecker(cfg, 5*time.Second, timeout, b.logger) + if err != nil { + return err + } + + var components = install.Components + components = append(components, install.ComponentsExtra...) + + var identifiers []object.ObjMetadata + for _, component := range components { + identifiers = append(identifiers, object.ObjMetadata{ + Namespace: install.Namespace, + Name: component, + GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, + }) + } + + b.logger.Actionf("confirming components are healthy") + if err := checker.Assess(identifiers...); err != nil { + return err + } + b.logger.Successf("all components are healthy") + return nil +} + +// cleanGitRepoDir cleans the directory meant for the Git repo. +func (b *PlainGitBootstrapper) cleanGitRepoDir() error { + dirs, err := os.ReadDir(b.gitClient.Path()) + if err != nil { + return err + } + var errs []error + for _, dir := range dirs { + if err := os.RemoveAll(filepath.Join(b.gitClient.Path(), dir.Name())); err != nil { + errs = append(errs, err) + } + } + return errors.Join(errs...) +} + +func getOpenPgpEntity(keyRing openpgp.EntityList, passphrase, keyID string) (*openpgp.Entity, error) { + if len(keyRing) == 0 { + return nil, fmt.Errorf("empty GPG key ring") + } + + var entity *openpgp.Entity + if keyID != "" { + keyID = strings.TrimPrefix(keyID, "0x") + if len(keyID) != 16 { + return nil, fmt.Errorf("invalid GPG key id length; expected %d, got %d", 16, len(keyID)) + } + keyID = strings.ToUpper(keyID) + + for _, ent := range keyRing { + if ent.PrimaryKey.KeyIdString() == keyID { + entity = ent + } + } + + if entity == nil { + return nil, fmt.Errorf("no GPG keyring matching key id '%s' found", keyID) + } + if entity.PrivateKey == nil { + return nil, fmt.Errorf("keyring does not contain private key for key id '%s'", keyID) + } + } else { + entity = keyRing[0] + } + + err := entity.PrivateKey.Decrypt([]byte(passphrase)) + if err != nil { + return nil, fmt.Errorf("unable to decrypt GPG private key: %w", err) + } + + return entity, nil +} diff --git a/internal/bootstrap/bootstrap_provider.go b/pkg/bootstrap/bootstrap_provider.go similarity index 85% rename from internal/bootstrap/bootstrap_provider.go rename to pkg/bootstrap/bootstrap_provider.go index dd23f75f..265ab0c1 100644 --- a/internal/bootstrap/bootstrap_provider.go +++ b/pkg/bootstrap/bootstrap_provider.go @@ -29,10 +29,10 @@ import ( "github.com/fluxcd/go-git-providers/gitprovider" - "github.com/fluxcd/flux2/internal/bootstrap/git" - "github.com/fluxcd/flux2/internal/bootstrap/provider" - "github.com/fluxcd/flux2/pkg/manifestgen/sourcesecret" - "github.com/fluxcd/flux2/pkg/manifestgen/sync" + "github.com/fluxcd/flux2/v2/pkg/bootstrap/provider" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sourcesecret" + "github.com/fluxcd/flux2/v2/pkg/manifestgen/sync" + "github.com/fluxcd/pkg/git/repository" ) type GitProviderBootstrapper struct { @@ -59,14 +59,17 @@ type GitProviderBootstrapper struct { sshHostname string + useDeployTokenAuth bool + provider gitprovider.Client } -func NewGitProviderBootstrapper(git git.Git, provider gitprovider.Client, kube client.Client, opts ...GitProviderOption) (*GitProviderBootstrapper, error) { +func NewGitProviderBootstrapper(git repository.Client, provider gitprovider.Client, + kube client.Client, opts ...GitProviderOption) (*GitProviderBootstrapper, error) { b := &GitProviderBootstrapper{ PlainGitBootstrapper: &PlainGitBootstrapper{ - git: git, - kube: kube, + gitClient: git, + kube: kube, }, bootstrapTransportType: "https", syncTransportType: "ssh", @@ -183,6 +186,16 @@ func (o reconcileOption) applyGitProvider(b *GitProviderBootstrapper) { b.reconcile = true } +func WithDeployTokenAuth() GitProviderOption { + return deployTokenAuthOption(true) +} + +type deployTokenAuthOption bool + +func (o deployTokenAuthOption) applyGitProvider(b *GitProviderBootstrapper) { + b.useDeployTokenAuth = true +} + func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, options sync.Options) error { if b.repository == nil { return errors.New("repository is required") @@ -207,6 +220,26 @@ func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, optio return b.PlainGitBootstrapper.ReconcileSyncConfig(ctx, options) } +func (b *GitProviderBootstrapper) ReconcileSourceSecret(ctx context.Context, options sourcesecret.Options) error { + if b.repository == nil { + return errors.New("repository is required") + } + + if b.useDeployTokenAuth { + deployTokenInfo, err := b.reconcileDeployToken(ctx, options) + if err != nil { + return err + } + + if deployTokenInfo != nil { + options.Username = deployTokenInfo.Username + options.Password = deployTokenInfo.Token + } + } + + return b.PlainGitBootstrapper.ReconcileSourceSecret(ctx, options) +} + // ReconcileRepository reconciles an organization or user repository with the // GitProviderBootstrapper configuration. On success, the URL in the embedded // PlainGitBootstrapper is set to clone URL for the configured protocol. @@ -260,6 +293,32 @@ func (b *GitProviderBootstrapper) reconcileDeployKey(ctx context.Context, secret return nil } +func (b *GitProviderBootstrapper) reconcileDeployToken(ctx context.Context, options sourcesecret.Options) (*gitprovider.DeployTokenInfo, error) { + dts, err := b.repository.DeployTokens() + if err != nil { + return nil, err + } + + b.logger.Actionf("checking to reconcile deploy token for source secret") + name := deployTokenName(options.Namespace, b.branch, options.Name, options.TargetPath) + deployTokenInfo := gitprovider.DeployTokenInfo{Name: name} + + deployToken, changed, err := dts.Reconcile(ctx, deployTokenInfo) + if err != nil { + return nil, err + } + + if changed { + b.logger.Successf("configured deploy token %q for %q", deployTokenInfo.Name, b.repository.Repository().String()) + deployTokenInfo := deployToken.Get() + return &deployTokenInfo, nil + } + + b.logger.Successf("reconciled deploy token for source secret") + + return nil, nil +} + // reconcileOrgRepository reconciles a gitprovider.OrgRepository // with the GitProviderBootstrapper values, including any // gitprovider.TeamAccessInfo configurations. @@ -275,7 +334,7 @@ func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (g subOrgs, repoName := splitSubOrganizationsFromRepositoryName(b.repositoryName) orgRef, err := b.getOrganization(ctx, subOrgs) if err != nil { - return nil, fmt.Errorf("failed to create new Git repository for the organization %q: %w", orgRef.String(), err) + return nil, fmt.Errorf("failed to create new Git repository %q: %w", b.repositoryName, err) } repoRef := newOrgRepositoryRef(*orgRef, repoName) repoInfo := newRepositoryInfo(b.description, b.defaultBranch, b.visibility) @@ -284,7 +343,7 @@ func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (g repo, err := b.provider.OrgRepositories().Get(ctx, repoRef) if err != nil { if !errors.Is(err, gitprovider.ErrNotFound) { - return nil, fmt.Errorf("failed to get Git repository %q: %w", repoRef.String(), err) + return nil, fmt.Errorf("failed to get Git repository %q: provider error: %w", repoRef.String(), err) } // go-git-providers has at present some issues with the idempotency // of the available Reconcile methods, and setting e.g. the default @@ -298,11 +357,8 @@ func (b *GitProviderBootstrapper) reconcileOrgRepository(ctx context.Context) (g var changed bool if b.reconcile { - // Set default branch before calling Reconcile due to bug described - // above. - repoInfo.DefaultBranch = repo.Get().DefaultBranch if err = retry(1, 2*time.Second, func() (err error) { - repo, changed, err = b.provider.OrgRepositories().Reconcile(ctx, repoRef, repoInfo) + changed, err = repo.Reconcile(ctx) return }); err != nil { return nil, fmt.Errorf("failed to reconcile Git repository %q: %w", repoRef.String(), err) @@ -360,25 +416,27 @@ func (b *GitProviderBootstrapper) reconcileUserRepository(ctx context.Context) ( repo, err := b.provider.UserRepositories().Get(ctx, repoRef) if err != nil { if !errors.Is(err, gitprovider.ErrNotFound) { - return nil, fmt.Errorf("failed to get Git repository %q: %w", repoRef.String(), err) + return nil, fmt.Errorf("failed to get Git repository %q: provider error: %w", repoRef.String(), err) } // go-git-providers has at present some issues with the idempotency // of the available Reconcile methods, and setting e.g. the default // branch correctly. Resort to Create until this has been resolved. repo, err = b.provider.UserRepositories().Create(ctx, repoRef, repoInfo) if err != nil { + var userErr *gitprovider.ErrIncorrectUser + if errors.As(err, &userErr) { + // return a better error message when the wrong owner is set + err = fmt.Errorf("the specified owner '%s' doesn't match the identity associated with the given token", b.owner) + } return nil, fmt.Errorf("failed to create new Git repository %q: %w", repoRef.String(), err) } b.logger.Successf("repository %q created", repoRef.String()) } if b.reconcile { - // Set default branch before calling Reconcile due to bug described - // above. - repoInfo.DefaultBranch = repo.Get().DefaultBranch var changed bool if err = retry(1, 2*time.Second, func() (err error) { - repo, changed, err = b.provider.UserRepositories().Reconcile(ctx, repoRef, repoInfo) + changed, err = repo.Reconcile(ctx) return }); err != nil { return nil, fmt.Errorf("failed to reconcile Git repository %q: %w", repoRef.String(), err) @@ -559,6 +617,17 @@ func deployKeyName(namespace, secretName, branch, path string) string { return name } +func deployTokenName(namespace, secretName, branch, path string) string { + var elems []string + for _, v := range []string{namespace, secretName, branch, path} { + if v == "" { + continue + } + elems = append(elems, v) + } + return strings.Join(elems, "-") +} + // setHostname is a helper to replace the hostname of the given URL. // TODO(hidde): support for this should be added in go-git-providers. func setHostname(URL, hostname string) (string, error) { diff --git a/pkg/bootstrap/bootstrap_test.go b/pkg/bootstrap/bootstrap_test.go new file mode 100644 index 00000000..47bf07f9 --- /dev/null +++ b/pkg/bootstrap/bootstrap_test.go @@ -0,0 +1,469 @@ +/* +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 bootstrap + +import ( + "context" + "testing" + + "github.com/fluxcd/pkg/apis/meta" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/fluxcd/flux2/v2/internal/utils" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" +) + +func Test_hasRevision(t *testing.T) { + var revision = "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a" + + tests := []struct { + name string + obj objectWithConditions + rev string + expectErr bool + expectedBool bool + }{ + { + name: "Kustomization revision", + obj: &kustomizev1.Kustomization{ + TypeMeta: metav1.TypeMeta{ + Kind: kustomizev1.KustomizationKind, + }, + Status: kustomizev1.KustomizationStatus{ + LastAttemptedRevision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a", + }, + }, + expectedBool: true, + }, + { + name: "GitRepository revision", + obj: &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + Status: sourcev1.GitRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Revision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a", + }, + }, + }, + expectedBool: true, + }, + { + name: "GitRepository revision (wrong revision)", + obj: &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + Status: sourcev1.GitRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Revision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a", + }, + }, + }, + }, + { + name: "Kustomization revision (empty revision)", + obj: &kustomizev1.Kustomization{ + TypeMeta: metav1.TypeMeta{ + Kind: kustomizev1.KustomizationKind, + }, + Status: kustomizev1.KustomizationStatus{ + LastAttemptedRevision: "", + }, + }, + }, + { + name: "OCIRepository revision", + obj: &sourcev1b2.OCIRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1b2.OCIRepositoryKind, + }, + Status: sourcev1b2.OCIRepositoryStatus{ + Artifact: &sourcev1.Artifact{ + Revision: "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a", + }, + }, + }, + expectedBool: true, + }, + { + name: "Alert revision(Not supported)", + obj: ¬ificationv1.Alert{ + TypeMeta: metav1.TypeMeta{ + Kind: notificationv1.AlertKind, + }, + Status: notificationv1.AlertStatus{ + ObservedGeneration: 1, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tt.obj) + g.Expect(err).To(BeNil()) + got, err := hasRevision(tt.obj.GetObjectKind().GroupVersionKind().Kind, obj, revision) + if tt.expectErr { + g.Expect(err).To(HaveOccurred()) + return + } + g.Expect(got).To(Equal(tt.expectedBool)) + }) + } +} + +func Test_objectReconciled(t *testing.T) { + expectedRev := "main@sha1:5bf3a8f9bb0aa5ae8afd6208f43757ab73fc033a" + + type updateStatus struct { + statusFn func(o client.Object) + expectedErr bool + expectedBool bool + } + tests := []struct { + name string + obj objectWithConditions + statuses []updateStatus + }{ + { + name: "GitRepository with no status", + obj: &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + }, + }, + statuses: []updateStatus{ + { + expectedErr: false, + expectedBool: false, + }, + }, + }, + { + name: "suspended Kustomization", + obj: &kustomizev1.Kustomization{ + TypeMeta: metav1.TypeMeta{ + Kind: kustomizev1.KustomizationKind, + APIVersion: kustomizev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + }, + Spec: kustomizev1.KustomizationSpec{ + Suspend: true, + }, + }, + statuses: []updateStatus{ + { + expectedErr: true, + }, + }, + }, + { + name: "Kustomization - status with old generation", + obj: &kustomizev1.Kustomization{ + TypeMeta: metav1.TypeMeta{ + Kind: kustomizev1.KustomizationKind, + APIVersion: kustomizev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + Generation: 1, + }, + Status: kustomizev1.KustomizationStatus{ + ObservedGeneration: -1, + }, + }, + statuses: []updateStatus{ + { + expectedErr: false, + expectedBool: false, + }, + }, + }, + { + name: "GitRepository - status with same generation but no conditions", + obj: &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + Generation: 1, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedGeneration: 1, + }, + }, + statuses: []updateStatus{ + { + expectedErr: false, + expectedBool: false, + }, + }, + }, + { + name: "GitRepository - status with conditions but no ready condition", + obj: &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + Generation: 1, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + {Type: meta.ReconcilingCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Progressing", Message: "Progressing"}, + }, + }, + }, + statuses: []updateStatus{ + { + expectedErr: false, + expectedBool: false, + }, + }, + }, + { + name: "Kustomization - status with false ready condition", + obj: &kustomizev1.Kustomization{ + TypeMeta: metav1.TypeMeta{ + Kind: kustomizev1.KustomizationKind, + APIVersion: kustomizev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + Generation: 1, + }, + Status: kustomizev1.KustomizationStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + {Type: meta.ReadyCondition, Status: metav1.ConditionFalse, ObservedGeneration: 1, Reason: "Failing", Message: "Failed to clone"}, + }, + }, + }, + statuses: []updateStatus{ + { + expectedErr: true, + expectedBool: false, + }, + }, + }, + { + name: "Kustomization - status with true ready condition but different revision", + obj: &kustomizev1.Kustomization{ + TypeMeta: metav1.TypeMeta{ + Kind: kustomizev1.KustomizationKind, + APIVersion: kustomizev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + Generation: 1, + }, + Status: kustomizev1.KustomizationStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Passing", Message: "Applied revision"}, + }, + LastAttemptedRevision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a", + }, + }, + statuses: []updateStatus{ + { + expectedErr: false, + expectedBool: false, + }, + }, + }, + { + name: "GitRepository - status with true ready condition but different revision", + obj: &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + Generation: 1, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"}, + }, + Artifact: &sourcev1.Artifact{ + Revision: "main@sha1:e7f3a8f9bb0aa5ae8afd6208f43757ab73fc043a", + }, + }, + }, + statuses: []updateStatus{ + { + expectedErr: false, + expectedBool: false, + }, + }, + }, + { + name: "GitRepository - ready with right revision", + obj: &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + Generation: 1, + }, + Status: sourcev1.GitRepositoryStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"}, + }, + Artifact: &sourcev1.Artifact{ + Revision: expectedRev, + }, + }, + }, + statuses: []updateStatus{ + { + expectedErr: false, + expectedBool: true, + }, + }, + }, + { + name: "GitRepository - sequence of status updates before ready", + obj: &sourcev1.GitRepository{ + TypeMeta: metav1.TypeMeta{ + Kind: sourcev1.GitRepositoryKind, + APIVersion: sourcev1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + Namespace: "flux-system", + Generation: 1, + }, + }, + statuses: []updateStatus{ + { + // observed gen different + statusFn: func(o client.Object) { + gitRepo := o.(*sourcev1.GitRepository) + gitRepo.Status = sourcev1.GitRepositoryStatus{ + ObservedGeneration: -1, + } + }, + }, + { + // ready failing + statusFn: func(o client.Object) { + gitRepo := o.(*sourcev1.GitRepository) + gitRepo.Status = sourcev1.GitRepositoryStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + {Type: meta.ReadyCondition, Status: metav1.ConditionFalse, ObservedGeneration: 1, Reason: "Not Ready", Message: "Transient connection issue"}, + }, + } + }, + expectedErr: true, + }, + { + // updated to a different revision + statusFn: func(o client.Object) { + gitRepo := o.(*sourcev1.GitRepository) + gitRepo.Status = sourcev1.GitRepositoryStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"}, + }, + Artifact: &sourcev1.Artifact{ + Revision: "wrong rev", + }, + } + }, + }, + { + // updated to the expected revision + statusFn: func(o client.Object) { + gitRepo := o.(*sourcev1.GitRepository) + gitRepo.Status = sourcev1.GitRepositoryStatus{ + ObservedGeneration: 1, + Conditions: []metav1.Condition{ + {Type: meta.ReadyCondition, Status: metav1.ConditionTrue, ObservedGeneration: 1, Reason: "Readyyy", Message: "Cloned successfully"}, + }, + Artifact: &sourcev1.Artifact{ + Revision: expectedRev, + }, + } + }, + expectedBool: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + builder := fake.NewClientBuilder().WithScheme(utils.NewScheme()) + builder.WithObjects(tt.obj) + + kubeClient := builder.Build() + + for _, updates := range tt.statuses { + if updates.statusFn != nil { + updates.statusFn(tt.obj) + g.Expect(kubeClient.Update(context.TODO(), tt.obj)).To(Succeed()) + } + + waitFunc := objectReconciled(kubeClient, client.ObjectKeyFromObject(tt.obj), tt.obj, expectedRev) + got, err := waitFunc(context.TODO()) + g.Expect(err != nil).To(Equal(updates.expectedErr)) + g.Expect(got).To(Equal(updates.expectedBool)) + } + }) + } +} diff --git a/internal/bootstrap/options.go b/pkg/bootstrap/options.go similarity index 64% rename from internal/bootstrap/options.go rename to pkg/bootstrap/options.go index 6d79055c..27830f4a 100644 --- a/internal/bootstrap/options.go +++ b/pkg/bootstrap/options.go @@ -17,9 +17,16 @@ limitations under the License. package bootstrap import ( - "github.com/fluxcd/flux2/internal/bootstrap/git" - "github.com/fluxcd/flux2/pkg/log" + "fmt" + "os" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/fluxcd/pkg/git" + runclient "github.com/fluxcd/pkg/runtime/client" + + "github.com/fluxcd/flux2/v2/pkg/log" ) type Option interface { @@ -41,42 +48,28 @@ func (o branchOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } -func WithAuthor(name, email string) Option { - return authorOption{ +func WithSignature(name, email string) Option { + return signatureOption{ Name: name, Email: email, } } -type authorOption git.Author +type signatureOption git.Signature -func (o authorOption) applyGit(b *PlainGitBootstrapper) { +func (o signatureOption) applyGit(b *PlainGitBootstrapper) { if o.Name != "" { - b.author.Name = o.Name + b.signature.Name = o.Name } if o.Email != "" { - b.author.Email = o.Email + b.signature.Email = o.Email } } -func (o authorOption) applyGitProvider(b *GitProviderBootstrapper) { +func (o signatureOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } -func WithCABundle(b []byte) Option { - return caBundleOption(b) -} - -type caBundleOption []byte - -func (o caBundleOption) applyGit(b *PlainGitBootstrapper) { - b.caBundle = o -} - -func (o caBundleOption) applyGitProvider(b *GitProviderBootstrapper) { - b.caBundle = o -} - func WithCommitMessageAppendix(appendix string) Option { return commitMessageAppendixOption(appendix) } @@ -91,18 +84,21 @@ func (o commitMessageAppendixOption) applyGitProvider(b *GitProviderBootstrapper o.applyGit(b.PlainGitBootstrapper) } -func WithKubeconfig(rcg genericclioptions.RESTClientGetter) Option { +func WithKubeconfig(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) Option { return kubeconfigOption{ - rcg: rcg, + rcg: rcg, + opts: opts, } } type kubeconfigOption struct { - rcg genericclioptions.RESTClientGetter + rcg genericclioptions.RESTClientGetter + opts *runclient.Options } func (o kubeconfigOption) applyGit(b *PlainGitBootstrapper) { b.restClientGetter = o.rcg + b.restClientOptions = o.opts } func (o kubeconfigOption) applyGitProvider(b *GitProviderBootstrapper) { @@ -125,22 +121,22 @@ func (o loggerOption) applyGitProvider(b *GitProviderBootstrapper) { b.logger = o.logger } -func WithGitCommitSigning(path, passphrase, keyID string) Option { +func WithGitCommitSigning(gpgKeyRing openpgp.EntityList, passphrase, keyID string) Option { return gitCommitSigningOption{ - gpgKeyRingPath: path, - gpgPassphrase: passphrase, - gpgKeyID: keyID, + gpgKeyRing: gpgKeyRing, + gpgPassphrase: passphrase, + gpgKeyID: keyID, } } type gitCommitSigningOption struct { - gpgKeyRingPath string - gpgPassphrase string - gpgKeyID string + gpgKeyRing openpgp.EntityList + gpgPassphrase string + gpgKeyID string } func (o gitCommitSigningOption) applyGit(b *PlainGitBootstrapper) { - b.gpgKeyRingPath = o.gpgKeyRingPath + b.gpgKeyRing = o.gpgKeyRing b.gpgPassphrase = o.gpgPassphrase b.gpgKeyID = o.gpgKeyID } @@ -148,3 +144,18 @@ func (o gitCommitSigningOption) applyGit(b *PlainGitBootstrapper) { func (o gitCommitSigningOption) applyGitProvider(b *GitProviderBootstrapper) { o.applyGit(b.PlainGitBootstrapper) } + +func LoadEntityListFromPath(path string) (openpgp.EntityList, error) { + if path == "" { + return nil, nil + } + r, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("unable to open GPG key ring: %w", err) + } + entityList, err := openpgp.ReadKeyRing(r) + if err != nil { + return nil, fmt.Errorf("unable to read GPG key ring: %w", err) + } + return entityList, nil +} diff --git a/internal/bootstrap/provider/factory.go b/pkg/bootstrap/provider/factory.go similarity index 85% rename from internal/bootstrap/provider/factory.go rename to pkg/bootstrap/provider/factory.go index 575cb551..bd490334 100644 --- a/internal/bootstrap/provider/factory.go +++ b/pkg/bootstrap/provider/factory.go @@ -19,6 +19,7 @@ package provider import ( "fmt" + "github.com/fluxcd/go-git-providers/gitea" "github.com/fluxcd/go-git-providers/github" "github.com/fluxcd/go-git-providers/gitlab" "github.com/fluxcd/go-git-providers/gitprovider" @@ -45,6 +46,17 @@ func BuildGitProvider(config Config) (gitprovider.Client, error) { if client, err = github.NewClient(opts...); err != nil { return nil, err } + case GitProviderGitea: + opts := []gitprovider.ClientOption{} + if config.Hostname != "" { + opts = append(opts, gitprovider.WithDomain(config.Hostname)) + } + if config.CaBundle != nil { + opts = append(opts, gitprovider.WithCustomCAPostChainTransportHook(config.CaBundle)) + } + if client, err = gitea.NewClient(config.Token, opts...); err != nil { + return nil, err + } case GitProviderGitLab: opts := []gitprovider.ClientOption{ gitprovider.WithConditionalRequests(true), diff --git a/internal/bootstrap/provider/provider.go b/pkg/bootstrap/provider/provider.go similarity index 96% rename from internal/bootstrap/provider/provider.go rename to pkg/bootstrap/provider/provider.go index 4d1f92ac..f458184b 100644 --- a/internal/bootstrap/provider/provider.go +++ b/pkg/bootstrap/provider/provider.go @@ -21,6 +21,7 @@ type GitProvider string const ( GitProviderGitHub GitProvider = "github" + GitProviderGitea GitProvider = "gitea" GitProviderGitLab GitProvider = "gitlab" GitProviderStash GitProvider = "stash" ) diff --git a/pkg/log/nop.go b/pkg/log/nop.go new file mode 100644 index 00000000..576c78f0 --- /dev/null +++ b/pkg/log/nop.go @@ -0,0 +1,31 @@ +/* +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 log + +type NopLogger struct{} + +func (NopLogger) Actionf(format string, a ...interface{}) {} + +func (NopLogger) Generatef(format string, a ...interface{}) {} + +func (NopLogger) Waitingf(format string, a ...interface{}) {} + +func (NopLogger) Successf(format string, a ...interface{}) {} + +func (NopLogger) Warningf(format string, a ...interface{}) {} + +func (NopLogger) Failuref(format string, a ...interface{}) {} diff --git a/pkg/manifestgen/install/install.go b/pkg/manifestgen/install/install.go index ce6d1e19..594997c2 100644 --- a/pkg/manifestgen/install/install.go +++ b/pkg/manifestgen/install/install.go @@ -27,8 +27,9 @@ import ( "time" securejoin "github.com/cyphar/filepath-securejoin" + "github.com/hashicorp/go-cleanhttp" - "github.com/fluxcd/flux2/pkg/manifestgen" + "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) // Generate returns the install manifests as a multi-doc YAML. @@ -54,7 +55,7 @@ func Generate(options Options, manifestsBase string) (*manifestgen.Manifest, err } else { // download the manifests base from GitHub if manifestsBase == "" { - manifestsBase, err = os.MkdirTemp("", options.Namespace) + manifestsBase, err = manifestgen.MkdirTempAbs("", options.Namespace) if err != nil { return nil, fmt.Errorf("temp dir error: %w", err) } @@ -91,7 +92,7 @@ func Generate(options Options, manifestsBase string) (*manifestgen.Manifest, err // GetLatestVersion calls the GitHub API and returns the latest released version. func GetLatestVersion() (string, error) { ghURL := "https://api.github.com/repos/fluxcd/flux2/releases/latest" - c := http.DefaultClient + c := cleanhttp.DefaultClient() c.Timeout = 15 * time.Second res, err := c.Get(ghURL) @@ -121,7 +122,7 @@ func ExistingVersion(version string) (bool, error) { } ghURL := fmt.Sprintf("https://api.github.com/repos/fluxcd/flux2/releases/tags/%s", version) - c := http.DefaultClient + c := cleanhttp.DefaultClient() c.Timeout = 15 * time.Second res, err := c.Get(ghURL) diff --git a/pkg/manifestgen/install/manifests.go b/pkg/manifestgen/install/manifests.go index 5089074f..e48eac0a 100644 --- a/pkg/manifestgen/install/manifests.go +++ b/pkg/manifestgen/install/manifests.go @@ -26,10 +26,12 @@ import ( "path/filepath" "strings" - "github.com/fluxcd/pkg/untar" - "sigs.k8s.io/kustomize/api/filesys" + "github.com/hashicorp/go-cleanhttp" - "github.com/fluxcd/flux2/pkg/manifestgen/kustomization" + "github.com/fluxcd/pkg/kustomize/filesys" + "github.com/fluxcd/pkg/tar" + + "github.com/fluxcd/flux2/v2/pkg/manifestgen/kustomization" ) func fetch(ctx context.Context, url, version, dir string) error { @@ -44,7 +46,7 @@ func fetch(ctx context.Context, url, version, dir string) error { } // download - resp, err := http.DefaultClient.Do(req.WithContext(ctx)) + resp, err := cleanhttp.DefaultClient().Do(req.WithContext(ctx)) if err != nil { return fmt.Errorf("failed to download manifests.tar.gz from %s, error: %w", ghURL, err) } @@ -56,7 +58,7 @@ func fetch(ctx context.Context, url, version, dir string) error { } // extract - if _, err = untar.Untar(resp.Body, dir); err != nil { + if err = tar.Untar(resp.Body, dir, tar.WithMaxUntarSize(-1)); err != nil { return fmt.Errorf("failed to untar manifests.tar.gz from %s, error: %w", ghURL, err) } @@ -71,9 +73,9 @@ func generate(base string, options Options) error { // In such environments they normally add `.cluster.local` and `.local` // suffixes to `no_proxy` variable in order to prevent cluster-local // traffic from going through http proxy. Without fully specified - // domain they need to mention `notifications-controller` explicity in + // domain they need to mention `notifications-controller` explicitly in // `no_proxy` variable after debugging http proxy logs. - options.EventsAddr = fmt.Sprintf("http://%s.%s.svc.%s/", options.NotificationController, options.Namespace, options.ClusterDomain) + options.EventsAddr = fmt.Sprintf("http://%s.%s.svc.%s./", options.NotificationController, options.Namespace, options.ClusterDomain) } if err := execTemplate(options, namespaceTmpl, path.Join(base, "namespace.yaml")); err != nil { @@ -126,8 +128,12 @@ func build(base, output string) error { return err } - fs := filesys.MakeFsOnDisk() - if err := fs.WriteFile(output, resources); err != nil { + outputBase := filepath.Dir(strings.TrimSuffix(output, string(filepath.Separator))) + fs, err := filesys.MakeFsOnDiskSecure(outputBase) + if err != nil { + return err + } + if err = fs.WriteFile(output, resources); err != nil { return err } diff --git a/pkg/manifestgen/install/options.go b/pkg/manifestgen/install/options.go index a456007b..6b848eae 100644 --- a/pkg/manifestgen/install/options.go +++ b/pkg/manifestgen/install/options.go @@ -26,6 +26,7 @@ type Options struct { ComponentsExtra []string EventsAddr string Registry string + RegistryCredential string ImagePullSecret string WatchAllNamespaces bool NetworkPolicy bool @@ -46,6 +47,7 @@ func MakeDefaultOptions() Options { ComponentsExtra: []string{"image-reflector-controller", "image-automation-controller"}, EventsAddr: "", Registry: "ghcr.io/fluxcd", + RegistryCredential: "", ImagePullSecret: "", WatchAllNamespaces: true, NetworkPolicy: true, diff --git a/pkg/manifestgen/install/templates.go b/pkg/manifestgen/install/templates.go index ac47cf27..85af02b7 100644 --- a/pkg/manifestgen/install/templates.go +++ b/pkg/manifestgen/install/templates.go @@ -51,8 +51,6 @@ patches: - path: node-selector.yaml target: kind: Deployment - -patchesJson6902: {{- range $i, $component := .Components }} {{- if eq $component "notification-controller" }} - target: diff --git a/pkg/manifestgen/kustomization/kustomization.go b/pkg/manifestgen/kustomization/kustomization.go index 676f64b7..6a9fcf32 100644 --- a/pkg/manifestgen/kustomization/kustomization.go +++ b/pkg/manifestgen/kustomization/kustomization.go @@ -20,20 +20,23 @@ import ( "fmt" "os" "path/filepath" + "strings" "sync" - "sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/krusty" "sigs.k8s.io/kustomize/api/provider" kustypes "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/yaml" - "github.com/fluxcd/flux2/pkg/manifestgen" + "github.com/fluxcd/pkg/kustomize/filesys" + + "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) -// Generate scans the given directory for Kubernetes manifests and creates a kustomization.yaml -// including all discovered manifests as resources. +// Generate scans the given directory for Kubernetes manifests and creates a +// konfig.DefaultKustomizationFileName file, including all discovered manifests +// as resources. func Generate(options Options) (*manifestgen.Manifest, error) { kfile := filepath.Join(options.TargetPath, konfig.DefaultKustomizationFileName()) abskfile := filepath.Join(options.BaseDir, kfile) @@ -50,7 +53,7 @@ func Generate(options Options) (*manifestgen.Manifest, error) { return nil } if info.IsDir() { - // If a sub-directory contains an existing Kustomization file add the + // If a sub-directory contains an existing Kustomization file, add the // directory as a resource and do not decent into it. for _, kfilename := range konfig.RecognizedKustomizationFileNames() { if options.FileSystem.Exists(filepath.Join(path, kfilename)) { @@ -88,7 +91,9 @@ func Generate(options Options) (*manifestgen.Manifest, error) { if err != nil { return nil, err } - f.Close() + if err = f.Close(); err != nil { + return nil, err + } kus := kustypes.Kustomization{ TypeMeta: kustypes.TypeMeta{ @@ -128,20 +133,40 @@ func Generate(options Options) (*manifestgen.Manifest, error) { }, nil } +// kustomizeBuildMutex is a workaround for a concurrent map read and map write bug. +// TODO(stefan): https://github.com/kubernetes-sigs/kustomize/issues/3659 var kustomizeBuildMutex sync.Mutex -// Build takes a Kustomize overlays and returns the resulting manifests as multi-doc YAML. +// Build takes the path to a directory with a konfig.RecognizedKustomizationFileNames, +// builds it, and returns the resulting manifests as multi-doc YAML. It restricts the +// Kustomize file system to the parent directory of the base. func Build(base string) ([]byte, error) { - // TODO(stefan): temporary workaround for concurrent map read and map write bug - // https://github.com/kubernetes-sigs/kustomize/issues/3659 + // TODO(hidde): drop this when consumers have moved away to BuildWithRoot. + parent := filepath.Dir(strings.TrimSuffix(base, string(filepath.Separator))) + return BuildWithRoot(parent, base) +} + +// BuildWithRoot takes the path to a directory with a konfig.RecognizedKustomizationFileNames, +// builds it, and returns the resulting manifests as multi-doc YAML. +// The Kustomize file system is restricted to root. +func BuildWithRoot(root, base string) ([]byte, error) { kustomizeBuildMutex.Lock() defer kustomizeBuildMutex.Unlock() - kfile := filepath.Join(base, konfig.DefaultKustomizationFileName()) + fs, err := filesys.MakeFsOnDiskSecureBuild(root) + if err != nil { + return nil, err + } - fs := filesys.MakeFsOnDisk() - if !fs.Exists(kfile) { - return nil, fmt.Errorf("%s not found", kfile) + var kfile string + for _, f := range konfig.RecognizedKustomizationFileNames() { + if kf := filepath.Join(base, f); fs.Exists(kf) { + kfile = kf + break + } + } + if kfile == "" { + return nil, fmt.Errorf("%s not found", konfig.DefaultKustomizationFileName()) } // TODO(hidde): work around for a bug in kustomize causing it to @@ -161,11 +186,8 @@ func Build(base string) ([]byte, error) { } buildOptions := &krusty.Options{ - DoLegacyResourceSort: true, - LoadRestrictions: kustypes.LoadRestrictionsNone, - AddManagedbyLabel: false, - DoPrune: false, - PluginConfig: kustypes.DisabledPluginConfig(), + LoadRestrictions: kustypes.LoadRestrictionsNone, + PluginConfig: kustypes.DisabledPluginConfig(), } k := krusty.MakeKustomizer(buildOptions) diff --git a/pkg/manifestgen/kustomization/options.go b/pkg/manifestgen/kustomization/options.go index 57ffb9bd..fbb7b981 100644 --- a/pkg/manifestgen/kustomization/options.go +++ b/pkg/manifestgen/kustomization/options.go @@ -16,7 +16,7 @@ limitations under the License. package kustomization -import "sigs.k8s.io/kustomize/api/filesys" +import "sigs.k8s.io/kustomize/kyaml/filesys" type Options struct { FileSystem filesys.FileSystem @@ -25,6 +25,8 @@ type Options struct { } func MakeDefaultOptions() Options { + // TODO(hidde): switch MakeFsOnDisk to MakeFsOnDiskSecureBuild when we + // break API. return Options{ FileSystem: filesys.MakeFsOnDisk(), BaseDir: "", diff --git a/pkg/manifestgen/manifest.go b/pkg/manifestgen/manifest.go index db385f69..86df69d0 100644 --- a/pkg/manifestgen/manifest.go +++ b/pkg/manifestgen/manifest.go @@ -34,7 +34,7 @@ type Manifest struct { Content string } -// WriteFile writes the YAML content to a file inside the the root path. +// WriteFile writes the YAML content to a file inside the root path. // If the file does not exist, WriteFile creates it with permissions perm, // otherwise WriteFile overwrites the file, without changing permissions. func (m *Manifest) WriteFile(rootDir string) (string, error) { diff --git a/pkg/manifestgen/sourcesecret/options.go b/pkg/manifestgen/sourcesecret/options.go index c5239bfb..166ebb69 100644 --- a/pkg/manifestgen/sourcesecret/options.go +++ b/pkg/manifestgen/sourcesecret/options.go @@ -18,6 +18,8 @@ package sourcesecret import ( "crypto/elliptic" + + "github.com/fluxcd/pkg/ssh" ) type PrivateKeyAlgorithm string @@ -31,30 +33,61 @@ const ( const ( UsernameSecretKey = "username" PasswordSecretKey = "password" - CAFileSecretKey = "caFile" - CertFileSecretKey = "certFile" - KeyFileSecretKey = "keyFile" + CACrtSecretKey = "ca.crt" + TLSCrtSecretKey = "tls.crt" + TLSKeySecretKey = "tls.key" PrivateKeySecretKey = "identity" PublicKeySecretKey = "identity.pub" KnownHostsSecretKey = "known_hosts" + BearerTokenKey = "bearerToken" + TrustPolicyKey = "trustpolicy.json" + + // Deprecated: Replaced by CACrtSecretKey, but kept for backwards + // compatibility with deprecated TLS flags. + CAFileSecretKey = "caFile" + // Deprecated: Replaced by TLSCrtSecretKey, but kept for backwards + // compatibility with deprecated TLS flags. + CertFileSecretKey = "certFile" + // Deprecated: Replaced by TLSKeySecretKey, but kept for backwards + // compatibility with deprecated TLS flags. + KeyFileSecretKey = "keyFile" ) type Options struct { Name string Namespace string Labels map[string]string + Registry string SSHHostname string PrivateKeyAlgorithm PrivateKeyAlgorithm RSAKeyBits int ECDSACurve elliptic.Curve - PrivateKeyPath string + Keypair *ssh.KeyPair Username string Password string - CAFilePath string - CertFilePath string - KeyFilePath string + CACrt []byte + TLSCrt []byte + TLSKey []byte TargetPath string ManifestFile string + BearerToken string + VerificationCrts []VerificationCrt + TrustPolicy []byte + + // Deprecated: Replaced by CACrt, but kept for backwards compatibility + // with deprecated TLS flags. + CAFile []byte + // Deprecated: Replaced by TLSCrt, but kept for backwards compatibility + // with deprecated TLS flags. + CertFile []byte + // Deprecated: Replaced by TLSKey, but kept for backwards compatibility + // with deprecated TLS flags. + KeyFile []byte +} + +type VerificationCrt struct { + Name string + CACrt []byte } func MakeDefaultOptions() Options { @@ -63,12 +96,12 @@ func MakeDefaultOptions() Options { Namespace: "flux-system", Labels: map[string]string{}, PrivateKeyAlgorithm: RSAPrivateKeyAlgorithm, - PrivateKeyPath: "", Username: "", Password: "", - CAFilePath: "", - CertFilePath: "", - KeyFilePath: "", + CAFile: []byte{}, + CertFile: []byte{}, + KeyFile: []byte{}, ManifestFile: "secret.yaml", + BearerToken: "", } } diff --git a/pkg/manifestgen/sourcesecret/sourcesecret.go b/pkg/manifestgen/sourcesecret/sourcesecret.go index 91effffd..380115ca 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret.go @@ -18,6 +18,8 @@ package sourcesecret import ( "bytes" + "encoding/base64" + "encoding/json" "fmt" "net" "os" @@ -31,11 +33,32 @@ import ( "github.com/fluxcd/pkg/ssh" - "github.com/fluxcd/flux2/pkg/manifestgen" + "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) const defaultSSHPort = 22 +// types gotten from https://github.com/kubernetes/kubectl/blob/master/pkg/cmd/create/create_secret_docker.go#L64-L84 + +// DockerConfigJSON represents a local docker auth config file +// for pulling images. +type DockerConfigJSON struct { + Auths DockerConfig `json:"auths"` +} + +// DockerConfig represents the config file used by the docker CLI. +// This config that represents the credentials that should be used +// when pulling images from specific image repositories. +type DockerConfig map[string]DockerConfigEntry + +// DockerConfigEntry holds the user information that grant the access to docker registry +type DockerConfigEntry struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Email string `json:"email,omitempty"` + Auth string `json:"auth,omitempty"` +} + func Generate(options Options) (*manifestgen.Manifest, error) { var err error @@ -43,10 +66,8 @@ func Generate(options Options) (*manifestgen.Manifest, error) { switch { case options.Username != "" && options.Password != "": // noop - case len(options.PrivateKeyPath) > 0: - if keypair, err = loadKeyPair(options.PrivateKeyPath, options.Password); err != nil { - return nil, err - } + case options.Keypair != nil: + keypair = options.Keypair case len(options.PrivateKeyAlgorithm) > 0: if keypair, err = generateKeyPair(options); err != nil { return nil, err @@ -55,29 +76,20 @@ func Generate(options Options) (*manifestgen.Manifest, error) { var hostKey []byte if keypair != nil { - if hostKey, err = scanHostKey(options.SSHHostname); err != nil { + if hostKey, err = ScanHostKey(options.SSHHostname); err != nil { return nil, err } } - var caFile []byte - if options.CAFilePath != "" { - if caFile, err = os.ReadFile(options.CAFilePath); err != nil { - return nil, fmt.Errorf("failed to read CA file: %w", err) + var dockerCfgJson []byte + if options.Registry != "" { + dockerCfgJson, err = GenerateDockerConfigJson(options.Registry, options.Username, options.Password) + if err != nil { + return nil, fmt.Errorf("failed to generate json for docker config: %w", err) } } - var certFile, keyFile []byte - if options.CertFilePath != "" && options.KeyFilePath != "" { - if certFile, err = os.ReadFile(options.CertFilePath); err != nil { - return nil, fmt.Errorf("failed to read cert file: %w", err) - } - if keyFile, err = os.ReadFile(options.KeyFilePath); err != nil { - return nil, fmt.Errorf("failed to read key file: %w", err) - } - } - - secret := buildSecret(keypair, hostKey, caFile, certFile, keyFile, options) + secret := buildSecret(keypair, hostKey, dockerCfgJson, options) b, err := yaml.Marshal(secret) if err != nil { return nil, err @@ -89,7 +101,36 @@ func Generate(options Options) (*manifestgen.Manifest, error) { }, nil } -func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte, options Options) (secret corev1.Secret) { +func LoadKeyPairFromPath(path, password string) (*ssh.KeyPair, error) { + if path == "" { + return nil, nil + } + + b, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to open private key file: %w", err) + } + return LoadKeyPair(b, password) +} + +func LoadKeyPair(privateKey []byte, password string) (*ssh.KeyPair, error) { + var ppk cryptssh.Signer + var err error + if password != "" { + ppk, err = cryptssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(password)) + } else { + ppk, err = cryptssh.ParsePrivateKey(privateKey) + } + if err != nil { + return nil, err + } + return &ssh.KeyPair{ + PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()), + PrivateKey: privateKey, + }, nil +} + +func buildSecret(keypair *ssh.KeyPair, hostKey, dockerCfg []byte, options Options) (secret corev1.Secret) { secret.TypeMeta = metav1.TypeMeta{ APIVersion: "v1", Kind: "Secret", @@ -101,21 +142,36 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte secret.Labels = options.Labels secret.StringData = map[string]string{} + if dockerCfg != nil { + secret.Type = corev1.SecretTypeDockerConfigJson + secret.StringData[corev1.DockerConfigJsonKey] = string(dockerCfg) + return + } + if options.Username != "" && options.Password != "" { secret.StringData[UsernameSecretKey] = options.Username secret.StringData[PasswordSecretKey] = options.Password } + if options.BearerToken != "" { + secret.StringData[BearerTokenKey] = options.BearerToken + } - if caFile != nil { - secret.StringData[CAFileSecretKey] = string(caFile) + if len(options.CACrt) != 0 { + secret.StringData[CACrtSecretKey] = string(options.CACrt) + } else if len(options.CAFile) != 0 { + secret.StringData[CAFileSecretKey] = string(options.CAFile) } - if certFile != nil && keyFile != nil { - secret.StringData[CertFileSecretKey] = string(certFile) - secret.StringData[KeyFileSecretKey] = string(keyFile) + if len(options.TLSCrt) != 0 && len(options.TLSKey) != 0 { + secret.Type = corev1.SecretTypeTLS + secret.StringData[TLSCrtSecretKey] = string(options.TLSCrt) + secret.StringData[TLSKeySecretKey] = string(options.TLSKey) + } else if len(options.CertFile) != 0 && len(options.KeyFile) != 0 { + secret.StringData[CertFileSecretKey] = string(options.CertFile) + secret.StringData[KeyFileSecretKey] = string(options.KeyFile) } - if keypair != nil && hostKey != nil { + if keypair != nil && len(hostKey) != 0 { secret.StringData[PrivateKeySecretKey] = string(keypair.PrivateKey) secret.StringData[PublicKeySecretKey] = string(keypair.PublicKey) secret.StringData[KnownHostsSecretKey] = string(hostKey) @@ -125,30 +181,17 @@ func buildSecret(keypair *ssh.KeyPair, hostKey, caFile, certFile, keyFile []byte } } - return -} - -func loadKeyPair(path string, password string) (*ssh.KeyPair, error) { - b, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("failed to open private key file: %w", err) - } - - var ppk cryptssh.Signer - if password != "" { - ppk, err = cryptssh.ParsePrivateKeyWithPassphrase(b, []byte(password)) - } else { - ppk, err = cryptssh.ParsePrivateKey(b) + if len(options.VerificationCrts) != 0 { + for _, crts := range options.VerificationCrts { + secret.StringData[crts.Name] = string(crts.CACrt) + } } - if err != nil { - return nil, err + if len(options.TrustPolicy) != 0 { + secret.StringData[TrustPolicyKey] = string(options.TrustPolicy) } - return &ssh.KeyPair{ - PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()), - PrivateKey: b, - }, nil + return } func generateKeyPair(options Options) (*ssh.KeyPair, error) { @@ -170,14 +213,14 @@ func generateKeyPair(options Options) (*ssh.KeyPair, error) { return pair, nil } -func scanHostKey(host string) ([]byte, error) { +func ScanHostKey(host string) ([]byte, error) { if _, _, err := net.SplitHostPort(host); err != nil { // Assume we are dealing with a hostname without a port, // append the default SSH port as this is required for // host key scanning to work. host = fmt.Sprintf("%s:%d", host, defaultSSHPort) } - hostKey, err := ssh.ScanHostKey(host, 30*time.Second) + hostKey, err := ssh.ScanHostKey(host, 30*time.Second, []string{}, false) if err != nil { return nil, fmt.Errorf("SSH key scan for host %s failed, error: %w", host, err) } @@ -189,3 +232,19 @@ func resourceToString(data []byte) string { data = bytes.Replace(data, []byte("status: {}\n"), []byte(""), 1) return string(data) } + +func GenerateDockerConfigJson(url, username, password string) ([]byte, error) { + cred := fmt.Sprintf("%s:%s", username, password) + auth := base64.StdEncoding.EncodeToString([]byte(cred)) + cfg := DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + url: { + Username: username, + Password: password, + Auth: auth, + }, + }, + } + + return json.Marshal(cfg) +} diff --git a/pkg/manifestgen/sourcesecret/sourcesecret_test.go b/pkg/manifestgen/sourcesecret/sourcesecret_test.go index e225bbd8..8eb619d4 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret_test.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret_test.go @@ -48,7 +48,7 @@ func Test_passwordLoadKeyPair(t *testing.T) { pk, _ := os.ReadFile(tt.privateKeyPath) ppk, _ := os.ReadFile(tt.publicKeyPath) - got, err := loadKeyPair(tt.privateKeyPath, tt.password) + got, err := LoadKeyPair(pk, tt.password) if err != nil { t.Errorf("loadKeyPair() error = %v", err) return @@ -67,24 +67,13 @@ func Test_passwordLoadKeyPair(t *testing.T) { func Test_PasswordlessLoadKeyPair(t *testing.T) { for algo, privateKey := range testdata.PEMBytes { t.Run(algo, func(t *testing.T) { - f, err := os.CreateTemp("", "test-private-key-") - if err != nil { - t.Fatalf("unable to create temporary file. err: %s", err) - } - defer os.Remove(f.Name()) - - if _, err = f.Write(privateKey); err != nil { - t.Fatalf("unable to write private key to file. err: %s", err) - } - - got, err := loadKeyPair(f.Name(), "") + got, err := LoadKeyPair(privateKey, "") if err != nil { t.Errorf("loadKeyPair() error = %v", err) return } - pk, _ := os.ReadFile(f.Name()) - if !reflect.DeepEqual(got.PrivateKey, pk) { + if !reflect.DeepEqual(got.PrivateKey, privateKey) { t.Errorf("PrivateKey %s != %s", got.PrivateKey, string(privateKey)) } diff --git a/pkg/manifestgen/sync/options.go b/pkg/manifestgen/sync/options.go index 7085d56e..4a209827 100644 --- a/pkg/manifestgen/sync/options.go +++ b/pkg/manifestgen/sync/options.go @@ -32,20 +32,18 @@ type Options struct { Secret string TargetPath string ManifestFile string - GitImplementation string RecurseSubmodules bool } func MakeDefaultOptions() Options { return Options{ - Interval: 1 * time.Minute, - URL: "", - Name: "flux-system", - Namespace: "flux-system", - Branch: "main", - Secret: "flux-system", - ManifestFile: "gotk-sync.yaml", - TargetPath: "", - GitImplementation: "", + Interval: 1 * time.Minute, + URL: "", + Name: "flux-system", + Namespace: "flux-system", + Branch: "main", + Secret: "flux-system", + ManifestFile: "gotk-sync.yaml", + TargetPath: "", } } diff --git a/pkg/manifestgen/sync/sync.go b/pkg/manifestgen/sync/sync.go index 757d6bef..26de1827 100644 --- a/pkg/manifestgen/sync/sync.go +++ b/pkg/manifestgen/sync/sync.go @@ -26,11 +26,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" - "github.com/fluxcd/flux2/pkg/manifestgen" + "github.com/fluxcd/flux2/v2/pkg/manifestgen" ) func Generate(options Options) (*manifestgen.Manifest, error) { @@ -67,7 +67,6 @@ func Generate(options Options) (*manifestgen.Manifest, error) { SecretRef: &meta.LocalObjectReference{ Name: options.Secret, }, - GitImplementation: options.GitImplementation, RecurseSubmodules: options.RecurseSubmodules, }, } diff --git a/pkg/manifestgen/sync/sync_test.go b/pkg/manifestgen/sync/sync_test.go index 3ab0a743..1ac3b133 100644 --- a/pkg/manifestgen/sync/sync_test.go +++ b/pkg/manifestgen/sync/sync_test.go @@ -24,8 +24,8 @@ import ( "strings" "testing" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" ) func TestGenerate(t *testing.T) { diff --git a/pkg/manifestgen/tmpdir.go b/pkg/manifestgen/tmpdir.go new file mode 100644 index 00000000..db4daf13 --- /dev/null +++ b/pkg/manifestgen/tmpdir.go @@ -0,0 +1,38 @@ +/* +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 manifestgen + +import ( + "fmt" + "os" + "path/filepath" +) + +// MkdirTempAbs creates a tmp dir and returns the absolute path to the dir. +// This is required since certain OSes like MacOS create temporary files in +// e.g. `/private/var`, to which `/var` is a symlink. +func MkdirTempAbs(dir, pattern string) (string, error) { + tmpDir, err := os.MkdirTemp(dir, pattern) + if err != nil { + return "", err + } + tmpDir, err = filepath.EvalSymlinks(tmpDir) + if err != nil { + return "", fmt.Errorf("error evaluating symlink: %w", err) + } + return tmpDir, nil +} diff --git a/pkg/printers/dyff.go b/pkg/printers/dyff.go new file mode 100644 index 00000000..50db14d2 --- /dev/null +++ b/pkg/printers/dyff.go @@ -0,0 +1,56 @@ +/* +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 printers + +import ( + "fmt" + "io" + + "github.com/homeport/dyff/pkg/dyff" +) + +// DyffPrinter is a printer that prints dyff reports. +type DyffPrinter struct { + OmitHeader bool +} + +// NewDyffPrinter returns a new DyffPrinter. +func NewDyffPrinter() *DyffPrinter { + return &DyffPrinter{ + OmitHeader: true, + } +} + +// Print prints the given args to the given writer. +func (p *DyffPrinter) Print(w io.Writer, args ...interface{}) error { + for _, arg := range args { + switch arg := arg.(type) { + case dyff.Report: + reportWriter := &dyff.HumanReport{ + Report: arg, + OmitHeader: p.OmitHeader, + } + + if err := reportWriter.WriteReport(w); err != nil { + return fmt.Errorf("failed to print report: %w", err) + } + default: + return fmt.Errorf("unsupported type %T", arg) + } + } + return nil +} diff --git a/pkg/printers/interface.go b/pkg/printers/interface.go new file mode 100644 index 00000000..e01600e5 --- /dev/null +++ b/pkg/printers/interface.go @@ -0,0 +1,33 @@ +/* +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 printers + +import "io" + +// Printer is an interface for printing Flux cmd outputs. +type Printer interface { + // Print prints the given args to the given writer. + Print(io.Writer, ...interface{}) error +} + +// PrinterFunc is a function that can print args to a writer. +type PrinterFunc func(w io.Writer, args ...interface{}) error + +// Print implements Printer +func (fn PrinterFunc) Print(w io.Writer, args ...interface{}) error { + return fn(w, args) +} diff --git a/pkg/printers/table_printer.go b/pkg/printers/table_printer.go new file mode 100644 index 00000000..7f4f331e --- /dev/null +++ b/pkg/printers/table_printer.go @@ -0,0 +1,63 @@ +/* +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 printers + +import ( + "fmt" + "io" + + "github.com/olekukonko/tablewriter" +) + +// TablePrinter is a printer that prints Flux cmd outputs. +func TablePrinter(header []string) PrinterFunc { + return func(w io.Writer, args ...interface{}) error { + var rows [][]string + for _, arg := range args { + switch arg := arg.(type) { + case []interface{}: + for _, v := range arg { + s, ok := v.([][]string) + if !ok { + return fmt.Errorf("unsupported type %T", v) + } + rows = append(rows, s...) + } + default: + return fmt.Errorf("unsupported type %T", arg) + } + } + + table := tablewriter.NewWriter(w) + table.SetHeader(header) + table.SetAutoWrapText(false) + table.SetAutoFormatHeaders(true) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetTablePadding("\t") + table.SetNoWhiteSpace(true) + table.AppendBulk(rows) + table.Render() + + return nil + } +} diff --git a/pkg/status/status.go b/pkg/status/status.go index 3451b929..5f20e90f 100644 --- a/pkg/status/status.go +++ b/pkg/status/status.go @@ -24,16 +24,17 @@ import ( "time" "k8s.io/client-go/rest" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/aggregator" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/collector" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling/event" - "sigs.k8s.io/cli-utils/pkg/kstatus/status" - "sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" - "github.com/fluxcd/flux2/pkg/log" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/aggregator" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" + "github.com/fluxcd/cli-utils/pkg/kstatus/status" + "github.com/fluxcd/cli-utils/pkg/object" + runtimeclient "github.com/fluxcd/pkg/runtime/client" + + "github.com/fluxcd/flux2/v2/pkg/log" ) type StatusChecker struct { @@ -44,8 +45,18 @@ type StatusChecker struct { logger log.Logger } +func NewStatusCheckerWithClient(c client.Client, pollInterval time.Duration, timeout time.Duration, log log.Logger) (*StatusChecker, error) { + return &StatusChecker{ + pollInterval: pollInterval, + timeout: timeout, + client: c, + statusPoller: polling.NewStatusPoller(c, c.RESTMapper(), polling.Options{}), + logger: log, + }, nil +} + func NewStatusChecker(kubeConfig *rest.Config, pollInterval time.Duration, timeout time.Duration, log log.Logger) (*StatusChecker, error) { - restMapper, err := apiutil.NewDynamicRESTMapper(kubeConfig) + restMapper, err := runtimeclient.NewDynamicRESTMapper(kubeConfig) if err != nil { return nil, err } @@ -54,20 +65,14 @@ func NewStatusChecker(kubeConfig *rest.Config, pollInterval time.Duration, timeo return nil, err } - return &StatusChecker{ - pollInterval: pollInterval, - timeout: timeout, - client: c, - statusPoller: polling.NewStatusPoller(c, restMapper, nil), - logger: log, - }, nil + return NewStatusCheckerWithClient(c, pollInterval, timeout, log) } func (sc *StatusChecker) Assess(identifiers ...object.ObjMetadata) error { ctx, cancel := context.WithTimeout(context.Background(), sc.timeout) defer cancel() - opts := polling.Options{PollInterval: sc.pollInterval, UseCache: true} + opts := polling.PollOptions{PollInterval: sc.pollInterval} eventsChan := sc.statusPoller.Poll(ctx, identifiers, opts) coll := collector.NewResourceStatusCollector(identifiers) @@ -93,7 +98,7 @@ func (sc *StatusChecker) Assess(identifiers ...object.ObjMetadata) error { } if coll.Error != nil || ctx.Err() == context.DeadlineExceeded { - return fmt.Errorf("timed out waiting for condition") + return fmt.Errorf("timed out waiting for all resources to be ready") } return nil } diff --git a/pkg/uninstall/uninstall.go b/pkg/uninstall/uninstall.go new file mode 100644 index 00000000..5f657c04 --- /dev/null +++ b/pkg/uninstall/uninstall.go @@ -0,0 +1,395 @@ +/* +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 uninstall + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta2" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" + + "github.com/fluxcd/flux2/v2/pkg/log" + "github.com/fluxcd/flux2/v2/pkg/manifestgen" +) + +// Components removes all Kubernetes components that are part of Flux excluding the CRDs and namespace. +func Components(ctx context.Context, logger log.Logger, kubeClient client.Client, namespace string, dryRun bool) error { + var aggregateErr []error + opts, dryRunStr := getDeleteOptions(dryRun) + selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} + { + var list appsv1.DeploymentList + if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { + for i := range list.Items { + r := list.Items[i] + if err := kubeClient.Delete(ctx, &r, opts); err != nil { + logger.Failuref("Deployment/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("Deployment/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list corev1.ServiceList + if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { + for i := range list.Items { + r := list.Items[i] + if err := kubeClient.Delete(ctx, &r, opts); err != nil { + logger.Failuref("Service/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("Service/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list networkingv1.NetworkPolicyList + if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { + for i := range list.Items { + r := list.Items[i] + if err := kubeClient.Delete(ctx, &r, opts); err != nil { + logger.Failuref("NetworkPolicy/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("NetworkPolicy/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list corev1.ServiceAccountList + if err := kubeClient.List(ctx, &list, client.InNamespace(namespace), selector); err == nil { + for i := range list.Items { + r := list.Items[i] + if err := kubeClient.Delete(ctx, &r, opts); err != nil { + logger.Failuref("ServiceAccount/%s/%s deletion failed: %s", r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("ServiceAccount/%s/%s deleted %s", r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list rbacv1.ClusterRoleList + if err := kubeClient.List(ctx, &list, selector); err == nil { + for i := range list.Items { + r := list.Items[i] + if err := kubeClient.Delete(ctx, &r, opts); err != nil { + logger.Failuref("ClusterRole/%s deletion failed: %s", r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("ClusterRole/%s deleted %s", r.Name, dryRunStr) + } + } + } + } + { + var list rbacv1.ClusterRoleBindingList + if err := kubeClient.List(ctx, &list, selector); err == nil { + for i := range list.Items { + r := list.Items[i] + if err := kubeClient.Delete(ctx, &r, opts); err != nil { + logger.Failuref("ClusterRoleBinding/%s deletion failed: %s", r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("ClusterRoleBinding/%s deleted %s", r.Name, dryRunStr) + } + } + } + } + + return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr))) +} + +// Finalizers removes all finalizes on Kubernetes components that have been added by a Flux controller. +func Finalizers(ctx context.Context, logger log.Logger, kubeClient client.Client, dryRun bool) error { + var aggregateErr []error + opts, dryRunStr := getUpdateOptions(dryRun) + { + var list sourcev1.GitRepositoryList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list sourcev1b2.OCIRepositoryList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list sourcev1.HelmRepositoryList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list sourcev1.HelmChartList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list sourcev1b2.BucketList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list kustomizev1.KustomizationList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list helmv2.HelmReleaseList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list notificationv1b3.AlertList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list notificationv1b3.ProviderList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list notificationv1.ReceiverList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list imagev1.ImagePolicyList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list imagev1.ImageRepositoryList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + { + var list autov1.ImageUpdateAutomationList + if err := kubeClient.List(ctx, &list, client.InNamespace("")); err == nil { + for i := range list.Items { + r := list.Items[i] + r.Finalizers = []string{} + if err := kubeClient.Update(ctx, &r, opts); err != nil { + logger.Failuref("%s/%s/%s removing finalizers failed: %s", r.Kind, r.Namespace, r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("%s/%s/%s finalizers deleted %s", r.Kind, r.Namespace, r.Name, dryRunStr) + } + } + } + } + return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr))) +} + +// CustomResourceDefinitions removes all Kubernetes CRDs that are a part of Flux. +func CustomResourceDefinitions(ctx context.Context, logger log.Logger, kubeClient client.Client, dryRun bool) error { + var aggregateErr []error + opts, dryRunStr := getDeleteOptions(dryRun) + selector := client.MatchingLabels{manifestgen.PartOfLabelKey: manifestgen.PartOfLabelValue} + { + var list apiextensionsv1.CustomResourceDefinitionList + if err := kubeClient.List(ctx, &list, selector); err == nil { + for i := range list.Items { + r := list.Items[i] + if err := kubeClient.Delete(ctx, &r, opts); err != nil { + logger.Failuref("CustomResourceDefinition/%s deletion failed: %s", r.Name, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("CustomResourceDefinition/%s deleted %s", r.Name, dryRunStr) + } + } + } + } + return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr))) +} + +// Namespace removes the namespace Flux is installed in. +func Namespace(ctx context.Context, logger log.Logger, kubeClient client.Client, namespace string, dryRun bool) error { + var aggregateErr []error + opts, dryRunStr := getDeleteOptions(dryRun) + ns := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + if err := kubeClient.Delete(ctx, &ns, opts); err != nil { + logger.Failuref("Namespace/%s deletion failed: %s", namespace, err.Error()) + aggregateErr = append(aggregateErr, err) + } else { + logger.Successf("Namespace/%s deleted %s", namespace, dryRunStr) + } + return errors.Reduce(errors.Flatten(errors.NewAggregate(aggregateErr))) +} + +func getDeleteOptions(dryRun bool) (*client.DeleteOptions, string) { + opts := &client.DeleteOptions{} + var dryRunStr string + if dryRun { + client.DryRunAll.ApplyToDelete(opts) + dryRunStr = "(dry run)" + } + + return opts, dryRunStr +} + +func getUpdateOptions(dryRun bool) (*client.UpdateOptions, string) { + opts := &client.UpdateOptions{} + var dryRunStr string + if dryRun { + client.DryRunAll.ApplyToUpdate(opts) + dryRunStr = "(dry run)" + } + + return opts, dryRunStr +} diff --git a/rfcs/0001-authorization/README.md b/rfcs/0001-authorization/README.md index 4410dd4d..9f5f8cb7 100644 --- a/rfcs/0001-authorization/README.md +++ b/rfcs/0001-authorization/README.md @@ -109,8 +109,8 @@ fields that are not restricted to the namespace of the containing object, listed | | `.spec.targetNamespace` | This gives the namespace into which a Helm chart is installed (note: using impersonation) | | | `.spec.storageNamespace` | This gives the namespace in which the record of a Helm install is created (note: using impersonation) | | | `.spec.chart.spec.sourceRef` | This is a reference (in the created `HelmChart` object) that can include a namespace | -| **`alerts.notification.toolkit.fluxcd.io/v1beta1`** | `.spec.eventSources` | Items are references that can include a namespace | -| **`receivers.notification.toolkit.fluxcd.io/v1beta1`** | `.spec.resources` | Items in this field are references that can include a namespace | +| **`alerts.notification.toolkit.fluxcd.io/v1beta2`** | `.spec.eventSources` | Items are references that can include a namespace | +| **`receivers.notification.toolkit.fluxcd.io/v1beta2`** | `.spec.resources` | Items in this field are references that can include a namespace | | **`imagepolicies.image.toolkit.fluxcd.io/v1beta1`** | `.spec.imageRepositoryRef` | This reference can include a namespace[1] | [1] This particular cross-namespace reference is subject to additional access control; see "Access diff --git a/rfcs/0002-helm-oci/README.md b/rfcs/0002-helm-oci/README.md new file mode 100644 index 00000000..d0636a32 --- /dev/null +++ b/rfcs/0002-helm-oci/README.md @@ -0,0 +1,282 @@ +# RFC-0002 Flux OCI support for Helm + +**Status:** implemented (partially) + +**Creation date:** 2022-03-30 + +**Last update:** 2023-11-28 + +## Summary + +Given that Helm v3.8 supports [OCI](https://helm.sh/docs/topics/registries/) for package distribution, +we should extend the Flux Source API to allow fetching Helm charts from container registries. + +## Motivation + +Helm OCI support is one of the most requested feature in Flux +as seen on this [issue](https://github.com/fluxcd/source-controller/issues/124). + +With OCI support, Flux users can automate chart updates to Git in the same way +they do today for container images. + +### Goals + +- Add support for fetching Helm charts stored as OCI artifacts with minimal API changes to Flux. +- Add support for verifying the authenticity of Helm OCI charts signed with Cosign. +- Make it easy for users to switch from [HTTP/S Helm repositories](https://github.com/helm/helm-www/blob/416fabea6ffab8dc156b6a0c5eb5e8df5f5ef7dc/content/en/docs/topics/chart_repository.md) + to OCI repositories. + +### Non-Goals + +- Introduce a new API kind for referencing charts stored as OCI artifacts. + +## Proposal + +Introduce an optional field called `type` to the `HelmRepository` spec. +When not specified, the `spec.type` field defaults to `default` which preserve the current `HelmRepository` API behaviour. +When the `spec.type` field is set to `oci`, the `spec.url` field must be prefixed with `oci://` (to follow the Helm conventions). +For `oci://` URLs, source-controller will use the Helm SDK and the `oras` library to connect to the OCI remote storage. + +Introduce an optional field called `provider` for +[context-based authorization](https://fluxcd.io/flux/security/contextual-authorization/) +to AWS, Azure and Google Cloud. The `spec.provider` is ignored when `spec.type` is set to `default`. + +### Pull charts from private repositories + +#### Basic auth + +For private repositories hosted on GitHub, Quay, self-hosted Docker Registry and others, +the credentials can be supplied with: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: +spec: + type: oci + secretRef: + name: regcred +``` + +The `secretRef` points to a Kubernetes secret in the same namespace as the `HelmRepository`. +The [secret type](https://kubernetes.io/docs/concepts/configuration/secret/#secret-types) +must be `kubernetes.io/dockerconfigjson`: + +```shell +kubectl create secret docker-registry regcred \ + --docker-server= \ + --docker-username= \ + --docker-password= +``` + +#### OIDC auth + +When Flux runs on AKS, EKS or GKE, an IAM role (that grants read-only access to ACR, ECR or GCR) +can be used to bind the `source-controller` to the IAM role. + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: +spec: + type: oci + provider: azure +``` + +The provider accepts the following values: `generic`, `aws`, `azure` and `gcp`. When the provider is +not specified, it defaults to `generic`. When the provider is set to `aws`, `azure` or `gcp`, the +controller will use a specific cloud SDK for authentication purposes. + +If both `spec.secretRef` and a non-generic provider are present in the definition, +the controller will use the static credentials from the referenced secret. + +### Verify Helm charts + +To verify the authenticity of the Helm OCI charts, Flux will use the Sigstore Go SDK and implement verification +for artifacts which were either signed with keys generated by Cosign or signed using the Cosign +[keyless method](https://github.com/sigstore/cosign/blob/main/KEYLESS.md). + +To enable signature verification, the Cosign public keys can be supplied with: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmChart +metadata: + name: +spec: + verify: + provider: cosign + secretRef: + name: cosign-public-keys +``` + +Note that the Kubernetes secret containing the Cosign public keys, must use `.pub` extension: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: cosign-public-keys +type: Opaque +stringData: + key1.pub: + key2.pub: +``` + +For verifying public Helm charts which are signed using the keyless method, +the `spec.verify.secretRef` field must be omitted: + +```yaml +spec: + verify: + provider: cosign +``` + +When using the keyless method, Flux will verify the signatures in the Rekor +transparency log instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/). + +### User Stories + +#### Story 1 + +> As a developer I want to use Flux `HelmReleases` that refer to Helm charts stored +> as OCI artifacts in GitHub Container Registry. + +First create a secret using a GitHub token that allows access to GHCR: + +```sh +kubectl create secret docker-registry ghcr-charts \ + --docker-server=ghcr.io \ + --docker-username=$GITHUB_USER \ + --docker-password=$GITHUB_TOKEN +``` + +Then define a `HelmRepository` of type `oci` and reference the `dockerconfig` secret: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: ghcr-charts + namespace: default +spec: + type: oci + url: oci://ghcr.io/my-org/charts/ + secretRef: + name: ghcr-charts +``` + +And finally in Flux `HelmReleases`, refer to the ghcr-charts `HelmRepository`: + +```yaml +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: my-app + namespace: default +spec: + interval: 60m + chart: + spec: + chart: my-app + version: '1.0.x' + sourceRef: + kind: HelmRepository + name: ghcr-charts + interval: 1m # check for new OCI artifacts every minute +``` + +#### Story 2 + +> As a platform admin I want to automate Helm chart updates based on a semver ranges. +> When a new patch version is available in the container registry, I want Flux to open a PR +> with the version set in the `HelmRelease` manifests. + +Given that charts are stored in container registries, you can use Flux image automation +and patch the chart version in Git, in the same way Flux works for updating container image tags. + +Define an image registry and a policy for the chart artifact: + +```yaml +apiVersion: image.toolkit.fluxcd.io/v1beta1 +kind: ImageRepository +metadata: + name: my-app + namespace: default +spec: + image: ghcr.io/my-org/charts/my-app + interval: 1m0s +--- +apiVersion: image.toolkit.fluxcd.io/v1beta1 +kind: ImagePolicy +metadata: + name: my-app + namespace: default +spec: + imageRepositoryRef: + name: my-app + policy: + semver: + range: 1.0.x +``` + +Then add the policy marker to the `HelmRelease` manifests in Git: + +```yaml +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: my-app + namespace: default +spec: + interval: 60m + chart: + spec: + chart: my-app + version: 1.0.0 # {"$imagepolicy": "default:my-app:tag"} + sourceRef: + kind: HelmRepository + name: ghcr-charts + interval: 1m +``` + +### Alternatives + +We could introduce a new API type e.g. `HelmRegistry` to hold the reference to auth secret, +as proposed in [#2573](https://github.com/fluxcd/flux2/pull/2573). +That is considered unpractical, as there is no benefit for users in having a dedicated kind instead of +a `type` field in the current `HelmRepository` API. Adding a `type` field to the spec follows the Flux +Bucket API design, where the same Kind servers different implementations: AWS S3 vs Azure Blob vs Google Storage. + +## Design Details + +Unlike the default `HelmRepository`, the OCI `HelmRepository` does not need to +download any repository index file. The associated HelmChart can pull the chart +directly from the OCI registry based on the registry information in the +`HelmRepository` object. This makes the `HelmRepository` of type `oci` static, +not backed by a reconciler to move to a desired state. It becomes a data +container with information about the OCI registry. + +In source-controller, the `HelmRepositoryReconciler` will be updated to check +the `.spec.type` field of `HelmRepository` and do nothing if it is `oci`. + +The current `HelmChartReconciler` will be adapted to handle both types. + +### Enabling the feature + +The feature is enabled by default. + +## Implementation History + +* **2022-05-19** Partially implemented by [source-controller#690](https://github.com/fluxcd/source-controller/pull/690) +* **2022-06-06** First implementation released with [flux2 v0.31.0](https://github.com/fluxcd/flux2/releases/tag/v0.31.0) +* **2022-08-11** Resolve chart dependencies from OCI released with [flux2 v0.32.0](https://github.com/fluxcd/flux2/releases/tag/v0.32.0) +* **2022-08-29** Contextual login for AWS, Azure and GCP released with [flux2 v0.33.0](https://github.com/fluxcd/flux2/releases/tag/v0.33.0) +* **2022-10-21** Verifying Helm charts with Cosign released with [flux2 v0.36.0](https://github.com/fluxcd/flux2/releases/tag/v0.36.0) +* **2023-11-28** Update the design of HelmRepository of type OCI to be static object [flux2 v2.2.0](https://github.com/fluxcd/flux2/releases/tag/v2.2.0) + +### TODOs + +* [Add support for container registries with self-signed TLS certs](https://github.com/fluxcd/source-controller/issues/723) diff --git a/rfcs/0003-kubernetes-oci/README.md b/rfcs/0003-kubernetes-oci/README.md new file mode 100644 index 00000000..be3e7e63 --- /dev/null +++ b/rfcs/0003-kubernetes-oci/README.md @@ -0,0 +1,486 @@ +# RFC-0003 Flux OCI support for Kubernetes manifests + +**Status:** implemented + +**Creation date:** 2022-03-31 + +**Last update:** 2023-11-07 + +## Summary + +Flux should be able to distribute and reconcile Kubernetes configuration packaged as OCI artifacts. + +On the client-side, the Flux CLI should offer a command for packaging Kubernetes configs into +an OCI artifact and pushing the artifact to a container registry using the Docker config file +and the Docker credential helpers for authentication. + +On the server-side, the Flux source-controller should offer a dedicated API Kind for defining +how OCI artifacts are pulled from container registries and how the artifact's authenticity can be verified. +Flux should be able to work with any type of artifact even if it's not created with the Flux CLI. + +## Motivation + +Given that OCI registries are evolving into a generic artifact storage solution, +we should extend Flux to allow fetching Kubernetes manifests and related configs +from container registries similar to how Flux works with Git and Bucket storage. + +With OCI support, Flux users can automate artifact updates to Git in the same way +they do today for container images. + +### Goals + +- Add support to the Flux CLI for packaging Kubernetes manifests and related configs into OCI artifacts. +- Add support to Flux source-controller for fetching configs stored as OCI artifacts. +- Make it easy for users to switch from Git repositories and Buckets to OCI repositories. + +### Non-Goals + +- Enforce a specific OCI media type for artifacts containing Kubernetes manifests or any other configs. + +## Proposal + +### Push artifacts + +Flux users should be able to package a local directory containing Kubernetes configs into a tarball +and push the archive to a container registry as an OCI artifact. + +```sh +flux push artifact oci://docker.io/org/app-config:v1.0.0 \ + --source="$(git config --get remote.origin.url)" \ + --revision="sha1:$(git rev-parse HEAD)" \ + --path="./deploy" +``` + +The Flux CLI will produce OCI artifacts by setting the config layer +media type to `application/vnd.cncf.flux.config.v1+json`. + +The directory pointed to by `--path` is archived and compressed in the `tar+gzip` format +and the layer media type is set to `application/vnd.cncf.flux.content.v1.tar+gzip`. + +The source and revision are added to the OCI artifact as Open Containers standard annotations: + +```json +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "annotations": { + "org.opencontainers.image.created": "2023-02-10T09:06:09Z", + "org.opencontainers.image.revision": "sha1:6ea3e5b4da159fcb4a1288f072d34c3315644bcc", + "org.opencontainers.image.source": "https://github.com/fluxcd/flux2" + } +} +``` + +To ease the promotion workflow of a specific version from one environment to another, the CLI +should offer a tagging command. + +```sh +flux tag artifact oci://docker.io/org/app-config:v1.0.0 --tag=latest --tag=production +``` + +To view all the available artifacts in a repository and their metadata, the CLI should +offer a list command. + +```sh +flux list artifacts oci://docker.io/org/app-config +``` + +To help inspect artifacts, the Flux CLI will offer a `build` and a `pull` command for generating +tarballs locally and for downloading the tarballs from remote container registries. + +```sh +flux build artifact --path ./deploy --output tmp/artifact.tgz +flux pull artifact oci://docker.io/org/app-config:v1.0.0 --output ./manifests +``` + +### Pull artifacts + +Flux users should be able to define a source for pulling manifests inside the cluster from an OCI repository. + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: app-config + namespace: flux-system +spec: + interval: 10m + url: oci://docker.io/org/app-config + ref: + tag: v1.0.0 +``` + +The `spec.url` field points to the container image repository in the format `oci://://`. +Note that specifying a tag or digest is not in accepted for this field. The `spec.url` value is used by the controller +to fetch the list of tags from the remote OCI repository. + +An `OCIRepository` can refer to an artifact by tag, digest or semver range: + +```yaml +spec: + ref: + # one of + tag: "latest" + digest: "sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2" + semver: "6.0.x" +``` + +### Layer selection + +By default, Flux assumes that the first layer of the OCI artifact contains the Kubernetes configuration. +For multi-layer artifacts created by other tools than Flux CLI +(e.g. [oras](https://github.com/oras-project/oras), +[crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane)), +users can specify the [media type](https://github.com/opencontainers/image-spec/blob/v1.0.2/media-types.md) of the layer +which contains the tarball with Kubernetes manifests. + +```yaml +spec: + layerSelector: + mediaType: "application/vnd.cncf.flux.content.v1.tar+gzip" +``` + +If the layer selector matches more than one layer, +the first layer matching the specified media type will be used. +Note that Flux requires that the OCI layer is +[compressed](https://github.com/opencontainers/image-spec/blob/v1.0.2/layer.md#gzip-media-types) +in the `tar+gzip` format. + +### Pull artifacts from private repositories + +For authentication purposes, Flux users can choose between supplying static credentials with Kubernetes secrets +and cloud-based OIDC using an IAM role binding to the source-controller Kubernetes service account. + +#### Basic auth + +For private repositories hosted on DockerHub, GitHub, Quay, self-hosted Docker Registry and others, +the credentials can be supplied with: + +```yaml +spec: + secretRef: + name: regcred +``` + +The `secretRef` points to a Kubernetes secret in the same namespace as the `OCIRepository`, +the secret type must be `kubernetes.io/dockerconfigjson`: + +```shell +kubectl create secret docker-registry regcred \ + --docker-server= \ + --docker-username= \ + --docker-password= +``` + +For image pull secrets attached to a service account, the account name can be specified with: + +```yaml +spec: + serviceAccountName: regsa +``` + +#### Client cert auth + +For private repositories which require a certificate to authenticate, +the client certificate, private key and the CA certificate (if self-signed), can be provided with: + +```yaml +spec: + certSecretRef: + name: regcert +``` + +The `certSecretRef` points to a Kubernetes secret in the same namespace as the `OCIRepository`: + +```shell +kubectl create secret generic regcert \ + --from-file=certFile=client.crt \ + --from-file=keyFile=client.key \ + --from-file=caFile=ca.crt +``` + +#### OIDC auth + +When Flux runs on AKS, EKS or GKE, an IAM role (that grants read-only access to ACR, ECR or GCR) +can be used to bind the `source-controller` to the IAM role. + +```yaml +spec: + provider: aws +``` + +The provider accepts the following values: `generic`, `aws`, `azure` and `gcp`. When the provider is +not specified, it defaults to `generic`. When the provider is set to `aws`, `azure` or `gcp`, the +controller will use a specific cloud SDK for authentication purposes. If both `spec.secretRef` and +a non-generic provider are present in the definition, the controller will use the static credentials +from the referenced secret. + +### Verify artifacts + +To verify the authenticity of the OCI artifacts, Flux will use the Sigstore Go SDK and implement verification +for artifacts which were either signed with keys generated by Cosign or signed using the Cosign +[keyless method](https://github.com/sigstore/cosign/blob/main/KEYLESS.md). + +To enable signature verification, the Cosign public key can be supplied with: + +```yaml +spec: + verify: + provider: cosign + secretRef: + name: cosign-key +``` + +For verifying public artifacts which are signed using the keyless method, +the `.spec.verify.matchOIDCIdentity` field must be used instead of + `spec.verify.secretRef`. + +```yaml +spec: + verify: + provider: cosign + matchOIDCIdentity: + - issuer: "^https://token.actions.githubusercontent.com$" + subject: "^https://github.com/org/app-repository.*$" +``` + +The `matchOIDCIdentity` entries must contain the following fields: + +- `.issuer`, regexp that matches against the OIDC issuer. +- `.subject`, regexp that matches against the subject identity in the certificate. + +The entries are evaluated in an OR fashion, i.e. the identity is deemed to be +verified if any one entry successfully matches against the identity. + +When using the keyless method, Flux will verify the signatures in the Rekor +transparency log instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/). + +### Reconcile artifacts + +The `OCIRepository` can be used as a drop-in replacement for `GitRepository` and `Bucket` sources. +For example, a Flux Kustomization can refer to an `OCIRepository` and reconcile the manifests found in the OCI artifact: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: app + namespace: flux-system +spec: + interval: 10m + sourceRef: + kind: OCIRepository + name: app-config + path: ./ +``` + +### User Stories + +#### Story 1 + +> As a developer I want to publish my app Kubernetes manifests to the same GHCR registry +> where I publish my app containers. + +First login to GHCR with Docker: + +```sh +docker login ghcr.io -u ${GITHUB_USER} -p ${GITHUB_TOKEN} +``` + +Build your app container image and push it to GHCR: + +```sh +docker build -t ghcr.io/org/my-app:v1.0.0 . +docker push ghcr.io/org/my-app:v1.0.0 +``` + +Edit the app deployment manifest and set the new image tag. +Then push the Kubernetes manifests to GHCR: + +```sh +flux push artifact oci://ghcr.io/org/my-app-config:v1.0.0 \ + --source="$(git config --get remote.origin.url)" \ + --revision="sha1:$(git rev-parse HEAD)"\ + --path="./deploy" +``` + +Sign the config image with cosign: + +```sh +cosign sign --key cosign.key ghcr.io/org/my-app-config:v1.0.0 +``` + +Mark `v1.0.0` as latest: + +```sh +flux tag artifact oci://ghcr.io/org/my-app-config:v1.0.0 --tag latest +``` + +List the artifacts and their metadata with: + +```console +$ flux list artifacts oci://ghcr.io/org/my-app-config +ARTIFACT DIGEST SOURCE REVISION +ghcr.io/org/my-app-config:latest sha256:45b95019d30af335137977a369ad56e9ea9e9c75bb01afb081a629ba789b890c https://github.com/org/my-app-config.git sha1:20b3a674391df53f05e59a33554973d1cbd4d549 +ghcr.io/org/my-app-config:v1.0.0 sha256:45b95019d30af335137977a369ad56e9ea9e9c75bb01afb081a629ba789b890c https://github.com/org/my-app-config.git sha1:3f45e72f0d3457e91e3c530c346d86969f9f4034 +``` + +#### Story 2 + +> As a developer I want to deploy my app using Kubernetes manifests published as OCI artifacts to GHCR. + +First create a secret using a GitHub token that allows access to GHCR: + +```sh +kubectl create secret docker-registry my-app-regcred \ + --docker-server=ghcr.io \ + --docker-username=$GITHUB_USER \ + --docker-password=$GITHUB_TOKEN +``` + +Then create a secret with your cosgin public key: + +```sh +kubectl create secret generic my-app-cosgin-key \ + --from-file=cosign.pub=cosign/my-key.pub +``` + +Then define an `OCIRepository` to fetch and verify the latest app config version: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + name: app-config + namespace: default +spec: + interval: 10m + url: oci://ghcr.io/org/my-app-config + ref: + semver: "1.x" + secretRef: + name: my-app-regcred + verify: + provider: cosign + secretRef: + name: my-app-cosgin-key +``` + +And finally, create a Flux Kustomization to reconcile the app on the cluster: + +```yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 +kind: Kustomization +metadata: + name: app + namespace: default +spec: + interval: 10m + sourceRef: + kind: OCIRepository + name: app-config + path: ./deploy + prune: true + wait: true + timeout: 2m +``` + +## Design Details + +The Flux controllers and CLI will use the [fluxcd/pkg/oci](https://github.com/fluxcd/pkg/tree/main/oci) +library for OCI operations such as push, pull, tag, list tags, etc. + +For authentication purposes, the `flux artifact` commands will use the `~/.docker/config.json` +config file and the Docker credential helpers. On Cloud VMs without Docker installed, the CLI will +use context-based authorization for AWS, Azure and GCP. + +The Flux CLI will produce OCI artifacts with the following format: + +```json +{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.cncf.flux.config.v1+json", + "size": 233, + "digest": "sha256:1b80ecb1c04d4a9718a6094a00ed17b76ea8ff2bb846695fa38e7492d34f505c" + }, + "layers": [ + { + "mediaType": "application/vnd.cncf.flux.content.v1.tar+gzip", + "size": 19081, + "digest": "sha256:46c2b334705cd08db1a6fa46f860cd3364fc1a3636eea37a9b35537549086a1c" + } + ], + "annotations": { + "org.opencontainers.image.created": "2023-02-10T09:06:09Z", + "org.opencontainers.image.revision": "sha1:6ea3e5b4da159fcb4a1288f072d34c3315644bcc", + "org.opencontainers.image.source": "https://github.com/fluxcd/flux2" + } +} +``` + +The source-controller will extract the first layer from the OCI artifact, and will repackage it +as an internal `sourcev1.Artifact`. The internal artifact revision will be set to the OCI SHA256 digest +and the OpenContainers annotation will be copied to the internal artifact metadata: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: OCIRepository +metadata: + creationTimestamp: "2022-06-22T09:14:19Z" + finalizers: + - finalizers.fluxcd.io + generation: 1 + name: podinfo + namespace: oci + resourceVersion: "6603" + uid: 42e0b9f0-021c-476d-86c7-2cd20747bfff +spec: + interval: 10m + ref: + tag: 6.1.6 + timeout: 60s + url: oci://ghcr.io/stefanprodan/manifests/podinfo +status: + artifact: + checksum: d7e924b4882e55b97627355c7b3d2e711e9b54303afa2f50c25377f4df66a83b + lastUpdateTime: "2022-06-22T09:14:21Z" + metadata: + org.opencontainers.image.created: "2023-02-10T09:06:09Z" + org.opencontainers.image.revision: sha1:b3b00fe35424a45d373bf4c7214178bc36fd7872 + org.opencontainers.image.source: https://github.com/stefanprodan/podinfo.git + path: ocirepository/oci/podinfo/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de.tar.gz + revision: sha256:3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de + size: 1105 + url: http://source-controller.flux-system.svc.cluster.local./ocirepository/oci/podinfo/3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de.tar.gz + conditions: + - lastTransitionTime: "2022-06-22T09:14:21Z" + message: stored artifact for revision 'sha256:3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' + observedGeneration: 1 + reason: Succeeded + status: "True" + type: Ready + - lastTransitionTime: "2022-06-22T09:14:21Z" + message: stored artifact for revision 'sha256:3b6cdcc7adcc9a84d3214ee1c029543789d90b5ae69debe9efa3f66e982875de' + observedGeneration: 1 + reason: Succeeded + status: "True" + type: ArtifactInStorage + observedGeneration: 1 + url: http://source-controller.flux-system.svc.cluster.local./ocirepository/oci/podinfo/latest.tar.gz +``` + +### Enabling the feature + +The feature is enabled by default. + +## Implementation History + +* **2022-08-08** Partially implemented by [source-controller#788](https://github.com/fluxcd/source-controller/pull/788) +* **2022-08-11** First implementation released with [flux2 v0.32.0](https://github.com/fluxcd/flux2/releases/tag/v0.32.0) +* **2022-08-29** Select layer by OCI media type released with [flux2 v0.33.0](https://github.com/fluxcd/flux2/releases/tag/v0.33.0) +* **2022-09-29** Verifying OCI artifacts with Cosign released with [flux2 v0.35.0](https://github.com/fluxcd/flux2/releases/tag/v0.35.0) +* **2023-02-20** Custom OCI media types released with [flux2 v0.40.0](https://github.com/fluxcd/flux2/releases/tag/v0.40.0) +* **2023-10-31** OIDC identity verification implemented in + [source-controller#1250](https://github.com/fluxcd/source-controller/pull/1250) diff --git a/rfcs/0004-insecure-http/README.md b/rfcs/0004-insecure-http/README.md new file mode 100644 index 00000000..349eab69 --- /dev/null +++ b/rfcs/0004-insecure-http/README.md @@ -0,0 +1,230 @@ +# RFC-0004 Block insecure HTTP connections across Flux + +**Status:** implementable + +**Creation Date:** 2022-09-08 + +**Last update:** 2023-07-26 + +## Summary + +Flux should have a consistent way of disabling insecure HTTP connections. + +At the controller level, a flag should be present which would disable all outgoing HTTP connections. +At the object level, a field should be provided which would enable the use of non-TLS endpoints. + +If the use of a non-TLS endpoint is not supported, reconciliation will fail and the object will be marked +as stalled, signalling that human intervention is required. + +## Motivation + +Today the use of non-TLS based connections is inconsistent across Flux controllers. + +Controllers that deal only with `http` and `https` schemes have no way to block use of the `http` scheme at controller-level. +Some Flux objects provide a `.spec.insecure` field to enable the use of non-TLS based endpoints, but they don't clearly notify +users when the option is not supported (e.g. Azure/GCP Buckets). + +### Goals + +* Provide a flag across relevant Flux controllers which disables all outgoing HTTP connections. +* Add a field which enables the use of non-TLS endpoints to appropriate Flux objects. +* Provide a way for users to be made aware that their use of non-TLS endpoints is not supported if that is the case. + +### Non-Goals + +* Break Flux's current behavior of allowing HTTP connections. +* Change in behavior of communication between Flux components. + +## Proposal + +### Controllers + +Flux users should be able to enforce that controllers are using HTTPS connections only. +This shall be enabled by adding a new boolean flag `--insecure-allow-http` to the following controllers: +* source-controller +* notification-controller +* image-automation-controller +* image-reflector-controller + +The default value of this flag shall be `true`. This would ensure that there is no breaking change with controllers +still being able to access non-TLS endpoints. To disable this behavior and enforce the use of HTTPS connections, users would +have to explicitly pass the flag to the controller: + +```yaml +spec: + template: + spec: + containers: + - name: manager + image: fluxcd/source-controller + args: + - --watch-all-namespaces + - --log-level=info + - --log-encoding=json + - --enable-leader-election + - --storage-path=/data + - --storage-adv-addr=source-controller.$(RUNTIME_NAMESPACE).svc.cluster.local. + - --insecure-allow-http=false +``` + +**Note:** The flag shall not be added to the following controllers: +* kustomize-controller: This flag is excluded from this controller, as the upstream `kubenetes-sigs/kustomize` project +does not support disabling HTTP connections while fetching resources from remote bases. We can revisit this if the +upstream project adds support for this at a later point in time. +* helm-controller: This flag does not serve a purpose in this controller, as the controller does not make any HTTP calls. +Furthermore although both controllers can also do remote applies, serving `kube-apiserver` over plain +HTTP is disabled by default. While technically this can be enabled, the option for this configuration was also disabled +quite a while back (ref: https://github.com/kubernetes/kubernetes/pull/65830/). + +### Objects + +Some Flux objects, like `GitRepository`, provide a field for specifying a URL, and the URL would contain the scheme. +In such cases, the scheme can be used for inferring the transport type of the connection and consequently, +whether to use HTTP or HTTPS connections for that object. +But there are a few objects that don't allow such behavior, for example: + +* `ImageRepository`: It provides a field, `.spec.image`, which is used for specifying the address of the image present on +a container registry. But any address containing a scheme is considered invalid and HTTPS is the default transport used. +This prevents users from using images present on insecure registries. +* OCI `HelmRepository`: When using an OCI registry as a Helm repository, the `.spec.url` is expected to begin with `oci://`. +Since the scheme part of the URL is used to specify the type of `HelmRepository`, there is no way for users to specify +that the registry is hosted at a non-TLS endpoint. + +For such objects, we shall introduce a new boolean field `.spec.insecure`, which shall be `false` by default. Users that +need their object to point to an HTTP endpoint, can set this to `true`. + +### CLI + +The Flux CLI offers several commands for creating Flux specific resources. Some of these commands may involve specifying +an endpoint such as creating an `OCIRepository`: + +```sh + flux create source oci podinfo \ + --url=oci://ghcr.io/stefanprodan/manifests/podinfo \ + --tag=6.1.6 \ + --interval=10m +``` + +Since these commands essentially create object definitions, the CLI should offer a boolean flag `--insecure` +for the required commands, which will be used for specifying the value of `.spec.insecure` of such objects. + +> Note: This flag should not be confused with `--insecure-skip-tls-verify` which is meant to skip TLS verification +> when using an HTTPS connection. + +### Proxy + +The flag shall also apply to all possible proxy configurations. If the flag `--insecure-allow-http` is set to +`false`, then specifying the `HTTP_PROXY` environment variable to the controller will lead to the controller +exiting with a failure on startup. This also applies for when the `HTTPS_PROXY` enviornment variable's value is +a URL that has `http` as its scheme. + +Similarly, if a proxy is specified using the object's API, such as through `.spec.secretRef` in `Provider` in the +`notification.toolkit.fluxcd.io` API group and the proxy URL has `http` as its scheme, the reconciler will fail and +return an error, which can be viewed in the controller logs and the object's events. + +### Precedence & Validity + +Objects with `.spec.insecure` as `true` will only be allowed if HTTP connections are allowed at the controller level. +Similarly, an object can have `.spec.insecure` as `true` only if the Saas/Cloud provider allows HTTP connections. +For example, using a `Bucket` with its `.spec.provider` set to `azure` would be invalid since Azure doesn't allow +HTTP connections. + +### User Stories + +#### Story 1 + +> As a cluster admin of a multi-tenant cluster, I want to ensure all controllers access endpoints using only HTTPS +> regardless of tenants' object definitions. + +Apply a `kustomize` patch which prevents the use of HTTP connections: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --insecure-allow-http=false + target: + kind: Deployment + name: "(source-controller|notification-controller|image-reflector-controller|image-automation-controller)" + # Since the above flag is not available in kustomize-controller for reasons explained in a previous section, + # we disable Kustomize remote builds by disallowing use of remote bases. This ensures that kustomize-controller + # won't initiate any plain HTTP connections. + - patch: | + - op: add + path: /spec/template/spec/containers/0/args/- + value: --no-remote-bases=true + target: + kind: Deployment + name: kustomize-controller +``` + +#### Story 2 + +> As an application developer, I'm trying to debug a new image pushed to my local registry which +> is not served over HTTPS. + +Modify the object spec to use HTTP connections explicitly: +```yaml +apiVersion: image.toolkit.fluxcd.io/v1beta1 +kind: ImageRepository +metadata: + name: podinfo + namespace: flux-system +spec: + image: kind-registry:5000/stefanprodan/podinfo + interval: 1m0s + insecure: true +``` + +### Alternatives + +Instead of adding a flag, we can instruct users to make use of Kyverno policies to enforce that +all objects have `.spec.insecure` as `false` and any URLs present in the definition don't have `http` +as the scheme. This is less attractive, as this would ask users to install another software and prevent +Flux multi-tenancy from being standalone. + +## Design Details + +If a controller is started with `--insecure-allow-http=false`, any URL in a Flux object which has `http` +as the scheme will result in an unsuccessful reconciliation and the following condition will be added to the object's +`.status.conditions`: + +```yaml +status: + conditions: + - lastTransitionTime: "2022-09-06T09:14:21Z" + message: "Use of insecure HTTP connections isn't allowed for this controller" + observedGeneration: 1 + reason: InsecureConnectionsDisallowed + status: "True" + type: Stalled +``` + +Similarly, if an object has `.spec.insecure` as `true` but the Cloud provider doesn't allow HTTP connections, +the reconciliation will fail and the following condition will be added to the object's `.status.conditions`: + +```yaml +status: + conditions: + - lastTransitionTime: "2022-09-06T09:14:21Z" + message: "Use of insecure HTTP connections isn't allowed for Azure Storage" + observedGeneration: 1 + reason: UnsupportedConnectionType + status: "True" + type: Stalled +``` + +If an object has `.spec.insecure` as `true`, the registry client or bucket client shall be created with the use +of HTTP connections enabled explicitly. + +## Implementation History + +**2022-08-12** Allow defining OCI sources for non-TLS container registries with `flux create source oci --insecure` +released with [flux2 v0.34.0](https://github.com/fluxcd/flux2/releases/tag/v0.34.0) + diff --git a/rfcs/0005-artifact-revision-and-digest/README.md b/rfcs/0005-artifact-revision-and-digest/README.md new file mode 100644 index 00000000..32b4c466 --- /dev/null +++ b/rfcs/0005-artifact-revision-and-digest/README.md @@ -0,0 +1,357 @@ +# RFC-0005 Artifact `Revision` format and introduction of `Digest` + +**Status:** implemented + +**Creation date:** 2022-10-20 + +**Last update:** 2023-02-20 + +## Summary + +This RFC proposes to establish a canonical `Revision` format for an `Artifact` +which points to a specific revision represented as a checksum (e.g. an OCI +manifest digest or Git commit SHA) of a named pointer (e.g. an OCI repository +name or Git tag). In addition, it proposes to include the algorithm name (e.g. +`sha256`) as a prefix to an advertised checksum for an `Artifact` and +further referring to it as a `Digest`, deprecating the `Checksum` field. + +## Motivation + +The current `Artifact` type's `Revision` field format is not "officially" +standardized (albeit assumed throughout our code bases), and has mostly been +derived from `GitRepository` which uses `/` as a separator between the named +pointer (a Git branch or tag) and a specific (SHA-1, or theoretical SHA-256) +revision. + +Since the introduction of `OCIRepository` and with the recent changes around +`HelmChart` objects to allow the consumption of charts from OCI registries, +this could be seen as outdated or confusing due to the format differing from +the canonical format used by OCI, which is `@:` (the +part after `@` formally known as a ["digest"][digest-spec]) to refer to a +specific version of an OCI manifest. + +While also taking note that Git does not have an official canonical format for +e.g. branch references at a specific commit, and `/` has less of a symbolic +meaning than `@`, which could be interpreted as "`` _at_ +``". + +In addition, with the introduction of algorithm prefixes for an `Artifact`'s +checksum, it would be possible to add support and allow user configuration of +other algorithms than SHA-256. For example SHA-384 and SHA-512, or the more +performant (parallelizable) [BLAKE3][]. + +Besides this, it would make it easier to implement a client that can verify the +checksum without having to resort to an assumed format or guessing +method based on the length of it, and allows for a more robust solution in +which it can continue to calculate against the algorithm of a previous +configuration. + +The inclusion of the `Artifact`'s algorithm prefix has been proposed before in +[source-controller#855](https://github.com/fluxcd/source-controller/issues/855), +with supportive response from Core Maintainers. + +### Goals + +- Establish a canonical format to refer to an `Artifact`'s `Revision` field + which consists of a named pointer and a specific checksum reference. +- Allow easier verification of the `Artifact`'s checksum by including an + alias for the algorithm. +- Deprecate the `Artifact`'s `Checksum` field in favor of the `Digest` field. +- Allow configuration of the algorithm used to calculate the checksum of an + `Artifact`. +- Allow configuration of algorithms other than SHA-256 to calculate the + `Digest` of an `Artifact`. +- Allow compatibility with SemVer name references which might contain an `@` + symbol already (e.g. `package@v1.0.0@sha256:...`, as opposed to OCI's + `name:v1.0.0@sha256:...`). + +### Non-Goals + +- Define a canonical format for an `Artifact`'s `Revision` field which contains + a named pointer and a different reference than a checksum. + +## Proposal + +### Establish an Artifact Revision format + +Change the format of the `Revision` field of the `source.toolkit.fluxcd.io` +Group's `Artifact` type across all `Source` kinds to contain an `@` separator +opposed to `/`, and include the algorithm alias as a prefix to the checksum +(creating a "digest"). + +```text +[ ] [ [ "@" ] ":" ] +``` + +Where `` is the name of e.g. a Git branch or OCI repository +name, `` is the exact revision (e.g. a Git commit SHA or OCI manifest +digest), and `` is the alias of the algorithm used to calculate the +checksum (e.g. `sha256`). In case only a named pointer or digest is advertised, +the `@` is omitted. + +For a `GitRepository`'s `Artifact` pointing towards an SHA-1 Git commit on +branch `main`, the `Revision` field value would become: + +```text +main@sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f +``` + +For a `GitRepository`'s `Artifact` pointing towards a specific SHA-1 Git commit +without a defined branch or tag, the `Revision` field value would become: + +```text +sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f +``` + +For a `Bucket`'s `Artifact` with a revision based on an SHA-256 calculation of +a list of object keys and their etags, the `Revision` field value would become: + +```text +sha256:8fb62a09c9e48ace5463bf940dc15e85f525be4f230e223bbceef6e13024110c +``` + +For a `HelmChart`'s `Artifact` pointing towards a Helm chart version, the +`Revision` field value would become: + +```text +1.2.3 +``` + +### Introduce a `Digest` field + +Introduce a new field to the `source.toolkit.fluxcd.io` Group's `Artifact` type +across all `Source` kinds called `Digest`, containing the checksum of the file +advertised in the `Path`, and alias of the algorithm used to calculate it +(creating a ["digest"][digest-spec]). + +```text + ":" +``` + +For a `GitRepository` `Artifact`'s checksum calculated using SHA-256, the +`Digest` field value would become: + +```text +sha256:1111f92aba67995f108b3ee3ffdc00edcfe206b11fbbb459c8ef4c4a8209fca8 +``` + +#### Deprecate the `Checksum` field + +In favor of the `Digest` field, the `Checksum` field of the `source.toolkit.fluxcd.io` +Group's `Artifact` type across all `Source` kinds is deprecated, and removed in +a future version. + +### User Stories + +#### Artifact revision verification + +> As a user of the source-controller, I want to be able to see the exact +> revision of an Artifact that is being used, so that I can verify that it +> matches the expected revision at a remote source. + +For a Source kind that has an `Artifact` with a `Revision` which contains a +checksum, the field value can be retrieved using the Kubernetes API. For +example: + +```console +$ kubectl get gitrepository -o jsonpath='{.status.artifact.revision}' +main@sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f +``` + +#### Artifact checksum verification + +> As a user of the source-controller, I want to be able to verify the checksum +> of an Artifact. + +For a Source kind with an `Artifact` the digest consisting of the algorithm +alias and checksum is advertised in the `Digest` field, and can be retrieved +using the Kubernetes API. For example: + +```console +$ kubectl get gitrepository -o jsonpath='{.status.artifact.digest}' +sha256:1111f92aba67995f108b3ee3ffdc00edcfe206b11fbbb459c8ef4c4a8209fca8 +``` + +#### Artifact checksum algorithm configuration + +> As a user of the source-controller, I want to be able to configure the +> algorithm used to calculate the checksum of an Artifact. + +The source-controller binary accepts a `--artifact-digest-algo` flag which +configures the algorithm used to calculate the checksum of an `Artifact`. +The default value is `sha256`, but can be changed to `sha384`, `sha512` +or `blake3`. + +When set, newly advertised `Artifact`'s `Digest` fields will be calculated +using the configured algorithm. For previous `Artifact`'s that were set using +a previous configuration, the `Artifact`'s `Digest` field will be recalculated +using the advertised algorithm. + +#### Artifact revisions in notifications + +> As a user of the notification-controller, I want to be able to see the +> exact revision a notification is referring to. + +The notification-controller can use the revision for a Source's `Artifact` +attached as an annotation to an `Event`, and correctly parses the value field +when attempting to extract e.g. a Git commit digest from an event for a +`GitRepository`. As currently already applicable for the `/` separator. + +> As a user of the notification-controller, I want to be able to observe what +> commit has been applied on my (supported) Git provider. + +The notification-controller can use the revision attached as an annotation to +an `Event`, and is capable of extracting the correct reference for a Git +provider integration (e.g. GitHub, GitLab) to construct a payload. For example, +extracting `1eabc9a41ca088515cab83f1cce49eb43e84b67f` from +`main@sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f`. + +#### Artifact revisions in listed views + +> As a Flux CLI user, I want to see the current revision of my Source in a +> listed overview. + +By running `flux get source `, the listed view of Sources would show a +truncated version of the checksum in the `Revision` field. + +```console +$ flux get source gitrepository +NAME REVISION SUSPENDED READY MESSAGE +flux-monitoring main@sha1:1eabc9a4 False True stored artifact for revision 'main@sha1:1eabc9a41ca088515cab83f1cce49eb43e84b67f' + +$ flux get source oci +NAME REVISION SUSPENDED READY MESSAGE +apps-source local@sha256:e5fa481b False True stored artifact for digest 'local@sha256:e5fa481bb17327bd269927d0a223862d243d76c89fe697ea8c9adefc47c47e17' + +$ flux get source bucket +NAME REVISION SUSPENDED READY MESSAGE +apps-source sha256:e3b0c442 False True stored artifact for revision 'sha256:8fb62a09c9e48ace5463bf940dc15e85f525be4f230e223bbceef6e13024110c' +``` + +### Alternatives + +The two main alternatives around the `Revision` parts in this RFC are to either +keep the current field value formats as is, or to invent another format. Given +the [motivation](#motivation) for this RFC outlines the reasoning for not +keeping the current `Revision` format, and the proposed is a commonly known +format. Neither strike as a better alternative. + +For the changes related to `Checksum` and `Digest`, the alternative is to keep +the current field name as is, and only change the field value format. However, +given the naming of the field is more accurate with the introduction of the +algorithm alias, and now is the time to make last (breaking) changes to the +API. This does not strike as a better alternative. + +## Design Details + +### Artifact Revision format + +For an `Artifact`'s `Revision` which contains a checksum referring to an exact +revision, the checksum within the value MUST be appended with an alias for the +algorithm separated by `:` (e.g. `sha256:...`), further referred to as a +"digest". The algorithm alias and checksum of the digest MUST be lowercase and +alphanumeric. + +For an `Artifact`'s `Revision` which contains a digest and a named pointer, +it MUST be prefixed with `@`, and appended at the end of the `Revision` value. +The named pointer MAY contain arbitrary characters, including but not limited +to `/` and `@`. + +#### Format + +```text +[ ] [ [ "@" ] ":" ] +``` + +Where `[ ]` indicates an optional element, `" "` a literal string, and `< >` +a variable. + +#### Parsing + +When parsing the `Revision` field value of an `Artifact` to extract the digest, +the value after the last `@` is considered to be the digest. The remaining +value on the left side is considered to be the named pointer, which MAY contain +an additional `@` separator if applicable for the domain of the `Source` +implementation. + +#### Truncation + +When truncating the `Revision` field value of an `Artifact` to display in a +view with limited space, the `` of the digest MAY be truncated to +7 or more characters. The `` of the digest MUST NOT be truncated. +In addition, a digest MUST always contain the full length checksum for the +algorithm. + +#### Backwards compatibility + +To allow backwards compatibility in the notification-controller, Flux CLI and +other applicable components, the `Revision` new field value format could be +detected by the presence of the `@` or `:` characters. Falling back to their +current behaviour if not present, phasing out the old format in a future +release. + +### Artifact Digest + +The `Artifact`'s `Digest` field advertises the checksum of the file in the +`URL`. The checksum within the value MUST be appended with an alias for the +algorithm separated by `:` (e.g. `sha256:...`). This follows the +[digest format][go-digest] of OCI. + +#### Format + +```text + ":" +``` + +Where `" "` indicates a literal string, and `< >` a variable. + +#### Library + +The library used for calculating the `Digest` field value is +`github.com/opencontainers/go-digest`. This library is stable and extensible, +and used by various OCI libraries which we already depend on. + +#### Calculation + +The checksum in the `Digest` field value MUST be calculated using the canonical +algorithm [set at runtime](#configuration). + +#### Configuration + +The algorithm used for calculating the `Digest` field value MAY be configured +using the `--artifact-digest-algo` flag of the source-controller binary. The +default value is `sha256`, but can be changed to `sha384`, `sha512` or +`blake3`. + +**Note:** availability of BLAKE3 is at present dependent on an explicit import +of `github.com/opencontainers/go-digest/blake3`. + +When the provided algorithm is NOT supported, the source-controller MUST +fail to start. + +When the configured algorithm changes, the `Digest` MAY be recalculated to +update the value. + +#### Verification + +The checksum of a downloaded artifact MUST be verified against the `Digest` +field value. If the checksum does not match, the verification MUST fail. + +### Deprecation of Checksum + +The `Artifact`'s `Checksum` field is deprecated and MUST be removed in a +future release. The `Digest` field MUST be used instead. + +#### Backwards compatibility + +To allow backwards compatibility, the source-controller could continue +to advertise the checksum part of a `Digest` in the `Checksum` field until +the field is removed. + +## Implementation History + +* **2023-02-20** First implementation released with [flux2 v0.40.0](https://github.com/fluxcd/flux2/releases/tag/v0.40.0) + +[BLAKE3]: https://github.com/BLAKE3-team/BLAKE3 +[digest-spec]: https://github.com/opencontainers/image-spec/blob/main/descriptor.md#digests +[go-digest]: https://pkg.go.dev/github.com/opencontainers/go-digest#hdr-Basics \ No newline at end of file diff --git a/rfcs/0006-cdevents/CDEvents-Flux-RFC-Adapter.png b/rfcs/0006-cdevents/CDEvents-Flux-RFC-Adapter.png new file mode 100644 index 00000000..270a98a8 Binary files /dev/null and b/rfcs/0006-cdevents/CDEvents-Flux-RFC-Adapter.png differ diff --git a/rfcs/0006-cdevents/Flux-CDEvents-RFC.png b/rfcs/0006-cdevents/Flux-CDEvents-RFC.png new file mode 100644 index 00000000..8d49225e Binary files /dev/null and b/rfcs/0006-cdevents/Flux-CDEvents-RFC.png differ diff --git a/rfcs/0006-cdevents/README.md b/rfcs/0006-cdevents/README.md new file mode 100644 index 00000000..e7017768 --- /dev/null +++ b/rfcs/0006-cdevents/README.md @@ -0,0 +1,109 @@ +# RFC-0006 Flux CDEvents Receiver + +**Status:** implementable + + + +**Creation date:** 2023-12-08 + +**Last update:** 2024-03-13 + +## Summary + +This RFC proposes to add a `Receiver` type to the Flux notification-controller API +for handling [CDEvents](https://cdevents.dev/). + +For `Receiver` objects configured to accept CDEvents, +notification-controller will verify the events sent to the receiver's webhook URL, +check that their type matches the expected type, and trigger the reconciliation +of the configured resources. + +## Motivation + +CDEvents enables interoperability between CI/CD tools in a workflow, and Flux is a +very popular continuous delivery tool, and consequently the CDF team received many questions +about integrating CDEvents with Flux. + +### Goals + +Allow Flux to receive CDEvents and trigger the reconciliation of resources based on the received events. + +### Non-Goals + +Make the Flux controllers emit CDEvents. + +## Proposal + +Add CDEvents to the list of available receivers in Flux notification-controller. +Similar to other receivers such as GitHub, Flux users will be able to use `spec.events` +in order to specify which event types the receiver will allow. +The receiver will also verify using the [CDEvents Go SDK](https://github.com/cdevents/sdk-go) that the +payload sent to the webhook URL is a valid CDEvent. + +### User Stories + +Users of multiple CI/CD tools such as Tekton and Flux +could use CDEvents as a way to enable interoperability. + +For example, a user may want a Flux resource to reconcile as part of a Tekton `pipeline`. +The Tekton `pipeline` will fire off a CDEvent to the CloudEvents Broker. +A subscription that the user will have set up externally, e.g. with the [knative broker](https://knative.dev/docs/eventing/brokers/), will then +send a relevant CDEvent to the Flux webhook receiver endpoint. + +![usecase](cdevents-flux-tekton.png) + +### Alternatives + +Certain use cases for CDEvents could be done alternatively using +available receivers such as the generic webhook. + +## Design Details + +Adding a Flux `Receiver` for CDEvents that works much like the other event-based receivers already implemented. + +The user will be able to define a Flux `Receiver` custom resource and deploy it to their cluster. +The receiver takes the payload sent to the webhook URL by an external events broker, +checks the headers for the event type, and filters out events based on the user-defined +list of events in `spec.events`. If left empty, it will act on all valid CDEvents. +It then validates the payload body using the [CDEvents Go SDK](https://github.com/cdevents/sdk-go). +Valid events will then trigger the reconciliation of all Flux objects specified in `.spec.resources`. + +The CDEvents broker is not a part of this design and is left to the users to set up however they wish. + +Example Receiver: + +```yaml +apiVersion: notification.toolkit.fluxcd.io/v1 +kind: Receiver +metadata: + name: cdevents-receiver + namespace: flux-system +spec: + type: cdevents + events: + - "dev.cdevents.change.merged" + secretRef: + name: receiver-token + resources: + - apiVersion: source.toolkit.fluxcd.io/v1 + kind: GitRepository + name: webapp + namespace: flux-system +``` + +![User Flowchart](Flux-CDEvents-RFC.png) + +![Adapter](CDEvents-Flux-RFC-Adapter.png) + + +## Implementation History + + diff --git a/rfcs/0006-cdevents/cdevents-flux-tekton.png b/rfcs/0006-cdevents/cdevents-flux-tekton.png new file mode 100644 index 00000000..72223a0a Binary files /dev/null and b/rfcs/0006-cdevents/cdevents-flux-tekton.png differ diff --git a/rfcs/0007-git-repo-passwordless-auth/README.md b/rfcs/0007-git-repo-passwordless-auth/README.md new file mode 100644 index 00000000..6c2c9381 --- /dev/null +++ b/rfcs/0007-git-repo-passwordless-auth/README.md @@ -0,0 +1,404 @@ +# RFC-0007 Passwordless authentication for Git repositories + +**Status:** implementable + +**Creation date:** 2023-31-07 +**Last update:** 2024-06-12 + +## Summary + +Flux should provide a mechanism to authenticate against Git repositories without +the use of passwords. This RFC proposes the use of alternative authentication +methods like OIDC, OAuth2 and IAM to access Git repositories hosted on specific +Git SaaS platforms and cloud providers. + +## Motivation + +At the moment, Flux supports HTTP basic and bearer authentication. Users are +required to create a Secret containing the username and the password/bearer +token, which is then referred to in the GitRepository using `.spec.secretRef`. + +While this works fine, it has a couple of drawbacks: +* Scalability: Each new GitRepository potentially warrants another credentials +pair, which doesn't scale well in big organizations with hundreds of +repositories with different owners, increasing the risk of mismanagement and +leaks. +* Identity: A username is associated with an actual human. But often, the +repository belongs to a team of 2 or more people. This leads to a problem where +teams have to decide whose credentials should Flux use for authentication. + +These problems exist not due to flaws in Flux, but because of the inherent +nature of password based authentication. + +With support for OIDC, OAuth2 and IAM based authentication, we can eliminate +these problems: +* Scalability: Since OIDC is fully handled by the cloud provider, it eliminates +any user involvement in managing credentials. For OAuth2 and IAM, users do need +to provide certain information like the ID of the resource, private key, etc. +but these are still a better alternative to passwords since the same resource +can be reused by multiple teams with different members. +* Identity: Since all the above authentication methods are associated with a +virtual resource independent of a user, it solves the problem of a single person +being tied to automation that several people are involved in. + +### Goals + +* Integrate with major cloud providers' OIDC and IAM offerings to provide a +seamless way of Git repository authentication. +* Integrate with major Git SaaS providers to support their app based OAuth2 +mechanism. + +### Non-Goals + +* Replace the existing basic and bearer authentication API. + +## Proposal + +A new string field `.spec.provider` shall be added to the `GitRepository` API. +The field will be an enum with the following variants: + +* `generic` +* `aws` +* `azure` +* `gcp` +* `github` +* `gitlab` + +`.spec.provider` will be an optional field which defaults to `generic` indicating +that the user wants to authenticate via HTTP basic/bearer auth or SSH by providing +the existing `.spec.secretRef` field. The sections below define the behavior when +`.spec.provider` is set to one of the other providers. + +### AWS + +Git repositories hosted on AWS CodeCommit can be accessed by Flux via [IAM roles +for service accounts +(IRSA)](https://docs.aws.amazon.com/eks/latest/userguide/iam-roles-for-service-accounts.html) +and +[git-remote-codecommit (GRC)](https://docs.aws.amazon.com/codecommit/latest/userguide/setting-up-git-remote-codecommit.html) +signed URLs. + +The IAM role associated with service account used in Flux can be granted access +to the CodeCommit repository. The Flux service account can be patched with the +name of the IAM role to be assumed as an annotation. The CodeCommit HTTPS (GRC) +repository URL is of the format `codecommit::://`. This can +be converted to a signed URL before performing a go-git Git operation. + +The following patch can be used to add the IAM role name to Flux service accounts: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: | + apiVersion: v1 + kind: ServiceAccount + metadata: + name: source-controller + annotations: + eks.amazonaws.com/role-arn: + target: + kind: ServiceAccount + name: source-controller +``` + +Example of using AWS CodeCommit with `aws` provider: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: aws-repo +spec: + interval: 1m + url: codecommit:::// + ref: + branch: master + provider: aws +``` + +### Azure + +Git repositories hosted on Azure Devops can be accessed using [managed +identity](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops). +Seamless access from Flux to Azure devops repository can be achieved through +[Workload +Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview?tabs=dotnet). +The user creates a managed identity and establishes a federated identity between +Flux service account and the managed identity. Flux service account is patched +to add an annotation specifying the client id of the managed identity. Flux +service account and deployments are patched with labels to use workload +identity. The managed identity must have sufficient permissions to be able to +access Azure Devops resources. This enables Flux pod to access the Git +repository without the need for any credentials. + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: |- + apiVersion: v1 + kind: ServiceAccount + metadata: + name: source-controller + namespace: flux-system + annotations: + azure.workload.identity/client-id: + labels: + azure.workload.identity/use: "true" + - patch: |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: source-controller + namespace: flux-system + labels: + azure.workload.identity/use: "true" + spec: + template: + metadata: + labels: + azure.workload.identity/use: "true" +``` + +Example of using an Azure Devops repository with `azure` provider: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: azure-devops +spec: + interval: 1m + url: https://dev.azure.com///_git/ + ref: + branch: master + # notice the lack of secretRef + provider: azure +``` + +### GCP + +Git repositories hosted on Google Cloud Source Repositories can be accessed by +Flux via a [GCP Service Account](https://cloud.google.com/iam/docs/service-account-overview). + +Workload Identity Federation for GKE is [unsupported](https://cloud.google.com/iam/docs/federated-identity-supported-services) +for Cloud Source Repositories. The user must instead create the GCP Service Account and +link it to the Flux service account in order to enable workload identity. + +In order to link the GCP Service Account to the Flux service +account, the following patch must be applied: + +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - gotk-components.yaml + - gotk-sync.yaml +patches: + - patch: | + apiVersion: v1 + kind: ServiceAccount + metadata: + name: source-controller + annotations: + iam.gke.io/gcp-service-account: + target: + kind: ServiceAccount + name: source-controller +``` + +The Service Account must have sufficient permissions to be able to access Google +Cloud Source Repositories. The Cloud Source Repositories uses the `source.repos.get` +permission to access the repository, which is under the `roles/source.reader` role. +Take a look at [this guide](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity) +for more information about setting up GKE Workload Identity. + +Example of using a Google Cloud Source Repository with `gcp` provider: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: gcp-repo +spec: + interval: 1m + url: https://source.developers.google.com/p//r/ + ref: + branch: master + provider: gcp +``` + +### GitHub + +Git repositories hosted on GitHub can be accessed via [GitHub Apps](https://docs.github.com/en/apps/overview). +This allows users to create a single resource from which they can access all +their GitHub repositories. The app must have sufficient permissions to be able +to access repositories. The app's ID, private key and installation ID should +be mentioned in the Secret referred to by `.spec.secretRef`. GitHub Enterprise +users will also need to mention their GitHub API URL in the Secret. + +Example of using a Github repository with `github` provider: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: github-repo +spec: + interval: 1m + url: https://github.com// + ref: + branch: master + provider: github + secretRef: + name: github-app +--- +kind: Secret +metadata: + name: github-sa +stringData: + githubAppID: + githubInstallationID: + githubPrivateKey: | + + githubApiURl: #optional, required only for GitHub Enterprise users +``` + +### Gitlab + +Git repositories hosted on Gitlab can be accessed via OAuth2 Gitlab Applications +created from the +[UI](https://docs.gitlab.com/ee/integration/oauth_provider.html) or using +[API](https://docs.gitlab.com/ee/api/applications.html). The Gitlab Oauth2 +application must be created with the required scope to access gitlab +repositories. The application's `application_id`, `secret` and `redirect_uri` +are used to request an [access +token](https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow). +These parameters are configured in the secret referred to by `.spec.secretRef`. + +Example of using gitlab repository with `gitlab` provider: + +```yaml +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: gitlab-repo +spec: + interval: 1m + url: https://gitlab.com// + ref: + branch: main + provider: gitlab + secretRef: + name: gitlab-app +--- +kind: Secret +metadata: + name: gitlab-app +stringData: + gitlabAppID: + gitlabAppSecret: + gitlabAppRedirectUrl: +``` + +### User Stories + +#### User Story 1 + +> As a user running flux controllers, deployed from a private repository in +> a cloud provider that supports context-based authentication, I want to securely +> authenticate to the repository without setting up secrets and having to manage +> authentication tokens (refreshing, rotating, etc.). + +To enable this scenario, the user would enable context-based authentication in +their cloud provider and integrate it with their kubernetes cluster. For example, +in Azure, using AKS and Azure Devops, the user would create a managed identity and +establish a federated identity between Flux service account and the managed identity. +Flux would then be able to access the Git repository by requesting a token from the +Azure service. The user would not need to create a secret or manage any tokens. + +#### User Story 2 + +> As a user running flux controllers, deployed from a private repository, I want +> to configure authentication to the repository that is not associated to a +> personal account and does not expire. + +To enable this scenario, the user would either enable context-based authentication +in their cloud provider and integrate it with their kubernetes cluster, or set +up an OAuth2 application in their Git SaaS provider and provide the OAuth2 application +details (application ID, secret, redirect URL) in a kubernetes secret. +Flux would then be able to access the Git repository by requesting a token from the +cloud provider or Git SaaS provider. The user would not need to create any credentials +tied to a personal account. + +## Design Details + +Flux source controller uses `GitRepository` API to define a source to produce an +Artifact for a Git repository revision. Flux image automation controller updates +YAML files when new images are available and commits changes to a given Git +repository. The `ImageUpdateAutomation` API defines an automation process that +updates the Git repository referenced in it's `.spec.sourceRef`. If the new +optional string field `.spec.provider` is specified in the `GitRepository` API, +the respective provider is used to configure the authentication to check out the +source for flux controllers. + +### AWS + +If `.spec.provider` is set to `aws`, Flux controllers will use the aws-sdk-go-v2 +to assume the role of the IAM role associated with the pod service account and +obtain a short-lived [Security Token Service +(STS)](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) +credential. This credential will then be used to create a signed HTTP URL to the +CodeCommit repository, similar to what git-remote-codecommit (GRC) does in +python using the boto library, see +[here](https://github.com/aws/git-remote-codecommit/blob/1.17/git_remote_codecommit/__init__.py#L176-L194). +For example, the GRC URL `codecommit::us-east-1://test-repo-1` results in a +typical Git HTTP repository address `https://AKIAYKF23ZCZFAVYGOEX:20240607T151729Zf17c9b36ba154efc81adf3df9dc3253de52e0a1ab6c81c00a5f9a26b06a103df@git-codecommit.us-east-1.amazonaws.com/v1/repos/test-repo-1`. +This URL contains a basic auth credential. This can be passed to go-git to +perform HTTP Git operations. + +### Azure + +If `.spec.provider` is set to `azure`, Flux controllers will use +[DefaultAzureCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#DefaultAzureCredential) +to build the workload identity credential. This credential type uses the +environment variables injected by the Azure Workload Identity mutating webhook. +The [access token from the credential will be then used as a bearer +token](https://learn.microsoft.com/en-us/azure/devops/integrate/get-started/authentication/service-principal-managed-identity?view=azure-devops#q-can-i-use-a-service-principal-to-do-git-operations-like-clone-a-repo) +to perform HTTP bearer authentication. + +### GCP + +If `.spec.provider` is set to `gcp`, Flux source controller will fetch the access token +from the [GKE metadata server](https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity#metadata_server). +The GKE metadata server runs as a DaemonSet, with one Pod on every Linux node or +a native Windows service on every Windows node in the cluster. The metadata server +intercepts HTTP requests to `http://metadata.google.internal`. + +The source controller will use the url `http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token` +to retrieve a token for the IAM service account that the Pod is configured to impersonate. +This access token will be then used to perform HTTP basic authentication. + +### GitHub + +If `.spec.provider` is set to `github`, Flux controllers will get the app +details from the specified Secret and use it to [generate an app installation +token](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-an-installation-access-token-for-a-github-app). +This token is then used as the password and [`x-access-token` as the username](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app#choosing-permissions-for-git-access) +to perform HTTP basic authentication. + +### Gitlab + +If `.spec.provider` is set to `gitlab`, Flux controllers will use the +application_id, secret and redirect_url specified in `.spec.secret` to generate +an access token. The git repository can then be accessed by specifying [oauth2 +as the username and the access token as the +password](https://docs.gitlab.com/ee/api/oauth2.html#access-git-over-https-with-access-token) +to perform HTTP basic authentication. diff --git a/rfcs/RFC-0000/README.md b/rfcs/RFC-0000/README.md index b935b064..309c7dd0 100644 --- a/rfcs/RFC-0000/README.md +++ b/rfcs/RFC-0000/README.md @@ -51,7 +51,7 @@ you're proposing, but should not include things like API designs or implementation. If the RFC goal is to document best practices, -then this section can be replaced with the the actual documentation. +then this section can be replaced with the actual documentation. --> ### User Stories diff --git a/tests/.gitignore b/tests/.gitignore index 3ca910aa..0acf4705 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -5,11 +5,14 @@ ### Terraform ### # Local .terraform directories **/.terraform/* +*.terraform.lock.hcl + +# test files +build/ # .tfstate files *.tfstate *.tfstate.* - # Crash log files crash.log @@ -37,4 +40,5 @@ override.tf.json .terraformrc terraform.rc +.env # End of https://www.toptal.com/developers/gitignore/api/terraform diff --git a/tests/azure/README.md b/tests/azure/README.md deleted file mode 100644 index 80f99382..00000000 --- a/tests/azure/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# Azure E2E - -E2E tests for Azure are needed to mitigate introduction of new bugs in dependencies like libgit2. The goal is to verify that Flux integration with -Azure services are actually working now and in the future. - -## Architecture - -The tests are run with the help of pre configured Azure subscriptions and Azure DevOps organization. Access to thse accounts are currently limited to -Flux maintainers. -* [Azure Subscription](https://portal.azure.com/#@weaveworksendtoend.onmicrosoft.com/resource/subscriptions/71e8dce4-9af6-405a-8e96-425f5d3c302b/overview) -* [Azure DevOps organization](https://dev.azure.com/flux-azure/) - -All infrastructure is and should be created with Terraform. There are two separate Terraform states. All state should be configured to use remote -state in Azure. They should all be placed in the [same container](https://portal.azure.com/#@weaveworksendtoend.onmicrosoft.com/resource/subscriptions/71e8dce4-9af6-405a-8e96-425f5d3c302b/resourceGroups/terraform-state/providers/Microsoft.Storage/storageAccounts/terraformstate0419/containersList) -but use different keys. - -The [shared](./terraform/shared) Terraform creates long running cheaper infrastructure that is used across all tests. This includes a Key Vault which -contains an ssh key and Azure DevOps Personal Access Token which cant be created automatically. It also includes an Azure Container Registry which the -forked [podinfo](https://dev.azure.com/flux-azure/e2e/_git/podinfo) repository pushes an Helm Chart and Docker image to. - -The [aks](./terraform/aks) Terraform creates the AKS cluster and related resources to run the tests. It creates the AKS cluster, Azure DevOps -repositories, Key Vault Key for Sops, and Azure EventHub. The resources should be created and destroyed before and after every test run. Currently -the same state is reused between runs to make sure that resources are left running after each test run. - -## Tests - -Each test run is intiated by running `terraform apply` on the aks Terraform, it does this by using the library [terraform-exec](github.com/hashicorp/terraform-exec). -It then reads the output of the Terraform to get credentials and ssh keys, this means that a lot of the communication with the Azure API is offest to -Terraform instead of requiring it to be implemented in the test. - -The following tests are currently implemented: - -- [x] Flux can be successfully installed on AKS using the CLI e.g.: -- [x] source-controller can clone Azure DevOps repositories (https+ssh) -- [x] image-reflector-controller can list tags from Azure Container Registry image repositories -- [x] kustomize-controller can decrypt secrets using SOPS and Azure Key Vault -- [x] image-automation-controller can create branches and push to Azure DevOps repositories (https+ssh) -- [x] notification-controller can send commit status to Azure DevOps -- [x] notification-controller can forward events to Azure Event Hub -- [x] source-controller can pull charts from Azure Container Registry Helm repositories - -## Give User Access - -There are a couple of steps required when adding a new user to get access to the Azure portal and Azure DevOps. To begin with add the new user to -[Azure AD](https://portal.azure.com/#blade/Microsoft_AAD_IAM/UsersManagementMenuBlade/MsGraphUsers), and add the user to the [Azure AD group -flux-contributors](https://portal.azure.com/#blade/Microsoft_AAD_IAM/GroupDetailsMenuBlade/Overview/groupId/24e0f3f6-6555-4d3d-99ab-414c869cab5d). The -new users should now go through the invite process, and be able to sign in to both the Azure Portal and Azure DevOps. - -After the new user has signed into Azure DevOps you will need to modify the users permissions. This cannot be done before the user has signed in a -first time. In the [organization users page](https://dev.azure.com/flux-azure/_settings/users) chose "Manage User" and set the "Access Level" to basic -and make the user "Project Contributor" of the "e2e" Azure DevOps project. diff --git a/tests/azure/azure_test.go b/tests/azure/azure_test.go deleted file mode 100644 index 04542659..00000000 --- a/tests/azure/azure_test.go +++ /dev/null @@ -1,935 +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 test - -import ( - "bytes" - "context" - b64 "encoding/base64" - "encoding/json" - "fmt" - "log" - "os" - "path/filepath" - "strings" - "testing" - "time" - - eventhub "github.com/Azure/azure-event-hubs-go/v3" - "github.com/hashicorp/terraform-exec/tfexec" - "github.com/hashicorp/terraform-exec/tfinstall" - git2go "github.com/libgit2/git2go/v31" - "github.com/microsoft/azure-devops-go-api/azuredevops" - "github.com/microsoft/azure-devops-go-api/azuredevops/git" - "github.com/stretchr/testify/require" - giturls "github.com/whilp/git-urls" - "go.uber.org/multierr" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - automationv1beta1 "github.com/fluxcd/image-automation-controller/api/v1beta1" - reflectorv1beta1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - notiv1beta1 "github.com/fluxcd/notification-controller/api/v1beta1" - "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/pkg/runtime/events" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" -) - -const ( - aksTerraformPath = "./terraform/aks" - azureDevOpsKnownHosts = "ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H" -) - -type config struct { - kubeconfigPath string - kubeClient client.Client - - azdoPat string - idRsa string - idRsaPub string - knownHosts string - fleetInfraRepository repoConfig - applicationRepository repoConfig - - fluxAzureSp spConfig - sopsId string - acr acrConfig - eventHubSas string -} - -type spConfig struct { - tenantId string - clientId string - clientSecret string -} - -type repoConfig struct { - http string - ssh string -} - -type acrConfig struct { - url string - username string - password string -} - -var cfg config - -func TestMain(m *testing.M) { - exitVal, err := setup(m) - if err != nil { - log.Printf("Received an error while running setup: %v", err) - os.Exit(1) - } - os.Exit(exitVal) -} - -func setup(m *testing.M) (exitVal int, err error) { - ctx := context.TODO() - - // Setup Terraform binary and init state - log.Println("Setting up Azure test infrastructure") - execPath, err := tfinstall.Find(ctx, &whichTerraform{}) - if err != nil { - return 0, fmt.Errorf("terraform exec path not found: %v", err) - } - tf, err := tfexec.NewTerraform(aksTerraformPath, execPath) - if err != nil { - return 0, fmt.Errorf("could not create terraform instance: %v", err) - } - log.Println("Init Terraform") - err = tf.Init(ctx, tfexec.Upgrade(true)) - if err != nil { - return 0, fmt.Errorf("error running init: %v", err) - } - - // Always destroy the infrastructure before exiting - defer func() { - log.Println("Tearing down Azure test infrastructure") - if ferr := tf.Destroy(ctx); ferr != nil { - err = multierr.Append(fmt.Errorf("could not destroy Azure infrastructure: %v", ferr), err) - } - }() - - // Check that we are starting from a clean state - log.Println("Checking for an empty Terraform state") - state, err := tf.Show(ctx) - if err != nil { - return 0, fmt.Errorf("could not read state: %v", err) - } - if state.Values != nil { - return 0, fmt.Errorf("expected an empty state but got existing resources") - } - - // Apply Terraform and read the output values - log.Println("Applying Terraform") - err = tf.Apply(ctx) - if err != nil { - return 0, fmt.Errorf("error running apply: %v", err) - } - state, err = tf.Show(ctx) - if err != nil { - return 0, fmt.Errorf("could not read state: %v", err) - } - outputs := state.Values.Outputs - kubeconfig := outputs["aks_kube_config"].Value.(string) - aksHost := outputs["aks_host"].Value.(string) - aksCert := outputs["aks_client_certificate"].Value.(string) - aksKey := outputs["aks_client_key"].Value.(string) - aksCa := outputs["aks_cluster_ca_certificate"].Value.(string) - azdoPat := outputs["shared_pat"].Value.(string) - idRsa := outputs["shared_id_rsa"].Value.(string) - idRsaPub := outputs["shared_id_rsa_pub"].Value.(string) - fleetInfraRepository := outputs["fleet_infra_repository"].Value.(map[string]interface{}) - applicationRepository := outputs["application_repository"].Value.(map[string]interface{}) - fluxAzureSp := outputs["flux_azure_sp"].Value.(map[string]interface{}) - sharedSopsId := outputs["sops_id"].Value.(string) - acr := outputs["acr"].Value.(map[string]interface{}) - eventHubSas := outputs["event_hub_sas"].Value.(string) - - // Setup Kubernetes clients for test cluster - log.Println("Creating Kubernetes client") - kubeconfigPath, kubeClient, err := getKubernetesCredentials(kubeconfig, aksHost, aksCert, aksKey, aksCa) - if err != nil { - return 0, fmt.Errorf("error create Kubernetes client: %v", err) - } - defer func() { - if ferr := os.RemoveAll(filepath.Dir(kubeconfigPath)); ferr != nil { - err = multierr.Append(fmt.Errorf("could not clean up kubeconfig file: %v", ferr), err) - } - }() - - // Install Flux in the new cluster - cfg = config{ - kubeconfigPath: kubeconfigPath, - kubeClient: kubeClient, - azdoPat: azdoPat, - idRsa: idRsa, - idRsaPub: idRsaPub, - knownHosts: azureDevOpsKnownHosts, - fleetInfraRepository: repoConfig{ - http: fleetInfraRepository["http"].(string), - ssh: fleetInfraRepository["ssh"].(string), - }, - applicationRepository: repoConfig{ - http: applicationRepository["http"].(string), - ssh: applicationRepository["ssh"].(string), - }, - fluxAzureSp: spConfig{ - tenantId: fluxAzureSp["tenant_id"].(string), - clientId: fluxAzureSp["client_id"].(string), - clientSecret: fluxAzureSp["client_secret"].(string), - }, - sopsId: sharedSopsId, - acr: acrConfig{ - url: acr["url"].(string), - username: acr["username"].(string), - password: acr["password"].(string), - }, - eventHubSas: eventHubSas, - } - err = installFlux(ctx, kubeClient, kubeconfigPath, cfg.fleetInfraRepository.http, azdoPat, cfg.fluxAzureSp) - if err != nil { - return 0, fmt.Errorf("error installing Flux: %v", err) - } - - // Run tests - log.Println("Running Azure e2e tests") - result := m.Run() - return result, nil -} - -func TestFluxInstallation(t *testing.T) { - ctx := context.TODO() - require.Eventually(t, func() bool { - err := verifyGitAndKustomization(ctx, cfg.kubeClient, "flux-system", "flux-system") - if err != nil { - return false - } - return true - }, 60*time.Second, 5*time.Second) -} - -func TestAzureDevOpsCloning(t *testing.T) { - ctx := context.TODO() - branchName := "feature/branch" - tagName := "v1" - - tests := []struct { - name string - refType string - cloneType string - }{ - { - name: "https-feature-branch", - refType: "branch", - cloneType: "http", - }, - { - name: "https-v1", - refType: "tag", - cloneType: "http", - }, - { - name: "ssh-feature-branch", - refType: "branch", - cloneType: "ssh", - }, - { - name: "ssh-v1", - refType: "tag", - cloneType: "ssh", - }, - } - - t.Log("Creating application sources") - repo, repoDir, err := getRepository(cfg.applicationRepository.http, branchName, true, cfg.azdoPat) - require.NoError(t, err) - for _, tt := range tests { - manifest := fmt.Sprintf(` - apiVersion: v1 - kind: ConfigMap - metadata: - name: foobar - namespace: %s - `, tt.name) - err = runCommand(ctx, repoDir, fmt.Sprintf("mkdir -p ./cloning-test/%s", tt.name)) - require.NoError(t, err) - err = runCommand(ctx, repoDir, fmt.Sprintf("echo '%s' > ./cloning-test/%s/configmap.yaml", manifest, tt.name)) - require.NoError(t, err) - } - err = commitAndPushAll(repo, branchName, cfg.azdoPat) - require.NoError(t, err) - err = createTagAndPush(repo, branchName, tagName, cfg.azdoPat) - require.NoError(t, err) - - t.Log("Verifying application-gitops namespaces") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ref := &sourcev1.GitRepositoryRef{ - Branch: branchName, - } - if tt.refType == "tag" { - ref = &sourcev1.GitRepositoryRef{ - Tag: tagName, - } - } - url := cfg.applicationRepository.http - secretData := map[string]string{ - "username": "git", - "password": cfg.azdoPat, - } - if tt.cloneType == "ssh" { - url = cfg.applicationRepository.ssh - secretData = map[string]string{ - "identity": cfg.idRsa, - "identity.pub": cfg.idRsaPub, - "known_hosts": cfg.knownHosts, - } - } - - namespace := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: tt.name, - }, - } - _, err := controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &namespace, func() error { - return nil - }) - gitSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "git-credentials", - Namespace: namespace.Name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, gitSecret, func() error { - gitSecret.StringData = secretData - return nil - }) - source := &sourcev1.GitRepository{ObjectMeta: metav1.ObjectMeta{Name: tt.name, Namespace: namespace.Name}} - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, source, func() error { - source.Spec = sourcev1.GitRepositorySpec{ - GitImplementation: sourcev1.LibGit2Implementation, - Reference: ref, - SecretRef: &meta.LocalObjectReference{ - Name: gitSecret.Name, - }, - URL: url, - } - return nil - }) - require.NoError(t, err) - kustomization := &kustomizev1.Kustomization{ObjectMeta: metav1.ObjectMeta{Name: tt.name, Namespace: namespace.Name}} - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, kustomization, func() error { - kustomization.Spec = kustomizev1.KustomizationSpec{ - Path: fmt.Sprintf("./cloning-test/%s", tt.name), - SourceRef: kustomizev1.CrossNamespaceSourceReference{ - Kind: sourcev1.GitRepositoryKind, - Name: tt.name, - Namespace: namespace.Name, - }, - Interval: metav1.Duration{Duration: 1 * time.Minute}, - Prune: true, - } - return nil - }) - require.NoError(t, err) - - // Wait for configmap to be deployed - require.Eventually(t, func() bool { - err := verifyGitAndKustomization(ctx, cfg.kubeClient, namespace.Name, tt.name) - if err != nil { - return false - } - nn := types.NamespacedName{Name: "foobar", Namespace: namespace.Name} - cm := &corev1.ConfigMap{} - err = cfg.kubeClient.Get(ctx, nn, cm) - if err != nil { - return false - } - return true - }, 120*time.Second, 5*time.Second) - }) - } -} - -func TestImageRepositoryACR(t *testing.T) { - ctx := context.TODO() - name := "image-repository-acr" - repoUrl := cfg.applicationRepository.http - oldVersion := "1.0.0" - newVersion := "1.0.1" - manifest := fmt.Sprintf(` - apiVersion: apps/v1 - kind: Deployment - metadata: - name: podinfo - namespace: %s - spec: - selector: - matchLabels: - app: podinfo - template: - metadata: - labels: - app: podinfo - spec: - containers: - - name: podinfod - image: %s/container/podinfo:%s # {"$imagepolicy": "%s:podinfo"} - readinessProbe: - exec: - command: - - podcli - - check - - http - - localhost:9898/readyz - initialDelaySeconds: 5 - timeoutSeconds: 5`, name, cfg.acr.url, oldVersion, name) - - repo, repoDir, err := getRepository(repoUrl, name, true, cfg.azdoPat) - require.NoError(t, err) - err = addFile(repoDir, "podinfo.yaml", manifest) - require.NoError(t, err) - err = commitAndPushAll(repo, name, cfg.azdoPat) - require.NoError(t, err) - - err = setupNamespace(ctx, cfg.kubeClient, repoUrl, cfg.azdoPat, name) - require.NoError(t, err) - acrSecret := corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "acr-docker", Namespace: name}} - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &acrSecret, func() error { - acrSecret.Type = corev1.SecretTypeDockerConfigJson - acrSecret.StringData = map[string]string{ - ".dockerconfigjson": fmt.Sprintf(` - { - "auths": { - "%s": { - "auth": "%s" - } - } - } - `, cfg.acr.url, b64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", cfg.acr.username, cfg.acr.password)))), - } - return nil - }) - require.NoError(t, err) - imageRepository := reflectorv1beta1.ImageRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "podinfo", - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &imageRepository, func() error { - imageRepository.Spec = reflectorv1beta1.ImageRepositorySpec{ - Image: fmt.Sprintf("%s/container/podinfo", cfg.acr.url), - Interval: metav1.Duration{ - Duration: 1 * time.Minute, - }, - SecretRef: &meta.LocalObjectReference{ - Name: acrSecret.Name, - }, - } - return nil - }) - require.NoError(t, err) - imagePolicy := reflectorv1beta1.ImagePolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "podinfo", - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &imagePolicy, func() error { - imagePolicy.Spec = reflectorv1beta1.ImagePolicySpec{ - ImageRepositoryRef: meta.NamespacedObjectReference{ - Name: imageRepository.Name, - }, - Policy: reflectorv1beta1.ImagePolicyChoice{ - SemVer: &reflectorv1beta1.SemVerPolicy{ - Range: "1.0.x", - }, - }, - } - return nil - }) - require.NoError(t, err) - imageAutomation := automationv1beta1.ImageUpdateAutomation{ - ObjectMeta: metav1.ObjectMeta{ - Name: "podinfo", - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &imageAutomation, func() error { - imageAutomation.Spec = automationv1beta1.ImageUpdateAutomationSpec{ - Interval: metav1.Duration{ - Duration: 1 * time.Minute, - }, - SourceRef: automationv1beta1.SourceReference{ - Kind: "GitRepository", - Name: name, - }, - GitSpec: &automationv1beta1.GitSpec{ - Checkout: &automationv1beta1.GitCheckoutSpec{ - Reference: sourcev1.GitRepositoryRef{ - Branch: name, - }, - }, - Commit: automationv1beta1.CommitSpec{ - Author: automationv1beta1.CommitUser{ - Email: "imageautomation@example.com", - Name: "imageautomation", - }, - }, - }, - } - return nil - }) - require.NoError(t, err) - - // Wait for image repository to be ready - require.Eventually(t, func() bool { - _, repoDir, err := getRepository(repoUrl, name, false, cfg.azdoPat) - if err != nil { - return false - } - b, err := os.ReadFile(filepath.Join(repoDir, "podinfo.yaml")) - if err != nil { - return false - } - if bytes.Contains(b, []byte(newVersion)) == false { - return false - } - return true - }, 120*time.Second, 5*time.Second) -} - -func TestKeyVaultSops(t *testing.T) { - ctx := context.TODO() - name := "key-vault-sops" - repoUrl := cfg.applicationRepository.http - secretYaml := `apiVersion: v1 -kind: Secret -metadata: - name: "test" - namespace: "key-vault-sops" -stringData: - foo: "bar"` - - repo, tmpDir, err := getRepository(repoUrl, name, true, cfg.azdoPat) - err = runCommand(ctx, tmpDir, "mkdir -p ./key-vault-sops") - require.NoError(t, err) - err = runCommand(ctx, tmpDir, fmt.Sprintf("echo \"%s\" > ./key-vault-sops/secret.enc.yaml", secretYaml)) - require.NoError(t, err) - err = runCommand(ctx, tmpDir, fmt.Sprintf("sops --encrypt --encrypted-regex '^(data|stringData)$' --azure-kv %s --in-place ./key-vault-sops/secret.enc.yaml", cfg.sopsId)) - require.NoError(t, err) - err = commitAndPushAll(repo, name, cfg.azdoPat) - require.NoError(t, err) - - err = setupNamespace(ctx, cfg.kubeClient, repoUrl, cfg.azdoPat, name) - require.NoError(t, err) - - source := &sourcev1.GitRepository{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: name}} - require.Eventually(t, func() bool { - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, source, func() error { - source.Spec = sourcev1.GitRepositorySpec{ - GitImplementation: sourcev1.LibGit2Implementation, - Reference: &sourcev1.GitRepositoryRef{ - Branch: name, - }, - SecretRef: &meta.LocalObjectReference{ - Name: "https-credentials", - }, - URL: repoUrl, - } - return nil - }) - if err != nil { - return false - } - return true - }, 10*time.Second, 1*time.Second) - kustomization := &kustomizev1.Kustomization{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: name}} - require.Eventually(t, func() bool { - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, kustomization, func() error { - kustomization.Spec = kustomizev1.KustomizationSpec{ - Path: "./key-vault-sops", - SourceRef: kustomizev1.CrossNamespaceSourceReference{ - Kind: sourcev1.GitRepositoryKind, - Name: source.Name, - Namespace: source.Namespace, - }, - Interval: metav1.Duration{Duration: 1 * time.Minute}, - Prune: true, - Decryption: &kustomizev1.Decryption{ - Provider: "sops", - }, - } - return nil - }) - if err != nil { - return false - } - return true - }, 10*time.Second, 1*time.Second) - - require.Eventually(t, func() bool { - nn := types.NamespacedName{Name: "test", Namespace: name} - secret := &corev1.Secret{} - err = cfg.kubeClient.Get(ctx, nn, secret) - if err != nil { - return false - } - return true - }, 120*time.Second, 5*time.Second) -} - -func TestAzureDevOpsCommitStatus(t *testing.T) { - ctx := context.TODO() - name := "commit-status" - repoUrl := cfg.applicationRepository.http - manifest := fmt.Sprintf(` - apiVersion: v1 - kind: ConfigMap - metadata: - name: foobar - namespace: %s - `, name) - - repo, repoDir, err := getRepository(repoUrl, name, true, cfg.azdoPat) - require.NoError(t, err) - err = addFile(repoDir, "configmap.yaml", manifest) - require.NoError(t, err) - err = commitAndPushAll(repo, name, cfg.azdoPat) - require.NoError(t, err) - - err = setupNamespace(ctx, cfg.kubeClient, repoUrl, cfg.azdoPat, name) - require.NoError(t, err) - - kustomization := &kustomizev1.Kustomization{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: name}} - require.Eventually(t, func() bool { - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, kustomization, func() error { - kustomization.Spec.HealthChecks = []meta.NamespacedObjectKindReference{ - { - APIVersion: "v1", - Kind: "ConfigMap", - Name: "foobar", - Namespace: name, - }, - } - return nil - }) - if err != nil { - return false - } - return true - }, 10*time.Second, 1*time.Second) - - require.Eventually(t, func() bool { - err := verifyGitAndKustomization(ctx, cfg.kubeClient, name, name) - if err != nil { - return false - } - return true - }, 10*time.Second, 1*time.Second) - - secret := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "azuredevops-token", - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &secret, func() error { - secret.StringData = map[string]string{ - "token": cfg.azdoPat, - } - return nil - }) - provider := notiv1beta1.Provider{ - ObjectMeta: metav1.ObjectMeta{ - Name: "azuredevops", - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &provider, func() error { - provider.Spec = notiv1beta1.ProviderSpec{ - Type: "azuredevops", - Address: repoUrl, - SecretRef: &meta.LocalObjectReference{ - Name: "azuredevops-token", - }, - } - return nil - }) - require.NoError(t, err) - alert := notiv1beta1.Alert{ - ObjectMeta: metav1.ObjectMeta{ - Name: "azuredevops", - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &alert, func() error { - alert.Spec = notiv1beta1.AlertSpec{ - ProviderRef: meta.LocalObjectReference{ - Name: provider.Name, - }, - EventSources: []notiv1beta1.CrossNamespaceObjectReference{ - { - Kind: "Kustomization", - Name: name, - Namespace: name, - }, - }, - } - return nil - }) - require.NoError(t, err) - - u, err := giturls.Parse(repoUrl) - require.NoError(t, err) - id := strings.TrimLeft(u.Path, "/") - id = strings.TrimSuffix(id, ".git") - comp := strings.Split(id, "/") - orgUrl := fmt.Sprintf("%s://%s/%v", u.Scheme, u.Host, comp[0]) - project := comp[1] - repoId := comp[3] - branch, err := repo.LookupBranch(name, git2go.BranchLocal) - require.NoError(t, err) - commit, err := repo.LookupCommit(branch.Target()) - rev := commit.Id().String() - connection := azuredevops.NewPatConnection(orgUrl, cfg.azdoPat) - client, err := git.NewClient(ctx, connection) - require.NoError(t, err) - getArgs := git.GetStatusesArgs{ - Project: &project, - RepositoryId: &repoId, - CommitId: &rev, - } - require.Eventually(t, func() bool { - statuses, err := client.GetStatuses(ctx, getArgs) - if err != nil { - return false - } - if len(*statuses) != 1 { - return false - } - return true - }, 500*time.Second, 5*time.Second) -} - -func TestEventHubNotification(t *testing.T) { - ctx := context.TODO() - name := "event-hub" - - // Start listening to eventhub with latest offset - hub, err := eventhub.NewHubFromConnectionString(cfg.eventHubSas) - require.NoError(t, err) - c := make(chan string, 10) - handler := func(ctx context.Context, event *eventhub.Event) error { - c <- string(event.Data) - return nil - } - runtimeInfo, err := hub.GetRuntimeInformation(ctx) - require.NoError(t, err) - require.Equal(t, 1, len(runtimeInfo.PartitionIDs)) - listenerHandler, err := hub.Receive(ctx, runtimeInfo.PartitionIDs[0], handler, eventhub.ReceiveWithLatestOffset()) - require.NoError(t, err) - - // Setup Flux resources - repoUrl := cfg.applicationRepository.http - manifest := fmt.Sprintf(` - apiVersion: v1 - kind: ConfigMap - metadata: - name: foobar - namespace: %s - `, name) - - repo, repoDir, err := getRepository(repoUrl, name, true, cfg.azdoPat) - require.NoError(t, err) - err = addFile(repoDir, "configmap.yaml", manifest) - require.NoError(t, err) - err = commitAndPushAll(repo, name, cfg.azdoPat) - require.NoError(t, err) - - err = setupNamespace(ctx, cfg.kubeClient, repoUrl, cfg.azdoPat, name) - require.NoError(t, err) - - secret := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &secret, func() error { - secret.StringData = map[string]string{ - "address": cfg.eventHubSas, - } - return nil - }) - provider := notiv1beta1.Provider{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &provider, func() error { - provider.Spec = notiv1beta1.ProviderSpec{ - Type: "azureeventhub", - Address: repoUrl, - SecretRef: &meta.LocalObjectReference{ - Name: name, - }, - } - return nil - }) - require.NoError(t, err) - alert := notiv1beta1.Alert{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &alert, func() error { - alert.Spec = notiv1beta1.AlertSpec{ - ProviderRef: meta.LocalObjectReference{ - Name: provider.Name, - }, - EventSources: []notiv1beta1.CrossNamespaceObjectReference{ - { - Kind: "Kustomization", - Name: name, - Namespace: name, - }, - }, - } - return nil - }) - require.NoError(t, err) - - kustomization := &kustomizev1.Kustomization{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: name}} - require.Eventually(t, func() bool { - _, err := controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, kustomization, func() error { - kustomization.Spec.HealthChecks = []meta.NamespacedObjectKindReference{ - { - APIVersion: "v1", - Kind: "ConfigMap", - Name: "foobar", - Namespace: name, - }, - } - return nil - }) - if err != nil { - return false - } - return true - }, 10*time.Second, 1*time.Second) - - require.Eventually(t, func() bool { - err := verifyGitAndKustomization(ctx, cfg.kubeClient, name, name) - if err != nil { - return false - } - return true - }, 60*time.Second, 5*time.Second) - - // Wait to read even from event hub - require.Eventually(t, func() bool { - select { - case eventJson := <-c: - event := &events.Event{} - err := json.Unmarshal([]byte(eventJson), event) - if err != nil { - t.Logf("the received event type does not match Flux format, error: %v", err) - return false - } - - if event.InvolvedObject.Kind == kustomizev1.KustomizationKind && - strings.Contains(event.Message, "Health check passed") { - return true - } - - t.Logf("event received from '%s/%s': %s", - event.InvolvedObject.Kind, event.InvolvedObject.Name, event.Message) - return false - default: - return false - } - }, 60*time.Second, 1*time.Second) - err = listenerHandler.Close(ctx) - require.NoError(t, err) - err = hub.Close(ctx) - require.NoError(t, err) -} - -// TODO: Enable when source-controller supports Helm charts from OCI sources. -/*func TestACRHelmRelease(t *testing.T) { - ctx := context.TODO() - - // Create namespace for test - namespace := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "acr-helm-release", - }, - } - err := kubeClient.Create(ctx, &namespace) - require.NoError(t, err) - defer func() { - kubeClient.Delete(ctx, &namespace) - require.NoError(t, err) - }() - - // Copy ACR credentials to new namespace - acrNn := types.NamespacedName{ - Name: "acr-helm", - Namespace: "flux-system", - } - acrSecret := corev1.Secret{} - err = kubeClient.Get(ctx, acrNn, &acrSecret) - require.NoError(t, err) - acrSecret.ObjectMeta = metav1.ObjectMeta{ - Name: acrSecret.Name, - Namespace: namespace.Name, - } - err = kubeClient.Create(ctx, &acrSecret) - require.NoError(t, err) - - // Create HelmRepository and wait for it to sync - helmRepository := sourcev1.HelmRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "acr", - Namespace: namespace.Name, - }, - Spec: sourcev1.HelmRepositorySpec{ - URL: "https://acrappsoarfish.azurecr.io/helm/podinfo", - Interval: metav1.Duration{ - Duration: 5 * time.Minute, - }, - SecretRef: &meta.LocalObjectReference{ - Name: acrSecret.Name, - }, - PassCredentials: true, - }, - } - err = kubeClient.Create(ctx, &helmRepository) - require.NoError(t, err) -}*/ diff --git a/tests/azure/go.mod b/tests/azure/go.mod deleted file mode 100644 index 19f5a349..00000000 --- a/tests/azure/go.mod +++ /dev/null @@ -1,112 +0,0 @@ -module github.com/fluxcd/flux2/tests/azure - -go 1.17 - -require ( - github.com/Azure/azure-event-hubs-go/v3 v3.3.13 - github.com/fluxcd/helm-controller/api v0.15.0 - github.com/fluxcd/image-automation-controller/api v0.19.0 - github.com/fluxcd/image-reflector-controller/api v0.15.0 - github.com/fluxcd/kustomize-controller/api v0.19.1 - github.com/fluxcd/notification-controller/api v0.20.1 - github.com/fluxcd/pkg/apis/meta v0.10.2 - github.com/fluxcd/pkg/runtime v0.12.3 - github.com/fluxcd/source-controller/api v0.20.1 - github.com/hashicorp/terraform-exec v0.14.0 - github.com/libgit2/git2go/v31 v31.6.1 - github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 - github.com/stretchr/testify v1.7.0 - github.com/whilp/git-urls v1.0.0 - go.uber.org/multierr v1.6.0 - k8s.io/api v0.23.1 - k8s.io/apimachinery v0.23.1 - k8s.io/client-go v0.23.1 - sigs.k8s.io/controller-runtime v0.11.0 -) - -require ( - cloud.google.com/go v0.81.0 // indirect - cloud.google.com/go/storage v1.10.0 // indirect - github.com/Azure/azure-amqp-common-go/v3 v3.0.1 // indirect - github.com/Azure/azure-sdk-for-go v51.1.0+incompatible // indirect - github.com/Azure/go-amqp v0.13.12 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.18 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect - github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect - github.com/aws/aws-sdk-go v1.15.78 // indirect - github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/devigned/tab v0.1.1 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect - github.com/fluxcd/pkg/apis/acl v0.0.3 // indirect - github.com/fluxcd/pkg/apis/kustomize v0.3.1 // indirect - github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect - github.com/go-logr/logr v1.2.2 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-cmp v0.5.6 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/uuid v1.1.2 // indirect - github.com/googleapis/gax-go/v2 v2.0.5 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect - github.com/hashicorp/go-checkpoint v0.5.0 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-getter v1.5.3 // indirect - github.com/hashicorp/go-retryablehttp v0.6.8 // indirect - github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-uuid v1.0.1 // indirect - github.com/hashicorp/go-version v1.3.0 // indirect - github.com/hashicorp/terraform-json v0.12.0 // indirect - github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 // indirect - github.com/jpillora/backoff v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/jstemmer/go-junit-report v0.9.1 // indirect - github.com/klauspost/compress v1.11.2 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.4.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/ulikunitz/xz v0.5.8 // indirect - github.com/zclconf/go-cty v1.8.4 // indirect - go.opencensus.io v0.23.0 // indirect - go.uber.org/atomic v1.7.0 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect - golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect - golang.org/x/mod v0.4.2 // indirect - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect - golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect - golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 // indirect - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect - golang.org/x/text v0.3.7 // indirect - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect - golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect - google.golang.org/api v0.44.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 // indirect - google.golang.org/grpc v1.40.0 // indirect - google.golang.org/protobuf v1.27.1 // indirect - gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - k8s.io/apiextensions-apiserver v0.23.1 // indirect - k8s.io/klog/v2 v2.30.0 // indirect - k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 // indirect - sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.0 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect -) - -// Fix for CVE-2020-29652: https://github.com/golang/crypto/commit/8b5274cf687fd9316b4108863654cc57385531e8 -// Fix for CVE-2021-43565: https://github.com/golang/crypto/commit/5770296d904e90f15f38f77dfc2e43fdf5efc083 -replace golang.org/x/crypto => golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 diff --git a/tests/azure/go.sum b/tests/azure/go.sum deleted file mode 100644 index b5ff98ea..00000000 --- a/tests/azure/go.sum +++ /dev/null @@ -1,1159 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-amqp-common-go/v3 v3.0.1 h1:mXh+eyOxGLBfqDtfmbtby0l7XfG/6b2NkuZ3B7i6zHA= -github.com/Azure/azure-amqp-common-go/v3 v3.0.1/go.mod h1:PBIGdzcO1teYoufTKMcGibdKaYZv4avS+O6LNIp8bq0= -github.com/Azure/azure-event-hubs-go/v3 v3.3.13 h1:aiI2RLjp0MzLCuFUXzR8b3h3bdPIc2c3vBYXRK8jX3E= -github.com/Azure/azure-event-hubs-go/v3 v3.3.13/go.mod h1:dJ/WqDn0KEJkNznL9UT/UbXzfmkffCjSNl9x2Y8JI28= -github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= -github.com/Azure/azure-pipeline-go v0.1.9/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9achrP7OxIzeTn1Yg= -github.com/Azure/azure-sdk-for-go v51.1.0+incompatible h1:7uk6GWtUqKg6weLv2dbKnzwb0ml1Qn70AdtRccZ543w= -github.com/Azure/azure-sdk-for-go v51.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-storage-blob-go v0.6.0/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y= -github.com/Azure/go-amqp v0.13.0/go.mod h1:qj+o8xPCz9tMSbQ83Vp8boHahuRDl5mkNHyt1xlxUTs= -github.com/Azure/go-amqp v0.13.12 h1:u/m0QvBgNVlcMqj4bPHxtEyANOzS+cXXndVMYGsC29A= -github.com/Azure/go-amqp v0.13.12/go.mod h1:D5ZrjQqB1dyp1A+G73xeL/kNn7D5qHJIIsNNps7YNmk= -github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= -github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0= -github.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= -github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= -github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= -github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.13 h1:Mp5hbtOePIzM8pJVRa3YLrWWmZtoxRXqUEzCfJt3+/Q= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 h1:iM6UAvjR97ZIeR93qTcwpKNMpV+/FTWjwEbuPD495Tk= -github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM= -github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U= -github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= -github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= -github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= -github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= -github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= -github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= -github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= -github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andybalholm/crlf v0.0.0-20171020200849-670099aa064f/go.mod h1:k8feO4+kXDxro6ErPXBRTJ/ro2mf0SsFG8s7doP9kJE= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= -github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.78 h1:LaXy6lWR0YK7LKyuU0QWy2ws/LWTPfYV/UgfiBu4tvY= -github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/devigned/tab v0.0.1/go.mod h1:oVYrfgGyond090gxCvvbjZji79+peOiSV6vhZhKJM0Y= -github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= -github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= -github.com/devigned/tab/opencensus v0.1.2/go.mod h1:U6xXMXnNwXJpdaK0mnT3zdng4WTi+vCfqn7YHofEv2A= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= -github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= -github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/fluxcd/helm-controller/api v0.15.0 h1:1uei4JWf5cOEbixj8d5mZ3EMruuarR8yCSiPIsaotKo= -github.com/fluxcd/helm-controller/api v0.15.0/go.mod h1:/OeNzk18BVa7UmhGphENJdD/2GerKpMeDSxY8QYlO0o= -github.com/fluxcd/image-automation-controller/api v0.19.0 h1:XR2yBR3RxB6i1mS6ZpqgbEnuV23s9q4JfkyKLyOTViU= -github.com/fluxcd/image-automation-controller/api v0.19.0/go.mod h1:e9hAvFZT5y1X6NaSNUHXkabpMkPA3Z1bDr3yea8gMzE= -github.com/fluxcd/image-reflector-controller/api v0.15.0 h1:2XUKXLhWjbS7X8k1Ur/LJaIv2C8kbpErB46yw4Xmf4U= -github.com/fluxcd/image-reflector-controller/api v0.15.0/go.mod h1:SPUqO4kodOglDFpZ+GhW/XBhKo71mWIqFRc+oT0jCfc= -github.com/fluxcd/kustomize-controller/api v0.19.1 h1:71E9/7WNQN7aNVhTvfweyOEPwOLVnohAhcR0qMnA67g= -github.com/fluxcd/kustomize-controller/api v0.19.1/go.mod h1:q0AA6fxVlm8fvXZEaqSMMw8ANPharpywBve7dlcARhk= -github.com/fluxcd/notification-controller/api v0.20.1 h1:iK1+icG0ESuSUki6O9tL/4uxPx6eymYwaFxGprlKSQA= -github.com/fluxcd/notification-controller/api v0.20.1/go.mod h1:YFW/YQ6kScEzpnuKgvOJWak+9zGyF3FJ73kKsSQ4LDk= -github.com/fluxcd/pkg/apis/acl v0.0.3 h1:Lw0ZHdpnO4G7Zy9KjrzwwBmDZQuy4qEjaU/RvA6k1lc= -github.com/fluxcd/pkg/apis/acl v0.0.3/go.mod h1:XPts6lRJ9C9fIF9xVWofmQwftvhY25n1ps7W9xw0XLU= -github.com/fluxcd/pkg/apis/kustomize v0.3.1 h1:wmb5D9e1+Rr3/5O3235ERuj+h2VKUArVfYYk68QKP+w= -github.com/fluxcd/pkg/apis/kustomize v0.3.1/go.mod h1:k2HSRd68UwgNmOYBPOd6WbX6a2MH2X/Jeh7e3s3PFPc= -github.com/fluxcd/pkg/apis/meta v0.10.2 h1:pnDBBEvfs4HaKiVAYgz+e/AQ8dLvcgmVfSeBroZ/KKI= -github.com/fluxcd/pkg/apis/meta v0.10.2/go.mod h1:KQ2er9xa6koy7uoPMZjIjNudB5p4tXs+w0GO6fRcy7I= -github.com/fluxcd/pkg/runtime v0.12.3 h1:h21AZ3YG5MAP7DxFF9hfKrP+vFzys2L7CkUbPFjbP/0= -github.com/fluxcd/pkg/runtime v0.12.3/go.mod h1:imJ2xYy/d4PbSinX2IefmZk+iS2c1P5fY0js8mCE4SM= -github.com/fluxcd/source-controller/api v0.20.1 h1:BfYw1gNHykiCVFNtDz3atcf3Vph+arfuveKmouI98wE= -github.com/fluxcd/source-controller/api v0.20.1/go.mod h1:Ab2qDmAUz6ZCp8UaHYLYzxyFrC1FQqEqjxiROb/Rdiw= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= -github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= -github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= -github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= -github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= -github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/zapr v1.2.0 h1:n4JnPI1T3Qq1SFEi/F8rwLrZERp2bso19PJZDB9dayk= -github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= -github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= -github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-getter v1.5.3 h1:NF5+zOlQegim+w/EUhSLh6QhXHmZMEeHLQzllkQ3ROU= -github.com/hashicorp/go-getter v1.5.3/go.mod h1:BrrV/1clo8cCYu6mxvboYg+KutTiFnXjMEgDD8+i7ZI= -github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-retryablehttp v0.6.8 h1:92lWxgpa+fF3FozM4B3UZtHZMJX8T5XT+TFdCxsPyWs= -github.com/hashicorp/go-retryablehttp v0.6.8/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= -github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= -github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= -github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= -github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/terraform-exec v0.14.0 h1:UQoUcxKTZZXhyyK68Cwn4mApT4mnFPmEXPiqaHL9r+w= -github.com/hashicorp/terraform-exec v0.14.0/go.mod h1:qrAASDq28KZiMPDnQ02sFS9udcqEkRly002EA2izXTA= -github.com/hashicorp/terraform-json v0.12.0 h1:8czPgEEWWPROStjkWPUnTQDXmpmZPlkQAwYYLETaTvw= -github.com/hashicorp/terraform-json v0.12.0/go.mod h1:pmbq9o4EuL43db5+0ogX10Yofv1nozM+wskr/bGFJpI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8 h1:12VvqtR6Aowv3l/EQUlocDHW2Cp4G9WJVH7uyH8QFJE= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= -github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ= -github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/libgit2/git2go/v31 v31.6.1 h1:FnKHHDDBgltSsu9RpKuL4rSR8dQ1JTf9dfvFhZ1y7Aw= -github.com/libgit2/git2go/v31 v31.6.1/go.mod h1:c/rkJcBcUFx6wHaT++UwNpKvIsmPNqCeQ/vzO4DrEec= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= -github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= -github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.28.0 h1:vGVfV9KrDTvWt5boZO0I19g2E3CsWfpPPKZM9dt3mEw= -github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= -github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= -github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= -github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -github.com/whilp/git-urls v1.0.0 h1:95f6UMWN5FKW71ECsXRUd3FVYiXdrE7aX4NZKcPmIjU= -github.com/whilp/git-urls v1.0.0/go.mod h1:J16SAmobsqc3Qcy98brfl5f5+e0clUvg1krgwk/qCfE= -github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= -github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= -github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -github.com/zclconf/go-cty v1.8.4 h1:pwhhz5P+Fjxse7S7UriBrMu6AUJSZM5pKqGem1PjGAs= -github.com/zclconf/go-cty v1.8.4/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= -go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= -go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= -go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= -go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= -go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= -go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= -go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= -go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211215060638-4ddde0e984e9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk= -golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff h1:VX/uD7MK0AHXGiScH3fsieUQUcpmRERPDYtqZdJnA+Q= -golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= -gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0 h1:URs6qR1lAxDsqWITsQXI4ZkGiYJ5dHtRNiCpfs2OeKA= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 h1:NHN4wOCScVzKhPenJ2dt+BTs3X/XkBVI/Rh4iDt55T8= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.23.0/go.mod h1:8wmDdLBHBNxtOIytwLstXt5E9PddnZb0GaMcqsvDBpg= -k8s.io/api v0.23.1 h1:ncu/qfBfUoClqwkTGbeRqqOqBCRoUAflMuOaOD7J0c8= -k8s.io/api v0.23.1/go.mod h1:WfXnOnwSqNtG62Y1CdjoMxh7r7u9QXGCkA1u0na2jgo= -k8s.io/apiextensions-apiserver v0.23.0/go.mod h1:xIFAEEDlAZgpVBl/1VSjGDmLoXAWRG40+GsWhKhAxY4= -k8s.io/apiextensions-apiserver v0.23.1 h1:xxE0q1vLOVZiWORu1KwNRQFsGWtImueOrqSl13sS5EU= -k8s.io/apiextensions-apiserver v0.23.1/go.mod h1:0qz4fPaHHsVhRApbtk3MGXNn2Q9M/cVWWhfHdY2SxiM= -k8s.io/apimachinery v0.23.0/go.mod h1:fFCTTBKvKcwTPFzjlcxp91uPFZr+JA0FubU4fLzzFYc= -k8s.io/apimachinery v0.23.1 h1:sfBjlDFwj2onG0Ijx5C+SrAoeUscPrmghm7wHP+uXlo= -k8s.io/apimachinery v0.23.1/go.mod h1:SADt2Kl8/sttJ62RRsi9MIV4o8f5S3coArm0Iu3fBno= -k8s.io/apiserver v0.23.0/go.mod h1:Cec35u/9zAepDPPFyT+UMrgqOCjgJ5qtfVJDxjZYmt4= -k8s.io/apiserver v0.23.1/go.mod h1:Bqt0gWbeM2NefS8CjWswwd2VNAKN6lUKR85Ft4gippY= -k8s.io/client-go v0.23.0/go.mod h1:hrDnpnK1mSr65lHHcUuIZIXDgEbzc7/683c6hyG4jTA= -k8s.io/client-go v0.23.1 h1:Ma4Fhf/p07Nmj9yAB1H7UwbFHEBrSPg8lviR24U2GiQ= -k8s.io/client-go v0.23.1/go.mod h1:6QSI8fEuqD4zgFK0xbdwfB/PthBsIxCJMa3s17WlcO0= -k8s.io/code-generator v0.23.0/go.mod h1:vQvOhDXhuzqiVfM/YHp+dmg10WDZCchJVObc9MvowsE= -k8s.io/code-generator v0.23.1/go.mod h1:V7yn6VNTCWW8GqodYCESVo95fuiEg713S8B7WacWZDA= -k8s.io/component-base v0.23.0/go.mod h1:DHH5uiFvLC1edCpvcTDV++NKULdYYU6pR9Tt3HIKMKI= -k8s.io/component-base v0.23.1/go.mod h1:6llmap8QtJIXGDd4uIWJhAq0Op8AtQo6bDW2RrNMTeo= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211208161948-7d6a63dca704 h1:ZKMMxTvduyf5WUtREOqg5LiXaN1KO/+0oOQPRFrClpo= -k8s.io/utils v0.0.0-20211208161948-7d6a63dca704/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.25/go.mod h1:Mlj9PNLmG9bZ6BHFwFKDo5afkpWyUISkb9Me0GnK66I= -sigs.k8s.io/controller-runtime v0.11.0 h1:DqO+c8mywcZLFJWILq4iktoECTyn30Bkj0CwgqMpZWQ= -sigs.k8s.io/controller-runtime v0.11.0/go.mod h1:KKwLiTooNGu+JmLZGn9Sl3Gjmfj66eMbCQznLP5zcqA= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 h1:kDi4JBNAsJWfz1aEXhO8Jg87JJaPNLh5tIzYHgStQ9Y= -sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2/go.mod h1:B+TnT182UBxE84DiCz4CVE26eOSDAeYCpfDnC2kdKMY= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/structured-merge-diff/v4 v4.2.0 h1:kDvPBbnPk+qYmkHmSo8vKGp438IASWofnbbUKDE/bv0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.0/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/tests/azure/terraform/aks/.terraform.lock.hcl b/tests/azure/terraform/aks/.terraform.lock.hcl deleted file mode 100644 index b5c9f96d..00000000 --- a/tests/azure/terraform/aks/.terraform.lock.hcl +++ /dev/null @@ -1,78 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/azuread" { - version = "1.6.0" - constraints = "1.6.0" - hashes = [ - "h1:BlO53mX+Y2W//YqlCKvoxzofegFQk636XlKtmZYH0PY=", - "zh:0db70045a464d325fdb3d71809f0467844c3e2fcf1349e568bc51ad5035c99d9", - "zh:3629f1d7b4eba48d744b24c7cf7fe878d5ef5910a36b525507bd3d588010ccec", - "zh:5a73a45b6d1ff353810cc9b00d7c90a2fb328ba0a9ef3d24392b1500fb98741a", - "zh:7a6a9c390cf1bf752321abb8d0643c9f623e8c2ad871dfb378d64c9d90fada2d", - "zh:7d6de55d326b046dabc16bd7b655f008ff780c36ffc884b139a7c7da37b446d5", - "zh:8d725c618396ccae290e411296c892e08e776c3e9e5a82b0ef1f633a917146ec", - "zh:a206d1d8042bf66ca12b97334bbd6fcdf12fd6131f8cb4547c82b9fa7a701612", - "zh:b03ab4ff07dcb5ed8be8b0619c6ec9fb0da0c83594ccb0a1bff72f346083b530", - "zh:b6131f9d438b340a4016c770b569139ec7ac2532358a8ab783234e8c93d141d5", - "zh:ce9372d38e9e62accfd54f4669753000d3dcbae4b45686d74630eb63eb879f37", - "zh:df9a607c333d464d8bdeb248b1ff41e493c1d0661453a1e1ce396b89952a74ee", - ] -} - -provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.76.0" - constraints = "2.76.0" - hashes = [ - "h1:kF+u0s0DPnE5gMKhzQACWRUIdwZG1Ax4atXt9hk1J8M=", - "zh:137eb7c07d3d3c9fe123e74381c108c4442efba9fc051faa2ca603503ff2840f", - "zh:142a354dffd59a1d6b7f1614ab66a468ace3636d95933589a8d704ee8dbc4ea6", - "zh:4c343b4da8b86e4213c1b11f73337cec73a55b1fa95a0e0e0c79f34597d37cc3", - "zh:75d3109d48726fdbaad840d2fa294ec3362b32a3628c261af00f5c5608427521", - "zh:7b1e78c144c6ad2beebc798abb9e76c725bf34ced41df36dc0120a0f2426e801", - "zh:981235b01c3d4acf94c78cdd96624fd01d0a3622bc06b5c62aef3e788f1481c3", - "zh:bad819efae7293ce371409e1ed34197c3e879f61d3e44893af0ce68e6aaffde7", - "zh:c8008967722929deccfec9695754ae55028ce12311c321ae7a7c753dde162a44", - "zh:d38513d1138864269b2ff333b08a64a7949630d489f18e660630bbaff3b7ebb8", - "zh:e1f64d2d91b5f5cba6a9c5d35278a4918d332d7385a87f8e3466aaadb782a90f", - "zh:e93a377a1e823df69718686703b07f1712046eeb742006022e982f2e8a594161", - ] -} - -provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" - hashes = [ - "h1:BZMEPucF+pbu9gsPk0G0BHx7YP04+tKdq2MrRDF1EDM=", - "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", - "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", - "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", - "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", - "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", - "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", - "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", - "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", - "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", - "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", - "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", - ] -} - -provider "registry.terraform.io/microsoft/azuredevops" { - version = "0.1.7" - constraints = "0.1.7" - hashes = [ - "h1:AWNWqJ3XhlKp3xdJF+3WKdK1zVoCFYInQvi06exsBzg=", - "zh:0c024992f2282ef73d4829e487ec8482dd98e9272b903f2e5979f5f62567ee4e", - "zh:47fef8f57dfdca6aebe5a907b4866880007512019d9bec29805fc83501412309", - "zh:692736c501c6b987a4a74c69fb7702a54969180706d1f67eff13e6ed2a0f9fec", - "zh:6c3c4339206f5dcbc9d10fb2fe343652e7e14255223dcece5bf79ef9030858ef", - "zh:77dfc63377b8d8fe24cbbe479ead18bfd1c7ded067fd694b6532434d6305ad31", - "zh:93dba26dbade208a1cba43333f104a64252ca2404636ab033702da29648bfaaa", - "zh:952d28b3e6c137de9b8700d2b748e5a4a2aa53ed07005f0f7abdd66b84cc63fe", - "zh:a7b8238b8b2f04ad2d720a207377bfc2066d54b1d9d7285f2535afc43ff80fdb", - "zh:bb23d8fc3cdd3c01d7620dadb2ba7b724706f2112d7738e135d1be1455682f5e", - "zh:cb4da640beb5fc59296479c201a03351789496c04aaa57ae1530a7aac9095b92", - "zh:ede6fb7ab598081fdddac56d470bae14448271dfd43a645bc02d136643391ebe", - "zh:fd8291e6dc9118323a744660326a0f11de2a475c4a358e50f480feed1f3bb080", - ] -} diff --git a/tests/azure/terraform/aks/aks.tf b/tests/azure/terraform/aks/aks.tf deleted file mode 100644 index 1356be34..00000000 --- a/tests/azure/terraform/aks/aks.tf +++ /dev/null @@ -1,37 +0,0 @@ -resource "azurerm_kubernetes_cluster" "this" { - name = "aks-${local.name_suffix}" - location = azurerm_resource_group.this.location - resource_group_name = azurerm_resource_group.this.name - - dns_prefix = "aks${local.name_suffix}" - - default_node_pool { - name = "default" - node_count = 2 - vm_size = "Standard_B2s" - os_disk_size_gb = 30 - } - - identity { - type = "SystemAssigned" - } - - role_based_access_control { - enabled = true - } - - network_profile { - network_plugin = "kubenet" - network_policy = "calico" - } - - tags = { - environment = "e2e" - } -} - -resource "azurerm_role_assignment" "aks_acr_pull" { - scope = data.azurerm_container_registry.shared.id - role_definition_name = "AcrPull" - principal_id = azurerm_kubernetes_cluster.this.kubelet_identity[0].object_id -} diff --git a/tests/azure/terraform/aks/azuredevops.tf b/tests/azure/terraform/aks/azuredevops.tf deleted file mode 100644 index dd1ebc50..00000000 --- a/tests/azure/terraform/aks/azuredevops.tf +++ /dev/null @@ -1,21 +0,0 @@ -data "azuredevops_project" "e2e" { - name = "e2e" -} - -resource "azuredevops_git_repository" "fleet_infra" { - project_id = data.azuredevops_project.e2e.id - name = "fleet-infra-${local.name_suffix}" - default_branch = "refs/heads/main" - initialization { - init_type = "Clean" - } -} - -resource "azuredevops_git_repository" "application" { - project_id = data.azuredevops_project.e2e.id - name = "application-${local.name_suffix}" - default_branch = "refs/heads/main" - initialization { - init_type = "Clean" - } -} diff --git a/tests/azure/terraform/aks/keyvault.tf b/tests/azure/terraform/aks/keyvault.tf deleted file mode 100644 index 9133306a..00000000 --- a/tests/azure/terraform/aks/keyvault.tf +++ /dev/null @@ -1,37 +0,0 @@ -resource "azurerm_key_vault" "this" { - name = "kv-${random_pet.suffix.id}" - resource_group_name = azurerm_resource_group.this.name - location = azurerm_resource_group.this.location - tenant_id = data.azurerm_client_config.current.tenant_id - sku_name = "standard" -} - -resource "azurerm_key_vault_access_policy" "sops_write" { - key_vault_id = azurerm_key_vault.this.id - tenant_id = data.azurerm_client_config.current.tenant_id - object_id = data.azurerm_client_config.current.object_id - - key_permissions = [ - "Encrypt", - "Decrypt", - "Create", - "Delete", - "Purge", - "Get", - "List", - ] -} - -resource "azurerm_key_vault_key" "sops" { - depends_on = [azurerm_key_vault_access_policy.sops_write] - - name = "sops" - key_vault_id = azurerm_key_vault.this.id - key_type = "RSA" - key_size = 2048 - - key_opts = [ - "decrypt", - "encrypt", - ] -} diff --git a/tests/azure/terraform/aks/main.tf b/tests/azure/terraform/aks/main.tf deleted file mode 100644 index 5dc44b99..00000000 --- a/tests/azure/terraform/aks/main.tf +++ /dev/null @@ -1,52 +0,0 @@ -terraform { - backend "azurerm" { - resource_group_name = "terraform-state" - storage_account_name = "terraformstate0419" - container_name = "aks-tfstate" - key = "prod.terraform.tfstate" - } - - required_version = "1.0.7" - - required_providers { - azurerm = { - source = "hashicorp/azurerm" - version = "2.76.0" - } - azuread = { - source = "hashicorp/azuread" - version = "1.6.0" - } - azuredevops = { - source = "microsoft/azuredevops" - version = "0.1.7" - } - } -} - -provider "azurerm" { - features {} -} - -provider "azuredevops" { - org_service_url = "https://dev.azure.com/${local.azure_devops_org}" - personal_access_token = data.azurerm_key_vault_secret.shared_pat.value -} - -data "azurerm_client_config" "current" {} - -resource "random_pet" "suffix" {} - -locals { - azure_devops_org = "flux-azure" - name_suffix = "e2e-${random_pet.suffix.id}" -} - -resource "azurerm_resource_group" "this" { - name = "rg-${local.name_suffix}" - location = "West Europe" - - tags = { - environment = "e2e" - } -} diff --git a/tests/azure/terraform/aks/outputs.tf b/tests/azure/terraform/aks/outputs.tf deleted file mode 100644 index 4833c70a..00000000 --- a/tests/azure/terraform/aks/outputs.tf +++ /dev/null @@ -1,76 +0,0 @@ -output "aks_kube_config" { - sensitive = true - value = azurerm_kubernetes_cluster.this.kube_config_raw -} - -output "aks_host" { - value = azurerm_kubernetes_cluster.this.kube_config[0].host -} - -output "aks_client_certificate" { - value = base64decode(azurerm_kubernetes_cluster.this.kube_config[0].client_certificate) -} - -output "aks_client_key" { - value = base64decode(azurerm_kubernetes_cluster.this.kube_config[0].client_key) -} - -output "aks_cluster_ca_certificate" { - value = base64decode(azurerm_kubernetes_cluster.this.kube_config[0].cluster_ca_certificate) -} - -output "shared_pat" { - sensitive = true - value = data.azurerm_key_vault_secret.shared_pat.value -} - -output "shared_id_rsa" { - sensitive = true - value = data.azurerm_key_vault_secret.shared_id_rsa.value -} - -output "shared_id_rsa_pub" { - sensitive = true - value = data.azurerm_key_vault_secret.shared_id_rsa_pub.value -} - -output "fleet_infra_repository" { - value = { - http = azuredevops_git_repository.fleet_infra.remote_url - ssh = "ssh://git@ssh.dev.azure.com/v3/${local.azure_devops_org}/${azuredevops_git_repository.fleet_infra.project_id}/${azuredevops_git_repository.fleet_infra.name}" - } -} - -output "application_repository" { - value = { - http = azuredevops_git_repository.application.remote_url - ssh = "ssh://git@ssh.dev.azure.com/v3/${local.azure_devops_org}/${azuredevops_git_repository.application.project_id}/${azuredevops_git_repository.application.name}" - } -} - -output "flux_azure_sp" { - value = { - tenant_id = data.azurerm_client_config.current.tenant_id - client_id = azuread_service_principal.flux.application_id - client_secret = azuread_service_principal_password.flux.value - } - sensitive = true -} - -output "event_hub_sas" { - value = azurerm_eventhub_authorization_rule.this.primary_connection_string - sensitive = true -} - -output "sops_id" { - value = azurerm_key_vault_key.sops.id -} - -output "acr" { - value = { - url = data.azurerm_container_registry.shared.login_server - username = azuread_service_principal.flux.application_id - password = azuread_service_principal_password.flux.value - } - sensitive = true -} diff --git a/tests/azure/terraform/aks/service-principal.tf b/tests/azure/terraform/aks/service-principal.tf deleted file mode 100644 index db46718b..00000000 --- a/tests/azure/terraform/aks/service-principal.tf +++ /dev/null @@ -1,52 +0,0 @@ -resource "azuread_application" "flux" { - display_name = "flux-${local.name_suffix}" - - required_resource_access { - resource_app_id = "00000003-0000-0000-c000-000000000000" - - resource_access { - id = "df021288-bdef-4463-88db-98f22de89214" - type = "Role" - } - } - - required_resource_access { - resource_app_id = "00000002-0000-0000-c000-000000000000" - - resource_access { - id = "1cda74f2-2616-4834-b122-5cb1b07f8a59" - type = "Role" - } - resource_access { - id = "78c8a3c8-a07e-4b9e-af1b-b5ccab50a175" - type = "Role" - } - } -} - -resource "azuread_service_principal" "flux" { - application_id = azuread_application.flux.application_id -} - -resource "azuread_service_principal_password" "flux" { - service_principal_id = azuread_service_principal.flux.object_id -} - -resource "azurerm_role_assignment" "acr" { - scope = data.azurerm_container_registry.shared.id - role_definition_name = "AcrPull" - principal_id = azuread_service_principal.flux.object_id -} - -resource "azurerm_key_vault_access_policy" "sops_decrypt" { - key_vault_id = azurerm_key_vault.this.id - tenant_id = data.azurerm_client_config.current.tenant_id - object_id = azuread_service_principal.flux.object_id - - key_permissions = [ - "Encrypt", - "Decrypt", - "Get", - "List", - ] -} diff --git a/tests/azure/terraform/aks/shared.tf b/tests/azure/terraform/aks/shared.tf deleted file mode 100644 index 30fccf5f..00000000 --- a/tests/azure/terraform/aks/shared.tf +++ /dev/null @@ -1,32 +0,0 @@ -locals { - shared_suffix = "oarfish" -} - -data "azurerm_resource_group" "shared" { - name = "e2e-shared" -} - -data "azurerm_container_registry" "shared" { - name = "acrapps${local.shared_suffix}" - resource_group_name = data.azurerm_resource_group.shared.name -} - -data "azurerm_key_vault" "shared" { - resource_group_name = data.azurerm_resource_group.shared.name - name = "kv-credentials-${local.shared_suffix}" -} - -data "azurerm_key_vault_secret" "shared_pat" { - key_vault_id = data.azurerm_key_vault.shared.id - name = "pat" -} - -data "azurerm_key_vault_secret" "shared_id_rsa" { - key_vault_id = data.azurerm_key_vault.shared.id - name = "id-rsa" -} - -data "azurerm_key_vault_secret" "shared_id_rsa_pub" { - key_vault_id = data.azurerm_key_vault.shared.id - name = "id-rsa-pub" -} diff --git a/tests/azure/terraform/shared/.terraform.lock.hcl b/tests/azure/terraform/shared/.terraform.lock.hcl deleted file mode 100644 index 749c3583..00000000 --- a/tests/azure/terraform/shared/.terraform.lock.hcl +++ /dev/null @@ -1,58 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/azuread" { - version = "1.6.0" - constraints = "1.6.0" - hashes = [ - "h1:BlO53mX+Y2W//YqlCKvoxzofegFQk636XlKtmZYH0PY=", - "zh:0db70045a464d325fdb3d71809f0467844c3e2fcf1349e568bc51ad5035c99d9", - "zh:3629f1d7b4eba48d744b24c7cf7fe878d5ef5910a36b525507bd3d588010ccec", - "zh:5a73a45b6d1ff353810cc9b00d7c90a2fb328ba0a9ef3d24392b1500fb98741a", - "zh:7a6a9c390cf1bf752321abb8d0643c9f623e8c2ad871dfb378d64c9d90fada2d", - "zh:7d6de55d326b046dabc16bd7b655f008ff780c36ffc884b139a7c7da37b446d5", - "zh:8d725c618396ccae290e411296c892e08e776c3e9e5a82b0ef1f633a917146ec", - "zh:a206d1d8042bf66ca12b97334bbd6fcdf12fd6131f8cb4547c82b9fa7a701612", - "zh:b03ab4ff07dcb5ed8be8b0619c6ec9fb0da0c83594ccb0a1bff72f346083b530", - "zh:b6131f9d438b340a4016c770b569139ec7ac2532358a8ab783234e8c93d141d5", - "zh:ce9372d38e9e62accfd54f4669753000d3dcbae4b45686d74630eb63eb879f37", - "zh:df9a607c333d464d8bdeb248b1ff41e493c1d0661453a1e1ce396b89952a74ee", - ] -} - -provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.76.0" - constraints = "2.76.0" - hashes = [ - "h1:kF+u0s0DPnE5gMKhzQACWRUIdwZG1Ax4atXt9hk1J8M=", - "zh:137eb7c07d3d3c9fe123e74381c108c4442efba9fc051faa2ca603503ff2840f", - "zh:142a354dffd59a1d6b7f1614ab66a468ace3636d95933589a8d704ee8dbc4ea6", - "zh:4c343b4da8b86e4213c1b11f73337cec73a55b1fa95a0e0e0c79f34597d37cc3", - "zh:75d3109d48726fdbaad840d2fa294ec3362b32a3628c261af00f5c5608427521", - "zh:7b1e78c144c6ad2beebc798abb9e76c725bf34ced41df36dc0120a0f2426e801", - "zh:981235b01c3d4acf94c78cdd96624fd01d0a3622bc06b5c62aef3e788f1481c3", - "zh:bad819efae7293ce371409e1ed34197c3e879f61d3e44893af0ce68e6aaffde7", - "zh:c8008967722929deccfec9695754ae55028ce12311c321ae7a7c753dde162a44", - "zh:d38513d1138864269b2ff333b08a64a7949630d489f18e660630bbaff3b7ebb8", - "zh:e1f64d2d91b5f5cba6a9c5d35278a4918d332d7385a87f8e3466aaadb782a90f", - "zh:e93a377a1e823df69718686703b07f1712046eeb742006022e982f2e8a594161", - ] -} - -provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" - hashes = [ - "h1:BZMEPucF+pbu9gsPk0G0BHx7YP04+tKdq2MrRDF1EDM=", - "zh:2bbb3339f0643b5daa07480ef4397bd23a79963cc364cdfbb4e86354cb7725bc", - "zh:3cd456047805bf639fbf2c761b1848880ea703a054f76db51852008b11008626", - "zh:4f251b0eda5bb5e3dc26ea4400dba200018213654b69b4a5f96abee815b4f5ff", - "zh:7011332745ea061e517fe1319bd6c75054a314155cb2c1199a5b01fe1889a7e2", - "zh:738ed82858317ccc246691c8b85995bc125ac3b4143043219bd0437adc56c992", - "zh:7dbe52fac7bb21227acd7529b487511c91f4107db9cc4414f50d04ffc3cab427", - "zh:a3a9251fb15f93e4cfc1789800fc2d7414bbc18944ad4c5c98f466e6477c42bc", - "zh:a543ec1a3a8c20635cf374110bd2f87c07374cf2c50617eee2c669b3ceeeaa9f", - "zh:d9ab41d556a48bd7059f0810cf020500635bfc696c9fc3adab5ea8915c1d886b", - "zh:d9e13427a7d011dbd654e591b0337e6074eef8c3b9bb11b2e39eaaf257044fd7", - "zh:f7605bd1437752114baf601bdf6931debe6dc6bfe3006eb7e9bb9080931dca8a", - ] -} diff --git a/tests/azure/terraform/shared/acr.tf b/tests/azure/terraform/shared/acr.tf deleted file mode 100644 index 72a132c6..00000000 --- a/tests/azure/terraform/shared/acr.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "azurerm_container_registry" "this" { - name = "acrapps${random_pet.suffix.id}" - resource_group_name = azurerm_resource_group.this.name - location = azurerm_resource_group.this.location - sku = "standard" -} diff --git a/tests/azure/terraform/shared/keyvault.tf b/tests/azure/terraform/shared/keyvault.tf deleted file mode 100644 index baf1eca0..00000000 --- a/tests/azure/terraform/shared/keyvault.tf +++ /dev/null @@ -1,43 +0,0 @@ -resource "azurerm_key_vault" "this" { - name = "kv-credentials-${random_pet.suffix.id}" - resource_group_name = azurerm_resource_group.this.name - location = azurerm_resource_group.this.location - tenant_id = data.azurerm_client_config.current.tenant_id - sku_name = "standard" -} - -resource "azurerm_key_vault_access_policy" "admin" { - key_vault_id = azurerm_key_vault.this.id - tenant_id = data.azurerm_client_config.current.tenant_id - object_id = data.azurerm_client_config.current.object_id - - key_permissions = [ - "Backup", - "Create", - "Decrypt", - "Delete", - "Encrypt", - "Get", - "Import", - "List", - "Purge", - "Recover", - "Restore", - "Sign", - "UnwrapKey", - "Update", - "Verify", - "WrapKey", - ] - - secret_permissions = [ - "Backup", - "Delete", - "Get", - "List", - "Purge", - "Recover", - "Restore", - "Set", - ] -} diff --git a/tests/azure/terraform/shared/main.tf b/tests/azure/terraform/shared/main.tf deleted file mode 100644 index 3be9d61f..00000000 --- a/tests/azure/terraform/shared/main.tf +++ /dev/null @@ -1,39 +0,0 @@ -terraform { - backend "azurerm" { - resource_group_name = "terraform-state" - storage_account_name = "terraformstate0419" - container_name = "shared-tfstate" - key = "prod.terraform.tfstate" - } - - required_version = "1.0.7" - - required_providers { - azurerm = { - source = "hashicorp/azurerm" - version = "2.76.0" - } - azuread = { - source = "hashicorp/azuread" - version = "1.6.0" - } - } -} - -provider "azurerm" { - features {} -} - -resource "random_pet" "suffix" { - length = 1 - separator = "" -} - -data "azurerm_client_config" "current" {} - -data "azurerm_subscription" "current" {} - -resource "azurerm_resource_group" "this" { - name = "e2e-shared" - location = "West Europe" -} diff --git a/tests/azure/terraform/shared/outputs.tf b/tests/azure/terraform/shared/outputs.tf deleted file mode 100644 index ff0e09f4..00000000 --- a/tests/azure/terraform/shared/outputs.tf +++ /dev/null @@ -1,18 +0,0 @@ -output "azure_devops_sp" { - value = { - client_id = azuread_service_principal.azure_devops.application_id - client_secret = azuread_application_password.azure_devops.value - } - sensitive = true -} - -output "github_sp" { - value = { - tenant_id = data.azurerm_client_config.current.tenant_id - subscription_id = data.azurerm_client_config.current.subscription_id - client_id = azuread_service_principal.github.application_id - client_secret = azuread_application_password.github.value - } - sensitive = true -} - diff --git a/tests/azure/terraform/shared/service-principal.tf b/tests/azure/terraform/shared/service-principal.tf deleted file mode 100644 index eaed3a6e..00000000 --- a/tests/azure/terraform/shared/service-principal.tf +++ /dev/null @@ -1,105 +0,0 @@ -# Service Principal used by Azure DevOps to push OCI and Helm Charts -resource "azuread_application" "azure_devops" { - display_name = "azure-devops-${random_pet.suffix.id}" - - required_resource_access { - resource_app_id = "00000003-0000-0000-c000-000000000000" - - resource_access { - id = "df021288-bdef-4463-88db-98f22de89214" - type = "Role" - } - } - - required_resource_access { - resource_app_id = "00000002-0000-0000-c000-000000000000" - - resource_access { - id = "1cda74f2-2616-4834-b122-5cb1b07f8a59" - type = "Role" - } - resource_access { - id = "78c8a3c8-a07e-4b9e-af1b-b5ccab50a175" - type = "Role" - } - } -} - -resource "azuread_application_password" "azure_devops" { - display_name = "password" - application_object_id = azuread_application.azure_devops.object_id -} - -resource "azuread_service_principal" "azure_devops" { - application_id = azuread_application.azure_devops.application_id -} - -resource "azurerm_role_assignment" "azure_devops_acr" { - scope = azurerm_container_registry.this.id - role_definition_name = "Contributor" - principal_id = azuread_service_principal.azure_devops.object_id -} - -# Service Principal that is used to run the tests in GitHub Actions -resource "azuread_application" "github" { - display_name = "github-${random_pet.suffix.id}" - - required_resource_access { - resource_app_id = "00000003-0000-0000-c000-000000000000" - - resource_access { - id = "df021288-bdef-4463-88db-98f22de89214" - type = "Role" - } - } - - required_resource_access { - resource_app_id = "00000002-0000-0000-c000-000000000000" - - resource_access { - id = "1cda74f2-2616-4834-b122-5cb1b07f8a59" - type = "Role" - } - resource_access { - id = "78c8a3c8-a07e-4b9e-af1b-b5ccab50a175" - type = "Role" - } - } -} - -resource "azuread_application_password" "github" { - display_name = "password" - application_object_id = azuread_application.github.object_id -} - -resource "azuread_service_principal" "github" { - application_id = azuread_application.github.application_id -} - -data "azurerm_storage_account" "terraform_state" { - resource_group_name = "terraform-state" - name = "terraformstate0419" -} - -resource "azurerm_role_assignment" "github_resource_group" { - scope = data.azurerm_subscription.current.id - role_definition_name = "Contributor" - principal_id = azuread_service_principal.github.object_id -} - -resource "azurerm_role_assignment" "github_acr" { - scope = azurerm_container_registry.this.id - role_definition_name = "Owner" - principal_id = azuread_service_principal.github.object_id -} - -resource "azurerm_key_vault_access_policy" "github_keyvault_secret_read" { - key_vault_id = azurerm_key_vault.this.id - tenant_id = data.azurerm_client_config.current.tenant_id - object_id = azuread_service_principal.github.object_id - - secret_permissions = [ - "Get", - "List", - ] -} diff --git a/tests/azure/terraform/shared/variables.tf b/tests/azure/terraform/shared/variables.tf deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/azure/util_test.go b/tests/azure/util_test.go deleted file mode 100644 index 958b40fd..00000000 --- a/tests/azure/util_test.go +++ /dev/null @@ -1,476 +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 test - -import ( - "context" - "fmt" - "os" - "os/exec" - "path/filepath" - "time" - - git2go "github.com/libgit2/git2go/v31" - - corev1 "k8s.io/api/core/v1" - apimeta "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" - automationv1beta1 "github.com/fluxcd/image-automation-controller/api/v1beta1" - reflectorv1beta1 "github.com/fluxcd/image-reflector-controller/api/v1beta1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - notiv1beta1 "github.com/fluxcd/notification-controller/api/v1beta1" - "github.com/fluxcd/pkg/apis/meta" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" -) - -const defaultBranch = "main" - -// getKubernetesCredentials returns a path to a kubeconfig file and a kube client instance. -func getKubernetesCredentials(kubeconfig, aksHost, aksCert, aksKey, aksCa string) (string, client.Client, error) { - tmpDir, err := os.MkdirTemp("", "*-azure-e2e") - if err != nil { - return "", nil, err - } - kubeconfigPath := fmt.Sprintf("%s/kubeconfig", tmpDir) - os.WriteFile(kubeconfigPath, []byte(kubeconfig), 0750) - kubeCfg := &rest.Config{ - Host: aksHost, - TLSClientConfig: rest.TLSClientConfig{ - CertData: []byte(aksCert), - KeyData: []byte(aksKey), - CAData: []byte(aksCa), - }, - } - err = sourcev1.AddToScheme(scheme.Scheme) - if err != nil { - return "", nil, err - } - err = kustomizev1.AddToScheme(scheme.Scheme) - if err != nil { - return "", nil, err - } - err = helmv2beta1.AddToScheme(scheme.Scheme) - if err != nil { - return "", nil, err - } - err = reflectorv1beta1.AddToScheme(scheme.Scheme) - if err != nil { - return "", nil, err - } - err = automationv1beta1.AddToScheme(scheme.Scheme) - if err != nil { - return "", nil, err - } - err = notiv1beta1.AddToScheme(scheme.Scheme) - if err != nil { - return "", nil, err - } - kubeClient, err := client.New(kubeCfg, client.Options{Scheme: scheme.Scheme}) - if err != nil { - return "", nil, err - } - return kubeconfigPath, kubeClient, nil -} - -// installFlux adds the core Flux components to the cluster specified in the kubeconfig file. -func installFlux(ctx context.Context, kubeClient client.Client, kubeconfigPath, repoUrl, azdoPat string, sp spConfig) error { - namespace := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "flux-system", - }, - } - _, err := controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &namespace, func() error { - return nil - }) - httpsCredentials := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "https-credentials", Namespace: "flux-system"}} - _, err = controllerutil.CreateOrUpdate(ctx, kubeClient, httpsCredentials, func() error { - httpsCredentials.StringData = map[string]string{ - "username": "git", - "password": azdoPat, - } - return nil - }) - if err != nil { - return err - } - azureSp := &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "azure-sp", Namespace: "flux-system"}} - _, err = controllerutil.CreateOrUpdate(ctx, kubeClient, azureSp, func() error { - azureSp.StringData = map[string]string{ - "AZURE_TENANT_ID": sp.tenantId, - "AZURE_CLIENT_ID": sp.clientId, - "AZURE_CLIENT_SECRET": sp.clientSecret, - } - return nil - }) - if err != nil { - return err - } - - // Install Flux and push files to git repository - repo, repoDir, err := getRepository(repoUrl, defaultBranch, true, azdoPat) - if err != nil { - return err - } - err = runCommand(ctx, repoDir, "mkdir -p ./clusters/e2e/flux-system") - if err != nil { - return err - } - err = runCommand(ctx, repoDir, "flux install --components-extra=\"image-reflector-controller,image-automation-controller\" --export > ./clusters/e2e/flux-system/gotk-components.yaml") - if err != nil { - return err - } - err = runCommand(ctx, repoDir, fmt.Sprintf("flux create source git flux-system --git-implementation=libgit2 --url=%s --branch=%s --secret-ref=https-credentials --interval=1m --export > ./clusters/e2e/flux-system/gotk-sync.yaml", repoUrl, defaultBranch)) - if err != nil { - return err - } - err = runCommand(ctx, repoDir, "flux create kustomization flux-system --source=flux-system --path='./clusters/e2e' --prune=true --interval=1m --export >> ./clusters/e2e/flux-system/gotk-sync.yaml") - if err != nil { - return err - } - kustomizeYaml := ` - resources: - - gotk-components.yaml - - gotk-sync.yaml - patchesStrategicMerge: - - |- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: kustomize-controller - namespace: flux-system - spec: - template: - spec: - containers: - - name: manager - envFrom: - - secretRef: - name: azure-sp - - |- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: source-controller - namespace: flux-system - spec: - template: - spec: - containers: - - name: manager - envFrom: - - secretRef: - name: azure-sp - ` - err = runCommand(ctx, repoDir, fmt.Sprintf("echo \"%s\" > ./clusters/e2e/flux-system/kustomization.yaml", kustomizeYaml)) - if err != nil { - return err - } - err = commitAndPushAll(repo, defaultBranch, azdoPat) - if err != nil { - return err - } - // Need to apply CRDs first to make sure that the sync resources will apply properly - err = runCommand(ctx, repoDir, fmt.Sprintf("kubectl --kubeconfig=%s apply -f ./clusters/e2e/flux-system/gotk-components.yaml", kubeconfigPath)) - if err != nil { - return err - } - err = runCommand(ctx, repoDir, fmt.Sprintf("kubectl --kubeconfig=%s apply -k ./clusters/e2e/flux-system/", kubeconfigPath)) - if err != nil { - return err - } - return nil -} - -func runCommand(ctx context.Context, dir, command string) error { - timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) - defer cancel() - cmd := exec.CommandContext(timeoutCtx, "bash", "-c", command) - cmd.Dir = dir - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("failure to run command %s: %v", string(output), err) - } - return nil -} - -// verifyGitAndKustomization checks that the gitrespository and kustomization combination are working properly. -func verifyGitAndKustomization(ctx context.Context, kubeClient client.Client, namespace, name string) error { - nn := types.NamespacedName{ - Name: name, - Namespace: namespace, - } - source := &sourcev1.GitRepository{} - err := kubeClient.Get(ctx, nn, source) - if err != nil { - return err - } - if apimeta.IsStatusConditionPresentAndEqual(source.Status.Conditions, meta.ReadyCondition, metav1.ConditionTrue) == false { - return fmt.Errorf("source condition not ready") - } - kustomization := &kustomizev1.Kustomization{} - err = kubeClient.Get(ctx, nn, kustomization) - if err != nil { - return err - } - if apimeta.IsStatusConditionPresentAndEqual(kustomization.Status.Conditions, meta.ReadyCondition, metav1.ConditionTrue) == false { - return fmt.Errorf("kustomization condition not ready") - } - return nil -} - -func setupNamespace(ctx context.Context, kubeClient client.Client, repoUrl, password, name string) error { - namespace := corev1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - }, - } - _, err := controllerutil.CreateOrUpdate(ctx, kubeClient, &namespace, func() error { - return nil - }) - secret := corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "https-credentials", - Namespace: name, - }, - } - _, err = controllerutil.CreateOrUpdate(ctx, kubeClient, &secret, func() error { - secret.StringData = map[string]string{ - "username": "git", - "password": password, - } - return nil - }) - source := &sourcev1.GitRepository{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace.Name}} - _, err = controllerutil.CreateOrUpdate(ctx, kubeClient, source, func() error { - source.Spec = sourcev1.GitRepositorySpec{ - Interval: metav1.Duration{ - Duration: 1 * time.Minute, - }, - GitImplementation: sourcev1.LibGit2Implementation, - Reference: &sourcev1.GitRepositoryRef{ - Branch: name, - }, - SecretRef: &meta.LocalObjectReference{ - Name: "https-credentials", - }, - URL: repoUrl, - } - return nil - }) - if err != nil { - return err - } - kustomization := &kustomizev1.Kustomization{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace.Name}} - _, err = controllerutil.CreateOrUpdate(ctx, kubeClient, kustomization, func() error { - kustomization.Spec = kustomizev1.KustomizationSpec{ - SourceRef: kustomizev1.CrossNamespaceSourceReference{ - Kind: sourcev1.GitRepositoryKind, - Name: source.Name, - Namespace: source.Namespace, - }, - Interval: metav1.Duration{ - Duration: 1 * time.Minute, - }, - Prune: true, - } - return nil - }) - if err != nil { - return err - } - return nil -} - -func getRepository(url, branchName string, overrideBranch bool, password string) (*git2go.Repository, string, error) { - checkoutBranch := defaultBranch - if overrideBranch == false { - checkoutBranch = branchName - } - - tmpDir, err := os.MkdirTemp("", "*-repository") - if err != nil { - return nil, "", err - } - repo, err := git2go.Clone(url, tmpDir, &git2go.CloneOptions{ - FetchOptions: &git2go.FetchOptions{ - RemoteCallbacks: git2go.RemoteCallbacks{ - CredentialsCallback: credentialCallback("git", password), - }, - }, - CheckoutBranch: checkoutBranch, - }) - if err != nil { - return nil, "", err - } - // Nothing to do further if correct branch is checked out - if checkoutBranch == branchName { - return repo, tmpDir, nil - } - head, err := repo.Head() - if err != nil { - return nil, "", err - } - headCommit, err := repo.LookupCommit(head.Target()) - if err != nil { - return nil, "", err - } - _, err = repo.CreateBranch(branchName, headCommit, true) - if err != nil { - return nil, "", err - } - return repo, tmpDir, nil -} - -func addFile(dir, path, content string) error { - err := os.WriteFile(filepath.Join(dir, path), []byte(content), 0777) - if err != nil { - return err - } - return nil -} - -func commitAndPushAll(repo *git2go.Repository, branchName, password string) error { - idx, err := repo.Index() - if err != nil { - return err - } - err = idx.AddAll([]string{}, git2go.IndexAddDefault, nil) - if err != nil { - return err - } - treeId, err := idx.WriteTree() - if err != nil { - return err - } - err = idx.Write() - if err != nil { - return err - } - tree, err := repo.LookupTree(treeId) - if err != nil { - return err - } - branch, err := repo.LookupBranch(branchName, git2go.BranchLocal) - if err != nil { - return err - } - commitTarget, err := repo.LookupCommit(branch.Target()) - if err != nil { - return err - } - sig := &git2go.Signature{ - Name: "git", - Email: "test@example.com", - When: time.Now(), - } - _, err = repo.CreateCommit(fmt.Sprintf("refs/heads/%s", branchName), sig, sig, "add file", tree, commitTarget) - if err != nil { - return err - } - origin, err := repo.Remotes.Lookup("origin") - if err != nil { - return err - } - err = origin.Push([]string{fmt.Sprintf("+refs/heads/%s", branchName)}, &git2go.PushOptions{ - RemoteCallbacks: git2go.RemoteCallbacks{ - CredentialsCallback: credentialCallback("git", password), - }, - }) - if err != nil { - return err - } - return nil -} - -func createTagAndPush(repo *git2go.Repository, branchName, tag, password string) error { - branch, err := repo.LookupBranch(branchName, git2go.BranchAll) - if err != nil { - return err - } - commit, err := repo.LookupCommit(branch.Target()) - if err != nil { - return err - } - - tags, err := repo.Tags.List() - if err != nil { - return err - } - for _, existingTag := range tags { - if existingTag == tag { - err = repo.Tags.Remove(tag) - if err != nil { - return err - } - } - } - - sig := &git2go.Signature{ - Name: "git", - Email: "test@example.com", - When: time.Now(), - } - _, err = repo.Tags.Create(tag, commit, sig, "create tag") - if err != nil { - return err - } - origin, err := repo.Remotes.Lookup("origin") - if err != nil { - return err - } - err = origin.Push([]string{fmt.Sprintf("+refs/tags/%s", tag)}, &git2go.PushOptions{ - RemoteCallbacks: git2go.RemoteCallbacks{ - CredentialsCallback: credentialCallback("git", password), - }, - }) - if err != nil { - return err - } - return nil -} - -func credentialCallback(username, password string) git2go.CredentialsCallback { - return func(url string, usernameFromURL string, allowedTypes git2go.CredType) (*git2go.Cred, error) { - cred, err := git2go.NewCredentialUserpassPlaintext(username, password) - if err != nil { - return nil, err - } - return cred, nil - } -} - -func getTestManifest(namespace string) string { - return fmt.Sprintf(` -apiVersion: v1 -kind: Namespace -metadata: - name: %s ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: foobar - namespace: %s -`, namespace, namespace) -} diff --git a/tests/image-automation/auto.yaml b/tests/image-automation/auto.yaml index 321012c0..a0f7bce2 100644 --- a/tests/image-automation/auto.yaml +++ b/tests/image-automation/auto.yaml @@ -1,4 +1,4 @@ -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageRepository metadata: name: podinfo @@ -7,7 +7,7 @@ spec: image: ghcr.io/stefanprodan/podinfo interval: 1m0s --- -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImagePolicy metadata: name: podinfo @@ -19,7 +19,7 @@ spec: semver: range: 5.2.x --- -apiVersion: image.toolkit.fluxcd.io/v1beta1 +apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImageUpdateAutomation metadata: name: flux-system diff --git a/tests/integration/.env.sample b/tests/integration/.env.sample new file mode 100644 index 00000000..ad9e1fe7 --- /dev/null +++ b/tests/integration/.env.sample @@ -0,0 +1,29 @@ +## Azure +# export TF_VAR_azuredevops_org= +# export TF_VAR_azuredevops_pat= +# export TF_VAR_azure_location= +## Set the following only when authenticating using Service Principal (suited +## for CI environment). +# export ARM_CLIENT_ID= +# export ARM_CLIENT_SECRET= +# export ARM_SUBSCRIPTION_ID= +# export ARM_TENANT_ID= + +## GCP +# export TF_VAR_gcp_project_id= +# export TF_VAR_gcp_zone= +# export TF_VAR_gcp_region= +# export TF_VAR_gcp_keyring= +# export TF_VAR_gcp_crypto_key= +## Email address of a GCP user used for git repository cloning over ssh. +# export TF_VAR_gcp_email= +## Set the following only when using service account. +## Provide absolute path to the service account JSON key file. +# export GOOGLE_APPLICATION_CREDENTIALS= + +## Common variables +# export TF_VAR_tags='{"environment"="dev", "createdat"='"\"$(date -u +x%Y-%m-%d_%Hh%Mm%Ss)\""'}' +## These are not terraform variables +## but they are needed for the bootstrap tests +# export GITREPO_SSH_PATH= +# export GITREPO_SSH_PUB_PATH= diff --git a/tests/integration/Makefile b/tests/integration/Makefile new file mode 100644 index 00000000..ddae646a --- /dev/null +++ b/tests/integration/Makefile @@ -0,0 +1,32 @@ +GO_TEST_ARGS ?= +PROVIDER_ARG ?= +TEST_TIMEOUT ?= 60m +FLUX_BINARY ?= ../../bin/flux + +test: sops-check + mkdir -p build + cp $(FLUX_BINARY) build/flux + # These two versions of podinfo are pushed to the cloud registry and used in tests for ImageUpdateAutomation + docker pull ghcr.io/stefanprodan/podinfo:6.0.0 + docker pull ghcr.io/stefanprodan/podinfo:6.0.1 + go test -timeout $(TEST_TIMEOUT) -v ./ $(GO_TEST_ARGS) $(PROVIDER_ARG) + +test-azure: + $(MAKE) test PROVIDER_ARG="-provider azure" GO_TEST_ARGS="--tags azure $(GO_TEST_ARGS)" + +test-gcp: + $(MAKE) test PROVIDER_ARG="-provider gcp" + +destroy: + go test -timeout $(TEST_TIMEOUT) -v ./ $(GO_TEST_ARGS) $(PROVIDER_ARG) -destroy-only + +destroy-azure: + $(MAKE) destroy PROVIDER_ARG="-provider azure" + +destroy-gcp: + $(MAKE) destroy PROVIDER_ARG="-provider gcp" + +sops-check: +ifeq ($(shell which sops),) + $(error "no sops in PATH, consider installing") +endif diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 00000000..0fa1f3e3 --- /dev/null +++ b/tests/integration/README.md @@ -0,0 +1,390 @@ +# E2E Tests + +The goal is to verify that Flux integration with cloud providers are actually working now and in the future. +Currently, we only have tests for Azure and GCP. + +## General requirements + +These CLI tools need to be installed for each of the tests to run successfully. + +- Docker CLI for registry login. +- [SOPS CLI](https://github.com/mozilla/sops) for encrypting files +- kubectl for applying certain install manifests. + +## Azure + +### Architecture + +The [azure](./terraform/azure) Terraform creates the AKS cluster and related resources to run the tests. It creates: +- An Azure Container Registry +- An Azure Kubernetes Cluster +- Two Azure DevOps repositories +- Azure EventHub for sending notifications +- An Azure Key Vault + +### Requirements + +- Azure account with an active subscription to be able to create AKS and ACR, + and permission to assign roles. Role assignment is required for allowing AKS workloads to access ACR. +- Azure CLI, need to be logged in using `az login` as a User or as a Service Principal +- An Azure DevOps organization, personal access token and ssh keys for accessing repositories + within the organization. The scope required for the personal access token is: + - `Project and Team` - read, write and manage access + - `Code` - Full + - Please take a look at the [terraform provider](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/guides/authenticating_using_the_personal_access_token#create-a-personal-access-token) + for more explanation. + - Azure DevOps only supports RSA keys. Please see + [documentation](https://learn.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops#set-up-ssh-key-authentication) + for how to set up SSH key authentication. + - When using in CI, create a test user and use the test user's PAT and SSH key + for all Azure DevOps interactions. To grant the test user access in Azure + DevOps: + - Go to `Organization Settings` on the sidebar of the organization page. + - Under `General` > `Users`, click on `Add User` and input the user's email, + select `Access Level` of `Basic`. + - Go to `Security` > `Permissions`, click on the `User` tab. + - For the invited user, set the following permissions to `Allow`: + - `General: Create new project`. + - The user will get an email invitation and would need to create a Microsoft + account if they don't have one yet. + +**NOTE:** To use Service Principal (for example in CI environment), set the +`ARM-*` variables in `.env`, source it and authenticate Azure CLI with: +```console +$ az login --service-principal -u $ARM_CLIENT_ID -p $ARM_CLIENT_SECRET --tenant $ARM_TENANT_ID +``` + +### Permissions + +Following permissions are needed for provisioning the infrastructure and running +the tests: +- `Microsoft.Kubernetes/*` +- `Microsoft.Resources/*` +- `Microsoft.Authorization/roleAssignments/{Read,Write,Delete}` +- `Microsoft.ContainerRegistry/*` +- `Microsoft.ContainerService/*` +- `Microsoft.KeyVault/*` +- `Microsoft.EventHub/*` + +### IAM and CI setup + +To create the necessary IAM role with all the permissions, set up CI secrets and +variables using +[azure-gh-actions](https://github.com/fluxcd/test-infra/tree/main/tf-modules/azure/github-actions) +use the terraform configuration below. Please make sure all the requirements of +azure-gh-actions are followed before running it. + +**NOTE:** When running the following for a repo under an organization, set the +environment variable `GITHUB_ORGANIZATION` if setting the `owner` in the +`github` provider doesn't work. + +```hcl +provider "github" { + owner = "fluxcd" +} + +resource "tls_private_key" "privatekey" { + algorithm = "RSA" + rsa_bits = 4096 +} + +module "azure_gh_actions" { + source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/azure/github-actions" + + azure_owners = ["owner-id-1", "owner-id-2"] + azure_app_name = "flux2-e2e" + azure_app_description = "flux2 e2e" + azure_app_secret_name = "flux2-e2e" + azure_permissions = [ + "Microsoft.Kubernetes/*", + "Microsoft.Resources/*", + "Microsoft.Authorization/roleAssignments/Read", + "Microsoft.Authorization/roleAssignments/Write", + "Microsoft.Authorization/roleAssignments/Delete", + "Microsoft.ContainerRegistry/*", + "Microsoft.ContainerService/*", + "Microsoft.KeyVault/*", + "Microsoft.EventHub/*" + ] + azure_location = "eastus" + + github_project = "flux2" + + github_secret_client_id_name = "AZ_ARM_CLIENT_ID" + github_secret_client_secret_name = "AZ_ARM_CLIENT_SECRET" + github_secret_subscription_id_name = "AZ_ARM_SUBSCRIPTION_ID" + github_secret_tenant_id_name = "AZ_ARM_TENANT_ID" + + github_secret_custom = { + "TF_VAR_azuredevops_org" = "", + "TF_VAR_azuredevops_pat" = "", + "AZURE_GITREPO_SSH_CONTENTS" = base64encode(tls_private_key.privatekey.private_key_openssh), + "AZURE_GITREPO_SSH_PUB_CONTENTS" = base64encode(tls_private_key.privatekey.public_key_openssh) + } +} + +output "publickey" { + value = tls_private_key.privatekey.public_key_openssh +} +``` + +Copy the `publickey` output printed after applying, or run `terraform output` to +print it again, and add it in the Azure DevOps SSH public keys under the user +account that'll be used by flux in the tests. + +**NOTE:** The environment variables used above are for the GitHub workflow that +runs the tests. Change the variable names if needed accordingly. + +## GCP + +### Architecture + +The [gcp](./terraform/gcp) terraform files create the GKE cluster and related resources to run the tests. It creates: +- A Google Container Registry and Artifact Registry +- A Google Kubernetes Cluster +- Two Google Cloud Source Repositories +- A Google Pub/Sub Topic and a subscription to the service that would be used in the tests + +Note: It doesn't create Google KMS keyrings and crypto keys because these cannot be destroyed. Instead, you have +to pass in the crypto key and keyring that would be used to test the sops encryption in Flux. Please see `.env.sample` +for the terraform variables + +### Requirements + +- GCP account with an active project to be able to create GKE and GCR, and permission to assign roles. +- Existing GCP KMS keyring and crypto key. + - [Create a Keyring](https://cloud.google.com/kms/docs/create-key-ring) in + `global` location. + - [Create a Crypto Key](https://cloud.google.com/kms/docs/create-key) with + symmetric algorithm for encryption and decryption, and software based + protection level. +- gcloud CLI, need to be logged in using `gcloud auth login` as a User (not a + Service Account), configure application default credentials with `gcloud auth + application-default login` and docker credential helper with `gcloud auth configure-docker`. + + **NOTE:** To use Service Account (for example in CI environment), set + `GOOGLE_APPLICATION_CREDENTIALS` variable in `.env` with the path to the JSON + key file, source it and authenticate gcloud CLI with: + ```console + $ gcloud auth activate-service-account --key-file=$GOOGLE_APPLICATION_CREDENTIALS + ``` + Depending on the Container/Artifact Registry host used in the test, authenticate + docker accordingly + ```console + $ gcloud auth print-access-token | docker login -u oauth2accesstoken --password-stdin https://us-central1-docker.pkg.dev + ``` + In this case, the GCP client in terraform uses the Service Account to + authenticate and the gcloud CLI is used only to authenticate with Google + Container Registry and Google Artifact Registry. + + **NOTE FOR CI USAGE:** When saving the JSON key file as a CI secret, compress + the file content with + ```console + $ cat key.json | jq -r tostring + ``` + to prevent aggressive masking in the logs. Refer + [aggressive replacement in logs](https://github.com/google-github-actions/auth/blob/v1.1.0/docs/TROUBLESHOOTING.md#aggressive--replacement-in-logs) + for more details. +- Register [SSH Keys with Google Cloud](https://cloud.google.com/source-repositories/docs/authentication#ssh) + - Google Cloud supports these three SSH key types: RSA (only for keys with + more than 2048 bits), ECDSA and ED25519. + - The SSH user doesn't have to be a member of the GCP project. The terraform + setup will grant the user permissions to the repository. Visit + https://source.cloud.google.com, login or create a GCP account with the SSH + user's email address and add SSH keys in the account. Set this email as the + value for the environment variable `TF_VAR_gcp_email` in `.env` file to be + used as a terraform variable. + + **Note:** Google doesn't allow a SSH key to be associated with a service + account email address. Therefore, there has to be an actual user that the SSH + key is registered to. + +### Permissions + +Following roles are needed for provisioning the infrastructure and running the tests: + +- Compute Instance Admin (v1) - `roles/compute.instanceAdmin.v1` +- Kubernetes Engine Admin - `roles/container.admin` +- Service Account User - `roles/iam.serviceAccountUser` +- Service Account Token Creator - `roles/iam.serviceAccountTokenCreator` +- Artifact Registry Administrator - `roles/artifactregistry.admin` +- Artifact Registry Repository Administrator - `roles/artifactregistry.repoAdmin` +- Cloud KMS Admin - `roles/cloudkms.admin` +- Cloud KMS CryptoKey Encrypter - `roles/cloudkms.cryptoKeyEncrypter` +- Source Repository Administrator - `roles/source.admin` +- Pub/Sub Admin - `roles/pubsub.admin` + +### IAM and CI setup + +To create the necessary IAM role with all the permissions, set up CI secrets and +variables using +[gcp-gh-actions](https://github.com/fluxcd/test-infra/tree/main/tf-modules/gcp/github-actions) +use the terraform configuration below. Please make sure all the requirements of +gcp-gh-actions are followed before running it. + +**NOTE:** When running the following for a repo under an organization, set the +environment variable `GITHUB_ORGANIZATION` if setting the `owner` in the +`github` provider doesn't work. + +```hcl +provider "google" {} + +provider "github" { + owner = "fluxcd" +} + +resource "tls_private_key" "privatekey" { + algorithm = "RSA" + rsa_bits = 4096 +} + +module "gcp_gh_actions" { + source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/gcp/github-actions" + + gcp_service_account_id = "flux2-e2e-test" + gcp_service_account_name = "flux2-e2e-test" + gcp_service_account_description = "For running fluxcd/flux2 e2e tests." + gcp_roles = [ + "roles/compute.instanceAdmin.v1", + "roles/container.admin", + "roles/iam.serviceAccountUser", + "roles/iam.serviceAccountTokenCreator", + "roles/artifactregistry.admin", + "roles/artifactregistry.repoAdmin", + "roles/cloudkms.admin", + "roles/cloudkms.cryptoKeyEncrypter", + "roles/source.admin", + "roles/pubsub.admin" + ] + + github_project = "flux2" + + github_secret_credentials_name = "FLUX2_E2E_GOOGLE_CREDENTIALS" + + github_secret_custom = { + "TF_VAR_gcp_keyring" = "", + "TF_VAR_gcp_crypto_key" = "", + "TF_VAR_gcp_email" = "", + "GCP_GITREPO_SSH_CONTENTS" = base64encode(tls_private_key.privatekey.private_key_openssh), + "GCP_GITREPO_SSH_PUB_CONTENTS" = base64encode(tls_private_key.privatekey.public_key_openssh) + } +} + +output "publickey" { + value = tls_private_key.privatekey.public_key_openssh +} +``` + +Copy the `publickey` output printed after applying, or run `terraform output` to +print it again, and add it in the Google Source Repository SSH public keys under +the user account with email address referred in `TF_VAR_gcp_email` above. + +**NOTE:** The environment variables used above are for the GitHub workflow that +runs the tests. Change the variable names if needed accordingly. + +## Tests + +Each test run is initiated by running `terraform apply` in the provider's terraform directory e.g terraform apply, +it does this by using the [tftestenv package](https://github.com/fluxcd/test-infra/blob/main/tftestenv/testenv.go) +within the `fluxcd/test-infra` repository. It then reads the output of the Terraform to get information needed +for the tests like the kubernetes client ID, the cloud repository urls, the key vault ID etc. This means that +a lot of the communication with the cloud provider API is offset to Terraform instead of requiring it to be implemented in the test. + +The following tests are currently implemented: + +- Flux can be successfully installed on the cluster using the Flux CLI +- source-controller can clone cloud provider repositories (Azure DevOps, Google Cloud Source Repositories) (https+ssh) +- image-reflector-controller can list tags from provider container Registry image repositories +- kustomize-controller can decrypt secrets using SOPS and provider key vault +- image-automation-controller can create branches and push to cloud repositories (https+ssh) +- source-controller can pull charts from cloud provider container registry Helm repositories +- notification-controller can forward events to cloud Events Service(EventHub for Azure and Google Pub/Sub) + +The following tests are run only for Azure since it is supported in the notification-controller: + +- notification-controller can send commit status to Azure DevOps + +### Running tests locally + +1. Ensure that you have the Flux CLI binary that is to be tested built and ready. You can build it by running + `make build` at the root of this repository. The binary is located at `./bin` directory at the root and by default + this is where the Makefile copies the binary for the tests from. If you have it in a different location, you can set it + with the `FLUX_BINARY` variable +2. Copy `.env.sample` to `.env` and add the values for the different variables for the provider that you are running the tests for. +3. Run `make test-`, setting the location of the flux binary with `FLUX_BINARY` variable + +```console +$ make test-azure +make test PROVIDER_ARG="-provider azure" +# These two versions of podinfo are pushed to the cloud registry and used in tests for ImageUpdateAutomation +mkdir -p build +cp ../../bin/flux build/flux +docker pull ghcr.io/stefanprodan/podinfo:6.0.0 +6.0.0: Pulling from stefanprodan/podinfo +Digest: sha256:e7eeab287181791d36c82c904206a845e30557c3a4a66a8143fa1a15655dae97 +Status: Image is up to date for ghcr.io/stefanprodan/podinfo:6.0.0 +ghcr.io/stefanprodan/podinfo:6.0.0 +docker pull ghcr.io/stefanprodan/podinfo:6.0.1 +6.0.1: Pulling from stefanprodan/podinfo +Digest: sha256:1169f220a670cf640e45e1a7ac42dc381a441e9d4b7396432cadb75beb5b5d68 +Status: Image is up to date for ghcr.io/stefanprodan/podinfo:6.0.1 +ghcr.io/stefanprodan/podinfo:6.0.1 +go test -timeout 60m -v ./ -existing -provider azure --tags=integration +2023/03/24 02:32:25 Setting up azure e2e test infrastructure +2023/03/24 02:32:25 Terraform binary: /usr/local/bin/terraform +2023/03/24 02:32:25 Init Terraform +....[some output has been cut out] +2023/03/24 02:39:33 helm repository condition not ready +--- PASS: TestACRHelmRelease (15.31s) +=== RUN TestKeyVaultSops +--- PASS: TestKeyVaultSops (15.98s) +PASS +2023/03/24 02:40:12 Destroying environment... +ok github.com/fluxcd/flux2/tests/integration 947.341s +``` + +In the above, the test created a build directory build/ and the flux cli binary is copied build/flux. It would be used +to bootstrap Flux on the cluster. You can configure the location of the Flux CLI binary by setting the FLUX_BINARY variable. +We also pull two version of `ghcr.io/stefanprodan/podinfo` image. These images are pushed to the cloud provider's +Container Registry and used to test `ImageRepository` and `ImageUpdateAutomation`. The terraform resources get created +and the tests are run. + +If not configured explicitly to retain the infrastructure, at the end of the +test, the test infrastructure is deleted. In case of any failure due to which +the resources don't get deleted, the `make destroy-*` commands can be run for +the respective provider. This will run terraform destroy in the respective +provider's terraform configuration directory. This can be used to quickly +destroy the infrastructure without going through the provision-test-destroy +steps. + +### Debugging the tests + +For debugging environment provisioning, enable verbose output with `-verbose` test flag. + +```sh +make test-azure GO_TEST_ARGS="-verbose" +``` + +The test environment is destroyed at the end by default. Run the tests with -retain flag to retain the created test infrastructure. + +```sh +make test-azure GO_TEST_ARGS="-retain" +``` +The tests require the infrastructure state to be clean. For re-running the tests with a retained infrastructure, set -existing flag. + +```sh +make test-azure GO_TEST_ARGS="-retain -existing" +``` + +To delete an existing infrastructure created with -retain flag: + +```sh +make test-azure GO_TEST_ARGS="-existing" +``` + +To debug issues on the cluster created by the test (provided you passed in the `-retain` flag): + +```sh +export KUBECONFIG=./build/kubeconfig +kubectl get pods +``` diff --git a/tests/integration/azure_specific_test.go b/tests/integration/azure_specific_test.go new file mode 100644 index 00000000..7f84c76b --- /dev/null +++ b/tests/integration/azure_specific_test.go @@ -0,0 +1,210 @@ +//go:build azure +// +build azure + +/* +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 integration + +import ( + "context" + "fmt" + "io" + "log" + "strings" + "testing" + "time" + + giturls "github.com/chainguard-dev/git-urls" + "github.com/microsoft/azure-devops-go-api/azuredevops" + "github.com/microsoft/azure-devops-go-api/azuredevops/git" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notiv1 "github.com/fluxcd/notification-controller/api/v1" + notiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3" + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" +) + +func TestAzureDevOpsCommitStatus(t *testing.T) { + g := NewWithT(t) + + ctx := context.TODO() + branchName := "commit-status" + testID := branchName + randStringRunes(5) + manifest := `apiVersion: v1 +kind: ConfigMap +metadata: + name: foobar` + + repoUrl := getTransportURL(cfg.applicationRepository) + tmpDir := t.TempDir() + c, err := getRepository(ctx, tmpDir, repoUrl, defaultBranch, cfg.defaultAuthOpts) + g.Expect(err).ToNot(HaveOccurred()) + files := make(map[string]io.Reader) + files["configmap.yaml"] = strings.NewReader(manifest) + err = commitAndPushAll(ctx, c, files, branchName) + g.Expect(err).ToNot(HaveOccurred()) + + modifyKsSpec := func(spec *kustomizev1.KustomizationSpec) { + spec.HealthChecks = []meta.NamespacedObjectKindReference{ + { + APIVersion: "v1", + Kind: "ConfigMap", + Name: "foobar", + Namespace: testID, + }, + } + } + err = setUpFluxConfig(ctx, testID, nsConfig{ + ref: &sourcev1.GitRepositoryRef{ + Branch: branchName, + }, + repoURL: repoUrl, + path: "./", + modifyKsSpec: modifyKsSpec, + }) + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + err := tearDownFluxConfig(ctx, testID) + if err != nil { + log.Printf("failed to delete resources in '%s' namespace: %s", testID, err) + } + }) + + g.Eventually(func() bool { + err := verifyGitAndKustomization(ctx, testEnv, testID, testID) + if err != nil { + return false + } + return true + }, testTimeout, testInterval) + + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "azuredevops-token", + Namespace: testID, + }, + StringData: map[string]string{ + "token": cfg.gitPat, + }, + } + g.Expect(testEnv.Create(ctx, &secret)).To(Succeed()) + defer testEnv.Delete(ctx, &secret) + + provider := notiv1beta3.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: "azuredevops", + Namespace: testID, + }, + Spec: notiv1beta3.ProviderSpec{ + Type: "azuredevops", + Address: repoUrl, + SecretRef: &meta.LocalObjectReference{ + Name: "azuredevops-token", + }, + }, + } + g.Expect(testEnv.Create(ctx, &provider)).To(Succeed()) + defer testEnv.Delete(ctx, &provider) + + alert := notiv1beta3.Alert{ + ObjectMeta: metav1.ObjectMeta{ + Name: "azuredevops", + Namespace: testID, + }, + Spec: notiv1beta3.AlertSpec{ + ProviderRef: meta.LocalObjectReference{ + Name: provider.Name, + }, + EventSources: []notiv1.CrossNamespaceObjectReference{ + { + Kind: "Kustomization", + Name: testID, + Namespace: testID, + }, + }, + }, + } + g.Expect(testEnv.Create(ctx, &alert)).To(Succeed()) + defer testEnv.Delete(ctx, &alert) + + url, err := ParseAzureDevopsURL(repoUrl) + g.Expect(err).ToNot(HaveOccurred()) + + rev, err := c.Head() + g.Expect(err).ToNot(HaveOccurred()) + + connection := azuredevops.NewPatConnection(url.OrgURL, cfg.gitPat) + client, err := git.NewClient(ctx, connection) + g.Expect(err).ToNot(HaveOccurred()) + getArgs := git.GetStatusesArgs{ + Project: &url.Project, + RepositoryId: &url.Repo, + CommitId: &rev, + } + g.Eventually(func() bool { + statuses, err := client.GetStatuses(ctx, getArgs) + if err != nil { + return false + } + if len(*statuses) != 1 { + return false + } + return true + }, 500*time.Second, 5*time.Second) +} + +type AzureDevOpsURL struct { + OrgURL string + Project string + Repo string +} + +// TODO(somtochiama): move this into fluxcd/pkg and reuse in NC +func ParseAzureDevopsURL(s string) (AzureDevOpsURL, error) { + var args AzureDevOpsURL + u, err := giturls.Parse(s) + if err != nil { + return args, nil + } + + scheme := u.Scheme + if u.Scheme == "ssh" { + scheme = "https" + } + + id := strings.TrimLeft(u.Path, "/") + id = strings.TrimSuffix(id, ".git") + + comp := strings.Split(id, "/") + if len(comp) != 4 { + return args, fmt.Errorf("invalid repository id %q", id) + } + + args = AzureDevOpsURL{ + OrgURL: fmt.Sprintf("%s://%s/%s", scheme, u.Host, comp[0]), + Project: comp[1], + Repo: comp[3], + } + + return args, nil + +} diff --git a/tests/integration/azure_test.go b/tests/integration/azure_test.go new file mode 100644 index 00000000..6c7cab14 --- /dev/null +++ b/tests/integration/azure_test.go @@ -0,0 +1,175 @@ +/* +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 integration + +import ( + "context" + "fmt" + "os" + + eventhub "github.com/Azure/azure-event-hubs-go/v3" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/test-infra/tftestenv" + tfjson "github.com/hashicorp/terraform-json" +) + +const ( + azureDevOpsKnownHosts = "ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H" +) + +// createKubeConfigAKS constructs kubeconfig for an AKS cluster from the +// terraform state output at the given kubeconfig path. +func createKubeConfigAKS(ctx context.Context, state map[string]*tfjson.StateOutput, kcPath string) error { + kubeconfigYaml, ok := state["aks_kubeconfig"].Value.(string) + if !ok || kubeconfigYaml == "" { + return fmt.Errorf("failed to obtain kubeconfig from tf output") + } + return tftestenv.CreateKubeconfigAKS(ctx, kubeconfigYaml, kcPath) +} + +func getTestConfigAKS(ctx context.Context, outputs map[string]*tfjson.StateOutput) (*testConfig, error) { + fleetInfraRepository := outputs["fleet_infra_repository"].Value.(map[string]interface{}) + applicationRepository := outputs["application_repository"].Value.(map[string]interface{}) + + eventHubSas := outputs["event_hub_sas"].Value.(string) + sharedSopsId := outputs["sops_id"].Value.(string) + + kustomizeYaml := ` +resources: + - gotk-components.yaml + - gotk-sync.yaml +patchesStrategicMerge: + - |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: kustomize-controller + namespace: flux-system + spec: + template: + spec: + containers: + - name: manager + env: + - name: AZURE_AUTH_METHOD + value: msi +` + + privateKeyFile, ok := os.LookupEnv(envVarGitRepoSSHPath) + if !ok { + return nil, fmt.Errorf("%s env variable isn't set", envVarGitRepoSSHPath) + } + privateKeyData, err := os.ReadFile(privateKeyFile) + if err != nil { + return nil, fmt.Errorf("error getting azure devops private key, '%s': %w", privateKeyFile, err) + } + + pubKeyFile, ok := os.LookupEnv(envVarGitRepoSSHPubPath) + if !ok { + return nil, fmt.Errorf("%s env variable isn't set", envVarGitRepoSSHPubPath) + } + pubKeyData, err := os.ReadFile(pubKeyFile) + if err != nil { + return nil, fmt.Errorf("error getting ssh pubkey '%s', %w", pubKeyFile, err) + } + + c := make(chan []byte, 10) + closefn, err := setupEventHubHandler(ctx, c, eventHubSas) + + var notificationCfg = notificationConfig{ + notificationChan: c, + providerType: "azureeventhub", + closeChan: closefn, + secret: map[string]string{ + "address": eventHubSas, + }, + } + + config := &testConfig{ + defaultGitTransport: git.HTTP, + gitUsername: git.DefaultPublicKeyAuthUser, + gitPat: outputs["azure_devops_access_token"].Value.(string), + gitPrivateKey: string(privateKeyData), + gitPublicKey: string(pubKeyData), + knownHosts: azureDevOpsKnownHosts, + fleetInfraRepository: gitUrl{ + http: fleetInfraRepository["http"].(string), + ssh: fleetInfraRepository["ssh"].(string), + }, + applicationRepository: gitUrl{ + http: applicationRepository["http"].(string), + ssh: applicationRepository["ssh"].(string), + }, + notificationCfg: notificationCfg, + sopsArgs: fmt.Sprintf("--azure-kv %s", sharedSopsId), + sopsSecretData: map[string]string{ + "sops.azure-kv": fmt.Sprintf(`clientId: %s`, outputs["aks_client_id"].Value.(string)), + }, + kustomizationYaml: kustomizeYaml, + } + + opts, err := authOpts(config.fleetInfraRepository.http, map[string][]byte{ + "password": []byte(config.gitPat), + "username": []byte("git"), + }) + if err != nil { + return nil, err + } + config.defaultAuthOpts = opts + + return config, nil +} + +// registryLoginACR logs into the Azure Container Registries using the +// provider's CLI tools and returns the test repositories. +func registryLoginACR(ctx context.Context, output map[string]*tfjson.StateOutput) (string, error) { + // NOTE: ACR registry accept dynamic repository creation by just pushing a + // new image with a new repository name. + registryURL := output["acr_url"].Value.(string) + if err := tftestenv.RegistryLoginACR(ctx, registryURL); err != nil { + return "", err + } + + return registryURL, nil +} + +func setupEventHubHandler(ctx context.Context, c chan []byte, eventHubSas string) (func(), error) { + hub, err := eventhub.NewHubFromConnectionString(eventHubSas) + if err != nil { + return nil, err + } + + handler := func(ctx context.Context, event *eventhub.Event) error { + c <- event.Data + return nil + } + runtimeInfo, err := hub.GetRuntimeInformation(ctx) + if err != nil { + return nil, err + } + listenerHandler, err := hub.Receive(ctx, runtimeInfo.PartitionIDs[0], handler, eventhub.ReceiveWithLatestOffset()) + if err != nil { + return nil, err + } + + closefn := func() { + listenerHandler.Close(ctx) + hub.Close(ctx) + } + + return closefn, nil +} diff --git a/tests/integration/flux_test.go b/tests/integration/flux_test.go new file mode 100644 index 00000000..6a82f1e6 --- /dev/null +++ b/tests/integration/flux_test.go @@ -0,0 +1,168 @@ +/* +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 integration + +import ( + "context" + "fmt" + "io" + "strings" + "testing" + "time" + + "github.com/fluxcd/pkg/git" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestFluxInstallation(t *testing.T) { + g := NewWithT(t) + ctx := context.TODO() + g.Eventually(func() bool { + err := verifyGitAndKustomization(ctx, testEnv.Client, "flux-system", "flux-system") + if err != nil { + return false + } + return true + }, 60*time.Second, 5*time.Second) +} + +func TestRepositoryCloning(t *testing.T) { + ctx := context.TODO() + branchName := "feature/branch" + tagName := "v1" + + g := NewWithT(t) + + type testStruct struct { + name string + refType string + cloneType git.TransportType + } + + tests := []testStruct{ + { + name: "ssh-feature-branch", + refType: "branch", + cloneType: git.SSH, + }, + { + name: "ssh-v1", + refType: "tag", + cloneType: git.SSH, + }, + } + + // Not all cloud providers have repositories that support authentication with an accessToken + // we don't run http tests for these. + if cfg.gitPat != "" { + httpTests := []testStruct{ + { + name: "https-feature-branch", + refType: "branch", + cloneType: git.HTTP, + }, + { + name: "https-v1", + refType: "tag", + cloneType: git.HTTP, + }, + } + + tests = append(tests, httpTests...) + } + + t.Log("Creating application sources") + url := getTransportURL(cfg.applicationRepository) + tmpDir := t.TempDir() + client, err := getRepository(ctx, tmpDir, url, defaultBranch, cfg.defaultAuthOpts) + g.Expect(err).ToNot(HaveOccurred()) + + files := make(map[string]io.Reader) + for _, tt := range tests { + manifest := `apiVersion: v1 +kind: ConfigMap +metadata: + name: foobar + ` + name := fmt.Sprintf("cloning-test/%s/configmap.yaml", tt.name) + files[name] = strings.NewReader(manifest) + } + + err = commitAndPushAll(ctx, client, files, branchName) + g.Expect(err).ToNot(HaveOccurred()) + err = createTagAndPush(ctx, client, branchName, tagName) + g.Expect(err).ToNot(HaveOccurred()) + + t.Log("Verifying application-gitops namespaces") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + ref := &sourcev1.GitRepositoryRef{ + Branch: branchName, + } + if tt.refType == "tag" { + ref = &sourcev1.GitRepositoryRef{ + Tag: tagName, + } + } + + url := cfg.applicationRepository.http + if tt.cloneType == git.SSH { + url = cfg.applicationRepository.ssh + } + + testID := fmt.Sprintf("%s-%s", tt.name, randStringRunes(5)) + err := setUpFluxConfig(ctx, testID, nsConfig{ + repoURL: url, + ref: ref, + protocol: tt.cloneType, + objectName: testID, + path: fmt.Sprintf("./cloning-test/%s", tt.name), + }) + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + err := tearDownFluxConfig(ctx, testID) + if err != nil { + t.Logf("failed to delete resources in '%s' namespace: %s", tt.name, err) + } + }) + + g.Eventually(func() bool { + err := verifyGitAndKustomization(ctx, testEnv.Client, testID, testID) + if err != nil { + return false + } + return true + }, 120*time.Second, 5*time.Second).Should(BeTrue()) + + // Wait for configmap to be deployed + g.Eventually(func() bool { + nn := types.NamespacedName{Name: "foobar", Namespace: testID} + cm := &corev1.ConfigMap{} + err = testEnv.Get(ctx, nn, cm) + if err != nil { + return false + } + + return true + }, 120*time.Second, 5*time.Second).Should(BeTrue()) + }) + } +} diff --git a/tests/integration/gcp_test.go b/tests/integration/gcp_test.go new file mode 100644 index 00000000..8d901dcb --- /dev/null +++ b/tests/integration/gcp_test.go @@ -0,0 +1,178 @@ +/* +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 integration + +import ( + "context" + "fmt" + "io" + "log" + "os" + "strings" + + "cloud.google.com/go/pubsub" + tfjson "github.com/hashicorp/terraform-json" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/test-infra/tftestenv" +) + +const ( + gcpSourceRepoKnownHosts = "[source.developers.google.com]:2022 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBB5Iy4/cq/gt/fPqe3uyMy4jwv1Alc94yVPxmnwNhBzJqEV5gRPiRk5u4/JJMbbu9QUVAguBABxL7sBZa5PH/xY=" +) + +// createKubeConfigGKE constructs kubeconfig for a GKE cluster from the +// terraform state output at the given kubeconfig path. +func createKubeConfigGKE(ctx context.Context, state map[string]*tfjson.StateOutput, kcPath string) error { + kubeconfigYaml, ok := state["gke_kubeconfig"].Value.(string) + if !ok || kubeconfigYaml == "" { + return fmt.Errorf("failed to obtain kubeconfig from tf output") + } + return tftestenv.CreateKubeconfigGKE(ctx, kubeconfigYaml, kcPath) +} + +// registryLoginGCR logs into the Artifact registries using the gcloud +// and returns a list of test repositories. +func registryLoginGCR(ctx context.Context, output map[string]*tfjson.StateOutput) (string, error) { + project := output["gcp_project_id"].Value.(string) + region := output["gcp_region"].Value.(string) + repositoryID := output["artifact_registry_id"].Value.(string) + artifactRegistryURL, artifactRepoURL := tftestenv.GetGoogleArtifactRegistryAndRepository(project, region, repositoryID) + if err := tftestenv.RegistryLoginGCR(ctx, artifactRegistryURL); err != nil { + return "", err + } + + return artifactRepoURL, nil +} + +func getTestConfigGKE(ctx context.Context, outputs map[string]*tfjson.StateOutput) (*testConfig, error) { + sharedSopsId := outputs["sops_id"].Value.(string) + + privateKeyFile, ok := os.LookupEnv(envVarGitRepoSSHPath) + if !ok { + return nil, fmt.Errorf("%s env variable isn't set", envVarGitRepoSSHPath) + } + privateKeyData, err := os.ReadFile(privateKeyFile) + if err != nil { + return nil, fmt.Errorf("error getting gcp source repositories private key, '%s': %w", privateKeyFile, err) + } + + pubKeyFile, ok := os.LookupEnv(envVarGitRepoSSHPubPath) + if !ok { + return nil, fmt.Errorf("%s env variable isn't set", envVarGitRepoSSHPubPath) + } + pubKeyData, err := os.ReadFile(pubKeyFile) + if err != nil { + return nil, fmt.Errorf("error getting ssh pubkey '%s', %w", pubKeyFile, err) + } + + c := make(chan []byte, 10) + projectID := outputs["gcp_project_id"].Value.(string) + topicID := outputs["pubsub_topic"].Value.(string) + + fn, err := setupPubSubReceiver(ctx, c, projectID, topicID) + if err != nil { + return nil, err + } + + var notificationCfg = notificationConfig{ + providerType: "googlepubsub", + providerChannel: topicID, + notificationChan: c, + closeChan: fn, + secret: map[string]string{ + "address": projectID, + }, + } + + config := &testConfig{ + defaultGitTransport: git.SSH, + gitUsername: "git", + gitPrivateKey: string(privateKeyData), + gitPublicKey: string(pubKeyData), + knownHosts: gcpSourceRepoKnownHosts, + fleetInfraRepository: gitUrl{ + ssh: outputs["fleet_infra_repository"].Value.(string), + }, + applicationRepository: gitUrl{ + ssh: outputs["application_repository"].Value.(string), + }, + notificationCfg: notificationCfg, + sopsArgs: fmt.Sprintf("--gcp-kms %s", sharedSopsId), + } + + opts, err := authOpts(config.fleetInfraRepository.ssh, map[string][]byte{ + "identity": []byte(config.gitPrivateKey), + "known_hosts": []byte(config.knownHosts), + }) + if err != nil { + return nil, err + } + + config.defaultAuthOpts = opts + + // In Azure, the repository is initialized with a default branch through + // terraform. We have to do it manually here for GCP to prevent errors + // when trying to clone later. We only need to do it for the application repository + // since flux bootstrap pushes to the main branch. + files := make(map[string]io.Reader) + files["README.md"] = strings.NewReader("# Flux test repo") + tmpDir, err := os.MkdirTemp("", "*-flux-test") + if err != nil { + return nil, err + } + defer os.RemoveAll(tmpDir) + + client, err := getRepository(context.Background(), tmpDir, config.applicationRepository.ssh, defaultBranch, config.defaultAuthOpts) + if err != nil { + return nil, err + } + err = commitAndPushAll(context.Background(), client, files, defaultBranch) + if err != nil { + return nil, err + } + + return config, nil +} + +func setupPubSubReceiver(ctx context.Context, c chan []byte, projectID string, topicID string) (func(), error) { + newCtx, cancel := context.WithCancel(ctx) + pubsubClient, err := pubsub.NewClient(newCtx, projectID) + if err != nil { + cancel() + return nil, fmt.Errorf("error creating pubsub client: %s", err) + } + + sub := pubsubClient.Subscription(topicID) + go func() { + err = sub.Receive(ctx, func(ctx context.Context, message *pubsub.Message) { + c <- message.Data + message.Ack() + }) + if err != nil && status.Code(err) != codes.Canceled { + log.Printf("error receiving message in subscription: %s\n", err) + return + } + }() + + return func() { + cancel() + pubsubClient.Close() + }, nil +} diff --git a/tests/integration/go.mod b/tests/integration/go.mod new file mode 100644 index 00000000..18732213 --- /dev/null +++ b/tests/integration/go.mod @@ -0,0 +1,152 @@ +module github.com/fluxcd/flux2/tests/integration + +go 1.22.0 + +require ( + cloud.google.com/go/pubsub v1.38.0 + github.com/Azure/azure-event-hubs-go/v3 v3.6.2 + github.com/chainguard-dev/git-urls v1.0.2 + github.com/fluxcd/helm-controller/api v1.0.1 + github.com/fluxcd/image-automation-controller/api v0.38.0 + github.com/fluxcd/image-reflector-controller/api v0.32.0 + github.com/fluxcd/kustomize-controller/api v1.3.0 + github.com/fluxcd/notification-controller/api v1.3.0 + github.com/fluxcd/pkg/apis/event v0.9.0 + github.com/fluxcd/pkg/apis/meta v1.5.0 + github.com/fluxcd/pkg/git v0.19.0 + github.com/fluxcd/pkg/git/gogit v0.19.0 + github.com/fluxcd/pkg/runtime v0.47.1 + github.com/fluxcd/source-controller/api v1.3.0 + github.com/fluxcd/test-infra/tftestenv v0.0.0-20240429114247-01ecf2cc78e4 + github.com/go-git/go-git/v5 v5.12.0 + github.com/google/go-containerregistry v0.19.1 + github.com/hashicorp/terraform-exec v0.20.0 + github.com/hashicorp/terraform-json v0.21.0 + github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 + github.com/onsi/gomega v1.33.1 + google.golang.org/grpc v1.63.2 + k8s.io/api v0.30.0 + k8s.io/apimachinery v0.30.0 + k8s.io/client-go v0.30.0 + sigs.k8s.io/controller-runtime v0.18.1 +) + +require ( + cloud.google.com/go v0.112.2 // indirect + cloud.google.com/go/auth v0.3.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.7 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/azure-amqp-common-go/v4 v4.2.0 // indirect + github.com/Azure/azure-sdk-for-go v65.0.0+incompatible // indirect + github.com/Azure/go-amqp v1.0.0 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.28 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect + github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect + github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.1 // indirect + github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/devigned/tab v0.1.1 // indirect + github.com/docker/cli v24.0.9+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.9+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/emicklei/go-restful/v3 v3.12.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fluxcd/pkg/apis/acl v0.3.0 // indirect + github.com/fluxcd/pkg/apis/kustomize v1.5.0 // indirect + github.com/fluxcd/pkg/ssh v0.13.0 // indirect + github.com/fluxcd/pkg/version v0.4.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/hc-install v0.6.2 // indirect + github.com/imdario/mergo v0.3.16 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.16.5 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc3 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.1 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zclconf/go-cty v1.14.1 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.20.0 // indirect + google.golang.org/api v0.177.0 // indirect + google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 // indirect + google.golang.org/protobuf v1.34.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiextensions-apiserver v0.30.0 // indirect + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect + k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/tests/integration/go.sum b/tests/integration/go.sum new file mode 100644 index 00000000..180de216 --- /dev/null +++ b/tests/integration/go.sum @@ -0,0 +1,529 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw= +cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms= +cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= +cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= +cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= +cloud.google.com/go/kms v1.15.8 h1:szIeDCowID8th2i8XE4uRev5PMxQFqW+JjwYxL9h6xs= +cloud.google.com/go/kms v1.15.8/go.mod h1:WoUHcDjD9pluCg7pNds131awnH429QGvRM3N/4MyoVs= +cloud.google.com/go/pubsub v1.38.0 h1:J1OT7h51ifATIedjqk/uBNPh+1hkvUaH4VKbz4UuAsc= +cloud.google.com/go/pubsub v1.38.0/go.mod h1:IPMJSWSus/cu57UyR01Jqa/bNOQA+XnPF6Z4dKW4fAA= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/azure-amqp-common-go/v4 v4.2.0 h1:q/jLx1KJ8xeI8XGfkOWMN9XrXzAfVTkyvCxPvHCjd2I= +github.com/Azure/azure-amqp-common-go/v4 v4.2.0/go.mod h1:GD3m/WPPma+621UaU6KNjKEo5Hl09z86viKwQjTpV0Q= +github.com/Azure/azure-event-hubs-go/v3 v3.6.2 h1:7rNj1/iqS/i3mUKokA2n2eMYO72TB7lO7OmpbKoakKY= +github.com/Azure/azure-event-hubs-go/v3 v3.6.2/go.mod h1:n+ocYr9j2JCLYqUqz9eI+lx/TEAtL/g6rZzyTFSuIpc= +github.com/Azure/azure-sdk-for-go v65.0.0+incompatible h1:HzKLt3kIwMm4KeJYTdx9EbjRYTySD/t8i1Ee/W5EGXw= +github.com/Azure/azure-sdk-for-go v65.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-amqp v1.0.0 h1:QfCugi1M+4F2JDTRgVnRw7PYXLXZ9hmqk3+9+oJh3OA= +github.com/Azure/go-amqp v1.0.0/go.mod h1:+bg0x3ce5+Q3ahCEXnCsGG3ETpDQe3MEVnOuT2ywPwc= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.28 h1:ndAExarwr5Y+GaHE6VCaY1kyS/HwwGGyuimVhWsHOEM= +github.com/Azure/go-autorest/autorest v0.11.28/go.mod h1:MrkzG3Y3AH668QyF9KRk5neJnGgmhQ6krbhR8Q5eMvA= +github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= +github.com/Azure/go-autorest/autorest/adal v0.9.21 h1:jjQnVFXPfekaqb8vIsv2G1lxshoW+oGv4MDlhRtnYZk= +github.com/Azure/go-autorest/autorest/adal v0.9.21/go.mod h1:zua7mBUaCc5YnSLKYgGJR/w5ePdMDA6H56upLsHzA9U= +github.com/Azure/go-autorest/autorest/azure/auth v0.4.2 h1:iM6UAvjR97ZIeR93qTcwpKNMpV+/FTWjwEbuPD495Tk= +github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM= +github.com/Azure/go-autorest/autorest/azure/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U= +github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw= +github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= +github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= +github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= +github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= +github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ= +github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/devigned/tab v0.1.1 h1:3mD6Kb1mUOYeLpJvTVSDwSg5ZsfSxfvxGRTxRsJsITA= +github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY= +github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= +github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= +github.com/docker/cli v24.0.9+incompatible h1:OxbimnP/z+qVjDLpq9wbeFU3Nc30XhSe+LkwYQisD50= +github.com/docker/cli v24.0.9+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= +github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= +github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= +github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= +github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo= +github.com/fluxcd/helm-controller/api v1.0.1 h1:Gn9qEVuif6D5+gHmVwTEZkR4+nmLOcOhKx4Sw2gL2EA= +github.com/fluxcd/helm-controller/api v1.0.1/go.mod h1:/6AD5a2qjo/ttxVM8GR33syLZwqigta60DCLdy8GrME= +github.com/fluxcd/image-automation-controller/api v0.38.0 h1:+phX67uf0INGDC4sghsPPNUiE8taVp7AcWgJH8LkiUk= +github.com/fluxcd/image-automation-controller/api v0.38.0/go.mod h1:FfWWRxG03514+MUNJ+uN6fXzjwdbqsJqCggukIZ1tx8= +github.com/fluxcd/image-reflector-controller/api v0.32.0 h1:mb/v9JzRHcjLcnGqmgsq0+yCcoOyae/TrOWae9T87PE= +github.com/fluxcd/image-reflector-controller/api v0.32.0/go.mod h1:Ap3/KK8MfQAdmuhakg9CweEa3Xwwmvausbqrgd3HBWY= +github.com/fluxcd/kustomize-controller/api v1.3.0 h1:IwXkU48lQ/YhU6XULlPXDgQlnpNyQdCNbUvhLdWVIbE= +github.com/fluxcd/kustomize-controller/api v1.3.0/go.mod h1:kg/WM9Uye5NOqGVW/F3jnkjrlgFZHHa84+4lnzOV8fI= +github.com/fluxcd/notification-controller/api v1.3.0 h1:e3Plvo44XIKP2pUjwx8U4/fMpPwVM3EvJrYLIIqcVrI= +github.com/fluxcd/notification-controller/api v1.3.0/go.mod h1:KUaWXACNwWpAYo/Q4mzBjGbsYlUzXdq654jc1XpgMQw= +github.com/fluxcd/pkg/apis/acl v0.3.0 h1:UOrKkBTOJK+OlZX7n8rWt2rdBmDCoTK+f5TY2LcZi8A= +github.com/fluxcd/pkg/apis/acl v0.3.0/go.mod h1:WVF9XjSMVBZuU+HTTiSebGAWMgM7IYexFLyVWbK9bNY= +github.com/fluxcd/pkg/apis/event v0.9.0 h1:iKxU+3v/3bAuC1C1iXg1mjbIiaEQet7WETh8lsfdcpY= +github.com/fluxcd/pkg/apis/event v0.9.0/go.mod h1:5LjcTeppPMEyOgtTbIP7q2GbVwIRUfujIxynIjHBV/k= +github.com/fluxcd/pkg/apis/kustomize v1.5.0 h1:ah4sfqccnio+/5Edz/tVz6LetFhiBoDzXAElj6fFCzU= +github.com/fluxcd/pkg/apis/kustomize v1.5.0/go.mod h1:nEzhnhHafhWOUUV8VMFLojUOH+HHDEsL75y54mt/c30= +github.com/fluxcd/pkg/apis/meta v1.5.0 h1:/G82d2Az5D9op3F+wJUpD8jw/eTV0suM6P7+cSURoUM= +github.com/fluxcd/pkg/apis/meta v1.5.0/go.mod h1:Y3u7JomuuKtr5fvP1Iji2/50FdRe5GcBug2jawNVkdM= +github.com/fluxcd/pkg/git v0.19.0 h1:zIv+GAT0ieIUpnGBVi3Bhax/qq4Rr28BW7Jv4DTt6zE= +github.com/fluxcd/pkg/git v0.19.0/go.mod h1:wkqUOSrTjtsVVk/gC6/7RxVpi9GcqAA+7O5HVJF5S14= +github.com/fluxcd/pkg/git/gogit v0.19.0 h1:SdoNAmC/HTPXniQjp609X59rCsBiA+Sdq1Hv8SnYC6I= +github.com/fluxcd/pkg/git/gogit v0.19.0/go.mod h1:8kOmrNMjq8daQTVLhp6klhuoY8+s81gydM0MozDjaHM= +github.com/fluxcd/pkg/gittestserver v0.12.0 h1:QGbIVyje9U6urSAeDw3diKb/5wdA+Cnw1YJN+3Zflaw= +github.com/fluxcd/pkg/gittestserver v0.12.0/go.mod h1:Eh82e+kzKdhpafnUwR5oCBmxqAqhF5QuCn290AFntPM= +github.com/fluxcd/pkg/runtime v0.47.1 h1:Q1tAFsp92uurWyoEe52AmMC4k+6DYTPBrUQDs+nz/9c= +github.com/fluxcd/pkg/runtime v0.47.1/go.mod h1:97a+PqpWMgQsoqh91uH3EQz+/DC7Uxc8xcu/rDHFC5c= +github.com/fluxcd/pkg/ssh v0.13.0 h1:lPU1Gst8XIz7AU2dhdqVFaaOWd54/O1LZu62vH4JB/s= +github.com/fluxcd/pkg/ssh v0.13.0/go.mod h1:J9eyirMd4s++tWG4euRRhmcthKX203GPHpzFpH++TP8= +github.com/fluxcd/pkg/version v0.4.0 h1:3F6oeIZ+ug/f7pALIBhcUhfURel37EPPOn7nsGfsnOg= +github.com/fluxcd/pkg/version v0.4.0/go.mod h1:izVsSDxac81qWRmpOL9qcxZYx+zAN1ajoP5SidGP6PA= +github.com/fluxcd/source-controller/api v1.3.0 h1:Z5Lq0aJY87yg0cQDEuwGLKS60GhdErCHtsi546HUt10= +github.com/fluxcd/source-controller/api v1.3.0/go.mod h1:+tfd0vltjcVs/bbnq9AlYR9AAHSVfM/Z4v4TpQmdJf4= +github.com/fluxcd/test-infra/tftestenv v0.0.0-20240429114247-01ecf2cc78e4 h1:Oe1h6fi+571n0HLw5u5nnaWBn3Yja1/7DKnHAxb10SE= +github.com/fluxcd/test-infra/tftestenv v0.0.0-20240429114247-01ecf2cc78e4/go.mod h1:liFlLEXgambGVdWSJ4JzbIHf1Vjpp1HwUyPazPIVZug= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hc-install v0.6.2 h1:V1k+Vraqz4olgZ9UzKiAcbman9i9scg9GgSt/U3mw/M= +github.com/hashicorp/hc-install v0.6.2/go.mod h1:2JBpd+NCFKiHiu/yYCGaPyPHhZLxXTpz8oreHa/a3Ps= +github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo= +github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= +github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= +github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 h1:YH424zrwLTlyHSH/GzLMJeu5zhYVZSx5RQxGKm1h96s= +github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5/go.mod h1:PoGiBqKSQK1vIfQ+yVaFcGjDySHvym6FM1cNYnwzbrY= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= +github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= +github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.1 h1:Ou41VVR3nMWWmTiEUnj0OlsgOSCUFgsPAOl6jRIcVtQ= +github.com/sirupsen/logrus v1.9.1/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= +github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +go.einride.tech/aip v0.67.1 h1:d/4TW92OxXBngkSOwWS2CH5rez869KpKMaN44mdxkFI= +go.einride.tech/aip v0.67.1/go.mod h1:ZGX4/zKw8dcgzdLsrvpOOGxfxI2QSk12SlP7d6c0/XI= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= +go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= +golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.177.0 h1:8a0p/BbPa65GlqGWtUKxot4p0TV8OGOfyTjtmkXNXmk= +google.golang.org/api v0.177.0/go.mod h1:srbhue4MLjkjbkux5p3dw/ocYOSZTaIEvf7bCOnFQDw= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= +google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= +google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6 h1:DTJM0R8LECCgFeUwApvcEJHz85HLagW8uRENYxHh1ww= +google.golang.org/genproto/googleapis/api v0.0.0-20240429193739-8cf5692501f6/go.mod h1:10yRODfgim2/T8csjQsMPgZOMvtytXKTDRzH6HRGzRw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6 h1:DujSIu+2tC9Ht0aPNA7jgj23Iq8Ewi5sgkQ++wdvonE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240429193739-8cf5692501f6/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs= +k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 h1:SbdLaI6mM6ffDSJCadEaD4IkuPzepLDGlkd2xV0t1uA= +k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= +k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.18.1 h1:RpWbigmuiylbxOCLy0tGnq1cU1qWPwNIQzoJk+QeJx4= +sigs.k8s.io/controller-runtime v0.18.1/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/tests/integration/image_repo_test.go b/tests/integration/image_repo_test.go new file mode 100644 index 00000000..0abee9c7 --- /dev/null +++ b/tests/integration/image_repo_test.go @@ -0,0 +1,192 @@ +/* +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 integration + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + automationv1 "github.com/fluxcd/image-automation-controller/api/v1beta2" + reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" +) + +func TestImageRepositoryAndAutomation(t *testing.T) { + g := NewWithT(t) + ctx := context.TODO() + branchName := "image-repository" + testID := branchName + "-" + randStringRunes(5) + imageURL := fmt.Sprintf("%s/podinfo", cfg.testRegistry) + + manifest := fmt.Sprintf(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: podinfo + namespace: %[1]s +spec: + selector: + matchLabels: + app: podinfo + template: + metadata: + labels: + app: podinfo + spec: + containers: + - name: podinfod + image: %[2]s:%[3]s # {"$imagepolicy": "%[1]s:podinfo"} + readinessProbe: + exec: + command: + - podcli + - check + - http + - localhost:9898/readyz + initialDelaySeconds: 5 + timeoutSeconds: 5 +`, testID, imageURL, oldPodinfoVersion) + + repoUrl := getTransportURL(cfg.applicationRepository) + client, err := getRepository(ctx, t.TempDir(), repoUrl, defaultBranch, cfg.defaultAuthOpts) + g.Expect(err).ToNot(HaveOccurred()) + files := make(map[string]io.Reader) + files[testID+"/podinfo.yaml"] = strings.NewReader(manifest) + + err = commitAndPushAll(ctx, client, files, branchName) + g.Expect(err).ToNot(HaveOccurred()) + + err = setUpFluxConfig(ctx, testID, nsConfig{ + repoURL: repoUrl, + path: testID, + ref: &sourcev1.GitRepositoryRef{ + Branch: branchName, + }, + }) + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + err := tearDownFluxConfig(ctx, testID) + if err != nil { + t.Logf("failed to delete resources in '%s' namespace: %s", testID, err) + } + }) + + g.Eventually(func() bool { + err := verifyGitAndKustomization(ctx, testEnv.Client, testID, testID) + if err != nil { + return false + } + return true + }, testTimeout, testInterval).Should(BeTrue()) + + imageRepository := reflectorv1.ImageRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: testID, + }, + Spec: reflectorv1.ImageRepositorySpec{ + Image: imageURL, + Interval: metav1.Duration{ + Duration: 1 * time.Minute, + }, + Provider: infraOpts.Provider, + }, + } + g.Expect(testEnv.Create(ctx, &imageRepository)).To(Succeed()) + defer testEnv.Delete(ctx, &imageRepository) + + imagePolicy := reflectorv1.ImagePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: testID, + }, + Spec: reflectorv1.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: imageRepository.Name, + }, + Policy: reflectorv1.ImagePolicyChoice{ + SemVer: &reflectorv1.SemVerPolicy{ + Range: "6.0.x", + }, + }, + }, + } + g.Expect(testEnv.Create(ctx, &imagePolicy)).To(Succeed()) + defer testEnv.Delete(ctx, &imagePolicy) + + imageAutomation := automationv1.ImageUpdateAutomation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: testID, + }, + Spec: automationv1.ImageUpdateAutomationSpec{ + Interval: metav1.Duration{ + Duration: 1 * time.Minute, + }, + SourceRef: automationv1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: testID, + }, + GitSpec: &automationv1.GitSpec{ + Checkout: &automationv1.GitCheckoutSpec{ + Reference: sourcev1.GitRepositoryRef{ + Branch: branchName, + }, + }, + Commit: automationv1.CommitSpec{ + Author: automationv1.CommitUser{ + Email: "imageautomation@example.com", + Name: "imageautomation", + }, + }, + }, + Update: &automationv1.UpdateStrategy{ + Path: testID, + Strategy: automationv1.UpdateStrategySetters, + }, + }, + } + g.Expect(testEnv.Create(ctx, &imageAutomation)).To(Succeed()) + defer testEnv.Delete(ctx, &imageAutomation) + + // Wait for image repository to be ready + g.Eventually(func() bool { + client, err := getRepository(ctx, t.TempDir(), repoUrl, branchName, cfg.defaultAuthOpts) + if err != nil { + return false + } + + b, err := os.ReadFile(filepath.Join(client.Path(), testID, "podinfo.yaml")) + if err != nil { + return false + } + if bytes.Contains(b, []byte(newPodinfoVersion)) == false { + return false + } + return true + }, testTimeout, testInterval).Should(BeTrue()) +} diff --git a/tests/integration/notification_test.go b/tests/integration/notification_test.go new file mode 100644 index 00000000..ef154147 --- /dev/null +++ b/tests/integration/notification_test.go @@ -0,0 +1,177 @@ +/* +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 integration + +import ( + "context" + "encoding/json" + "io" + "strings" + "testing" + "time" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notiv1 "github.com/fluxcd/notification-controller/api/v1" + notiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3" + events "github.com/fluxcd/pkg/apis/event/v1beta1" + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" +) + +func TestNotification(t *testing.T) { + g := NewWithT(t) + + ctx := context.TODO() + branchName := "test-notification" + testID := branchName + "-" + randStringRunes(5) + defer cfg.notificationCfg.closeChan() + + // Setup Flux resources + manifest := `apiVersion: v1 +kind: ConfigMap +metadata: + name: foobar` + repoUrl := getTransportURL(cfg.applicationRepository) + client, err := getRepository(ctx, t.TempDir(), repoUrl, defaultBranch, cfg.defaultAuthOpts) + g.Expect(err).ToNot(HaveOccurred()) + files := make(map[string]io.Reader) + files["configmap.yaml"] = strings.NewReader(manifest) + err = commitAndPushAll(ctx, client, files, branchName) + g.Expect(err).ToNot(HaveOccurred()) + + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testID, + }, + } + g.Expect(testEnv.Create(ctx, &namespace)).To(Succeed()) + defer testEnv.Delete(ctx, &namespace) + + provider := notiv1beta3.Provider{ + ObjectMeta: metav1.ObjectMeta{ + Name: testID, + Namespace: testID, + }, + Spec: notiv1beta3.ProviderSpec{ + Type: cfg.notificationCfg.providerType, + Address: cfg.notificationCfg.providerAddress, + Channel: cfg.notificationCfg.providerChannel, + }, + } + + if cfg.notificationCfg.secret != nil { + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: testID, + Namespace: testID, + }, + StringData: cfg.notificationCfg.secret, + } + + g.Expect(testEnv.Create(ctx, &secret)).To(Succeed()) + defer testEnv.Delete(ctx, &secret) + + provider.Spec.SecretRef = &meta.LocalObjectReference{ + Name: testID, + } + } + + g.Expect(testEnv.Create(ctx, &provider)).To(Succeed()) + defer testEnv.Delete(ctx, &provider) + + alert := notiv1beta3.Alert{ + ObjectMeta: metav1.ObjectMeta{ + Name: testID, + Namespace: testID, + }, + Spec: notiv1beta3.AlertSpec{ + ProviderRef: meta.LocalObjectReference{ + Name: provider.Name, + }, + EventSources: []notiv1.CrossNamespaceObjectReference{ + { + Kind: "Kustomization", + Name: testID, + Namespace: testID, + }, + }, + }, + } + g.Expect(testEnv.Create(ctx, &alert)).ToNot(HaveOccurred()) + defer testEnv.Delete(ctx, &alert) + + modifyKsSpec := func(spec *kustomizev1.KustomizationSpec) { + spec.Interval = metav1.Duration{Duration: 30 * time.Second} + spec.HealthChecks = []meta.NamespacedObjectKindReference{ + { + APIVersion: "v1", + Kind: "ConfigMap", + Name: "foobar", + Namespace: testID, + }, + } + } + g.Expect(setUpFluxConfig(ctx, testID, nsConfig{ + repoURL: repoUrl, + ref: &sourcev1.GitRepositoryRef{ + Branch: branchName, + }, + path: "./", + modifyKsSpec: modifyKsSpec, + })).To(Succeed()) + t.Cleanup(func() { + err := tearDownFluxConfig(ctx, testID) + if err != nil { + t.Logf("failed to delete resources in '%s' namespace: %s", testID, err) + } + }) + + g.Eventually(func() bool { + err := verifyGitAndKustomization(ctx, testEnv, testID, testID) + if err != nil { + t.Log(err) + return false + } + return true + }, testTimeout, testInterval).Should(BeTrue()) + + // Wait to read event from notification channel. + g.Eventually(func() bool { + select { + case eventJson := <-cfg.notificationCfg.notificationChan: + event := &events.Event{} + err := json.Unmarshal([]byte(eventJson), event) + if err != nil { + t.Logf("the received event type does not match Flux format, error: %v", err) + return false + } + + if event.InvolvedObject.Kind == kustomizev1.KustomizationKind && + event.InvolvedObject.Name == testID && event.InvolvedObject.Namespace == testID { + return true + } + + return false + default: + return false + } + }, testTimeout, 1*time.Second).Should(BeTrue()) +} diff --git a/tests/integration/oci_test.go b/tests/integration/oci_test.go new file mode 100644 index 00000000..cfacbfc0 --- /dev/null +++ b/tests/integration/oci_test.go @@ -0,0 +1,122 @@ +/* +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 integration + +import ( + "context" + "fmt" + "testing" + "time" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + sourcev1 "github.com/fluxcd/source-controller/api/v1" +) + +func TestOCIHelmRelease(t *testing.T) { + g := NewWithT(t) + ctx := context.TODO() + + // Create namespace for test + testID := "oci-helm-" + randStringRunes(5) + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testID, + }, + } + g.Expect(testEnv.Create(ctx, &namespace)).To(Succeed()) + defer testEnv.Delete(ctx, &namespace) + + repoURL := fmt.Sprintf("%s/charts/podinfo", cfg.testRegistry) + err := pushImagesFromURL(repoURL, "ghcr.io/stefanprodan/charts/podinfo:6.2.0", []string{"6.2.0"}) + g.Expect(err).ToNot(HaveOccurred()) + + // Create HelmRepository. + helmRepository := sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{Name: testID, Namespace: testID}, + Spec: sourcev1.HelmRepositorySpec{ + URL: fmt.Sprintf("oci://%s", cfg.testRegistry), + Interval: metav1.Duration{ + Duration: 5 * time.Minute, + }, + Provider: infraOpts.Provider, + PassCredentials: true, + Type: "oci", + }, + } + + g.Expect(testEnv.Create(ctx, &helmRepository)).To(Succeed()) + defer testEnv.Delete(ctx, &helmRepository) + + // create helm release + helmRelease := helmv2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{Name: testID, Namespace: testID}, + Spec: helmv2.HelmReleaseSpec{ + Chart: &helmv2.HelmChartTemplate{ + Spec: helmv2.HelmChartTemplateSpec{ + Interval: &metav1.Duration{ + Duration: 10 * time.Minute, + }, + Chart: "charts/podinfo", + Version: "6.2.0", + SourceRef: helmv2.CrossNamespaceObjectReference{ + Kind: sourcev1.HelmRepositoryKind, + Name: helmRepository.Name, + Namespace: helmRepository.Namespace, + }, + }, + }, + }, + } + + g.Expect(testEnv.Create(ctx, &helmRelease)).To(Succeed()) + defer testEnv.Delete(ctx, &helmRelease) + + g.Eventually(func() bool { + chart := &sourcev1.HelmChart{} + nn := types.NamespacedName{ + Name: fmt.Sprintf("%s-%s", helmRelease.Name, helmRelease.Namespace), + Namespace: helmRelease.Namespace, + } + if err := testEnv.Get(ctx, nn, chart); err != nil { + t.Logf("error getting helm chart %s\n", err.Error()) + return false + } + if err := checkReadyCondition(chart); err != nil { + t.Log(err) + return false + } + + obj := &helmv2.HelmRelease{} + nn = types.NamespacedName{Name: helmRelease.Name, Namespace: helmRelease.Namespace} + if err := testEnv.Get(ctx, nn, obj); err != nil { + t.Logf("error getting helm release %s\n", err.Error()) + return false + } + + if err := checkReadyCondition(obj); err != nil { + t.Log(err) + return false + } + + return true + }, testTimeout, testInterval).Should(BeTrue()) +} diff --git a/tests/integration/sops_encryption_test.go b/tests/integration/sops_encryption_test.go new file mode 100644 index 00000000..053c7ed1 --- /dev/null +++ b/tests/integration/sops_encryption_test.go @@ -0,0 +1,138 @@ +/* +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 integration + +import ( + "context" + "fmt" + "io" + "log" + "os" + "testing" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/pkg/apis/meta" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + "github.com/fluxcd/test-infra/tftestenv" +) + +func TestKeyVaultSops(t *testing.T) { + g := NewWithT(t) + ctx := context.TODO() + branchName := "key-vault" + testID := branchName + "-" + randStringRunes(5) + secretYaml := `apiVersion: v1 +kind: Secret +metadata: + name: "test" +stringData: + foo: "bar"` + + repoUrl := getTransportURL(cfg.applicationRepository) + tmpDir := t.TempDir() + client, err := getRepository(ctx, tmpDir, repoUrl, defaultBranch, cfg.defaultAuthOpts) + g.Expect(err).ToNot(HaveOccurred()) + + dir := client.Path() + "/key-vault-sops" + g.Expect(os.Mkdir(dir, 0o700)).To(Succeed()) + + filename := dir + "secret.enc.yaml" + f, err := os.Create(filename) + g.Expect(err).ToNot(HaveOccurred()) + defer f.Close() + + _, err = f.Write([]byte(secretYaml)) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(f.Sync()).To(Succeed()) + + err = tftestenv.RunCommand(ctx, client.Path(), + fmt.Sprintf("sops --encrypt --encrypted-regex '^(data|stringData)$' %s --in-place %s", cfg.sopsArgs, filename), + tftestenv.RunCommandOptions{}) + g.Expect(err).ToNot(HaveOccurred()) + + r, err := os.Open(filename) + g.Expect(err).ToNot(HaveOccurred()) + + files := make(map[string]io.Reader) + files["key-vault-sops/secret.enc.yaml"] = r + err = commitAndPushAll(ctx, client, files, branchName) + g.Expect(err).ToNot(HaveOccurred()) + + modifyKsSpec := func(spec *kustomizev1.KustomizationSpec) { + spec.Decryption = &kustomizev1.Decryption{ + Provider: "sops", + } + if cfg.sopsSecretData != nil { + spec.Decryption.SecretRef = &meta.LocalObjectReference{ + Name: "sops-keys", + } + } + } + + err = setUpFluxConfig(ctx, testID, nsConfig{ + ref: &sourcev1.GitRepositoryRef{ + Branch: branchName, + }, + repoURL: repoUrl, + path: "./key-vault-sops", + modifyKsSpec: modifyKsSpec, + protocol: cfg.defaultGitTransport, + }) + g.Expect(err).ToNot(HaveOccurred()) + t.Cleanup(func() { + err := tearDownFluxConfig(ctx, testID) + if err != nil { + log.Printf("failed to delete resources in '%s' namespace", testID) + } + }) + + if cfg.sopsSecretData != nil { + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "sops-keys", + Namespace: testID, + }, + StringData: cfg.sopsSecretData, + } + g.Expect(testEnv.Create(ctx, &secret)).To(Succeed()) + defer testEnv.Delete(ctx, &secret) + } + + g.Eventually(func() bool { + err := verifyGitAndKustomization(ctx, testEnv.Client, testID, testID) + if err != nil { + return false + } + nn := types.NamespacedName{Name: "test", Namespace: testID} + secret := &corev1.Secret{} + err = testEnv.Get(ctx, nn, secret) + if err != nil { + return false + } + + if string(secret.Data["foo"]) == "bar" { + return true + } + + return false + }, testTimeout, testInterval).Should(BeTrue()) +} diff --git a/tests/integration/suite_test.go b/tests/integration/suite_test.go new file mode 100644 index 00000000..0b55c4c2 --- /dev/null +++ b/tests/integration/suite_test.go @@ -0,0 +1,348 @@ +/* +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 integration + +import ( + "context" + "flag" + "fmt" + "log" + "math/rand" + "os" + "testing" + "time" + + "github.com/hashicorp/terraform-exec/tfexec" + tfjson "github.com/hashicorp/terraform-json" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/kubernetes/scheme" + + helmv2 "github.com/fluxcd/helm-controller/api/v2" + automationv1 "github.com/fluxcd/image-automation-controller/api/v1beta2" + reflectorv1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3" + "github.com/fluxcd/pkg/git" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1beta2 "github.com/fluxcd/source-controller/api/v1beta2" + "github.com/fluxcd/test-infra/tftestenv" +) + +const ( + // azureTerraformPath is the path to the folder containing the + // terraform files for azure infra + azureTerraformPath = "./terraform/azure" + // gcpTerraformPath is the path to the folder containing the + // terraform files for gcp infra + gcpTerraformPath = "./terraform/gcp" + + // kubeconfigPath is the path of the file containing the kubeconfig + kubeconfigPath = "./build/kubeconfig" + + // fluxBin is the path to the flux binary. + fluxBin = "./build/flux" + + // default branch to be used when cloning git repositories + defaultBranch = "main" + + // envVarGitRepoSSHPath is the environment variable that contains the path + // to the ssh key for the git repository + envVarGitRepoSSHPath = "GITREPO_SSH_PATH" + // envVarGitRepoSSHPubPath is the environment variable that contains the path + // to the ssh public key for the git repository + envVarGitRepoSSHPubPath = "GITREPO_SSH_PUB_PATH" +) + +var ( + // supportedProviders are the providers supported by the test. + supportedProviders = []string{"azure", "gcp"} + + // cfg is a struct containing different variables needed for the test. + cfg *testConfig + + // infraOpts are the options for running the terraform environment + infraOpts tftestenv.Options + + // versions to tag and push for the podinfo image + oldPodinfoVersion = "6.0.0" + newPodinfoVersion = "6.0.1" + podinfoTags = []string{oldPodinfoVersion, newPodinfoVersion} + + // testEnv is the test environment. It contains test infrastructure and + // kubernetes client of the created cluster. + testEnv *tftestenv.Environment + + // testTimeout is used as a timeout when testing a condition with gomega's eventually + testTimeout = 60 * time.Second + // testInterval is used as an interval when testing a condition with gomega's eventually + testInterval = 5 * time.Second + + random *rand.Rand + + letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890") + + localImg = "ghcr.io/stefanprodan/podinfo" +) + +// testConfig hold different variable that will be needed by the different test functions. +type testConfig struct { + // authentication info for git repositories + gitPat string + gitUsername string + gitPrivateKey string + gitPublicKey string + defaultGitTransport git.TransportType + defaultAuthOpts *git.AuthOptions + knownHosts string + fleetInfraRepository gitUrl + applicationRepository gitUrl + + // sopsArgs is the cloud provider dependent argument to pass to the sops cli + sopsArgs string + + // notificationCfg contains the values needed to properly set up notification on the + // cluster. + notificationCfg notificationConfig + + // sopsSecretData is the secret's data for the sops decryption + sopsSecretData map[string]string + // kustomizationYaml is the content of the kustomization.yaml for customizing the Flux manifests + kustomizationYaml string + + // testRegistry is the registry of the cloud provider. + testRegistry string +} + +// notificationConfig contains various fields for configuring +// providers and testing notifications for the different +// cloud providers. +type notificationConfig struct { + providerChannel string + providerType string + providerAddress string + secret map[string]string + notificationChan chan []byte + closeChan func() +} + +// gitUrl contains the http/ssh urls for the created git repositories +// on the various cloud providers. +type gitUrl struct { + http string + ssh string +} + +// getTestConfig gets the test configuration that contains different variables for running the tests +type getTestConfig func(ctx context.Context, output map[string]*tfjson.StateOutput) (*testConfig, error) + +// registryLoginFunc is used to perform registry login against a provider based +// on the terraform state output values. It returns the test registry +// to test against, read from the terraform state output. +type registryLoginFunc func(ctx context.Context, output map[string]*tfjson.StateOutput) (string, error) + +// providerConfig contains the test configurations for the different cloud providers +type providerConfig struct { + terraformPath string + createKubeconfig tftestenv.CreateKubeconfig + getTestConfig getTestConfig + // registryLogin is used to perform registry login. + registryLogin registryLoginFunc +} + +func init() { + utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) + utilruntime.Must(sourcev1beta2.AddToScheme(scheme.Scheme)) + utilruntime.Must(kustomizev1.AddToScheme(scheme.Scheme)) + utilruntime.Must(helmv2.AddToScheme(scheme.Scheme)) + utilruntime.Must(reflectorv1.AddToScheme(scheme.Scheme)) + utilruntime.Must(automationv1.AddToScheme(scheme.Scheme)) + utilruntime.Must(notiv1beta3.AddToScheme(scheme.Scheme)) + + random = rand.New(rand.NewSource(time.Now().UnixNano())) +} + +func TestMain(m *testing.M) { + ctx := context.TODO() + + infraOpts.Bindflags(flag.CommandLine) + flag.Parse() + + // Validate the provider. + if infraOpts.Provider == "" { + log.Fatalf("-provider flag must be set to one of %v", supportedProviders) + } + var supported bool + for _, p := range supportedProviders { + if p == infraOpts.Provider { + supported = true + break + } + } + if !supported { + log.Fatalf("Unsupported provider %q, must be one of %v", infraOpts.Provider, supportedProviders) + } + // get provider specific configuration + providerCfg := getProviderConfig(infraOpts.Provider) + if providerCfg == nil { + log.Fatalf("Failed to get provider config for %q", infraOpts.Provider) + } + + // Run destroy-only mode if enabled. + if infraOpts.DestroyOnly { + log.Println("Running in destroy-only mode...") + envOpts := []tftestenv.EnvironmentOption{ + tftestenv.WithVerbose(infraOpts.Verbose), + // Ignore any state lock in destroy-only mode. + tftestenv.WithTfDestroyOptions(tfexec.Lock(false)), + } + if err := tftestenv.Destroy(ctx, providerCfg.terraformPath, envOpts...); err != nil { + panic(err) + } + os.Exit(0) + } + + // Initialize with non-zero exit code to indicate failure by default unless + // set by a successful test run. + exitCode := 1 + + // Setup Terraform binary and init state + log.Printf("Setting up %s e2e test infrastructure", infraOpts.Provider) + envOpts := []tftestenv.EnvironmentOption{ + tftestenv.WithExisting(infraOpts.Existing), + tftestenv.WithRetain(infraOpts.Retain), + tftestenv.WithVerbose(infraOpts.Verbose), + tftestenv.WithCreateKubeconfig(providerCfg.createKubeconfig), + } + + // Create terraform infrastructure + var err error + testEnv, err = tftestenv.New(ctx, scheme.Scheme, providerCfg.terraformPath, kubeconfigPath, envOpts...) + if err != nil { + log.Fatalf("Failed to provision the test infrastructure: %v", err) + } + + defer func() { + if err := testEnv.Stop(ctx); err != nil { + log.Printf("Failed to stop environment: %v", err) + exitCode = 1 + } + + // Log the panic error before exit to surface the cause of panic. + if err := recover(); err != nil { + log.Printf("panic: %v", err) + } + os.Exit(exitCode) + }() + + // get terrraform infrastructure + outputs, err := testEnv.StateOutput(ctx) + if err != nil { + panic(fmt.Sprintf("Failed to get the terraform state output: %v", err)) + } + + // get provider specific test configuration + cfg, err = providerCfg.getTestConfig(ctx, outputs) + if err != nil { + panic(fmt.Sprintf("Failed to get test config: %v", err)) + } + + regUrl, err := providerCfg.registryLogin(ctx, outputs) + if err != nil { + panic(fmt.Sprintf("Failed to log into registry: %v", err)) + } + + cfg.testRegistry = regUrl + err = pushTestImages(ctx, cfg.testRegistry, podinfoTags) + if err != nil { + panic(fmt.Sprintf("Failed to push test images: %v", err)) + } + + tmpDir, err := os.MkdirTemp("", "*-flux-test") + if err != nil { + panic(fmt.Sprintf("Failed to create tmp dir: %v", err)) + } + defer func() { + err := os.RemoveAll(tmpDir) + if err != nil { + log.Printf("error removing tmp dir: %s\n", err) + } + }() + + log.Println("Installing flux") + err = installFlux(ctx, tmpDir, kubeconfigPath) + defer func() { + log.Println("Uninstalling Flux") + if err := uninstallFlux(ctx); err != nil { + log.Printf("Failed to uninstall: %v", err) + } + }() + if err != nil { + panic(fmt.Sprintf("error installing Flux: %v", err)) + } + + // On check failure, log and continue. Controllers may be ready by the time + // tests run. + log.Println("Running flux check") + if err := runFluxCheck(ctx); err != nil { + log.Printf("flux check failed: %v\n", err) + } + + log.Println("Running e2e tests") + exitCode = m.Run() +} + +func getProviderConfig(provider string) *providerConfig { + switch provider { + case "azure": + return &providerConfig{ + terraformPath: azureTerraformPath, + createKubeconfig: createKubeConfigAKS, + getTestConfig: getTestConfigAKS, + registryLogin: registryLoginACR, + } + case "gcp": + return &providerConfig{ + terraformPath: gcpTerraformPath, + createKubeconfig: createKubeConfigGKE, + getTestConfig: getTestConfigGKE, + registryLogin: registryLoginGCR, + } + } + return nil +} + +// pushTestImages pushes the local podinfo image to the remote repository specified +// by repoURL. The image should be existing on the machine. +func pushTestImages(ctx context.Context, repoURL string, tags []string) error { + for _, tag := range tags { + remoteImg := fmt.Sprintf("%s/podinfo:%s", repoURL, tag) + err := tftestenv.RetagAndPush(ctx, fmt.Sprintf("%s:%s", localImg, tag), remoteImg) + if err != nil { + return err + } + + } + return nil +} + +func randStringRunes(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[random.Intn(len(letterRunes))] + } + return string(b) +} diff --git a/tests/integration/terraform/azure/aks.tf b/tests/integration/terraform/azure/aks.tf new file mode 100644 index 00000000..8ffadfce --- /dev/null +++ b/tests/integration/terraform/azure/aks.tf @@ -0,0 +1,20 @@ +module "aks" { + source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/azure/aks" + + name = local.name + location = var.azure_location + tags = var.tags +} + +module "acr" { + source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/azure/acr" + + name = local.name + location = var.azure_location + aks_principal_id = [module.aks.principal_id] + resource_group = module.aks.resource_group + admin_enabled = true + tags = var.tags + + depends_on = [module.aks] +} diff --git a/tests/integration/terraform/azure/azuredevops.tf b/tests/integration/terraform/azure/azuredevops.tf new file mode 100644 index 00000000..f8a1a9b8 --- /dev/null +++ b/tests/integration/terraform/azure/azuredevops.tf @@ -0,0 +1,26 @@ +resource "azuredevops_project" "e2e" { + name = local.name + visibility = "private" + version_control = "Git" + work_item_template = "Agile" + description = "Test Project for Flux E2E test - Managed by Terraform" +} + + +resource "azuredevops_git_repository" "fleet_infra" { + project_id = azuredevops_project.e2e.id + name = "fleet-infra-${local.name}" + default_branch = "refs/heads/main" + initialization { + init_type = "Clean" + } +} + +resource "azuredevops_git_repository" "application" { + project_id = azuredevops_project.e2e.id + name = "application-${local.name}" + default_branch = "refs/heads/main" + initialization { + init_type = "Clean" + } +} diff --git a/tests/azure/terraform/aks/event-hub.tf b/tests/integration/terraform/azure/event-hub.tf similarity index 56% rename from tests/azure/terraform/aks/event-hub.tf rename to tests/integration/terraform/azure/event-hub.tf index 13162d29..cda95002 100644 --- a/tests/azure/terraform/aks/event-hub.tf +++ b/tests/integration/terraform/azure/event-hub.tf @@ -1,23 +1,24 @@ resource "azurerm_eventhub_namespace" "this" { - name = "ehns-${local.name_suffix}" - location = azurerm_resource_group.this.location - resource_group_name = azurerm_resource_group.this.name - sku = "Standard" + name = local.name + location = var.azure_location + resource_group_name = module.aks.resource_group + sku = "Basic" capacity = 1 + tags = var.tags } resource "azurerm_eventhub" "this" { - name = "eh-${local.name_suffix}" + name = local.name namespace_name = azurerm_eventhub_namespace.this.name - resource_group_name = azurerm_resource_group.this.name + resource_group_name = module.aks.resource_group partition_count = 1 message_retention = 1 } resource "azurerm_eventhub_authorization_rule" "this" { - name = "flux" - resource_group_name = azurerm_resource_group.this.name + name = local.name + resource_group_name = module.aks.resource_group namespace_name = azurerm_eventhub_namespace.this.name eventhub_name = azurerm_eventhub.this.name listen = true diff --git a/tests/integration/terraform/azure/keyvault.tf b/tests/integration/terraform/azure/keyvault.tf new file mode 100644 index 00000000..b86adc8e --- /dev/null +++ b/tests/integration/terraform/azure/keyvault.tf @@ -0,0 +1,61 @@ +resource "azurerm_key_vault" "this" { + name = local.name + resource_group_name = module.aks.resource_group + location = var.azure_location + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + tags = var.tags +} + +resource "azurerm_key_vault_access_policy" "admin" { + key_vault_id = azurerm_key_vault.this.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + key_permissions = [ + "Create", + "Update", + "Encrypt", + "Delete", + "Get", + "List", + "Purge", + "Recover", + "GetRotationPolicy", + "SetRotationPolicy" + ] + + secret_permissions = [ + "Get", + "Delete", + "Purge", + "Recover" + ] + +} + +resource "azurerm_key_vault_access_policy" "cluster_binding" { + key_vault_id = azurerm_key_vault.this.id + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = module.aks.principal_id + + key_permissions = [ + "Decrypt", + "Encrypt", + ] +} + +resource "azurerm_key_vault_key" "sops" { + depends_on = [azurerm_key_vault_access_policy.admin] + + name = "sops" + key_vault_id = azurerm_key_vault.this.id + key_type = "RSA" + key_size = 2048 + tags = var.tags + + key_opts = [ + "decrypt", + "encrypt", + ] +} diff --git a/tests/integration/terraform/azure/main.tf b/tests/integration/terraform/azure/main.tf new file mode 100644 index 00000000..e4d22b29 --- /dev/null +++ b/tests/integration/terraform/azure/main.tf @@ -0,0 +1,35 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">=3.20.0" + } + azuread = { + source = "hashicorp/azuread" + version = ">=2.28.0" + } + azuredevops = { + source = "microsoft/azuredevops" + version = ">=0.2.2" + } + } +} + +provider "azurerm" { + features {} +} + +provider "azuredevops" { + org_service_url = "https://dev.azure.com/${var.azuredevops_org}" + personal_access_token = var.azuredevops_pat +} + +data "azurerm_client_config" "current" {} + +resource "random_pet" "suffix" { + separator = "o" +} + +locals { + name = "e2e${random_pet.suffix.id}" +} diff --git a/tests/integration/terraform/azure/outputs.tf b/tests/integration/terraform/azure/outputs.tf new file mode 100644 index 00000000..a7f08904 --- /dev/null +++ b/tests/integration/terraform/azure/outputs.tf @@ -0,0 +1,41 @@ +output "aks_kubeconfig" { + description = "kubeconfig of the created AKS cluster" + value = module.aks.kubeconfig + sensitive = true +} + +output "azure_devops_access_token" { + sensitive = true + value = var.azuredevops_pat +} + +output "fleet_infra_repository" { + value = { + http = azuredevops_git_repository.fleet_infra.remote_url + ssh = "ssh://git@ssh.dev.azure.com/v3/${var.azuredevops_org}/${azuredevops_git_repository.fleet_infra.project_id}/${azuredevops_git_repository.fleet_infra.name}" + } +} + +output "application_repository" { + value = { + http = azuredevops_git_repository.application.remote_url + ssh = "ssh://git@ssh.dev.azure.com/v3/${var.azuredevops_org}/${azuredevops_git_repository.application.project_id}/${azuredevops_git_repository.application.name}" + } +} + +output "aks_client_id" { + value = module.aks.kubelet_client_id +} + +output "event_hub_sas" { + value = azurerm_eventhub_authorization_rule.this.primary_connection_string + sensitive = true +} + +output "sops_id" { + value = azurerm_key_vault_key.sops.id +} + +output "acr_url" { + value = module.acr.registry_url +} diff --git a/tests/integration/terraform/azure/variables.tf b/tests/integration/terraform/azure/variables.tf new file mode 100644 index 00000000..8f3b9376 --- /dev/null +++ b/tests/integration/terraform/azure/variables.tf @@ -0,0 +1,21 @@ +variable "azuredevops_org" { + type = string + description = "Name of Azure DevOps organizations were the repositories will be created" +} + +variable "azure_location" { + type = string + description = "Location of the resource group" + default = "eastus" +} + +variable "tags" { + type = map(string) + default = {} + description = "Tags for created Azure resources" +} + +variable "azuredevops_pat" { + type = string + description = "Personal access token for Azure DevOps repository" +} diff --git a/tests/integration/terraform/gcp/gke.tf b/tests/integration/terraform/gcp/gke.tf new file mode 100644 index 00000000..5de455db --- /dev/null +++ b/tests/integration/terraform/gcp/gke.tf @@ -0,0 +1,13 @@ +module "gke" { + source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/gcp/gke" + + name = local.name + tags = var.tags +} + +module "gcr" { + source = "git::https://github.com/fluxcd/test-infra.git//tf-modules/gcp/gcr" + + name = local.name + tags = var.tags +} diff --git a/tests/integration/terraform/gcp/kms.tf b/tests/integration/terraform/gcp/kms.tf new file mode 100644 index 00000000..6aed72a3 --- /dev/null +++ b/tests/integration/terraform/gcp/kms.tf @@ -0,0 +1,18 @@ +data "google_kms_key_ring" "keyring" { + name = var.gcp_keyring + location = "global" +} + +data "google_kms_crypto_key" "my_crypto_key" { + name = var.gcp_crypto_key + key_ring = data.google_kms_key_ring.keyring.id +} + +resource "google_kms_key_ring_iam_binding" "key_ring" { + key_ring_id = data.google_kms_key_ring.keyring.id + role = "roles/cloudkms.cryptoKeyEncrypterDecrypter" + + members = [ + "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com", + ] +} diff --git a/tests/integration/terraform/gcp/main.tf b/tests/integration/terraform/gcp/main.tf new file mode 100644 index 00000000..cc917453 --- /dev/null +++ b/tests/integration/terraform/gcp/main.tf @@ -0,0 +1,13 @@ +provider "google" { + project = var.gcp_project_id + region = var.gcp_region + zone = var.gcp_zone +} + +resource "random_pet" "suffix" {} + +locals { + name = "e2e-${random_pet.suffix.id}" +} + +data "google_project" "project" {} diff --git a/tests/integration/terraform/gcp/outputs.tf b/tests/integration/terraform/gcp/outputs.tf new file mode 100644 index 00000000..d356b188 --- /dev/null +++ b/tests/integration/terraform/gcp/outputs.tf @@ -0,0 +1,32 @@ +output "gke_kubeconfig" { + value = module.gke.kubeconfig + sensitive = true +} + +output "gcp_project_id" { + value = var.gcp_project_id +} + +output "gcp_region" { + value = var.gcp_region +} + +output "artifact_registry_id" { + value = module.gcr.artifact_repository_id +} + +output "sops_id" { + value = data.google_kms_crypto_key.my_crypto_key.id +} + +output "fleet_infra_repository" { + value = "ssh://${var.gcp_email}@source.developers.google.com:2022/p/${var.gcp_project_id}/r/${google_sourcerepo_repository.fleet-infra.name}" +} + +output "application_repository" { + value = "ssh://${var.gcp_email}@source.developers.google.com:2022/p/${var.gcp_project_id}/r/${google_sourcerepo_repository.application.name}" +} + +output "pubsub_topic" { + value = google_pubsub_topic.pubsub.name +} diff --git a/tests/integration/terraform/gcp/pubsub.tf b/tests/integration/terraform/gcp/pubsub.tf new file mode 100644 index 00000000..c59c0e99 --- /dev/null +++ b/tests/integration/terraform/gcp/pubsub.tf @@ -0,0 +1,11 @@ +resource "google_pubsub_topic" "pubsub" { + name = local.name + labels = var.tags + message_retention_duration = "7200s" +} + +resource "google_pubsub_subscription" "sub" { + project = var.gcp_project_id + name = local.name + topic = google_pubsub_topic.pubsub.name +} diff --git a/tests/integration/terraform/gcp/sourcerepo.tf b/tests/integration/terraform/gcp/sourcerepo.tf new file mode 100644 index 00000000..4d7f5ee9 --- /dev/null +++ b/tests/integration/terraform/gcp/sourcerepo.tf @@ -0,0 +1,26 @@ +resource "google_sourcerepo_repository" "fleet-infra" { + name = "fleet-infra-${random_pet.suffix.id}" +} + +resource "google_sourcerepo_repository" "application" { + name = "application-${random_pet.suffix.id}" +} + +resource "google_sourcerepo_repository_iam_binding" "application_binding" { + project = google_sourcerepo_repository.application.project + repository = google_sourcerepo_repository.application.name + role = "roles/source.admin" + members = [ + "user:${var.gcp_email}", + ] +} + +resource "google_sourcerepo_repository_iam_binding" "fleet-infra_binding" { + project = google_sourcerepo_repository.fleet-infra.project + repository = google_sourcerepo_repository.fleet-infra.name + role = "roles/source.admin" + members = [ + "user:${var.gcp_email}", + ] +} + diff --git a/tests/integration/terraform/gcp/variables.tf b/tests/integration/terraform/gcp/variables.tf new file mode 100644 index 00000000..6af9e7d6 --- /dev/null +++ b/tests/integration/terraform/gcp/variables.tf @@ -0,0 +1,37 @@ +variable "gcp_project_id" { + type = string + description = "GCP project to create the resources in" +} + +variable "gcp_email" { + type = string + description = "GCP user email" +} + +variable "gcp_region" { + type = string + default = "us-central1" + description = "GCP region" +} + +variable "gcp_zone" { + type = string + default = "us-central1" + description = "GCP zone" +} + +variable "gcp_keyring" { + type = string + description = "GCP keyring that contains crypto key for encrypting secrets" +} + +variable "gcp_crypto_key" { + type = string + description = "GCP crypto key for encrypting secrets" +} + +variable "tags" { + type = map(string) + default = {} + description = "tags for created resources" +} diff --git a/tests/integration/util_test.go b/tests/integration/util_test.go new file mode 100644 index 00000000..9d1c4637 --- /dev/null +++ b/tests/integration/util_test.go @@ -0,0 +1,420 @@ +/* +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 integration + +import ( + "context" + "errors" + "fmt" + "io" + "net/url" + "os" + "strings" + "time" + + extgogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/google/go-containerregistry/pkg/crane" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/gogit" + "github.com/fluxcd/pkg/git/repository" + "github.com/fluxcd/pkg/runtime/conditions" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + "github.com/fluxcd/test-infra/tftestenv" +) + +// installFlux adds the core Flux components to the cluster specified in the kubeconfig file. +func installFlux(ctx context.Context, tmpDir string, kubeconfigPath string) error { + // Create flux-system namespace + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "flux-system", + }, + } + err := testEnv.Create(ctx, &namespace) + if err != nil { + return err + } + + repoURL := getTransportURL(cfg.fleetInfraRepository) + if cfg.kustomizationYaml != "" { + files := make(map[string]io.Reader) + files["clusters/e2e/flux-system/kustomization.yaml"] = strings.NewReader(cfg.kustomizationYaml) + files["clusters/e2e/flux-system/gotk-components.yaml"] = strings.NewReader("") + files["clusters/e2e/flux-system/gotk-sync.yaml"] = strings.NewReader("") + c, err := getRepository(ctx, tmpDir, repoURL, defaultBranch, cfg.defaultAuthOpts) + if err != nil { + return err + } + + err = commitAndPushAll(ctx, c, files, defaultBranch) + if err != nil { + return err + } + } + + var bootstrapArgs string + if cfg.defaultGitTransport == git.SSH { + f, err := os.CreateTemp("", "flux-e2e-ssh-key-*") + if err != nil { + return err + } + err = os.WriteFile(f.Name(), []byte(cfg.gitPrivateKey), 0o600) + if err != nil { + return err + } + bootstrapArgs = fmt.Sprintf("--private-key-file=%s -s", f.Name()) + } else { + bootstrapArgs = fmt.Sprintf("--token-auth --password=%s", cfg.gitPat) + } + + bootstrapCmd := fmt.Sprintf("%s bootstrap git --url=%s %s --kubeconfig=%s --path=clusters/e2e "+ + " --components-extra image-reflector-controller,image-automation-controller", + fluxBin, repoURL, bootstrapArgs, kubeconfigPath) + + return tftestenv.RunCommand(ctx, "./", bootstrapCmd, tftestenv.RunCommandOptions{ + Timeout: 15 * time.Minute, + }) +} + +func runFluxCheck(ctx context.Context) error { + checkCmd := fmt.Sprintf("%s check --kubeconfig %s", fluxBin, kubeconfigPath) + return tftestenv.RunCommand(ctx, "./", checkCmd, tftestenv.RunCommandOptions{ + AttachConsole: true, + }) +} + +func uninstallFlux(ctx context.Context) error { + uninstallCmd := fmt.Sprintf("%s uninstall --kubeconfig %s -s", fluxBin, kubeconfigPath) + if err := tftestenv.RunCommand(ctx, "./", uninstallCmd, tftestenv.RunCommandOptions{ + Timeout: 15 * time.Minute, + }); err != nil { + return err + } + return nil +} + +// verifyGitAndKustomization checks that the gitrespository and kustomization combination are working properly. +func verifyGitAndKustomization(ctx context.Context, kubeClient client.Client, namespace, name string) error { + nn := types.NamespacedName{ + Name: name, + Namespace: namespace, + } + source := &sourcev1.GitRepository{} + if err := kubeClient.Get(ctx, nn, source); err != nil { + return err + } + if err := checkReadyCondition(source); err != nil { + return err + } + + kustomization := &kustomizev1.Kustomization{} + if err := kubeClient.Get(ctx, nn, kustomization); err != nil { + return err + } + if err := checkReadyCondition(kustomization); err != nil { + return err + } + + return nil +} + +type nsConfig struct { + repoURL string + ref *sourcev1.GitRepositoryRef + protocol git.TransportType + objectName string + path string + modifyKsSpec func(spec *kustomizev1.KustomizationSpec) +} + +// setUpFluxConfigs creates the namespace, then creates the git secret, +// git repository and kustomization in that namespace +func setUpFluxConfig(ctx context.Context, name string, opts nsConfig) error { + transport := cfg.defaultGitTransport + if opts.protocol != "" { + transport = opts.protocol + } + + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + if err := testEnv.Create(ctx, &namespace); err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + + secret := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "git-credentials", + Namespace: name, + }, + } + + secret.StringData = map[string]string{ + "username": cfg.gitUsername, + "password": cfg.gitPat, + } + + if transport == git.SSH { + secret.StringData = map[string]string{ + "identity": cfg.gitPrivateKey, + "identity.pub": cfg.gitPublicKey, + "known_hosts": cfg.knownHosts, + } + } + if err := testEnv.Create(ctx, &secret); err != nil { + return err + } + + ref := &sourcev1.GitRepositoryRef{ + Branch: name, + } + + if opts.ref != nil { + ref = opts.ref + } + + gitSpec := &sourcev1.GitRepositorySpec{ + Interval: metav1.Duration{ + Duration: 1 * time.Minute, + }, + Reference: ref, + SecretRef: &meta.LocalObjectReference{ + Name: secret.Name, + }, + URL: opts.repoURL, + } + + source := &sourcev1.GitRepository{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace.Name}, + Spec: *gitSpec, + } + if err := testEnv.Create(ctx, source); err != nil { + return err + } + + ksSpec := &kustomizev1.KustomizationSpec{ + Path: opts.path, + TargetNamespace: name, + SourceRef: kustomizev1.CrossNamespaceSourceReference{ + Kind: sourcev1.GitRepositoryKind, + Name: source.Name, + Namespace: source.Namespace, + }, + Interval: metav1.Duration{ + Duration: 1 * time.Minute, + }, + Prune: true, + } + if opts.modifyKsSpec != nil { + opts.modifyKsSpec(ksSpec) + } + kustomization := &kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace.Name}, + Spec: *ksSpec, + } + + return testEnv.Create(ctx, kustomization) +} + +func tearDownFluxConfig(ctx context.Context, name string) error { + var allErr []error + + source := &sourcev1.GitRepository{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: name}} + if err := testEnv.Delete(ctx, source); err != nil { + allErr = append(allErr, err) + } + + kustomization := &kustomizev1.Kustomization{ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: name}} + if err := testEnv.Delete(ctx, kustomization); err != nil { + allErr = append(allErr, err) + } + + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + if err := testEnv.Delete(ctx, &namespace); err != nil { + allErr = append(allErr, err) + } + + return kerrors.NewAggregate(allErr) +} + +// getRepository and clones the git repository to the directory. +func getRepository(ctx context.Context, dir, repoURL, branchName string, authOpts *git.AuthOptions) (*gogit.Client, error) { + c, err := gogit.NewClient(dir, authOpts, gogit.WithSingleBranch(false), gogit.WithDiskStorage()) + if err != nil { + return nil, err + } + + _, err = c.Clone(ctx, repoURL, repository.CloneConfig{ + CheckoutStrategy: repository.CheckoutStrategy{ + Branch: branchName, + }, + }) + if err != nil { + return nil, err + } + + return c, nil +} + +// commitAndPushAll checks out to the specified branch, creates the files, commits and then pushes them to +// the remote git repository. +func commitAndPushAll(ctx context.Context, client *gogit.Client, files map[string]io.Reader, branchName string) error { + err := client.SwitchBranch(ctx, branchName) + if err != nil && !errors.Is(err, plumbing.ErrReferenceNotFound) { + return err + } + + _, err = client.Commit(git.Commit{ + Author: git.Signature{ + Name: git.DefaultPublicKeyAuthUser, + Email: "test@example.com", + When: time.Now(), + }, + }, repository.WithFiles(files)) + if err != nil { + if errors.Is(err, git.ErrNoStagedFiles) { + return nil + } + + return err + } + + err = client.Push(ctx, repository.PushConfig{}) + if err != nil { + return fmt.Errorf("unable to push: %s", err) + } + + return nil +} + +func createTagAndPush(ctx context.Context, client *gogit.Client, branchName, newTag string) error { + repo, err := extgogit.PlainOpen(client.Path()) + if err != nil { + return err + } + + ref, err := repo.Reference(plumbing.NewBranchReferenceName(branchName), false) + if err != nil { + return err + } + + tags, err := repo.TagObjects() + if err != nil { + return err + } + + err = tags.ForEach(func(tag *object.Tag) error { + if tag.Name == newTag { + err = repo.DeleteTag(tag.Name) + if err != nil { + return err + } + } + + return nil + }) + if err != nil { + return fmt.Errorf("error deleting local tag: %w", err) + } + + // Delete remote tag + if err := client.Push(ctx, repository.PushConfig{ + Refspecs: []string{fmt.Sprintf(":refs/tags/%s", newTag)}, + Force: true, + }); err != nil && !errors.Is(err, extgogit.NoErrAlreadyUpToDate) { + return fmt.Errorf("unable to delete existing tag: %w", err) + } + + sig := &object.Signature{ + Name: git.DefaultPublicKeyAuthUser, + Email: "test@example.com", + When: time.Now(), + } + if _, err = repo.CreateTag(newTag, ref.Hash(), &extgogit.CreateTagOptions{ + Tagger: sig, + Message: "create tag", + }); err != nil { + return fmt.Errorf("unable to create tag: %w", err) + } + + return client.Push(ctx, repository.PushConfig{ + Refspecs: []string{"refs/tags/*:refs/tags/*"}, + }) +} + +func pushImagesFromURL(repoURL, imgURL string, tags []string) error { + img, err := crane.Pull(imgURL) + if err != nil { + return err + } + + for _, tag := range tags { + if err := crane.Push(img, fmt.Sprintf("%s:%s", repoURL, tag)); err != nil { + return err + } + } + + return nil +} + +func getTransportURL(urls gitUrl) string { + if cfg.defaultGitTransport == git.SSH { + return urls.ssh + } + + return urls.http +} + +func authOpts(repoURL string, authData map[string][]byte) (*git.AuthOptions, error) { + u, err := url.Parse(repoURL) + if err != nil { + return nil, err + } + + return git.NewAuthOptions(*u, authData) +} + +// checkReadyCondition checks for a Ready condition, it returns nil if the condition is true +// or an error (with the message if the Ready condition is present). +func checkReadyCondition(from conditions.Getter) error { + if conditions.IsReady(from) { + return nil + } + errMsg := fmt.Sprintf("object not ready") + readyMsg := conditions.GetMessage(from, meta.ReadyCondition) + if readyMsg != "" { + errMsg += ": " + readyMsg + } + return errors.New(errMsg) +}