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 f9082627..6125498a 100644 --- a/.github/aur/flux-go/PKGBUILD.template +++ b/.github/aur/flux-go/PKGBUILD.template @@ -4,43 +4,52 @@ 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.16', '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" - ./manifests/scripts/bundle.sh "${PWD}/manifests" "${PWD}/cmd/flux/manifests" - go build -ldflags "-linkmode=external -X main.VERSION=${pkgver}" -o ${_srcname} ./cmd/flux + make cmd/flux/.manifests.done + 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 + ;; + armv7h) + export ENVTEST_ARCH=arm + ;; + esac make test } 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 c22a5c6e..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.16', 'kustomize>=3.0') +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" @@ -32,12 +32,20 @@ build() { export CGO_CXXFLAGS="$CXXFLAGS" export CGO_CPPFLAGS="$CPPFLAGS" export GOFLAGS="-buildmode=pie -trimpath -mod=readonly -modcacherw" - make cmd/flux/manifests + make cmd/flux/.manifests.done go build -ldflags "-linkmode=external -X main.VERSION=${pkgver}" -o ${_srcname} ./cmd/flux } check() { cd "flux2" + case $CARCH in + aarch64) + export ENVTEST_ARCH=arm64 + ;; + armv7h) + export ENVTEST_ARCH=arm + ;; + esac make test } 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/labels.yaml b/.github/labels.yaml new file mode 100644 index 00000000..c3b4b2ac --- /dev/null +++ b/.github/labels.yaml @@ -0,0 +1,55 @@ +# 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' diff --git a/.github/runners/README.md b/.github/runners/README.md index 29caf855..440c6865 100644 --- a/.github/runners/README.md +++ b/.github/runners/README.md @@ -1,42 +1,80 @@ -# Flux GitHub runners +# Flux ARM64 GitHub runners -How to provision GitHub Actions self-hosted runners for Flux conformance testing. +The Flux ARM64 end-to-end tests run on Equinix Metal instances provisioned with Docker and GitHub self-hosted runners. -## ARM64 Instance specs +## Current instances -In order to add a new runner to the GitHub Actions pool, -first create an instance on Oracle Cloud with the following configuration: -- OS: Canonical Ubuntu 20.04 -- Shape: VM.Standard.A1.Flex -- OCPU Count: 2 -- Memory (GB): 12 -- Network Bandwidth (Gbps): 2 -- Local Disk: Block Storage Only +| Repository | Runner | Instance | Location | +|-----------------------------|------------------|------------------------|---------------| +| flux2 | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC | +| flux2 | equinix-arm-dc-2 | flux-equinix-arm-dc-01 | Washington DC | +| flux2 | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas | +| flux2 | equinix-arm-da-2 | flux-equinix-arm-da-01 | Dallas | +| source-controller | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC | +| source-controller | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas | +| image-automation-controller | equinix-arm-dc-1 | flux-equinix-arm-dc-01 | Washington DC | +| image-automation-controller | equinix-arm-da-1 | flux-equinix-arm-da-01 | Dallas | + +Instance spec: +- Ampere Altra Q80-30 80-core processor @ 2.8GHz +- 2 x 960GB NVME +- 256GB RAM +- 2 x 25Gbps -Note that the instance image source must be **Canonical Ubuntu** instead of the default Oracle Linux. +## Instance setup -## ARM64 Instance setup +In order to add a new runner to the GitHub Actions pool, +first create a server on Equinix with the following configuration: +- Type: `c3.large.arm64` +- OS: `Ubuntu 22.04 LTS` + +### Install prerequisites - SSH into a newly created instance ```shell -ssh ubuntu@ +ssh root@ ``` -- Create the action runner dir + +- Create the ubuntu user +```shell +adduser ubuntu +usermod -aG sudo ubuntu +su - ubuntu +``` + +- Create the prerequisites dir +```shell +mkdir -p prereq && cd prereq +``` + +- Download the prerequisites script ```shell -mkdir -p actions-runner && cd actions-runner +curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/prereq.sh > prereq.sh \ + && chmod +x ./prereq.sh ``` -- Download the provisioning script + +- Install the prerequisites ```shell -curl -sL https://raw.githubusercontent.com/fluxcd/flux2/main/.github/runners/arm64.sh > arm64.sh \ - && chmod +x ./arm64.sh +sudo ./prereq.sh ``` + +### Install runners + - Retrieve the GitHub runner token from the repository [settings page](https://github.com/fluxcd/flux2/settings/actions/runners/new?arch=arm64&os=linux) -- Run the provisioning script passing the token as the first argument + +- Create two directories `flux2-01`, `flux2-02` + +- In each dir run: ```shell -sudo ./arm64.sh +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- ``` + - Reboot the instance ```shell sudo reboot -``` +``` + - Navigate to the GitHub repository [runners page](https://github.com/fluxcd/flux2/settings/actions/runners) and check the runner status diff --git a/.github/runners/arm64.sh b/.github/runners/prereq.sh similarity index 75% rename from .github/runners/arm64.sh rename to .github/runners/prereq.sh index 2cc1026a..043876eb 100755 --- a/.github/runners/arm64.sh +++ b/.github/runners/prereq.sh @@ -14,20 +14,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -# This script is meant to be run locally and in CI to validate the Kubernetes -# manifests (including Flux custom resources) before changes are merged into -# the branch synced by Flux in-cluster. +# This script installs the prerequisites for running Flux end-to-end tests with Docker and GitHub self-hosted runners. set -eu -REPOSITORY_TOKEN=$1 -REPOSITORY_URL=${2:-https://github.com/fluxcd/flux2} - -KIND_VERSION=0.11.1 -KUBECTL_VERSION=1.21.2 -KUSTOMIZE_VERSION=4.1.3 -GITHUB_RUNNER_VERSION=2.278.0 -PACKAGES="apt-transport-https ca-certificates software-properties-common build-essential libssl-dev gnupg lsb-release jq" +KIND_VERSION=0.17.0 +KUBECTL_VERSION=1.24.0 +KUSTOMIZE_VERSION=4.5.7 +HELM_VERSION=3.10.1 +GITHUB_RUNNER_VERSION=2.298.2 +PACKAGES="apt-transport-https ca-certificates software-properties-common build-essential libssl-dev gnupg lsb-release jq pkg-config" # install prerequisites apt-get update \ @@ -35,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 @@ -57,6 +57,12 @@ curl -Lo ./kustomize.tar.gz https://github.com/kubernetes-sigs/kustomize/release && rm kustomize.tar.gz install -o root -g root -m 0755 kustomize /usr/local/bin/kustomize +# install helm +curl -Lo ./helm.tar.gz https://get.helm.sh/helm-v${HELM_VERSION}-linux-arm64.tar.gz \ + && tar -zxvf helm.tar.gz \ + && rm helm.tar.gz +install -o root -g root -m 0755 linux-arm64/helm /usr/local/bin/helm + # download runner curl -o actions-runner-linux-arm64.tar.gz -L https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/actions-runner-linux-arm64-${GITHUB_RUNNER_VERSION}.tar.gz \ && tar xzf actions-runner-linux-arm64.tar.gz \ @@ -64,10 +70,3 @@ curl -o actions-runner-linux-arm64.tar.gz -L https://github.com/actions/runner/r # install runner dependencies ./bin/installdependencies.sh - -# register runner with GitHub -sudo -u ubuntu ./config.sh --unattended --url ${REPOSITORY_URL} --token ${REPOSITORY_TOKEN} - -# start runner -./svc.sh install -./svc.sh start diff --git a/.github/runners/runner-setup.sh b/.github/runners/runner-setup.sh new file mode 100755 index 00000000..b13d87e2 --- /dev/null +++ b/.github/runners/runner-setup.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Copyright 2021 The Flux authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script installs a GitHub self-hosted ARM64 runner for running Flux end-to-end tests. + +set -eu + +RUNNER_NAME=$1 +REPOSITORY_TOKEN=$2 +REPOSITORY_URL=${3:-https://github.com/fluxcd/flux2} + +GITHUB_RUNNER_VERSION=2.298.2 + +# download runner +curl -o actions-runner-linux-arm64.tar.gz -L https://github.com/actions/runner/releases/download/v${GITHUB_RUNNER_VERSION}/actions-runner-linux-arm64-${GITHUB_RUNNER_VERSION}.tar.gz \ + && tar xzf actions-runner-linux-arm64.tar.gz \ + && rm actions-runner-linux-arm64.tar.gz + +# register runner with GitHub +./config.sh --unattended --url ${REPOSITORY_URL} --token ${REPOSITORY_TOKEN} --name ${RUNNER_NAME} + +# start runner +sudo ./svc.sh install +sudo ./svc.sh start 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..e14dc34b --- /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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup flux + uses: ./action diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml new file mode 100644 index 00000000..54bd0682 --- /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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Create backport PRs + uses: korthout/backport-action@addffea45a2f0b5682f1d5ba0506f45bc18bf174 # v2.3.0 + # xref: https://github.com/korthout/backport-action#inputs + with: + # Use token to allow workflows to be triggered for the created PR + github_token: ${{ secrets.BOT_GITHUB_TOKEN }} + # Match labels with a pattern `backport:` + 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/e2e-arm64.yaml b/.github/workflows/e2e-arm64.yaml index a582f6f6..34a18134 100644 --- a/.github/workflows/e2e-arm64.yaml +++ b/.github/workflows/e2e-arm64.yaml @@ -3,34 +3,75 @@ name: e2e-arm64 on: workflow_dispatch: push: - branches: [ main, update-components, arm64-e2e ] + branches: [ 'main', 'update-components', 'e2e-*', 'release/**' ] + +permissions: + contents: read jobs: - ampere: - # Runner info - # Owner: Stefan Prodan + e2e-arm64-kubernetes: + # Hosted on Equinix # Docs: https://github.com/fluxcd/flux2/tree/main/.github/runners - runs-on: [self-hosted, Linux, ARM64] + runs-on: [self-hosted, Linux, ARM64, equinix] + strategy: + matrix: + # Keep this list up-to-date with https://endoflife.date/kubernetes + # Check which versions are available on DockerHub with 'crane ls kindest/node' + KUBERNETES_VERSION: [ 1.26.6, 1.27.3, 1.28.0, 1.29.0 ] + fail-fast: false steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: 1.16.x + go-version: 1.20.x + cache-dependency-path: | + **/go.sum + **/go.mod - 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) + 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 --name ${{ steps.prep.outputs.CLUSTER }} --kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }} + kind create cluster \ + --wait 5m \ + --name ${{ steps.prep.outputs.CLUSTER }} \ + --kubeconfig=/tmp/${{ steps.prep.outputs.CLUSTER }} \ + --image=kindest/node:v${{ matrix.KUBERNETES_VERSION }} - name: Run e2e tests run: TEST_KUBECONFIG=/tmp/${{ steps.prep.outputs.CLUSTER }} make e2e + - name: Run multi-tenancy tests + env: + KUBECONFIG: /tmp/${{ steps.prep.outputs.CLUSTER }} + run: | + ./bin/flux install + ./bin/flux create source git flux-system \ + --interval=15m \ + --url=https://github.com/fluxcd/flux2-multi-tenancy \ + --branch=main \ + --ignore-paths="./clusters/**/flux-system/" + ./bin/flux create kustomization flux-system \ + --interval=15m \ + --source=flux-system \ + --path=./clusters/staging + kubectl -n flux-system wait kustomization/tenants --for=condition=ready --timeout=5m + kubectl -n apps wait kustomization/dev-team --for=condition=ready --timeout=1m + kubectl -n apps wait helmrelease/podinfo --for=condition=ready --timeout=1m + - name: 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: | diff --git a/.github/workflows/e2e-azure.yaml b/.github/workflows/e2e-azure.yaml index d5dbd31e..14306dd4 100644 --- a/.github/workflows/e2e-azure.yaml +++ b/.github/workflows/e2e-azure.yaml @@ -3,50 +3,54 @@ 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-amd64-aks: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: ./tests/azure + # This job is currently disabled. Remove the false check when Azure subscription is enabled. + if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' steps: - name: Checkout - uses: actions/checkout@v2 - - name: Restore Go cache - uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go1.16-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go1.16- + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: 1.16.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.20.x + cache-dependency-path: tests/azure/go.sum - name: Setup Flux CLI run: | make build mkdir -p $HOME/.local/bin mv ./bin/flux $HOME/.local/bin + working-directory: ./ - name: Setup SOPS run: | - 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 + wget https://github.com/mozilla/sops/releases/download/v3.7.1/sops-v3.7.1.linux -O $HOME/.local/bin/sops + chmod +x $HOME/.local/bin/sops - name: Setup Terraform - uses: hashicorp/setup-terraform@v1 + uses: hashicorp/setup-terraform@a1502cd9e758c50496cc9ac5308c4843bcd56d36 # v2 with: - terraform_version: 1.0.7 + terraform_version: 1.2.8 terraform_wrapper: false - name: Setup Azure CLI run: | @@ -58,9 +62,75 @@ jobs: ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} run: | - echo $HOME - echo $PATH ls $HOME/.local/bin az login --service-principal -u ${ARM_CLIENT_ID} -p ${ARM_CLIENT_SECRET} -t ${ARM_TENANT_ID} - cd ./tests/azure go test -v -coverprofile cover.out -timeout 60m . + + refactored-e2e-amd64-aks: + runs-on: ubuntu-22.04 + defaults: + run: + working-directory: ./tests/integration + # This job is currently disabled. Remove the false check when Azure subscription is enabled. + if: false && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && github.actor != 'dependabot[bot]' + steps: + - name: CheckoutD + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: 1.20.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 Azure + uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.4.6 + with: + creds: '{"clientId":"${{ secrets.AZ_ARM_CLIENT_ID }}","clientSecret":"${{ secrets.AZ_ARM_CLIENT_SECRET }}","subscriptionId":"${{ secrets.AZ_ARM_SUBSCRIPTION_ID }}","tenantId":"${{ secrets.AZ_ARM_TENANT_ID }}"}' + - name: Set dynamic variables in .env + run: | + cat > .env < 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 68% rename from .github/workflows/bootstrap.yaml rename to .github/workflows/e2e-bootstrap.yaml index 5b2f9666..0d010405 100644 --- a/.github/workflows/bootstrap.yaml +++ b/.github/workflows/e2e-bootstrap.yaml @@ -1,36 +1,41 @@ -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.16-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go1.16- + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: 1.16.x + go-version: 1.20.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Setup Kubernetes - uses: engineerd/setup-kind@v0.5.0 + uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0 with: - version: v0.11.1 - image: kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6 + version: v0.20.0 + cluster_name: kind + # The versions below should target the newest Kubernetes version + # Keep this up-to-date with https://endoflife.date/kubernetes + node_image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c + kubectl_version: v1.28.0 - name: Setup Kustomize - uses: fluxcd/pkg//actions/kustomize@main + uses: fluxcd/pkg/actions/kustomize@main - name: Build run: | make cmd/flux/.manifests.done @@ -43,7 +48,7 @@ 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/ \ @@ -80,13 +85,6 @@ 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 @@ -101,15 +99,27 @@ jobs: --path=test-cluster \ --read-write-key /tmp/flux reconcile image repository podinfo - /tmp/flux reconcile image update flux-system /tmp/flux get images all - /tmp/flux get images policy podinfo | grep "5.2.1" - /tmp/flux get image update flux-system | grep commit + + retries=10 + count=0 + ok=false + until ${ok}; do + /tmp/flux get image update flux-system | grep 'commit' && ok=true || ok=false + count=$(($count + 1)) + if [[ ${count} -eq ${retries} ]]; then + echo "No more retries left" + exit 1 + fi + sleep 6 + /tmp/flux reconcile image update flux-system + done env: GITHUB_TOKEN: ${{ secrets.GITPROVIDER_BOT_TOKEN }} GITHUB_REPO_NAME: ${{ steps.vars.outputs.test_repo_name }} GITHUB_ORG_NAME: fluxcd-testing - name: delete repository + if: ${{ always() }} run: | curl \ -X DELETE \ diff --git a/.github/workflows/e2e-gcp.yaml b/.github/workflows/e2e-gcp.yaml new file mode 100644 index 00000000..256c10c0 --- /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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: 1.20.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@f6de81663f7788d05bd15bcce18f0e57f23f0846 # v2.0.1 + id: 'auth' + with: + credentials_json: '${{ secrets.FLUX2_E2E_GOOGLE_CREDENTIALS }}' + token_format: 'access_token' + - name: Setup gcloud + uses: google-github-actions/setup-gcloud@5a5f7b85fca43e76e53463acaa9d408a03c98d3a # v2.0.1 + - name: Setup QEMU + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + - name: Log into us-central1-docker.pkg.dev + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.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 87681bc2..de08bc7f 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -1,44 +1,50 @@ name: e2e on: + workflow_dispatch: push: - branches: [ main ] + branches: [ 'main', 'release/**' ] pull_request: - branches: [ main ] + branches: [ 'main', 'release/**' ] + paths-ignore: [ 'docs/**', 'rfcs/**' ] + +permissions: + contents: read jobs: - kind: + e2e-amd64-kubernetes: runs-on: ubuntu-latest + 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.16-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go1.16- + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: 1.16.x + go-version: 1.20.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Setup Kubernetes - uses: engineerd/setup-kind@v0.5.0 + uses: helm/kind-action@dda0770415bac9fc20092cacbc54aa298604d140 # v1.8.0 with: - version: v0.11.1 - image: kindest/node:v1.19.11@sha256:07db187ae84b4b7de440a73886f008cf903fcf5764ba8106a9fd5243d6f32729 + version: v0.20.0 + cluster_name: kind config: .github/kind/config.yaml # disable KIND-net - - name: Setup envtest - uses: fluxcd/pkg/actions/envtest@main - with: - version: "1.21.x" + # The versions below should target the newest Kubernetes version + # Keep this up-to-date with https://endoflife.date/kubernetes + node_image: kindest/node:v1.28.0@sha256:9f3ff58f19dcf1a0611d11e8ac989fdb30a28f40f236f59f0bea31fb956ccf5c + kubectl_version: v1.28.0 - name: Setup Calico for network policy run: | - kubectl apply -f https://docs.projectcalico.org/v3.16/manifests/calico.yaml + kubectl apply -f https://docs.projectcalico.org/v3.25/manifests/calico.yaml kubectl -n kube-system set env daemonset/calico-node FELIX_IGNORELOOSERPF=true - name: Setup Kustomize - uses: fluxcd/pkg//actions/kustomize@main + uses: fluxcd/pkg/actions/kustomize@main - name: Run tests run: make test - name: Run e2e tests @@ -72,21 +78,14 @@ jobs: run: | /tmp/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 \ --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 - name: flux get sources git run: | /tmp/flux get sources git @@ -141,7 +140,7 @@ jobs: --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 \ @@ -172,6 +171,36 @@ jobs: - name: flux delete source git run: | /tmp/flux delete source git podinfo --silent + - name: flux oci artifacts + run: | + /tmp/flux push artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ + --path="./manifests" \ + --source="${{ github.repositoryUrl }}" \ + --revision="${{ github.ref }}@sha1:${{ github.sha }}" + /tmp/flux tag artifact oci://localhost:5000/fluxcd/flux:${{ github.sha }} \ + --tag latest + /tmp/flux list artifacts oci://localhost:5000/fluxcd/flux + - name: flux oci repositories + run: | + /tmp/flux create source oci podinfo-oci \ + --url oci://ghcr.io/stefanprodan/manifests/podinfo \ + --tag-semver 6.3.x \ + --interval 10m + /tmp/flux create kustomization podinfo-oci \ + --source=OCIRepository/podinfo-oci \ + --path="./" \ + --prune=true \ + --interval=5m \ + --target-namespace=default \ + --wait=true \ + --health-check-timeout=3m + /tmp/flux reconcile source oci podinfo-oci + /tmp/flux suspend source oci podinfo-oci + /tmp/flux get sources oci + /tmp/flux resume source oci podinfo-oci + /tmp/flux export source oci podinfo-oci + /tmp/flux delete ks podinfo-oci --silent + /tmp/flux delete source oci podinfo-oci --silent - name: flux create tenant run: | /tmp/flux create tenant dev-team --with-namespace=apps @@ -180,21 +209,20 @@ jobs: /tmp/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 \ --url=https://github.com/fluxcd/flux2-kustomize-helm-example \ --branch=main \ + --ignore-paths="./clusters/**/flux-system/" \ --recurse-submodules /tmp/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: | diff --git a/.github/workflows/ossf.yaml b/.github/workflows/ossf.yaml new file mode 100644 index 00000000..fc15b787 --- /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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Run analysis + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 + with: + results_file: results.sarif + results_format: sarif + repo_token: ${{ secrets.GITHUB_TOKEN }} + publish_results: true + - name: Upload artifact + uses: actions/upload-artifact@1eb3cb2b3e0f29609092a73eb033bb759a334595 # v4.1.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + - name: Upload SARIF results + uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 + 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 6c599da5..9f7c8579 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,50 +4,52 @@ on: push: tags: [ 'v*' ] +permissions: + 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Unshallow run: git fetch --prune --unshallow - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: 1.16.x + go-version: 1.20.x + cache: false - name: Setup QEMU - uses: docker/setup-qemu-action@v1 - with: - platforms: all + uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0 - name: Setup Docker Buildx id: buildx - uses: docker/setup-buildx-action@v1 - with: - buildkitd-flags: "--debug" + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + - name: Setup Syft + uses: anchore/sbom-action/download-syft@c7f031d9249a826a082ea14c79d3b686a51d485a # v0.15.3 + - name: Setup Cosign + uses: sigstore/cosign-installer@9614fae9e5c5eddabb09f90a270fcb487c9f7149 # v3.3.0 + - name: Setup Kustomize + uses: fluxcd/pkg/actions/kustomize@main - name: Login to GitHub Container Registry - uses: docker/login-action@v1 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.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@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: username: fluxcdbot password: ${{ secrets.DOCKER_FLUXCD_PASSWORD }} - - name: Download release notes utility - env: - GH_REL_URL: https://github.com/buchanae/github-release-notes/releases/download/0.2.0/github-release-notes-linux-amd64-0.2.0.tar.gz - run: cd /tmp && curl -sSL ${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/ - - name: Generate release notes - run: | - echo 'CHANGELOG' > /tmp/release.txt - github-release-notes -org fluxcd -repo toolkit -since-latest-release -include-author >> /tmp/release.txt - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Setup Kustomize - uses: fluxcd/pkg//actions/kustomize@main - name: Generate manifests run: | make cmd/flux/.manifests.done @@ -56,22 +58,154 @@ 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 - name: Archive the OpenAPI JSON schemas run: | tar -czvf ./output/crd-schemas.tar.gz -C schemas . + - name: Download release notes utility + env: + GH_REL_URL: https://github.com/buchanae/github-release-notes/releases/download/0.2.0/github-release-notes-linux-amd64-0.2.0.tar.gz + run: cd /tmp && curl -sSL ${GH_REL_URL} | tar xz && sudo mv github-release-notes /usr/local/bin/ + - name: Generate release notes + run: | + NOTES="./output/notes.md" + echo '## CLI Changelog' > ${NOTES} + github-release-notes -org fluxcd -repo flux2 -since-latest-release -include-author >> ${NOTES} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v1 + id: run-goreleaser + uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 with: version: latest - args: release --release-notes=/tmp/release.txt --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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - 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@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ghcr.io + username: fluxcdbot + password: ${{ secrets.GHCR_TOKEN }} + - name: Login to DockerHub + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.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@9614fae9e5c5eddabb09f90a270fcb487c9f7149 # v3.3.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@v1.9.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@v1.9.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@v1.9.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..28b1b840 100644 --- a/.github/workflows/scan.yaml +++ b/.github/workflows/scan.yaml @@ -1,60 +1,85 @@ -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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - 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@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: 1.20.x + 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 --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 + uses: github/codeql-action/upload-sarif@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Setup Go + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: 1.20.x + cache-dependency-path: | + **/go.sum + **/go.mod - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 with: languages: go + # xref: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # xref: https://codeql.github.com/codeql-query-help/go/ + queries: security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@cdcdbb579706841c47f7063dda365e292e5cad7a # v2.13.4 diff --git a/.github/workflows/sync-labels.yaml b/.github/workflows/sync-labels.yaml new file mode 100644 index 00000000..e112ee5f --- /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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - uses: EndBug/label-sync@da00f2c11fdb78e4fae44adac2fdd713778ea3e8 # v2.3.2 + 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 5c310446..ec619fcd 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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Setup Go - uses: actions/setup-go@v2 + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 with: - go-version: 1.16.x + go-version: 1.20.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@153407881ec5c347639a548ade7d8ad1d6740e38 # v5.0.2 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/.gitignore b/.gitignore index c431bf85..69729b00 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ bin/ output/ cmd/flux/manifests/ cmd/flux/.manifests.done +testbin/ # Docs site/ diff --git a/.goreleaser.yml b/.goreleaser.yml index b57017b1..df71dd3b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -40,6 +40,37 @@ archives: format: zip files: - none* +source: + enabled: true + name_template: '{{ .ProjectName }}_{{ .Version }}_source_code' +sboms: + - id: source + artifacts: source + documents: + - "{{ .ProjectName }}_{{ .Version }}_sbom.spdx.json" +release: + extra_files: + - glob: output/crd-schemas.tar.gz + - glob: output/manifests.tar.gz + - glob: output/install.yaml +checksum: + extra_files: + - glob: output/crd-schemas.tar.gz + - glob: output/manifests.tar.gz + - glob: output/install.yaml +signs: + - cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + certificate: '${artifact}.pem' + args: + - sign-blob + - "--yes" + - '--output-certificate=${certificate}' + - '--output-signature=${signature}' + - '${artifact}' + artifacts: checksum + output: true brews: - name: flux tap: @@ -52,14 +83,7 @@ brews: 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: @@ -78,17 +102,12 @@ publishers: - AUR_BOT_SSH_PRIVATE_KEY={{ .Env.AUR_BOT_SSH_PRIVATE_KEY }} cmd: | .github/aur/flux-go/publish.sh {{ .Version }} -release: - extra_files: - - glob: ./output/crd-schemas.tar.gz - - glob: ./output/manifests.tar.gz - - glob: ./output/install.yaml dockers: - image_templates: - 'fluxcd/flux-cli:{{ .Tag }}-amd64' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-amd64' dockerfile: Dockerfile - use_buildx: true + use: buildx goos: linux goarch: amd64 build_flag_templates: @@ -104,7 +123,7 @@ dockers: - 'fluxcd/flux-cli:{{ .Tag }}-arm64' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm64' dockerfile: Dockerfile - use_buildx: true + use: buildx goos: linux goarch: arm64 build_flag_templates: @@ -120,7 +139,7 @@ dockers: - 'fluxcd/flux-cli:{{ .Tag }}-arm' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm' dockerfile: Dockerfile - use_buildx: true + use: buildx goos: linux goarch: arm goarm: 7 @@ -144,3 +163,13 @@ docker_manifests: - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-amd64' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm64' - 'ghcr.io/fluxcd/flux-cli:{{ .Tag }}-arm' +docker_signs: + - cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + args: + - sign + - "--yes" + - '${artifact}' + artifacts: all + output: true 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 438dbac6..65feaa66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,20 @@ -FROM alpine:3.14 as builder +FROM alpine:3.19 as builder RUN apk add --no-cache ca-certificates curl ARG ARCH=linux/amd64 -ARG KUBECTL_VER=1.22.2 +ARG KUBECTL_VER=1.28.4 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.14 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 3d375d42..44317f17 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ 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 -ENVTEST_BIN_VERSION?=latest -KUBEBUILDER_ASSETS?=$(shell $(SETUP_ENVTEST) use -i $(ENVTEST_BIN_VERSION) -p path) +# Architecture to use envtest with +ENVTEST_ARCH ?= amd64 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -16,7 +17,9 @@ rwildcard=$(foreach d,$(wildcard $(addsuffix *,$(1))),$(call rwildcard,$(d)/,$(2 all: test build tidy: - go mod tidy + go mod tidy -compat=1.20 + cd tests/azure && go mod tidy -compat=1.20 + cd tests/integration && go mod tidy -compat=1.20 fmt: go fmt ./... @@ -33,11 +36,14 @@ cleanup-kind: kind delete cluster --name=flux-e2e-test rm $(TEST_KUBECONFIG) +KUBEBUILDER_ASSETS?="$(shell $(ENVTEST) --arch=$(ENVTEST_ARCH) use -i $(ENVTEST_KUBERNETES_VERSION) --bin-dir=$(ENVTEST_ASSETS_DIR) -p path)" +TEST_PKG_PATH="./..." test: $(EMBEDDED_MANIFESTS_TARGET) tidy fmt vet install-envtest - KUBEBUILDER_ASSETS="$(KUBEBUILDER_ASSETS)" go test ./... -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 @@ -51,6 +57,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 @@ -58,27 +67,33 @@ install: install-dev: CGO_ENABLED=0 go build -o /usr/local/bin ./cmd/flux -install-envtest: setup-envtest - $(SETUP_ENVTEST) use $(ENVTEST_BIN_VERSION) - setup-bootstrap-patch: go run ./tests/bootstrap/main.go setup-image-automation: cd tests/image-automation && go run main.go -# Find or download setup-envtest -setup-envtest: -ifeq (, $(shell which setup-envtest)) - @{ \ - set -e ;\ - SETUP_ENVTEST_TMP_DIR=$$(mktemp -d) ;\ - cd $$SETUP_ENVTEST_TMP_DIR ;\ - go mod init tmp ;\ - go get sigs.k8s.io/controller-runtime/tools/setup-envtest@latest ;\ - rm -rf $$SETUP_ENVTEST_TMP_DIR ;\ - } -SETUP_ENVTEST=$(GOBIN)/setup-envtest -else -SETUP_ENVTEST=$(shell which setup-envtest) -endif +ENVTEST_ASSETS_DIR=$(shell pwd)/testbin +ENVTEST_KUBERNETES_VERSION?=latest +install-envtest: setup-envtest + mkdir -p ${ENVTEST_ASSETS_DIR} + $(ENVTEST) use $(ENVTEST_KUBERNETES_VERSION) --arch=$(ENVTEST_ARCH) --bin-dir=$(ENVTEST_ASSETS_DIR) + +ENVTEST = $(shell pwd)/bin/setup-envtest +.PHONY: envtest +setup-envtest: ## Download envtest-setup locally if necessary. + $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest@latest) + +# go-install-tool will 'go install' any package $2 and install it to $1. +PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) +define go-install-tool +@[ -f $(1) ] || { \ +set -e ;\ +TMP_DIR=$$(mktemp -d) ;\ +cd $$TMP_DIR ;\ +go mod init tmp ;\ +echo "Downloading $(2)" ;\ +GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ +rm -rf $$TMP_DIR ;\ +} +endef diff --git a/README.md b/README.md index cc96ed4c..5641b7be 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/)) 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://fluxcd.io/img/diagrams/gitops-toolkit.png) You can use the toolkit to extend Flux, or to build your own systems for continuous delivery -- see [the developer -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 038a5204..d88786ee 100644 --- a/action/action.yml +++ b/action/action.yml @@ -1,43 +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: "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: "Add flux binary to /usr/local/bin" - shell: bash - run: | - sudo cp /tmp/flux/flux /usr/local/bin - - 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..de02154a 100644 --- a/cmd/flux/bootstrap.go +++ b/cmd/flux/bootstrap.go @@ -17,27 +17,31 @@ limitations under the License. package main import ( + "context" "crypto/elliptic" "fmt" - "os" "strings" + "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 @@ -72,6 +76,8 @@ type bootstrapFlags struct { gpgPassphrase string gpgKeyID string + force bool + commitMessageAppendix string } @@ -88,12 +94,12 @@ 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.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,15 +108,15 @@ 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()) @@ -129,8 +135,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 diffrent tool such as Helm") bootstrapCmd.PersistentFlags().MarkHidden("manifests") rootCmd.AddCommand(bootstrapCmd) @@ -154,7 +159,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 } @@ -190,3 +195,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 07fb04c7..f4caf62d 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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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,21 +172,28 @@ 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{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, ImagePullSecret: bootstrapArgs.imagePullSecret, @@ -200,7 +214,7 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, TargetPath: bServerArgs.path.String(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } @@ -211,19 +225,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 } @@ -232,28 +245,33 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { // Sync manifest config syncOpts := sync.Options{ Interval: bServerArgs.interval, - Name: rootArgs.namespace, - Namespace: rootArgs.namespace, + Name: *kubeconfigArgs.Namespace, + Namespace: *kubeconfigArgs.Namespace, Branch: bootstrapArgs.branch, 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(rootArgs.kubeconfig, rootArgs.kubecontext), + 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 786a2692..eb66b0ad 100644 --- a/cmd/flux/bootstrap_git.go +++ b/cmd/flux/bootstrap_git.go @@ -24,53 +24,66 @@ 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/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" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/gogit" ) 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/// --ssh-key-algorithm=rsa --ssh-rsa-bits=4096 --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 } +const ( + gitPasswordEnvVar = "GIT_PASSWORD" +) + var gitArgs gitFlags func init() { @@ -80,11 +93,25 @@ 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") bootstrapCmd.AddCommand(bootstrapGitCmd) } func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { + 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 +120,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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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,18 +166,39 @@ 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{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, ImagePullSecret: bootstrapArgs.imagePullSecret, @@ -149,22 +219,26 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, TargetPath: gitArgs.path.String(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } if bootstrapArgs.tokenAuth { secretOpts.Username = gitArgs.username secretOpts.Password = gitArgs.password + secretOpts.CAFile = caBundle - if bootstrapArgs.caFile != "" { - secretOpts.CAFilePath = bootstrapArgs.caFile + // Remove port of the given host when not syncing over HTTP/S to not assume port for protocol + // This _might_ be overwritten later on by e.g. --ssh-hostname + if repositoryURL.Scheme != "https" && repositoryURL.Scheme != "http" { + repositoryURL.Host = repositoryURL.Hostname() } // Configure repository URL to match auth config for sync. repositoryURL.User = nil - repositoryURL.Scheme = "https" - repositoryURL.Host = repositoryURL.Hostname() + if !gitArgs.insecureHttpAllowed { + repositoryURL.Scheme = "https" + } } else { secretOpts.PrivateKeyAlgorithm = sourcesecret.PrivateKeyAlgorithm(bootstrapArgs.keyAlgorithm) secretOpts.Password = gitArgs.password @@ -183,9 +257,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 @@ -194,37 +271,31 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { // Sync manifest config syncOpts := sync.Options{ Interval: gitArgs.interval, - Name: rootArgs.namespace, - Namespace: rootArgs.namespace, + Name: *kubeconfigArgs.Namespace, + Namespace: *kubeconfigArgs.Namespace, URL: repositoryURL.String(), Branch: bootstrapArgs.branch, 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(rootArgs.kubeconfig, rootArgs.kubecontext), + 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 @@ -237,22 +308,47 @@ 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") + } + return &git.AuthOptions{ + Transport: git.HTTP, + Username: gitArgs.username, + Password: gitArgs.password, + }, nil case "https": - return &http.BasicAuth{ - Username: gitArgs.username, - Password: gitArgs.password, + return &git.AuthOptions{ + Transport: git.HTTPS, + Username: gitArgs.username, + Password: gitArgs.password, + CAFile: caBundle, }, 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..37d53c92 --- /dev/null +++ b/cmd/flux/bootstrap_gitea.go @@ -0,0 +1,275 @@ +/* +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, + 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 f2775da9..c2860d05 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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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,22 +167,28 @@ 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{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, ImagePullSecret: bootstrapArgs.imagePullSecret, @@ -196,23 +209,20 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, TargetPath: githubArgs.path.ToSlash(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } 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 } @@ -221,28 +231,32 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { // Sync manifest config syncOpts := sync.Options{ Interval: githubArgs.interval, - Name: rootArgs.namespace, - Namespace: rootArgs.namespace, + Name: *kubeconfigArgs.Namespace, + Namespace: *kubeconfigArgs.Namespace, Branch: bootstrapArgs.branch, 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(rootArgs.kubeconfig, rootArgs.kubecontext), + 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 88b85aeb..f281a883 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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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,21 +192,28 @@ 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{ BaseURL: rootArgs.defaults.BaseURL, Version: bootstrapArgs.version, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Components: bootstrapComponents(), Registry: bootstrapArgs.registry, ImagePullSecret: bootstrapArgs.imagePullSecret, @@ -207,26 +234,28 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { // Source generation and secret config secretOpts := sourcesecret.Options{ Name: bootstrapArgs.secretName, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, TargetPath: gitlabArgs.path.String(), ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } 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 } @@ -235,35 +264,42 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { // Sync manifest config syncOpts := sync.Options{ Interval: gitlabArgs.interval, - Name: rootArgs.namespace, - Namespace: rootArgs.namespace, + Name: *kubeconfigArgs.Namespace, + Namespace: *kubeconfigArgs.Namespace, Branch: bootstrapArgs.branch, 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(rootArgs.kubeconfig, rootArgs.kubecontext), + 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/tests/azure/terraform.go b/cmd/flux/build.go similarity index 63% rename from tests/azure/terraform.go rename to cmd/flux/build.go index ee6cd156..1ff140be 100644 --- a/tests/azure/terraform.go +++ b/cmd/flux/build.go @@ -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 buildCmd = &cobra.Command{ + Use: "build", + Short: "Build a flux resource", + Long: `The build command is used to build flux resources.`, +} -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(buildCmd) } 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 new file mode 100644 index 00000000..ea0517c0 --- /dev/null +++ b/cmd/flux/build_kustomization.go @@ -0,0 +1,156 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "os/signal" + + "github.com/fluxcd/pkg/ssa" + "github.com/spf13/cobra" + + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + + "github.com/fluxcd/flux2/v2/internal/build" +) + +var buildKsCmd = &cobra.Command{ + Use: "kustomization", + Aliases: []string{"ks"}, + Short: "Build Kustomization", + Long: `The build command queries the Kubernetes API and fetches the specified Flux Kustomization. +It then uses the fetched in cluster flux kustomization to perform needed transformation on the local kustomization.yaml +pointed at by --path. The local kustomization.yaml is generated if it does not exist. Finally it builds the overlays using the local kustomization.yaml, and write the resulting multi-doc YAML to stdout. + +It is possible to specify a Flux kustomization file using --kustomization-file.`, + Example: `# Build the local manifests as they were built on the cluster +flux build kustomization my-app --path ./path/to/local/manifests + +# Build using a local flux kustomization file +flux build kustomization my-app --path ./path/to/local/manifests --kustomization-file ./path/to/local/my-app.yaml + +# Build in dry-run mode without connecting to the cluster. +# Note that variable substitutions from Secrets and ConfigMaps are skipped in dry-run mode. +flux build kustomization my-app --path ./path/to/local/manifests \ + --kustomization-file ./path/to/local/my-app.yaml \ + --dry-run + +# Exclude files by providing a comma separated list of entries that follow the .gitignore pattern fromat. +flux build kustomization my-app --path ./path/to/local/manifests \ + --kustomization-file ./path/to/local/my-app.yaml \ + --ignore-paths "/to_ignore/**/*.yaml,ignore.yaml"`, + ValidArgsFunction: resourceNamesCompletionFunc(kustomizev1.GroupVersion.WithKind(kustomizev1.KustomizationKind)), + RunE: buildKsCmdRun, +} + +type buildKsFlags struct { + kustomizationFile string + path string + ignorePaths []string + dryRun bool +} + +var buildKsArgs buildKsFlags + +func init() { + buildKsCmd.Flags().StringVar(&buildKsArgs.path, "path", "", "Path to the manifests location.") + buildKsCmd.Flags().StringVar(&buildKsArgs.kustomizationFile, "kustomization-file", "", "Path to the Flux Kustomization YAML file.") + buildKsCmd.Flags().StringSliceVar(&buildKsArgs.ignorePaths, "ignore-paths", nil, "set paths to ignore in .gitignore format") + buildKsCmd.Flags().BoolVar(&buildKsArgs.dryRun, "dry-run", false, "Dry run mode.") + buildCmd.AddCommand(buildKsCmd) +} + +func buildKsCmdRun(cmd *cobra.Command, args []string) (err error) { + if len(args) < 1 { + return fmt.Errorf("%s name is required", kustomizationType.humanKind) + } + name := args[0] + + if buildKsArgs.path == "" { + return fmt.Errorf("invalid resource path %q", buildKsArgs.path) + } + + if fs, err := os.Stat(buildKsArgs.path); err != nil || !fs.IsDir() { + return fmt.Errorf("invalid resource path %q", buildKsArgs.path) + } + + if buildKsArgs.dryRun && buildKsArgs.kustomizationFile == "" { + return fmt.Errorf("dry-run mode requires a kustomization file") + } + + if buildKsArgs.kustomizationFile != "" { + if fs, err := os.Stat(buildKsArgs.kustomizationFile); os.IsNotExist(err) || fs.IsDir() { + return fmt.Errorf("invalid kustomization file %q", buildKsArgs.kustomizationFile) + } + } + + var builder *build.Builder + if buildKsArgs.dryRun { + builder, err = build.NewBuilder(name, buildKsArgs.path, + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(buildKsArgs.kustomizationFile), + build.WithDryRun(buildKsArgs.dryRun), + build.WithNamespace(*kubeconfigArgs.Namespace), + build.WithIgnore(buildKsArgs.ignorePaths), + ) + } else { + builder, err = build.NewBuilder(name, buildKsArgs.path, + build.WithClientConfig(kubeconfigArgs, kubeclientOptions), + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(buildKsArgs.kustomizationFile), + build.WithIgnore(buildKsArgs.ignorePaths), + ) + } + + if err != nil { + return err + } + + // create a signal channel + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt) + + errChan := make(chan error) + go func() { + objects, err := builder.Build() + if err != nil { + errChan <- err + } + + manifests, err := ssa.ObjectsToYAML(objects) + if err != nil { + errChan <- err + } + + cmd.Print(manifests) + errChan <- nil + }() + + select { + case <-sigc: + fmt.Println("Build cancelled... exiting.") + return builder.Cancel() + case err := <-errChan: + if err != nil { + return err + } + } + + return nil + +} diff --git a/cmd/flux/build_kustomization_test.go b/cmd/flux/build_kustomization_test.go new file mode 100644 index 00000000..383fb1fc --- /dev/null +++ b/cmd/flux/build_kustomization_test.go @@ -0,0 +1,202 @@ +//go:build unit +// +build unit + +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "os" + "testing" + "text/template" +) + +func setup(t *testing.T, tmpl map[string]string) { + t.Helper() + testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-source.yaml", tmpl, t) + testEnv.CreateObjectFile("./testdata/build-kustomization/podinfo-kustomization.yaml", tmpl, t) +} + +func TestBuildKustomization(t *testing.T) { + tests := []struct { + name string + args string + resultFile string + assertFunc string + }{ + { + name: "no args", + args: "build kustomization podinfo", + resultFile: "invalid resource path \"\"", + assertFunc: "assertError", + }, + { + name: "build podinfo", + args: "build kustomization podinfo --path ./testdata/build-kustomization/podinfo", + resultFile: "./testdata/build-kustomization/podinfo-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build podinfo without service", + args: "build kustomization podinfo --path ./testdata/build-kustomization/delete-service", + resultFile: "./testdata/build-kustomization/podinfo-without-service-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build deployment and configmap with var substitution", + args: "build kustomization podinfo --path ./testdata/build-kustomization/var-substitution", + resultFile: "./testdata/build-kustomization/podinfo-with-var-substitution-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + { + name: "build ignore", + args: "build kustomization podinfo --path ./testdata/build-kustomization/ignore --ignore-paths \"!configmap.yaml,!secret.yaml\"", + resultFile: "./testdata/build-kustomization/podinfo-with-ignore-result.yaml", + assertFunc: "assertGoldenTemplateFile", + }, + } + + tmpl := map[string]string{ + "fluxns": allocateNamespace("flux-system"), + } + setup(t, tmpl) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var assert assertFunc + + switch tt.assertFunc { + case "assertGoldenTemplateFile": + assert = assertGoldenTemplateFile(tt.resultFile, tmpl) + case "assertError": + assert = assertError(tt.resultFile) + } + + cmd := cmdTestCase{ + args: tt.args + " -n " + tmpl["fluxns"], + assert: assert, + } + + cmd.runTestCmd(t) + }) + } +} + +func TestBuildLocalKustomization(t *testing.T) { + podinfo := `apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 5m0s + path: ./kustomize + force: true + prune: true + sourceRef: + kind: GitRepository + name: podinfo + targetNamespace: default + postBuild: + substitute: + cluster_env: "prod" + cluster_region: "eu-central-1" +` + + 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 42c6891e..99ecfd8c 100644 --- a/cmd/flux/check.go +++ b/cmd/flux/check.go @@ -18,28 +18,31 @@ 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", 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 +59,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.26.0-0", } var checkArgs checkFlags @@ -82,7 +82,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 +107,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 +149,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(rootArgs.kubeconfig, rootArgs.kubecontext) - 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 +192,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(rootArgs.kubeconfig, rootArgs.kubecontext) - if err != nil { - return false - } - - statusChecker, err := status.NewStatusChecker(kubeConfig, checkArgs.pollInterval, rootArgs.timeout, logger) - if err != nil { - return false - } - - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) +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 +201,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(rootArgs.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 +222,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 c70d6b09..e5745915 100644 --- a/cmd/flux/check_test.go +++ b/cmd/flux/check_test.go @@ -1,3 +1,4 @@ +//go:build e2e // +build e2e /* @@ -24,27 +25,28 @@ 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) { - jsonOutput, err := utils.ExecKubectlCommand(context.TODO(), utils.ModeCapture, rootArgs.kubeconfig, rootArgs.kubecontext, "version", "--output", "json") + jsonOutput, err := utils.ExecKubectlCommand(context.TODO(), utils.ModeCapture, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, "version", "--output", "json") if err != nil { t.Fatalf("Error running utils.ExecKubectlCommand: %v", err.Error()) } - var versions map[string]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..560c881b --- /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" + "github.com/fluxcd/pkg/ssa" +) + +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 := ssa.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 077d768e..a3bc6696 100644 --- a/cmd/flux/completion.go +++ b/cmd/flux/completion.go @@ -20,21 +20,18 @@ 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" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" - memory "k8s.io/client-go/discovery/cached" "k8s.io/client-go/dynamic" - "k8s.io/client-go/restmapper" ) 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() { @@ -42,7 +39,7 @@ func init() { } func contextsCompletionFunc(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - rawConfig, err := utils.ClientConfig(rootArgs.kubeconfig, rootArgs.kubecontext).RawConfig() + rawConfig, err := kubeconfigArgs.ToRawKubeConfigLoader().RawConfig() if err != nil { return completionError(err) } @@ -63,16 +60,15 @@ func resourceNamesCompletionFunc(gvk schema.GroupVersionKind) func(cmd *cobra.Co ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - cfg, err := utils.KubeConfig(rootArgs.kubeconfig, rootArgs.kubecontext) + cfg, err := utils.KubeConfig(kubeconfigArgs, kubeclientOptions) if err != nil { return completionError(err) } - dc, err := discovery.NewDiscoveryClientForConfig(cfg) + mapper, err := kubeconfigArgs.ToRESTMapper() if err != nil { return completionError(err) } - mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(dc)) mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) if err != nil { @@ -86,7 +82,7 @@ func resourceNamesCompletionFunc(gvk schema.GroupVersionKind) func(cmd *cobra.Co var dr dynamic.ResourceInterface if mapping.Scope.Name() == meta.RESTScopeNameNamespace { - dr = client.Resource(mapping.Resource).Namespace(rootArgs.namespace) + dr = client.Resource(mapping.Resource).Namespace(*kubeconfigArgs.Namespace) } else { dr = client.Resource(mapping.Resource) } 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 3010c99c..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(rootArgs.kubeconfig, rootArgs.kubecontext) // 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 e260ae64..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: rootArgs.namespace, + 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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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 93ac7e89..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 == "" { @@ -94,7 +90,7 @@ func createAlertProviderCmdRun(cmd *cobra.Command, args []string) error { provider := notificationv1.Provider{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: notificationv1.ProviderSpec{ @@ -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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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 6a035858..d06e2ab3 100644 --- a/cmd/flux/create_helmrelease.go +++ b/cmd/flux/create_helmrelease.go @@ -21,30 +21,32 @@ import ( "encoding/json" "fmt" "os" + "strings" + "time" - "github.com/fluxcd/flux2/internal/flags" - "github.com/fluxcd/flux2/internal/utils" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/runtime/transform" + "github.com/fluxcd/flux2/v2/internal/flags" + "github.com/fluxcd/flux2/v2/internal/utils" + "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/v2beta2" ) var createHelmReleaseCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Create or update a HelmRelease resource", - Long: "The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.", + Long: withPreviewNote(`The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.`), Example: ` # Create a HelmRelease with a chart from a HelmRepository source flux create hr podinfo \ --interval=10m \ @@ -81,9 +83,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 \ @@ -108,21 +110,26 @@ var createHelmReleaseCmd = &cobra.Command{ } 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 + 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"} + 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,16 +139,16 @@ 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") 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 == "" { @@ -157,10 +164,15 @@ 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, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: helmv2.HelmReleaseSpec{ @@ -180,12 +192,27 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { Name: helmReleaseArgs.source.Name, Namespace: helmReleaseArgs.source.Namespace, }, + ReconcileStrategy: helmReleaseArgs.reconcileStrategy, }, }, Suspend: false, }, } + if helmReleaseArgs.kubeConfigSecretRef != "" { + helmRelease.Spec.KubeConfig = &meta.KubeConfigReference{ + SecretRef: meta.SecretKeyReference{ + Name: helmReleaseArgs.kubeConfigSecretRef, + }, + } + } + + if helmReleaseArgs.chartInterval != 0 { + helmRelease.Spec.Chart.Spec.Interval = &metav1.Duration{ + Duration: helmReleaseArgs.chartInterval, + } + } + if helmReleaseArgs.createNamespace { if helmRelease.Spec.Install == nil { helmRelease.Spec.Install = &helmv2.Install{} @@ -236,11 +263,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 +291,7 @@ func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -262,8 +303,8 @@ 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) @@ -303,19 +344,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_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 0cebd321..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 == "" { @@ -101,7 +97,7 @@ func createImagePolicyRun(cmd *cobra.Command, args []string) error { var policy = imagev1.ImagePolicy{ ObjectMeta: metav1.ObjectMeta{ Name: objectName, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: labels, }, Spec: imagev1.ImagePolicySpec{ @@ -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 7619c8a6..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 == "" { @@ -104,7 +101,7 @@ func createImageRepositoryRun(cmd *cobra.Command, args []string) error { var repo = imagev1.ImageRepository{ ObjectMeta: metav1.ObjectMeta{ Name: objectName, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: labels, }, Spec: imagev1.ImageRepositorySpec{ diff --git a/cmd/flux/create_image_update.go b/cmd/flux/create_image_update.go index 6ef81a28..c1036dfa 100644 --- a/cmd/flux/create_image_update.go +++ b/cmd/flux/create_image_update.go @@ -23,15 +23,15 @@ import ( 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" + 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)") } @@ -108,13 +120,14 @@ func createImageUpdateRun(cmd *cobra.Command, args []string) error { var update = autov1.ImageUpdateAutomation{ ObjectMeta: metav1.ObjectMeta{ Name: objectName, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, 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 eb898e7b..aa3a73a7 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/v2beta2" + 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 == "" { @@ -143,7 +149,7 @@ func createKsCmdRun(cmd *cobra.Command, args []string) error { kustomization := kustomizev1.Kustomization{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: kslabels, }, Spec: kustomizev1.KustomizationSpec{ @@ -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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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 c2d33a15..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 == "" { @@ -109,7 +105,7 @@ func createReceiverCmdRun(cmd *cobra.Command, args []string) error { receiver := notificationv1.Receiver{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: notificationv1.ReceiverSpec{ @@ -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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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 0e6d7106..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") @@ -132,25 +136,45 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { opts := sourcesecret.Options{ Name: name, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: labels, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } 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,14 +200,14 @@ func createSecretGitCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if err := upsertSecret(ctx, kubeClient, s); err != nil { return err } - logger.Actionf("git secret '%s' created in '%s' namespace", name, rootArgs.namespace) + logger.Actionf("git secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) return nil } 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 0fe8df8b..bca4b5fd 100644 --- a/cmd/flux/create_secret_helm.go +++ b/cmd/flux/create_secret_helm.go @@ -19,19 +19,20 @@ 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{ Use: "helm [name]", Short: "Create or update a Kubernetes secret for Helm repository authentication", - Long: `The create secret helm command generates a Kubernetes secret with basic authentication credentials.`, + Long: withPreviewNote(`The create secret helm command generates a Kubernetes secret with basic authentication credentials.`), Example: ` # Create a Helm authentication secret on disk and encrypt it with Mozilla SOPS flux create secret helm repo-auth \ --namespace=my-namespace \ @@ -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: rootArgs.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(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -112,6 +129,6 @@ func createSecretHelmCmdRun(cmd *cobra.Command, args []string) error { return err } - logger.Actionf("helm secret '%s' created in '%s' namespace", name, rootArgs.namespace) + logger.Actionf("helm secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) return nil } 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_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/internal/flags/git_implementation_test.go b/cmd/flux/create_secret_oci_test.go similarity index 50% rename from internal/flags/git_implementation_test.go rename to cmd/flux/create_secret_oci_test.go index e555dd27..a7897796 100644 --- a/internal/flags/git_implementation_test.go +++ b/cmd/flux/create_secret_oci_test.go @@ -1,7 +1,5 @@ -// +build !e2e - /* -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. @@ -16,34 +14,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -package flags +package main import ( "testing" - - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" ) -func TestGitImplementation_Set(t *testing.T) { +func TestCreateSecretOCI(t *testing.T) { tests := []struct { - name string - str string - expect string - expectErr bool + name string + args string + assert assertFunc }{ - {"supported", sourcev1.GoGitImplementation, sourcev1.GoGitImplementation, false}, - {"unsupported", "unsupported", "", true}, - {"empty", "", "", false}, + { + 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) { - 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) + 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 2b8c40d9..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: rootArgs.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(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -109,6 +149,6 @@ func createSecretTLSCmdRun(cmd *cobra.Command, args []string) error { return err } - logger.Actionf("tls secret '%s' created in '%s' namespace", name, rootArgs.namespace) + logger.Actionf("tls secret '%s' created in '%s' namespace", name, *kubeconfigArgs.Namespace) return nil } 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 4ec6aaea..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,10 +118,16 @@ 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, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: sourcev1.BucketSpec{ @@ -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(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -165,7 +173,7 @@ func createSourceBucketCmdRun(cmd *cobra.Command, args []string) error { secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: secretName, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, StringData: map[string]string{}, @@ -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_git.go b/cmd/flux/create_source_git.go index f2c6b918..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,10 +190,16 @@ 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, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: sourcev1.GitRepositorySpec{ @@ -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(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } @@ -244,21 +252,31 @@ func createSourceGitCmdRun(cmd *cobra.Command, args []string) error { if sourceGitArgs.secretRef == "" { secretOpts := sourcesecret.Options{ Name: name, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, ManifestFile: sourcesecret.MakeDefaultOptions().ManifestFile, } 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 943cbff3..b34e4208 100644 --- a/cmd/flux/create_source_git_test.go +++ b/cmd/flux/create_source_git_test.go @@ -1,3 +1,4 @@ +//go:build unit // +build unit /* @@ -20,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 @@ -82,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() @@ -95,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", @@ -121,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 a1edd409..522e980f 100644 --- a/cmd/flux/create_source_helm.go +++ b/cmd/flux/create_source_helm.go @@ -26,41 +26,51 @@ import ( "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" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" - "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{ Use: "helm [name]", Short: "Create or update a HelmRepository source", - Long: `The create source helm command generates a HelmRepository resource and waits for it to fetch the index. -For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`, - Example: ` # Create a source for a public Helm repository + Long: withPreviewNote(`The create source helm command generates a HelmRepository resource and waits for it to fetch the index. +For private Helm repositories, the basic authentication credentials are stored in a Kubernetes secret.`), + Example: ` # Create a source for an HTTPS public Helm repository flux create source helm podinfo \ --url=https://stefanprodan.github.io/podinfo \ --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 == "" { @@ -118,7 +127,7 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { helmRepository := &sourcev1.HelmRepository{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Labels: sourceLabels, }, Spec: sourcev1.HelmRepositorySpec{ @@ -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,22 +165,41 @@ func createSourceHelmCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + 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) secretOpts := sourcesecret.Options{ Name: secretName, - Namespace: rootArgs.namespace, + 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..6393380c --- /dev/null +++ b/cmd/flux/create_source_oci.go @@ -0,0 +1,236 @@ +/* +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/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.1.6 \ + --interval=10m +`, + RunE: createSourceOCIRepositoryCmdRun, +} + +type sourceOCIRepositoryFlags struct { + url string + tag string + semver string + digest string + secretRef string + serviceAccount string + certSecretRef string + verifyProvider flags.SourceOCIVerifyProvider + verifySecretRef string + ignorePaths []string + provider flags.SourceOCIProvider + insecure bool +} + +var sourceOCIRepositoryArgs = newSourceOCIFlags() + +func newSourceOCIFlags() sourceOCIRepositoryFlags { + return sourceOCIRepositoryFlags{ + provider: flags.SourceOCIProvider(sourcev1.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().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 := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: *kubeconfigArgs.Namespace, + Labels: sourceLabels, + }, + Spec: sourcev1.OCIRepositorySpec{ + Provider: sourceOCIRepositoryArgs.provider.String(), + URL: sourceOCIRepositoryArgs.url, + Insecure: sourceOCIRepositoryArgs.insecure, + Interval: metav1.Duration{ + Duration: createArgs.interval, + }, + Reference: &sourcev1.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, + } + } + } else if sourceOCIRepositoryArgs.verifySecretRef != "" { + return fmt.Errorf("a verification provider must be specified when a secret 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 *sourcev1.OCIRepository) (types.NamespacedName, error) { + namespacedName := types.NamespacedName{ + Namespace: ociRepository.GetNamespace(), + Name: ociRepository.GetName(), + } + + var existing sourcev1.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..743632a6 --- /dev/null +++ b/cmd/flux/create_source_oci_test.go @@ -0,0 +1,71 @@ +/* +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 provider not specified", + 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: "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 22bb978f..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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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 966f8800..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,13 +60,13 @@ func (del deleteCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } namespacedName := types.NamespacedName{ - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Name: name, } @@ -85,7 +85,7 @@ func (del deleteCommand) run(cmd *cobra.Command, args []string) error { } } - logger.Actionf("deleting %s %s in %s namespace", del.humanKind, name, rootArgs.namespace) + logger.Actionf("deleting %s %s in %s namespace", del.humanKind, name, *kubeconfigArgs.Namespace) err = kubeClient.Delete(ctx, del.object.asClientObject()) 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..a182c0a6 100644 --- a/cmd/flux/delete_helmrelease.go +++ b/cmd/flux/delete_helmrelease.go @@ -19,14 +19,14 @@ package main import ( "github.com/spf13/cobra" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2beta2" ) var deleteHelmReleaseCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Delete a HelmRelease resource", - Long: "The delete helmrelease command removes the given HelmRelease from the cluster.", + Long: withPreviewNote("The delete helmrelease command removes the given HelmRelease from the cluster."), Example: ` # Delete a Helm release and the Kubernetes resources created by it flux delete hr podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), 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..0c59ff89 100644 --- a/cmd/flux/delete_image_update.go +++ b/cmd/flux/delete_image_update.go @@ -25,7 +25,7 @@ import ( 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_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..bd057e8a 100644 --- a/cmd/flux/delete_source_helm.go +++ b/cmd/flux/delete_source_helm.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 deleteSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Delete a HelmRepository source", - Long: "The delete source helm command deletes the given HelmRepository from the cluster.", + Long: withPreviewNote("The delete source helm command deletes the given HelmRepository from the cluster."), Example: ` # Delete a Helm repository flux delete source helm podinfo`, ValidArgsFunction: resourceNamesCompletionFunc(sourcev1.GroupVersion.WithKind(sourcev1.HelmRepositoryKind)), 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 new file mode 100644 index 00000000..9f0ec4a9 --- /dev/null +++ b/cmd/flux/diff.go @@ -0,0 +1,31 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/spf13/cobra" +) + +var 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 prints the diff.`, +} + +func init() { + rootCmd.AddCommand(diffCmd) +} 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 new file mode 100644 index 00000000..0c4968eb --- /dev/null +++ b/cmd/flux/diff_kustomization.go @@ -0,0 +1,145 @@ +/* +Copyright 2021 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "os/signal" + + "github.com/spf13/cobra" + + "github.com/fluxcd/flux2/v2/internal/build" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" +) + +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 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 { + kustomizationFile string + path string + ignorePaths []string + progressBar 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().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.") + diffCmd.AddCommand(diffKsCmd) +} + +func diffKsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("%s name is required", kustomizationType.humanKind) + } + name := args[0] + + if 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 &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), + ) + } else { + builder, err = build.NewBuilder(name, diffKsArgs.path, + build.WithClientConfig(kubeconfigArgs, kubeclientOptions), + build.WithTimeout(rootArgs.timeout), + build.WithKustomizationFile(diffKsArgs.kustomizationFile), + build.WithIgnore(diffKsArgs.ignorePaths), + ) + } + + if err != nil { + return &RequestError{StatusCode: 2, Err: err} + } + + // create a signal channel + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt) + + errChan := make(chan error) + go func() { + output, hasChanged, err := builder.Diff() + if err != nil { + errChan <- &RequestError{StatusCode: 2, Err: err} + } + + cmd.Print(output) + + if hasChanged { + errChan <- &RequestError{StatusCode: 1, Err: fmt.Errorf("identified at least one change, exiting with non-zero exit code")} + } else { + errChan <- nil + } + }() + + select { + case <-sigc: + fmt.Println("Build cancelled... exiting.") + return builder.Cancel() + case err := <-errChan: + if err != nil { + return err + } + } + + return nil + +} diff --git a/cmd/flux/diff_kustomization_test.go b/cmd/flux/diff_kustomization_test.go new file mode 100644 index 00000000..a381a61f --- /dev/null +++ b/cmd/flux/diff_kustomization_test.go @@ -0,0 +1,153 @@ +//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" + "os" + "strings" + "testing" + + "github.com/fluxcd/flux2/v2/internal/build" + "github.com/fluxcd/pkg/ssa" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestDiffKustomization(t *testing.T) { + tests := []struct { + name string + args string + objectFile string + assert assertFunc + }{ + { + name: "no args", + args: "diff kustomization podinfo", + objectFile: "", + assert: assertError("invalid resource path \"\""), + }, + { + name: "diff nothing deployed", + 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 --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 --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 --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 --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 --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("podinfo", "", build.WithClientConfig(kubeconfigArgs, kubeclientOptions)) + + resourceManager, err := b.Manager() + if err != nil { + t.Fatal(err) + } + + setup(t, tmpl) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.objectFile != "" { + 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"], + assert: tt.assert, + } + cmd.runTestCmd(t) + if tt.objectFile != "" { + testEnv.DeleteObjectFile(tt.objectFile, tmpl, t) + } + }) + } +} + +func createObjectFromFile(objectFile string, templateValues map[string]string, t *testing.T) []*unstructured.Unstructured { + buf, err := os.ReadFile(objectFile) + if err != nil { + t.Fatalf("Error reading file '%s': %v", objectFile, err) + } + content, err := executeTemplate(string(buf), templateValues) + if err != nil { + t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) + } + clientObjects, err := readYamlObjects(strings.NewReader(content)) + if err != nil { + 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/events.go b/cmd/flux/events.go new file mode 100644 index 00000000..481f9e79 --- /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/v2beta2" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" + 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", sourcev1b2.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"}, + }, + sourcev1b2.HelmChartKind: { + gvk: sourcev1b2.GroupVersion.WithKind(sourcev1b2.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)}, + sourcev1b2.HelmRepositoryKind: {gvk: sourcev1b2.GroupVersion.WithKind(sourcev1b2.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..53fab161 --- /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" + "github.com/fluxcd/pkg/ssa" + + "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/v2beta2 +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/v1beta2 +kind: HelmRepository +metadata: + name: podinfo + namespace: flux-system +spec: + interval: 1m0s + url: https://stefanprodan.github.io/podinfo +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +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 := ssa.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 := ssa.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 173bb763..3ac9e215 100644 --- a/cmd/flux/export.go +++ b/cmd/flux/export.go @@ -20,18 +20,19 @@ import ( "bytes" "context" "fmt" + "github.com/spf13/cobra" "k8s.io/apimachinery/pkg/types" "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 { @@ -73,19 +74,19 @@ func (export exportCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if exportArgs.all { - err = kubeClient.List(ctx, export.list.asClientList(), client.InNamespace(rootArgs.namespace)) + err = kubeClient.List(ctx, export.list.asClientList(), client.InNamespace(*kubeconfigArgs.Namespace)) if err != nil { return err } if export.list.len() == 0 { - return fmt.Errorf("no objects found in %s namespace", rootArgs.namespace) + return fmt.Errorf("no objects found in %s namespace", *kubeconfigArgs.Namespace) } for i := 0; i < export.list.len(); i++ { @@ -96,7 +97,7 @@ func (export exportCommand) run(cmd *cobra.Command, args []string) error { } else { name := args[0] namespacedName := types.NamespacedName{ - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Name: name, } err = kubeClient.Get(ctx, namespacedName, export.object.asClientObject()) @@ -121,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..b55df13b 100644 --- a/cmd/flux/export_helmrelease.go +++ b/cmd/flux/export_helmrelease.go @@ -20,14 +20,14 @@ 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/v2beta2" ) var exportHelmReleaseCmd = &cobra.Command{ Use: "helmrelease [name]", Aliases: []string{"hr"}, Short: "Export HelmRelease resources in YAML format", - Long: "The export helmrelease command exports one or all HelmRelease resources in YAML format.", + Long: withPreviewNote("The export helmrelease command exports one or all HelmRelease resources in YAML format."), Example: ` # Export all HelmRelease resources flux export helmrelease --all > kustomizations.yaml 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..2bbc1700 100644 --- a/cmd/flux/export_image_update.go +++ b/cmd/flux/export_image_update.go @@ -26,7 +26,7 @@ import ( 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 74d018b2..76d6ec56 100644 --- a/cmd/flux/export_secret.go +++ b/cmd/flux/export_secret.go @@ -19,13 +19,14 @@ 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/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 @@ -45,7 +46,6 @@ type exportableWithSecretList interface { } type exportWithSecretCommand struct { - apiType object exportableWithSecret list exportableWithSecretList } @@ -58,19 +58,19 @@ func (export exportWithSecretCommand) run(cmd *cobra.Command, args []string) err ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } if exportArgs.all { - err = kubeClient.List(ctx, export.list.asClientList(), client.InNamespace(rootArgs.namespace)) + err = kubeClient.List(ctx, export.list.asClientList(), client.InNamespace(*kubeconfigArgs.Namespace)) if err != nil { return err } if export.list.len() == 0 { - return fmt.Errorf("no objects found in %s namespace", rootArgs.namespace) + return fmt.Errorf("no objects found in %s namespace", *kubeconfigArgs.Namespace) } for i := 0; i < export.list.len(); i++ { @@ -88,7 +88,7 @@ func (export exportWithSecretCommand) run(cmd *cobra.Command, args []string) err } else { name := args[0] namespacedName := types.NamespacedName{ - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Name: name, } err = kubeClient.Get(ctx, namespacedName, export.object.asClientObject()) 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_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..f52e5010 100644 --- a/cmd/flux/export_source_helm.go +++ b/cmd/flux/export_source_helm.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 exportSourceHelmCmd = &cobra.Command{ Use: "helm [name]", Short: "Export HelmRepository sources in YAML format", - Long: "The export source git command exports one or all HelmRepository sources in YAML format.", + Long: withPreviewNote("The export source git command exports one or all HelmRepository sources in YAML format."), Example: ` # Export all HelmRepository sources flux export source helm --all > sources.yaml 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 74b9bf5d..85ae174a 100644 --- a/cmd/flux/export_test.go +++ b/cmd/flux/export_test.go @@ -1,3 +1,4 @@ +//go:build unit // +build unit package main @@ -7,78 +8,92 @@ 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 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 db3d34dd..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,20 +139,35 @@ func (get getCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } var listOpts []client.ListOption if !getArgs.allNamespaces { - listOpts = append(listOpts, client.InNamespace(rootArgs.namespace)) + listOpts = append(listOpts, client.InNamespace(*kubeconfigArgs.Namespace)) } if len(args) > 0 { 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, rootArgs.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..7a40466b 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/v2beta2" + 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..92dc7215 100644 --- a/cmd/flux/get_helmrelease.go +++ b/cmd/flux/get_helmrelease.go @@ -19,18 +19,20 @@ 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/v2beta2" ) var getHelmReleaseCmd = &cobra.Command{ Use: "helmreleases", Aliases: []string{"hr", "helmrelease"}, Short: "Get HelmRelease statuses", - Long: "The get helmreleases command prints the statuses of the resources.", + Long: withPreviewNote("The get helmreleases command prints the statuses of the resources."), Example: ` # List all Helm releases and their status flux get helmreleases`, ValidArgsFunction: resourceNamesCompletionFunc(helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind)), @@ -75,11 +77,11 @@ func (a helmReleaseListAdapter) summariseItem(i int, includeNamespace bool, incl revision := item.Status.LastAppliedRevision 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..83c1680c 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" + 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..62399626 100644 --- a/cmd/flux/get_image_update.go +++ b/cmd/flux/get_image_update.go @@ -19,10 +19,11 @@ 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" @@ -31,7 +32,7 @@ import ( 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 a6f1eb6a..a95ec655 100644 --- a/cmd/flux/get_kustomization.go +++ b/cmd/flux/get_kustomization.go @@ -19,19 +19,22 @@ 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" - 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)), @@ -78,12 +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) + 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...) } 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..c6736c74 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, @@ -50,17 +54,17 @@ var getSourceAllCmd = &cobra.Command{ }, { apiType: helmRepositoryType, - list: &helmRepositoryListAdapter{&sourcev1.HelmRepositoryList{}}, + list: &helmRepositoryListAdapter{&sourcev1b2.HelmRepositoryList{}}, }, { apiType: helmChartType, - list: &helmChartListAdapter{&sourcev1.HelmChartList{}}, + list: &helmChartListAdapter{&sourcev1b2.HelmChartList{}}, }, } 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..b82bdd78 100644 --- a/cmd/flux/get_source_chart.go +++ b/cmd/flux/get_source_chart.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 getSourceHelmChartCmd = &cobra.Command{ Use: "chart", Short: "Get HelmChart statuses", - Long: "The get sources chart command prints the status of the HelmCharts.", + Long: withPreviewNote("The get sources chart command prints the status of the HelmCharts."), Example: ` # List all Helm charts and their status flux get sources chart @@ -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 31a8e5bd..78476de5 100644 --- a/cmd/flux/get_source_git.go +++ b/cmd/flux/get_source_git.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/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 @@ -80,12 +83,14 @@ func (a *gitRepositoryListAdapter) summariseItem(i int, includeNamespace bool, i 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 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..ec131714 100644 --- a/cmd/flux/get_source_helm.go +++ b/cmd/flux/get_source_helm.go @@ -19,18 +19,22 @@ 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/v1beta2" + + "github.com/fluxcd/flux2/v2/internal/utils" ) var getSourceHelmCmd = &cobra.Command{ Use: "helm", Short: "Get HelmRepository source statuses", - Long: "The get sources helm command prints the status of the HelmRepository sources.", + Long: withPreviewNote("The get sources helm command prints the status of the HelmRepository sources."), Example: ` # List all Helm repositories and their status flux get sources helm @@ -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..d01d04c3 100644 --- a/cmd/flux/helmrelease.go +++ b/cmd/flux/helmrelease.go @@ -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/v2beta2" ) // 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 64f5c164..bdf7d87c 100644 --- a/cmd/flux/helmrelease_test.go +++ b/cmd/flux/helmrelease_test.go @@ -1,3 +1,4 @@ +//go:build e2e // +build e2e /* @@ -21,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..38c264a7 100644 --- a/cmd/flux/image.go +++ b/cmd/flux/image.go @@ -20,7 +20,7 @@ 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" + 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 1ac0779f..8fe76a1e 100644 --- a/cmd/flux/image_test.go +++ b/cmd/flux/image_test.go @@ -1,10 +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 @@ -31,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 36937b16..bf0ee758 100644 --- a/cmd/flux/install.go +++ b/cmd/flux/install.go @@ -23,24 +23,31 @@ import ( "path/filepath" "time" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" - "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" + "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/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 version and a series of components - flux install --version=v0.0.7 --components="source-controller,kustomize-controller" + # Install a specific series of components + flux install --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,7 +62,6 @@ 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 @@ -65,11 +71,11 @@ type installFlags struct { 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 +83,17 @@ 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.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 +102,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 diffrent 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) } @@ -131,7 +134,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error { logger.Generatef("generating manifests") } - tmpDir, err := os.MkdirTemp("", rootArgs.namespace) + tmpDir, err := manifestgen.MkdirTempAbs("", *kubeconfigArgs.Namespace) if err != nil { return err } @@ -148,7 +151,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error { opts := install.Options{ BaseURL: installArgs.manifestsPath, Version: installArgs.version, - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Components: components, Registry: installArgs.registry, ImagePullSecret: installArgs.imagePullSecret, @@ -156,7 +159,7 @@ func installCmdRun(cmd *cobra.Command, args []string) error { NetworkPolicy: installArgs.networkPolicy, LogLevel: installArgs.logLevel.String(), NotificationController: rootArgs.defaults.NotificationController, - ManifestFile: fmt.Sprintf("%s.yaml", rootArgs.namespace), + ManifestFile: fmt.Sprintf("%s.yaml", *kubeconfigArgs.Namespace), Timeout: rootArgs.timeout, ClusterDomain: installArgs.clusterDomain, TolerationKeys: installArgs.tolerationKeys, @@ -183,21 +186,45 @@ func installCmdRun(cmd *cobra.Command, args []string) error { } logger.Successf("manifests build completed") - logger.Actionf("installing components in %s namespace", rootArgs.namespace) + 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, rootArgs.kubeconfig, rootArgs.kubecontext, 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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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..5cb0fa66 --- /dev/null +++ b/cmd/flux/install_test.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 "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"`), + }, + } + + 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 e997e451..2f6176ac 100644 --- a/cmd/flux/kustomization_test.go +++ b/cmd/flux/kustomization_test.go @@ -1,3 +1,4 @@ +//go:build e2e // +build e2e /* @@ -21,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/cmd/flux/list.go b/cmd/flux/list.go new file mode 100644 index 00000000..51c3784f --- /dev/null +++ b/cmd/flux/list.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 listCmd = &cobra.Command{ + Use: "list", + Short: "List artifacts", + Long: `The list command is used for printing the OCI artifacts metadata.`, +} + +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 23197363..0931b1c9 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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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 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(rootArgs.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 213e789f..46c1c11e 100644 --- a/cmd/flux/logs_test.go +++ b/cmd/flux/logs_e2e_test.go @@ -1,4 +1,5 @@ -// +build unit +//go:build e2e +// +build e2e /* Copyright 2021 The Flux authors @@ -30,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", @@ -73,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 6b00e4b0..1226a347 100644 --- a/cmd/flux/main.go +++ b/cmd/flux/main.go @@ -21,16 +21,21 @@ import ( "fmt" "log" "os" - "path/filepath" "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,34 +99,68 @@ 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} type rootFlags struct { - kubeconfig string - kubecontext string - namespace string timeout time.Duration verbose bool pollInterval time.Duration 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().StringVarP(&rootArgs.namespace, "namespace", "n", rootArgs.defaults.Namespace, - "the namespace scope for this operation, can be set with FLUX_SYSTEM_NAMESPACE env var") - rootCmd.RegisterFlagCompletionFunc("namespace", resourceNamesCompletionFunc(corev1.SchemeGroupVersion.WithKind("Namespace"))) - rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation") rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects") - rootCmd.PersistentFlags().StringVarP(&rootArgs.kubeconfig, "kubeconfig", "", "", - "absolute path to the kubeconfig file") - rootCmd.PersistentFlags().StringVarP(&rootArgs.kubecontext, "context", "", "", "kubernetes context to use") + configureDefaultNamespace() + kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag + kubeconfigArgs.Timeout = nil // prevent AddFlags from configuring --request-timeout flag, we have --timeout instead + kubeconfigArgs.AddFlags(rootCmd.PersistentFlags()) + + // Since some subcommands use the `-s` flag as a short version for `--silent`, we manually configure the server flag + // without the `-s` short version. While we're no longer on par with kubectl's flags, we maintain backwards compatibility + // on the CLI interface. + 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"))) rootCmd.DisableAutoGenTag = true rootCmd.SetOut(os.Stdout) @@ -138,38 +177,46 @@ func NewRootFlags() rootFlags { func main() { log.SetFlags(0) - configureKubeconfig() - configureDefaultNamespace() + + // 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 { - logger.Failuref("%v", err) - os.Exit(1) - } -} -func configureKubeconfig() { - switch { - case len(rootArgs.kubeconfig) > 0: - case len(os.Getenv("KUBECONFIG")) > 0: - rootArgs.kubeconfig = os.Getenv("KUBECONFIG") - default: - if home := homeDir(); len(home) > 0 { - rootArgs.kubeconfig = filepath.Join(home, ".kube", "config") + 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) } } func configureDefaultNamespace() { + *kubeconfigArgs.Namespace = rootArgs.defaults.Namespace fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE") - if fromEnv != "" && rootArgs.namespace == rootArgs.defaults.Namespace { - rootArgs.namespace = fromEnv - } -} + if 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 @@ -193,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 1e2c775e..f2b01cc6 100644 --- a/cmd/flux/main_e2e_test.go +++ b/cmd/flux/main_e2e_test.go @@ -1,3 +1,4 @@ +//go:build e2e // +build e2e /* @@ -24,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") @@ -35,12 +41,12 @@ func TestMain(m *testing.M) { if err != nil { panic(fmt.Errorf("error creating kube manager: '%w'", err)) } - rootArgs.kubeconfig = testEnv.kubeConfigPath + kubeconfigArgs.KubeConfig = &testEnv.kubeConfigPath // 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 @@ -49,12 +55,12 @@ 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 kubectlArgs := []string{"delete", "namespace", "flux-system"} - _, err = utils.ExecKubectlCommand(context.TODO(), utils.ModeStderrOS, rootArgs.kubeconfig, rootArgs.kubecontext, kubectlArgs...) + _, err = utils.ExecKubectlCommand(context.TODO(), utils.ModeStderrOS, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, kubectlArgs...) if err != nil { panic(fmt.Errorf("delete namespace error:'%w'", err)) } @@ -64,15 +70,15 @@ 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, rootArgs.kubeconfig, rootArgs.kubecontext, kubectlArgs...) + _, err := utils.ExecKubectlCommand(context.TODO(), utils.ModeStderrOS, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, kubectlArgs...) if err != nil { return nil, err } return func() { kubectlArgs := []string{"delete", "namespace", namespace} - utils.ExecKubectlCommand(context.TODO(), utils.ModeCapture, rootArgs.kubeconfig, rootArgs.kubecontext, kubectlArgs...) + utils.ExecKubectlCommand(context.TODO(), utils.ModeCapture, *kubeconfigArgs.KubeConfig, *kubeconfigArgs.Context, kubectlArgs...) }, nil } diff --git a/cmd/flux/main_test.go b/cmd/flux/main_test.go index 10879e23..d2f9622e 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,9 +31,10 @@ import ( "text/template" "time" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/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" @@ -42,6 +44,9 @@ import ( 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 { @@ -49,8 +54,8 @@ func allocateNamespace(prefix string) string { return fmt.Sprintf("%s-%d", prefix, id) } -func readYamlObjects(rdr io.Reader) ([]unstructured.Unstructured, error) { - objects := []unstructured.Unstructured{} +func readYamlObjects(rdr io.Reader) ([]*unstructured.Unstructured, error) { + objects := []*unstructured.Unstructured{} reader := k8syaml.NewYAMLReader(bufio.NewReader(rdr)) for { doc, err := reader.Read() @@ -65,7 +70,7 @@ func readYamlObjects(rdr io.Reader) ([]unstructured.Unstructured, error) { if err != nil { return nil, err } - objects = append(objects, *unstructuredObj) + objects = append(objects, unstructuredObj) } return objects, nil } @@ -96,7 +101,7 @@ func (m *testEnvKubeManager) CreateObjectFile(objectFile string, templateValues } } -func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstructured, t *testing.T) error { +func (m *testEnvKubeManager) CreateObjects(clientObjects []*unstructured.Unstructured, t *testing.T) error { for _, obj := range clientObjects { // First create the object then set its status if present in the // yaml file. Make a copy first since creating an object may overwrite @@ -107,11 +112,42 @@ func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstruct return err } obj.SetResourceVersion(createObj.GetResourceVersion()) - err = m.client.Status().Update(context.Background(), &obj) + err = m.client.Status().Update(context.Background(), obj) + // Updating status of static objects results in not found error. + if err != nil && !errors.IsNotFound(err) { + return err + } + } + return nil +} + +func (m *testEnvKubeManager) DeleteObjectFile(objectFile string, templateValues map[string]string, t *testing.T) { + buf, err := os.ReadFile(objectFile) + if err != nil { + t.Fatalf("Error reading file '%s': %v", objectFile, err) + } + content, err := executeTemplate(string(buf), templateValues) + if err != nil { + t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err) + } + clientObjects, err := readYamlObjects(strings.NewReader(content)) + if err != nil { + t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err) + } + err = m.DeleteObjects(clientObjects, t) + if err != nil { + t.Logf("Error deleting test objects: '%v'", err) + } +} + +func (m *testEnvKubeManager) DeleteObjects(clientObjects []*unstructured.Unstructured, t *testing.T) error { + for _, obj := range clientObjects { + err := m.client.Delete(context.Background(), obj) if err != nil { return err } } + return nil } @@ -148,7 +184,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(), }) @@ -169,6 +205,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, @@ -254,24 +293,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 @@ -289,12 +342,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) } @@ -312,6 +368,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 @@ -331,8 +393,62 @@ func executeCommand(cmd string) (string, error) { 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", + } + +} + +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 2b992cce..8e0af1aa 100644 --- a/cmd/flux/main_unit_test.go +++ b/cmd/flux/main_unit_test.go @@ -1,3 +1,4 @@ +//go:build unit // +build unit /* @@ -21,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 @@ -33,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") @@ -42,7 +48,8 @@ func TestMain(m *testing.M) { panic(fmt.Errorf("error creating kube manager: '%w'", err)) } testEnv = km - rootArgs.kubeconfig = testEnv.kubeConfigPath + // rootArgs.kubeconfig = testEnv.kubeConfigPath + kubeconfigArgs.KubeConfig = &testEnv.kubeConfigPath // Run tests code := m.Run() 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..228a826e --- /dev/null +++ b/cmd/flux/push_artifact.go @@ -0,0 +1,325 @@ +/* +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 +} + +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") + + 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, + } + + 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 c0c609d8..b2e74105 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/v2beta2" "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,13 +87,13 @@ func (reconcile reconcileCommand) run(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } namespacedName := types.NamespacedName{ - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Name: name, } @@ -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, rootArgs.namespace) - if err := requestReconciliation(ctx, kubeClient, namespacedName, reconcile.object); err != nil { + logger.Actionf("annotating %s %s in %s namespace", reconcile.kind, name, *kubeconfigArgs.Namespace) + 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 092e8ac9..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(rootArgs.kubeconfig, rootArgs.kubecontext) - if err != nil { - return err - } - - namespacedName := types.NamespacedName{ - Namespace: rootArgs.namespace, - Name: name, - } - - logger.Actionf("annotating Provider %s in %s namespace", name, rootArgs.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..1a1a5769 100644 --- a/cmd/flux/reconcile_helmrelease.go +++ b/cmd/flux/reconcile_helmrelease.go @@ -17,11 +17,13 @@ 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/v2beta2" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" ) var reconcileHrCmd = &cobra.Command{ @@ -44,13 +46,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 +67,24 @@ 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{}}, - } - case sourcev1.GitRepositoryKind: - cmd = reconcileCommand{ - apiType: gitRepositoryType, - object: gitRepositoryAdapter{&sourcev1.GitRepository{}}, - } - case sourcev1.BucketKind: - cmd = reconcileCommand{ - apiType: bucketType, - object: bucketAdapter{&sourcev1.Bucket{}}, - } +func (obj helmReleaseAdapter) getSource() (reconcileSource, types.NamespacedName) { + cmd := reconcileWithSourceCommand{ + apiType: helmChartType, + object: helmChartAdapter{&sourcev1b2.HelmChart{}}, + force: true, + } + + ns := obj.Spec.Chart.Spec.SourceRef.Namespace + if ns == "" { + ns = obj.Namespace } return cmd, types.NamespacedName{ - Name: obj.Spec.Chart.Spec.SourceRef.Name, - Namespace: obj.Spec.Chart.Spec.SourceRef.Namespace, + Name: fmt.Sprintf("%s-%s", obj.Namespace, obj.Name), + Namespace: ns, } } + +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..95bb42df 100644 --- a/cmd/flux/reconcile_image_updateauto.go +++ b/cmd/flux/reconcile_image_updateauto.go @@ -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 9b7e7fd0..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(rootArgs.kubeconfig, rootArgs.kubecontext) - if err != nil { - return err - } - - namespacedName := types.NamespacedName{ - Namespace: rootArgs.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, rootArgs.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..ead4fa76 --- /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(sourcev1b2.GroupVersion.WithKind(sourcev1b2.HelmChartKind)), + RunE: reconcileWithSourceCommand{ + apiType: helmChartType, + object: helmChartAdapter{&sourcev1b2.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 sourcev1b2.HelmRepositoryKind: + cmd = reconcileCommand{ + apiType: helmRepositoryType, + object: helmRepositoryAdapter{&sourcev1b2.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..a081fd0e 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/v1beta2" ) 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 7052b879..ac0a6f15 100644 --- a/cmd/flux/reconcile_with_source.go +++ b/cmd/flux/reconcile_with_source.go @@ -10,21 +10,25 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/pkg/apis/meta" - - "github.com/fluxcd/flux2/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,13 +40,13 @@ func (reconcile reconcileWithSourceCommand) run(cmd *cobra.Command, args []strin ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } namespacedName := types.NamespacedName{ - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Name: name, } @@ -55,34 +59,35 @@ 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 := rootArgs.namespace + nsCopy := *kubeconfigArgs.Namespace if nsName.Namespace != "" { - rootArgs.namespace = nsName.Namespace + *kubeconfigArgs.Namespace = nsName.Namespace } err := reconcileCmd.run(nil, []string{nsName.Name}) if err != nil { return err } - rootArgs.namespace = nsCopy + *kubeconfigArgs.Namespace = nsCopy } lastHandledReconcileAt := reconcile.object.lastHandledReconcileRequest() - logger.Actionf("annotating %s %s in %s namespace", reconcile.kind, name, rootArgs.namespace) - if err := requestReconciliation(ctx, kubeClient, namespacedName, reconcile.object); err != nil { + logger.Actionf("annotating %s %s in %s namespace", reconcile.kind, name, *kubeconfigArgs.Namespace) + 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 004bcfb6..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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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(rootArgs.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, rootArgs.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(), rootArgs.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: rootArgs.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..5c1bc15d 100644 --- a/cmd/flux/resume_helmrelease.go +++ b/cmd/flux/resume_helmrelease.go @@ -21,7 +21,7 @@ import ( "github.com/spf13/cobra" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2 "github.com/fluxcd/helm-controller/api/v2beta2" ) 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, } 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..d50fddf7 100644 --- a/cmd/flux/resume_image_updateauto.go +++ b/cmd/flux/resume_image_updateauto.go @@ -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..6322f06f 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/v1beta2" ) 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..0ac641eb 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/v1beta2" ) 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..7dc96fdf 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 { @@ -59,15 +95,16 @@ func (a bucketListAdapter) len() int { return len(a.BucketList.Items) } -// sourcev1.HelmChart +// sourcev1b2.HelmChart var helmChartType = apiType{ - kind: sourcev1.HelmChartKind, - humanKind: "source chart", + kind: sourcev1b2.HelmChartKind, + humanKind: "source chart", + groupVersion: sourcev1b2.GroupVersion, } type helmChartAdapter struct { - *sourcev1.HelmChart + *sourcev1b2.HelmChart } func (a helmChartAdapter) asClientObject() client.Object { @@ -78,10 +115,10 @@ func (a helmChartAdapter) deepCopyClientObject() client.Object { return a.HelmChart.DeepCopy() } -// sourcev1.HelmChartList +// sourcev1b2.HelmChartList type helmChartListAdapter struct { - *sourcev1.HelmChartList + *sourcev1b2.HelmChartList } func (a helmChartListAdapter) asClientList() client.ObjectList { @@ -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 { @@ -125,15 +163,16 @@ func (a gitRepositoryListAdapter) len() int { return len(a.GitRepositoryList.Items) } -// sourcev1.HelmRepository +// sourcev1b2.HelmRepository var helmRepositoryType = apiType{ - kind: sourcev1.HelmRepositoryKind, - humanKind: "source helm", + kind: sourcev1b2.HelmRepositoryKind, + humanKind: "source helm", + groupVersion: sourcev1b2.GroupVersion, } type helmRepositoryAdapter struct { - *sourcev1.HelmRepository + *sourcev1b2.HelmRepository } func (a helmRepositoryAdapter) asClientObject() client.Object { @@ -144,10 +183,10 @@ func (a helmRepositoryAdapter) deepCopyClientObject() client.Object { return a.HelmRepository.DeepCopy() } -// sourcev1.HelmRepositoryList +// sourcev1b2.HelmRepositoryList type helmRepositoryListAdapter struct { - *sourcev1.HelmRepositoryList + *sourcev1b2.HelmRepositoryList } func (a helmRepositoryListAdapter) asClientList() client.ObjectList { 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..0181ffab --- /dev/null +++ b/cmd/flux/stats.go @@ -0,0 +1,224 @@ +/* +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/v2beta2" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" + 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", + 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: sourcev1b2.HelmRepositoryKind, + Version: sourcev1b2.GroupVersion.Version, + Group: sourcev1b2.GroupVersion.Group, + }, + { + Kind: sourcev1b2.HelmChartKind, + Version: sourcev1b2.GroupVersion.Version, + Group: sourcev1b2.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 631a3e2f..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,43 +28,22 @@ 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) { var objRefs []object.ObjMetadata for _, deployment := range components { - objMeta, err := object.CreateObjMetadata(rootArgs.namespace, deployment, schema.GroupKind{Group: "apps", Kind: "Deployment"}) - if err != nil { - return nil, err - } - objRefs = append(objRefs, objMeta) + objRefs = append(objRefs, object.ObjMetadata{ + Namespace: *kubeconfigArgs.Namespace, + Name: deployment, + GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, + }) } return objRefs, nil } diff --git a/cmd/flux/suspend.go b/cmd/flux/suspend.go index 101f29bb..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(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } - var listOpts []client.ListOption - listOpts = append(listOpts, client.InNamespace(rootArgs.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, rootArgs.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(), rootArgs.namespace) - suspend.list.item(i).setSuspended() - if err := kubeClient.Update(ctx, suspend.list.item(i).asClientObject()); err != nil { + logger.Actionf("suspending %s %s in %s namespace", suspend.humanKind, suspend.list.item(i).asClientObject().GetName(), *kubeconfigArgs.Namespace) + + 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..b4c3eb8f 100644 --- a/cmd/flux/suspend_helmrelease.go +++ b/cmd/flux/suspend_helmrelease.go @@ -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/v2beta2" ) 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..1c8f11ef 100644 --- a/cmd/flux/suspend_image_updateauto.go +++ b/cmd/flux/suspend_image_updateauto.go @@ -25,9 +25,12 @@ import ( 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..e91327e8 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/v1beta2" ) 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..22598c64 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/v1beta2" ) 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/deployment.yaml b/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml new file mode 100644 index 00000000..25896dd8 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/delete-service/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + 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.3 + 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/delete-service/hpa.yaml b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml new file mode 100644 index 00000000..263e9128 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/delete-service/hpa.yaml @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: podinfo +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + # scale up if usage is above + # 99% of the requested CPU (100m) + averageUtilization: 99 diff --git a/manifests/monitoring/kustomization.yaml b/cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml similarity index 61% rename from manifests/monitoring/kustomization.yaml rename to cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml index b93baa2d..1d0e99c5 100644 --- a/manifests/monitoring/kustomization.yaml +++ b/cmd/flux/testdata/build-kustomization/delete-service/kustomization.yaml @@ -1,6 +1,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization -namespace: flux-system resources: -- prometheus -- grafana +- ./deployment.yaml +- ./hpa.yaml 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 new file mode 100644 index 00000000..9f5e3ef3 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-kustomization.yaml @@ -0,0 +1,19 @@ +--- +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" diff --git a/cmd/flux/testdata/build-kustomization/podinfo-result.yaml b/cmd/flux/testdata/build-kustomization/podinfo-result.yaml new file mode 100644 index 00000000..bf162251 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-result.yaml @@ -0,0 +1,174 @@ +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 + 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: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + maxReplicas: 4 + metrics: + - resource: + name: cpu + target: + averageUtilization: 99 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo +--- +apiVersion: v1 +kind: Service +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + ports: + - name: http + port: 9898 + protocol: TCP + targetPort: http + - name: grpc + port: 9999 + protocol: TCP + targetPort: grpc + selector: + app: podinfo + 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 +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque +--- +apiVersion: v1 +data: + password: MWYyZDFlMmU2N2Rm + username: YWRtaW4= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + 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 new file mode 100644 index 00000000..409482dd --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-source.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: podinfo + namespace: {{ .fluxns }} +spec: + interval: 30s + ref: + branch: master + url: https://github.com/stefanprodan/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 new file mode 100644 index 00000000..647ce020 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo-without-service-result.yaml @@ -0,0 +1,102 @@ +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 + 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.3 + 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: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + maxReplicas: 4 + metrics: + - resource: + name: cpu + target: + averageUtilization: 99 + type: Utilization + type: Resource + minReplicas: 2 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo +--- diff --git a/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml b/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml new file mode 100644 index 00000000..d96b3c3c --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/deployment.yaml @@ -0,0 +1,74 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + 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/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 new file mode 100644 index 00000000..263e9128 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/hpa.yaml @@ -0,0 +1,20 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: podinfo +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: podinfo + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + # scale up if usage is above + # 99% of the requested CPU (100m) + averageUtilization: 99 diff --git a/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml b/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml new file mode 100644 index 00000000..435c23c6 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/kustomization.yaml @@ -0,0 +1,16 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./deployment.yaml +- ./hpa.yaml +- ./service.yaml +- ./dockerconfigjson-sops-secret.yaml +- ./stringdata-secret.yaml +secretGenerator: + - files: + - token=token.encrypted + name: podinfo-token + - literals: + - username=admin + - password=1f2d1e2e67df + name: db-user-pass diff --git a/cmd/flux/testdata/build-kustomization/podinfo/service.yaml b/cmd/flux/testdata/build-kustomization/podinfo/service.yaml new file mode 100644 index 00000000..9450823d --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: podinfo +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/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/podinfo/token.encrypted b/cmd/flux/testdata/build-kustomization/podinfo/token.encrypted new file mode 100644 index 00000000..c88ac972 --- /dev/null +++ b/cmd/flux/testdata/build-kustomization/podinfo/token.encrypted @@ -0,0 +1,20 @@ + { + "data": "ENC[AES256_GCM,data:oBe5PlPmfQCUUc4sqKImjw==,iv:MLLEW15QC9kRdVVagJnzLCSk0xZGWIpAeTfHzyxT10g=,tag:K3GkBCGS+ut4Tpk6ndb0CA==,type:str]", + "sops": { + "kms": null, + "gcp_kms": null, + "azure_kv": null, + "hc_vault": null, + "age": [ + { + "recipient": "age10la2ge0wtvx3qr7datqf7rs4yngxszdal927fs9rukamr8u2pshsvtz7ce", + "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+ IFgyNTUxOSA1L2RpZHRrK1FSVmYrd1Va\nY0hxWFQzSDBsT1k3WjNtYmU1QmliaDJycXlNCnF1YjdNOThVbVNvMG9rNS9ZUXZw\nMnV0bnRUMGNtejFPbzM4U2UzWkszeVkKLS0tIGJ6UGhxMUV3YmVJTHlJSUJpRVRZ\nVjd0RVRadU8wekxXTHIrYUplYkN2aEEK0I/ MCEtXRk+b/N2G1JF3vHQT24dShWYD\nw+JIUSA3aLf2sv0zr2MdUEdVWBJoM8nT4D4xVbBORD+669W+9nDeSw==\n-----END AGE ENCRYPTED FILE-----\n" + } + ], + "lastmodified": "2021-11-26T16:34:51Z", + "mac": "ENC[AES256_GCM,data:COGzf5YCHNNP6z4JaEKrjN3M8f5+Q1uKUKTMHwj388/ICmLyi2sSrTmj7PP+X7M9jTVwa8wVgYTpNLiVJx+LcxqvIXM0Tyo+/Cu1zrfao98aiACP8+TSEDiFQNtEus23H+d/X1hqMwRHDI3kQ+ 6scgEGnqY57r3RDSA3E8EhHr4=,iv:LxitVIYm8srZVqFueJh9loClA44Y2Z3XAVYmxesMmOg=,tag:Y8qFD8UGlDfwNSv7xlcn6A==,type:str]", + "pgp": null, + "unencrypted_suffix": "_unencrypted", + "version": "3.7.1" + } + } \ No newline at end of file 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..9b097d1e 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.26.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_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/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_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..87235245 --- /dev/null +++ b/cmd/flux/testdata/create_source_helm/https.golden @@ -0,0 +1,9 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +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..760a265c --- /dev/null +++ b/cmd/flux/testdata/create_source_helm/oci-with-secret.golden @@ -0,0 +1,12 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +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..f6049e99 --- /dev/null +++ b/cmd/flux/testdata/create_source_helm/oci.golden @@ -0,0 +1,10 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1beta2 +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/deployment.yaml b/cmd/flux/testdata/diff-kustomization/deployment.yaml new file mode 100644 index 00000000..3910da6a --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/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-kustomization/diff-with-deployment.golden b/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden new file mode 100644 index 00000000..7856dd58 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-deployment.golden @@ -0,0 +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 new file mode 100644 index 00000000..5608c24e --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-key-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 created +► Secret/default/podinfo-token-77t89m9b67 drifted + +data + - one map entry removed: + one map entry added: + drift-key: "*****" token: "*****" + +► Secret/default/db-user-pass-bkbd782d2c created diff --git a/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden new file mode 100644 index 00000000..bfce21b5 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-secret.golden @@ -0,0 +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 new file mode 100644 index 00000000..a9fe09f2 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-service.golden @@ -0,0 +1,13 @@ +► Deployment/default/podinfo created +► HorizontalPodAutoscaler/default/podinfo created +► Service/default/podinfo drifted + +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 new file mode 100644 index 00000000..6c502226 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/diff-with-drifted-value-sops-secret.golden @@ -0,0 +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/key-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml new file mode 100644 index 00000000..52f7cf46 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/key-sops-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + drift-key: bXktc2VjcmV0LXRva2VuCg== +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque diff --git a/cmd/flux/testdata/diff-kustomization/kustomization.yaml b/cmd/flux/testdata/diff-kustomization/kustomization.yaml new file mode 100644 index 00000000..997ce70a --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/kustomization.yaml @@ -0,0 +1,11 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ./deployment.yaml +- ./hpa.yaml +- ./service.yaml +secretGenerator: + - literals: + - username=admin + - password=1f2d1e2e67df + name: secret-basic-auth diff --git a/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden b/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden new file mode 100644 index 00000000..5c320a1b --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/nothing-is-deployed.golden @@ -0,0 +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/secret.yaml b/cmd/flux/testdata/diff-kustomization/secret.yaml new file mode 100644 index 00000000..cf0b351e --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +data: + password: cGFzc3dvcmQK + username: YWRtaW4= +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: db-user-pass-bkbd782d2c + namespace: default +type: Opaque diff --git a/cmd/flux/testdata/diff-kustomization/service.yaml b/cmd/flux/testdata/diff-kustomization/service.yaml new file mode 100644 index 00000000..95068162 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo + namespace: default +spec: + type: ClusterIP + selector: + app: podinfo + ports: + - name: http + port: 9899 + protocol: TCP + targetPort: http + - port: 9999 + targetPort: grpc + protocol: TCP + name: grpc 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/diff-kustomization/value-sops-secret.yaml b/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml new file mode 100644 index 00000000..3ae9ada2 --- /dev/null +++ b/cmd/flux/testdata/diff-kustomization/value-sops-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +data: + token: ZHJpZnQtdmFsdWUK +kind: Secret +metadata: + labels: + kustomize.toolkit.fluxcd.io/name: podinfo + kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }} + name: podinfo-token-77t89m9b67 + namespace: default +type: Opaque 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-release.yaml b/cmd/flux/testdata/export/helm-release.yaml index 8f861110..c7aceaf1 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/v2beta2 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..c5366e0b 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/v1beta2 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..e2fcb3c1 100644 --- a/cmd/flux/testdata/export/image-update.yaml +++ b/cmd/flux/testdata/export/image-update.yaml @@ -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..ee5c228c 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 @@ -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/v1beta2 kind: HelmRepository metadata: name: flux-system @@ -124,7 +124,7 @@ spec: timeout: 1m0s url: https://stefanprodan.github.io/podinfo --- -apiVersion: helm.toolkit.fluxcd.io/v2beta1 +apiVersion: helm.toolkit.fluxcd.io/v2beta2 kind: HelmRelease metadata: name: flux-system @@ -139,7 +139,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 3c21df33..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/627d5c4bb67b77185f37e31d734b085019ff2951 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951 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_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_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..0d0f8f79 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/v2beta2 kind: HelmRelease metadata: labels: @@ -59,11 +59,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: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: HelmChart metadata: name: podinfo-podinfo @@ -73,24 +73,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/v1beta2 kind: HelmRepository metadata: labels: @@ -106,17 +106,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 @@ -132,12 +132,12 @@ 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 --- -apiVersion: source.toolkit.fluxcd.io/v1beta1 +apiVersion: source.toolkit.fluxcd.io/v1 kind: GitRepository metadata: labels: @@ -146,7 +146,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..0fc7b6c4 --- /dev/null +++ b/cmd/flux/testdata/trace/helmrelease-oci.yaml @@ -0,0 +1,92 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .fluxns }} +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ .ns }} +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta2 +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 + validation: client + 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..8c029cde 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/v2beta2 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 @@ -54,13 +54,13 @@ 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 - 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 +69,6 @@ metadata: name: flux-system namespace: {{ .fluxns }} spec: - gitImplementation: go-git ref: branch: main secretRef: @@ -79,12 +78,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 3b3b599d..bab5a457 100644 --- a/cmd/flux/trace.go +++ b/cmd/flux/trace.go @@ -27,33 +27,44 @@ import ( "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/runtime/schema" "k8s.io/apimachinery/pkg/types" + "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/v2beta2" + 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 [name]", - Short: "trace an in-cluster object throughout the GitOps delivery pipeline", - Long: `The trace command shows how an object is managed by Flux, -from which source and revision it comes, and what's the latest reconciliation status.'`, + Use: "trace [ ...]", + Short: "Trace in-cluster objects throughout the GitOps delivery pipeline", + 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.`), Example: ` # Trace a Kubernetes Deployment - flux trace my-app --kind=deployment --api-version=apps/v1 --namespace=apps + flux trace -n apps deployment my-app - # Trace a Kubernetes Pod - flux trace redis-master-0 --kind=pod --api-version=v1 -n redis + # Trace a Kubernetes Pod and a config map + flux trace -n redis pod/redis-master-0 cm/redis # Trace a Kubernetes global object - flux trace redis --kind=namespace --api-version=v1 + flux trace namespace redis # Trace a Kubernetes custom resource - flux trace redis --kind=helmrelease --api-version=helm.toolkit.fluxcd.io/v2beta1 -n redis`, + flux trace -n redis helmrelease redis + + # API Version and Kind can also be specified explicitly + # Note that either both, kind and api-version, or neither have to be specified. + flux trace redis --kind=helmrelease --api-version=helm.toolkit.fluxcd.io/v2beta2 -n redis`, RunE: traceCmdRun, } @@ -73,49 +84,43 @@ func init() { } func traceCmdRun(cmd *cobra.Command, args []string) error { - if len(args) < 1 { - return fmt.Errorf("object name is required") - } - name := args[0] - - if traceArgs.kind == "" { - return fmt.Errorf("object kind is required (--kind)") - } - - if traceArgs.apiVersion == "" { - return fmt.Errorf("object apiVersion is required (--api-version)") - } - ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } - gv, err := schema.ParseGroupVersion(traceArgs.apiVersion) + var objects []*unstructured.Unstructured + if traceArgs.kind != "" || traceArgs.apiVersion != "" { + var obj *unstructured.Unstructured + obj, err = getObjectStatic(ctx, kubeClient, args) + objects = []*unstructured.Unstructured{obj} + } else { + objects, err = getObjectDynamic(args) + } if err != nil { - return fmt.Errorf("invaild apiVersion: %w", err) + return err } - obj := &unstructured.Unstructured{} - obj.SetGroupVersionKind(schema.GroupVersionKind{ - Group: gv.Group, - Version: gv.Version, - Kind: traceArgs.kind, - }) - - objName := types.NamespacedName{ - Namespace: rootArgs.namespace, - Name: name, - } + return traceObjects(ctx, kubeClient, objects) +} - err = kubeClient.Get(ctx, objName, obj) - if err != nil { - return fmt.Errorf("failed to find object: %w", err) +func traceObjects(ctx context.Context, kubeClient client.Client, objects []*unstructured.Unstructured) error { + for i, obj := range objects { + err := traceObject(ctx, kubeClient, obj) + if err != nil { + rootCmd.PrintErrf("failed to trace %v/%v in namespace %v: %v", obj.GetKind(), obj.GetName(), obj.GetNamespace(), err) + } + if i < len(objects)-1 { + rootCmd.Println("---") + } } + return nil +} +func traceObject(ctx context.Context, kubeClient client.Client, obj *unstructured.Unstructured) error { if ks, ok := isOwnerManagedByFlux(ctx, kubeClient, obj, kustomizev1.GroupVersion.Group); ok { report, err := traceKustomization(ctx, kubeClient, ks, obj) if err != nil { @@ -137,19 +142,92 @@ func traceCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("object not managed by Flux") } +func getObjectStatic(ctx context.Context, kubeClient client.Client, args []string) (*unstructured.Unstructured, error) { + if len(args) < 1 { + return nil, fmt.Errorf("object name is required") + } + + if traceArgs.kind == "" { + return nil, fmt.Errorf("object kind is required (--kind)") + } + + if traceArgs.apiVersion == "" { + return nil, fmt.Errorf("object apiVersion is required (--api-version)") + } + + gv, err := schema.ParseGroupVersion(traceArgs.apiVersion) + if err != nil { + return nil, fmt.Errorf("invaild apiVersion: %w", err) + } + + obj := &unstructured.Unstructured{} + obj.SetGroupVersionKind(schema.GroupVersionKind{ + Group: gv.Group, + Version: gv.Version, + Kind: traceArgs.kind, + }) + + objName := types.NamespacedName{ + Namespace: *kubeconfigArgs.Namespace, + Name: args[0], + } + + if err = kubeClient.Get(ctx, objName, obj); err != nil { + return nil, fmt.Errorf("failed to find object: %w", err) + } + return obj, nil +} + +func getObjectDynamic(args []string) ([]*unstructured.Unstructured, error) { + r := resource.NewBuilder(kubeconfigArgs). + Unstructured(). + NamespaceParam(*kubeconfigArgs.Namespace).DefaultNamespace(). + ResourceTypeOrNameArgs(false, args...). + ContinueOnError(). + Latest(). + Do() + + if err := r.Err(); err != nil { + if resource.IsUsageError(err) { + return nil, fmt.Errorf("either `/` or ` ` is required as an argument") + } + return nil, err + } + + infos, err := r.Infos() + if err != nil { + return nil, fmt.Errorf("x: %v", err) + } + if len(infos) == 0 { + return nil, fmt.Errorf("failed to find object: %w", err) + } + + objects := []*unstructured.Unstructured{} + for _, info := range infos { + obj := &unstructured.Unstructured{} + obj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(info.Object) + if err != nil { + return objects, err + } + objects = append(objects, obj) + } + return objects, nil +} + func traceKustomization(ctx context.Context, kubeClient client.Client, ksName types.NamespacedName, obj *unstructured.Unstructured) (string, error) { ks := &kustomizev1.Kustomization{} - ksReady := &metav1.Condition{} err := kubeClient.Get(ctx, ksName, ks) if err != nil { return "", fmt.Errorf("failed to find kustomization: %w", err) } - ksReady = meta.FindStatusCondition(ks.Status.Conditions, fluxmeta.ReadyCondition) + ksReady := meta.FindStatusCondition(ks.Status.Conditions, fluxmeta.ReadyCondition) - var ksRepository *sourcev1.GitRepository + var gitRepository *sourcev1.GitRepository + var ociRepository *sourcev1b2.OCIRepository var ksRepositoryReady *metav1.Condition - if ks.Spec.SourceRef.Kind == sourcev1.GitRepositoryKind { - ksRepository = &sourcev1.GitRepository{} + switch ks.Spec.SourceRef.Kind { + case sourcev1.GitRepositoryKind: + gitRepository = &sourcev1.GitRepository{} sourceNamespace := ks.Namespace if ks.Spec.SourceRef.Namespace != "" { sourceNamespace = ks.Spec.SourceRef.Namespace @@ -157,61 +235,109 @@ func traceKustomization(ctx context.Context, kubeClient client.Client, ksName ty err = kubeClient.Get(ctx, types.NamespacedName{ Namespace: sourceNamespace, Name: ks.Spec.SourceRef.Name, - }, ksRepository) + }, gitRepository) if err != nil { return "", fmt.Errorf("failed to find GitRepository: %w", err) } - ksRepositoryReady = meta.FindStatusCondition(ksRepository.Status.Conditions, fluxmeta.ReadyCondition) + ksRepositoryReady = meta.FindStatusCondition(gitRepository.Status.Conditions, fluxmeta.ReadyCondition) + case sourcev1b2.OCIRepositoryKind: + ociRepository = &sourcev1b2.OCIRepository{} + sourceNamespace := ks.Namespace + if ks.Spec.SourceRef.Namespace != "" { + sourceNamespace = ks.Spec.SourceRef.Namespace + } + err = kubeClient.Get(ctx, types.NamespacedName{ + Namespace: sourceNamespace, + Name: ks.Spec.SourceRef.Name, + }, ociRepository) + if err != nil { + return "", fmt.Errorf("failed to find OCIRepository: %w", err) + } + ksRepositoryReady = meta.FindStatusCondition(ociRepository.Status.Conditions, fluxmeta.ReadyCondition) } var traceTmpl = ` -Object: {{.ObjectName}} +Object: {{.ObjectName}} {{- if .ObjectNamespace }} -Namespace: {{.ObjectNamespace}} +Namespace: {{.ObjectNamespace}} {{- end }} -Status: Managed by Flux +Status: Managed by Flux {{- if .Kustomization }} --- -Kustomization: {{.Kustomization.Name}} -Namespace: {{.Kustomization.Namespace}} +Kustomization: {{.Kustomization.Name}} +Namespace: {{.Kustomization.Namespace}} {{- if .Kustomization.Spec.TargetNamespace }} -Target: {{.Kustomization.Spec.TargetNamespace}} +Target: {{.Kustomization.Spec.TargetNamespace}} {{- end }} -Path: {{.Kustomization.Spec.Path}} -Revision: {{.Kustomization.Status.LastAppliedRevision}} +Path: {{.Kustomization.Spec.Path}} +Revision: {{.Kustomization.Status.LastAppliedRevision}} {{- if .KustomizationReady }} -Status: Last reconciled at {{.KustomizationReady.LastTransitionTime}} -Message: {{.KustomizationReady.Message}} +Status: Last reconciled at {{.KustomizationReady.LastTransitionTime}} +Message: {{.KustomizationReady.Message}} {{- else }} -Status: Unknown +Status: Unknown {{- end }} {{- end }} {{- if .GitRepository }} --- -GitRepository: {{.GitRepository.Name}} -Namespace: {{.GitRepository.Namespace}} -URL: {{.GitRepository.Spec.URL}} +GitRepository: {{.GitRepository.Name}} +Namespace: {{.GitRepository.Namespace}} +URL: {{.GitRepository.Spec.URL}} {{- if .GitRepository.Spec.Reference }} {{- if .GitRepository.Spec.Reference.Tag }} -Tag: {{.GitRepository.Spec.Reference.Tag}} +Tag: {{.GitRepository.Spec.Reference.Tag}} {{- else if .GitRepository.Spec.Reference.SemVer }} -Tag: {{.GitRepository.Spec.Reference.SemVer}} +Tag: {{.GitRepository.Spec.Reference.SemVer}} {{- else if .GitRepository.Spec.Reference.Branch }} -Branch: {{.GitRepository.Spec.Reference.Branch}} +Branch: {{.GitRepository.Spec.Reference.Branch}} {{- end }} {{- end }} {{- if .GitRepository.Status.Artifact }} -Revision: {{.GitRepository.Status.Artifact.Revision}} +Revision: {{.GitRepository.Status.Artifact.Revision}} {{- end }} -{{- if .GitRepositoryReady }} -{{- if eq .GitRepositoryReady.Status "False" }} -Status: Last reconciliation failed at {{.GitRepositoryReady.LastTransitionTime}} +{{- if .RepositoryReady }} +{{- if eq .RepositoryReady.Status "False" }} +Status: Last reconciliation failed at {{.RepositoryReady.LastTransitionTime}} {{- else }} -Status: Last reconciled at {{.GitRepositoryReady.LastTransitionTime}} +Status: Last reconciled at {{.RepositoryReady.LastTransitionTime}} {{- end }} -Message: {{.GitRepositoryReady.Message}} +Message: {{.RepositoryReady.Message}} {{- else }} -Status: Unknown +Status: Unknown +{{- end }} +{{- end }} +{{- if .OCIRepository }} +--- +OCIRepository: {{.OCIRepository.Name}} +Namespace: {{.OCIRepository.Namespace}} +URL: {{.OCIRepository.Spec.URL}} +{{- if .OCIRepository.Spec.Reference }} +{{- if .OCIRepository.Spec.Reference.Tag }} +Tag: {{.OCIRepository.Spec.Reference.Tag}} +{{- else if .OCIRepository.Spec.Reference.SemVer }} +Tag: {{.OCIRepository.Spec.Reference.SemVer}} +{{- else if .OCIRepository.Spec.Reference.Digest }} +Digest: {{.OCIRepository.Spec.Reference.Digest}} +{{- end }} +{{- end }} +{{- if .OCIRepository.Status.Artifact }} +Revision: {{.OCIRepository.Status.Artifact.Revision}} +{{- if .OCIRepository.Status.Artifact.Metadata }} +{{- $metadata := .OCIRepository.Status.Artifact.Metadata }} +{{- range $k, $v := .Annotations }} +{{ with (index $metadata $v) }}{{ $k }}{{ . }}{{ end }} +{{- end }} +{{- end }} +{{- end }} +{{- if .RepositoryReady }} +{{- if eq .RepositoryReady.Status "False" }} +Status: Last reconciliation failed at {{.RepositoryReady.LastTransitionTime}} +{{- else }} +Status: Last reconciled at {{.RepositoryReady.LastTransitionTime}} +{{- end }} +Message: {{.RepositoryReady.Message}} +{{- else }} +Status: Unknown {{- end }} {{- end }} ` @@ -222,14 +348,18 @@ Status: Unknown Kustomization *kustomizev1.Kustomization KustomizationReady *metav1.Condition GitRepository *sourcev1.GitRepository - GitRepositoryReady *metav1.Condition + OCIRepository *sourcev1b2.OCIRepository + RepositoryReady *metav1.Condition + Annotations map[string]string }{ ObjectName: obj.GetKind() + "/" + obj.GetName(), ObjectNamespace: obj.GetNamespace(), Kustomization: ks, KustomizationReady: ksReady, - GitRepository: ksRepository, - GitRepositoryReady: ksRepositoryReady, + GitRepository: gitRepository, + OCIRepository: ociRepository, + RepositoryReady: ksRepositoryReady, + Annotations: map[string]string{"Origin Source: ": oci.SourceAnnotation, "Origin Revision: ": oci.RevisionAnnotation}, } t, err := template.New("tmpl").Parse(traceTmpl) @@ -252,17 +382,16 @@ Status: Unknown func traceHelm(ctx context.Context, kubeClient client.Client, hrName types.NamespacedName, obj *unstructured.Unstructured) (string, error) { hr := &helmv2.HelmRelease{} - hrReady := &metav1.Condition{} err := kubeClient.Get(ctx, hrName, hr) if err != nil { return "", fmt.Errorf("failed to find HelmRelease: %w", err) } - hrReady = meta.FindStatusCondition(hr.Status.Conditions, fluxmeta.ReadyCondition) + hrReady := meta.FindStatusCondition(hr.Status.Conditions, fluxmeta.ReadyCondition) - var hrChart *sourcev1.HelmChart + var hrChart *sourcev1b2.HelmChart var hrChartReady *metav1.Condition if chart := hr.Status.HelmChart; chart != "" { - hrChart = &sourcev1.HelmChart{} + hrChart = &sourcev1b2.HelmChart{} err = kubeClient.Get(ctx, utils.ParseNamespacedName(chart), hrChart) if err != nil { return "", fmt.Errorf("failed to find HelmChart: %w", err) @@ -288,10 +417,10 @@ func traceHelm(ctx context.Context, kubeClient client.Client, hrName types.Names hrGitRepositoryReady = meta.FindStatusCondition(hrGitRepository.Status.Conditions, fluxmeta.ReadyCondition) } - var hrHelmRepository *sourcev1.HelmRepository + var hrHelmRepository *sourcev1b2.HelmRepository var hrHelmRepositoryReady *metav1.Condition - if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1.HelmRepositoryKind { - hrHelmRepository = &sourcev1.HelmRepository{} + if hr.Spec.Chart.Spec.SourceRef.Kind == sourcev1b2.HelmRepositoryKind { + hrHelmRepository = &sourcev1b2.HelmRepository{} sourceNamespace := hr.Namespace if hr.Spec.Chart.Spec.SourceRef.Namespace != "" { sourceNamespace = hr.Spec.Chart.Spec.SourceRef.Namespace @@ -393,11 +522,11 @@ Status: Unknown ObjectNamespace string HelmRelease *helmv2.HelmRelease HelmReleaseReady *metav1.Condition - HelmChart *sourcev1.HelmChart + HelmChart *sourcev1b2.HelmChart HelmChartReady *metav1.Condition GitRepository *sourcev1.GitRepository GitRepositoryReady *metav1.Condition - HelmRepository *sourcev1.HelmRepository + HelmRepository *sourcev1b2.HelmRepository HelmRepositoryReady *metav1.Condition }{ ObjectName: obj.GetKind() + "/" + obj.GetName(), diff --git a/cmd/flux/trace_test.go b/cmd/flux/trace_test.go index 943c6bd7..c9604715 100644 --- a/cmd/flux/trace_test.go +++ b/cmd/flux/trace_test.go @@ -1,49 +1,81 @@ +//go:build unit // +build unit package main import ( "testing" + "time" ) func TestTraceNoArgs(t *testing.T) { cmd := cmdTestCase{ args: "trace", - assert: assertError("object name is required"), + assert: assertError("either `/` or ` ` is required as an argument"), } cmd.runTestCmd(t) } +func toLocalTime(t *testing.T, in string) string { + ts, err := time.Parse(time.RFC3339, in) + if err != nil { + t.Fatalf("Error converting golden test time '%s': %v", in, err) + } + return ts.Local().String() +} + func TestTrace(t *testing.T) { cases := []struct { name string args string objectFile string goldenFile string + tmpl map[string]string }{ { "Deployment", "trace podinfo --kind deployment --api-version=apps/v1", "testdata/trace/deployment.yaml", "testdata/trace/deployment.golden", + map[string]string{ + "ns": allocateNamespace("podinfo"), + "fluxns": allocateNamespace("flux-system"), + "helmReleaseLastReconcile": toLocalTime(t, "2021-07-16T15:42:20Z"), + "helmChartLastReconcile": toLocalTime(t, "2021-07-16T15:32:09Z"), + "helmRepositoryLastReconcile": toLocalTime(t, "2021-07-11T00:25:46Z"), + }, }, { "HelmRelease", - "trace podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1", + "trace podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta2", "testdata/trace/helmrelease.yaml", "testdata/trace/helmrelease.golden", + map[string]string{ + "ns": allocateNamespace("podinfo"), + "fluxns": allocateNamespace("flux-system"), + "kustomizationLastReconcile": toLocalTime(t, "2021-08-01T04:52:56Z"), + "gitRepositoryLastReconcile": toLocalTime(t, "2021-07-20T00:48:16Z"), + }, + }, + { + "HelmRelease from OCI registry", + "trace podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta2", + "testdata/trace/helmrelease-oci.yaml", + "testdata/trace/helmrelease-oci.golden", + map[string]string{ + "ns": allocateNamespace("podinfo"), + "fluxns": allocateNamespace("flux-system"), + "kustomizationLastReconcile": toLocalTime(t, "2021-08-01T04:52:56Z"), + "ociRepositoryLastReconcile": toLocalTime(t, "2021-07-20T00:48:16Z"), + }, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - tmpl := map[string]string{ - "ns": allocateNamespace("podinfo"), - "fluxns": allocateNamespace("flux-system"), - } - testEnv.CreateObjectFile(tc.objectFile, tmpl, t) + testEnv.CreateObjectFile(tc.objectFile, tc.tmpl, t) cmd := cmdTestCase{ - args: tc.args + " -n=" + tmpl["ns"], - assert: assertGoldenTemplateFile(tc.goldenFile, tmpl), + args: tc.args + " -n=" + tc.tmpl["ns"], + assert: assertGoldenTemplateFile(tc.goldenFile, tc.tmpl), } cmd.runTestCmd(t) }) diff --git a/cmd/flux/tree.go b/cmd/flux/tree.go index 8e2503c0..5bcf3ffd 100644 --- a/cmd/flux/tree.go +++ b/cmd/flux/tree.go @@ -23,7 +23,7 @@ import ( var treeCmd = &cobra.Command{ Use: "tree", Short: "Print the resources reconciled by Flux", - Long: `The tree command shows the list of resources reconciled by a Flux object.'`, + Long: withPreviewNote(`The tree command shows the list of resources reconciled by a Flux object.`), } func init() { diff --git a/cmd/flux/tree_kustomization.go b/cmd/flux/tree_kustomization.go index 25d4142d..e345f0f2 100644 --- a/cmd/flux/tree_kustomization.go +++ b/cmd/flux/tree_kustomization.go @@ -26,25 +26,29 @@ import ( "io" "strings" - "github.com/fluxcd/flux2/internal/tree" - "github.com/fluxcd/flux2/internal/utils" - helmv2 "github.com/fluxcd/helm-controller/api/v2beta1" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1beta2" - "github.com/fluxcd/pkg/ssa" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/cli-utils/pkg/object" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/yaml" + + "github.com/fluxcd/cli-utils/pkg/object" + helmv2 "github.com/fluxcd/helm-controller/api/v2beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + "github.com/fluxcd/pkg/ssa" + + "github.com/fluxcd/flux2/v2/internal/tree" + "github.com/fluxcd/flux2/v2/internal/utils" ) var treeKsCmd = &cobra.Command{ Use: "kustomization [name]", Aliases: []string{"ks", "kustomization"}, Short: "Print the resource inventory of a Kustomization", - Long: `The tree command prints the resource list reconciled by a Kustomization.'`, + Long: withPreviewNote(`The tree command prints the resource list reconciled by a Kustomization.'`), Example: ` # Print the resources managed by the root Kustomization flux tree kustomization flux-system @@ -77,27 +81,26 @@ func treeKsCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - kubeClient, err := utils.KubeClient(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } k := &kustomizev1.Kustomization{} err = kubeClient.Get(ctx, client.ObjectKey{ - Namespace: rootArgs.namespace, + Namespace: *kubeconfigArgs.Namespace, Name: name, }, k) if err != nil { return err } - kMeta, err := object.CreateObjMetadata(k.Namespace, k.Name, - schema.GroupKind{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}) - if err != nil { - return err - } + kTree := tree.New(object.ObjMetadata{ + Namespace: k.Namespace, + Name: k.Name, + GroupKind: schema.GroupKind{Group: kustomizev1.GroupVersion.Group, Kind: kustomizev1.KustomizationKind}, + }) - kTree := tree.New(kMeta) err = treeKustomization(ctx, kTree, k, kubeClient, treeKsArgs.compact) if err != nil { return err @@ -206,27 +209,16 @@ func getHelmReleaseInventory(ctx context.Context, objectKey client.ObjectKey, ku return nil, nil } - storageNamespace := hr.GetNamespace() - if hr.Spec.StorageNamespace != "" { - storageNamespace = hr.Spec.StorageNamespace - } - - storageName := hr.GetName() - if hr.Spec.ReleaseName != "" { - storageName = hr.Spec.ReleaseName - } else if hr.Spec.TargetNamespace != "" { - storageName = strings.Join([]string{hr.Spec.TargetNamespace, hr.Name}, "-") - } - - storageVersion := hr.Status.LastReleaseRevision - // skip release if it failed to install - if storageVersion < 1 { + storageNamespace := hr.Status.StorageNamespace + latest := hr.Status.History.Latest() + if len(storageNamespace) == 0 || latest == nil { + // Skip release if it has no current return nil, nil } storageKey := client.ObjectKey{ Namespace: storageNamespace, - Name: fmt.Sprintf("sh.helm.release.v1.%s.v%v", storageName, storageVersion), + Name: fmt.Sprintf("sh.helm.release.v1.%s.v%v", latest.Name, latest.Version), } storageSecret := &corev1.Secret{} @@ -263,6 +255,7 @@ func getHelmReleaseInventory(ctx context.Context, objectKey client.ObjectKey, ku b = b2 } + // extract objects from Helm storage var rls hrStorage if err := json.Unmarshal(b, &rls); err != nil { return nil, fmt.Errorf("failed to decode the Helm storage object for HelmRelease '%s': %w", objectKey.String(), err) @@ -273,5 +266,48 @@ func getHelmReleaseInventory(ctx context.Context, objectKey client.ObjectKey, ku return nil, fmt.Errorf("failed to read the Helm storage object for HelmRelease '%s': %w", objectKey.String(), err) } - return object.UnstructuredsToObjMetas(objects) + // set the namespace on namespaced objects + for _, obj := range objects { + if obj.GetNamespace() == "" { + if isNamespaced, _ := apiutil.IsObjectNamespaced(obj, kubeClient.Scheme(), kubeClient.RESTMapper()); isNamespaced { + obj.SetNamespace(latest.Namespace) + } + } + } + + result := object.UnstructuredSetToObjMetadataSet(objects) + + // search for CRDs managed by the HelmRelease if installing or upgrading CRDs is enabled in spec + if (hr.Spec.Install != nil && len(hr.Spec.Install.CRDs) > 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/tree_kustomization_test.go b/cmd/flux/tree_kustomization_test.go index be5ce57e..045b869b 100644 --- a/cmd/flux/tree_kustomization_test.go +++ b/cmd/flux/tree_kustomization_test.go @@ -1,3 +1,4 @@ +//go:build unit // +build unit /* diff --git a/cmd/flux/uninstall.go b/cmd/flux/uninstall.go index 448137a1..0128a833 100644 --- a/cmd/flux/uninstall.go +++ b/cmd/flux/uninstall.go @@ -22,25 +22,16 @@ 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", 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 +60,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(rootArgs.kubeconfig, rootArgs.kubecontext) + kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions) if err != nil { return err } - logger.Actionf("deleting components in %s namespace", rootArgs.namespace) - uninstallComponents(ctx, kubeClient, rootArgs.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, rootArgs.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 93ee3f90..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,23 +78,37 @@ 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(rootArgs.kubeconfig, rootArgs.kubecontext) + 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(rootArgs.namespace), selector); err != nil { + if err := kubeClient.List(ctx, &list, client.InNamespace(*kubeconfigArgs.Namespace), selector); err != nil { return err } if len(list.Items) == 0 { - return fmt.Errorf("no deployments found in %s namespace", rootArgs.namespace) + return fmt.Errorf("no deployments found in %s namespace", *kubeconfigArgs.Namespace) } for _, d := range list.Items { @@ -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/cmd/flux/version_utils_test.go b/cmd/flux/version_utils_test.go index 0aab01d9..fc9e8e76 100644 --- a/cmd/flux/version_utils_test.go +++ b/cmd/flux/version_utils_test.go @@ -1,3 +1,4 @@ +//go:build unit // +build unit /* 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/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 e346f11d..a43c6315 100644 --- a/go.mod +++ b/go.mod @@ -1,42 +1,220 @@ -module github.com/fluxcd/flux2 +module github.com/fluxcd/flux2/v2 -go 1.16 +go 1.20 + +// 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-20210428141323-04723f9f07d7 - github.com/cyphar/filepath-securejoin v0.2.2 - github.com/fluxcd/go-git-providers v0.4.0 - github.com/fluxcd/helm-controller/api v0.14.0 - github.com/fluxcd/image-automation-controller/api v0.18.0 - github.com/fluxcd/image-reflector-controller/api v0.14.0 - github.com/fluxcd/kustomize-controller/api v0.18.1 - github.com/fluxcd/notification-controller/api v0.19.0 - github.com/fluxcd/pkg/apis/meta v0.10.1 - github.com/fluxcd/pkg/runtime v0.12.2 - github.com/fluxcd/pkg/ssa v0.3.1 - github.com/fluxcd/pkg/ssh v0.0.5 - github.com/fluxcd/pkg/untar v0.0.5 - github.com/fluxcd/pkg/version v0.0.1 - github.com/fluxcd/source-controller/api v0.19.0 - github.com/go-errors/errors v1.4.0 // indirect - github.com/go-git/go-git/v5 v5.4.2 - github.com/google/go-cmp v0.5.6 - github.com/google/go-containerregistry v0.2.0 + github.com/Masterminds/semver/v3 v3.2.1 + github.com/ProtonMail/go-crypto v0.0.0-20231012073058-a7379d079e0e + github.com/cyphar/filepath-securejoin v0.2.4 + github.com/distribution/distribution/v3 v3.0.0-20230823142118-4f7424c8eb41 + github.com/fluxcd/cli-utils v0.36.0-flux.2 + github.com/fluxcd/go-git-providers v0.19.2 + github.com/fluxcd/helm-controller/api v0.37.2 + github.com/fluxcd/image-automation-controller/api v0.37.0 + github.com/fluxcd/image-reflector-controller/api v0.31.1 + github.com/fluxcd/kustomize-controller/api v1.2.1 + github.com/fluxcd/notification-controller/api v1.2.3 + github.com/fluxcd/pkg/apis/event v0.6.0 + github.com/fluxcd/pkg/apis/meta v1.2.0 + github.com/fluxcd/pkg/git v0.16.0 + github.com/fluxcd/pkg/git/gogit v0.16.1 + github.com/fluxcd/pkg/kustomize v1.5.0 + github.com/fluxcd/pkg/oci v0.33.3 + github.com/fluxcd/pkg/runtime v0.43.2 + github.com/fluxcd/pkg/sourceignore v0.4.0 + github.com/fluxcd/pkg/ssa v0.35.0 + github.com/fluxcd/pkg/ssh v0.10.0 + github.com/fluxcd/pkg/tar v0.4.0 + github.com/fluxcd/pkg/version v0.2.2 + github.com/fluxcd/source-controller/api v1.2.3 + github.com/go-git/go-git/v5 v5.11.0 + github.com/go-logr/logr v1.3.0 + 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.17.0 + github.com/hashicorp/go-cleanhttp v0.5.2 + github.com/homeport/dyff v1.6.0 + 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.1.3 + github.com/olekukonko/tablewriter v0.0.5 + github.com/onsi/gomega v1.30.0 + 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-20210421170649-83a5a9bb288b - golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d - k8s.io/api v0.22.2 - k8s.io/apiextensions-apiserver v0.22.2 - k8s.io/apimachinery v0.22.2 - k8s.io/client-go v0.22.2 - k8s.io/kubectl v0.21.1 - sigs.k8s.io/cli-utils v0.26.0 - sigs.k8s.io/controller-runtime v0.10.2 - sigs.k8s.io/kustomize/api v0.8.10 - sigs.k8s.io/yaml v1.3.0 + github.com/theckman/yacspin v0.13.12 + golang.org/x/crypto v0.17.0 + golang.org/x/term v0.15.0 + golang.org/x/text v0.14.0 + k8s.io/api v0.28.4 + k8s.io/apiextensions-apiserver v0.28.4 + k8s.io/apimachinery v0.28.4 + k8s.io/cli-runtime v0.28.4 + k8s.io/client-go v0.28.4 + k8s.io/kubectl v0.28.4 + sigs.k8s.io/controller-runtime v0.16.3 + sigs.k8s.io/kustomize/api v0.16.0 + sigs.k8s.io/kustomize/kyaml v0.16.0 + sigs.k8s.io/yaml v1.4.0 +) + +require ( + code.gitea.io/sdk/gitea v0.17.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // 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.24.0 // indirect + github.com/aws/aws-sdk-go-v2/config v1.26.1 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.24.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 // indirect + github.com/aws/smithy-go v1.19.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.7+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/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 // indirect + github.com/drone/envsubst v1.0.3 // indirect + github.com/emicklei/go-restful/v3 v3.11.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.7.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.3 // indirect + github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect + github.com/fluxcd/pkg/apis/kustomize v1.2.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // 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-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/gomodule/redigo v1.8.2 // indirect + github.com/gonvenience/neat v1.3.12 // indirect + github.com/gonvenience/term v1.0.2 // indirect + github.com/gonvenience/text v1.0.7 // indirect + github.com/gonvenience/wrap v1.1.2 // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-github/v57 v57.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.4.0 // indirect + github.com/gorilla/handlers v1.5.1 // indirect + github.com/gorilla/mux v1.8.0 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // 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 v1.2.0 // indirect + github.com/klauspost/compress v1.16.5 // 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.7 // indirect + github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // 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/matttproud/golang_protobuf_extensions/v2 v2.0.0 // 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.1 // indirect + github.com/mitchellh/hashstructure v1.1.0 // indirect + github.com/moby/spdystream v0.2.0 // 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/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0-rc3 // 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-20210911075715-681adbf594b8 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.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.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.2.1 // 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.95.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect + golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.15.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.16.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/evanphx/json-patch.v5 v5.7.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/component-base v0.28.4 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8 // indirect + k8s.io/utils v0.0.0-20231127182322-b307cd553661 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/go.sum b/go.sum index b08135d8..8ca1859b 100644 --- a/go.sum +++ b/go.sum @@ -1,1373 +1,632 @@ -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.57.0 h1:EpMNVUorLiZIELdMZbCYX/ByTFCdoYopYAGxaGVz9ms= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -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/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/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= -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.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -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.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/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= -github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -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 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/purell v1.1.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/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -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/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= -github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= +code.gitea.io/sdk/gitea v0.17.0 h1:8JPBss4+Jf7AE1YcfyiGrngTXE8dFSG3si/bypsTH34= +code.gitea.io/sdk/gitea v0.17.0/go.mod h1:ndkDk99BnfiUCCYEUhpNzi0lpmApXlwRFqClBlOlEBg= +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-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0 h1:fb8kj/Dh4CSwgsOzHeZY4Xh68cFVbzXx+ONXGMY//4w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.0/go.mod h1:uReU2sSxZExRPBAg3qKzmAucSi51+SP1OhohieR821Q= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0 h1:d81/ng9rET2YqdVkVwkb6EXeRrLJIwyGnJcAlAWKwhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.0/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +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/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/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 v0.0.0-20231012073058-a7379d079e0e h1:NfjGPY2A8SSRJvXny111ZPoB57LT5lWgX4XiUjW10eY= +github.com/ProtonMail/go-crypto v0.0.0-20231012073058-a7379d079e0e/go.mod h1:K4vciqCJaZ1Ghw/SvtJbEAM4soEtwDCNVqkdQIIujwU= 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/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= -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/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -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/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 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/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= -github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= -github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -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/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -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.24.0 h1:890+mqQ+hTpNuw0gGP6/4akolQkSToDJgHfQE7AwGuk= +github.com/aws/aws-sdk-go-v2 v1.24.0/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/config v1.26.1 h1:z6DqMxclFGL3Zfo+4Q0rLnAZ6yVkzCRxhRMsiRQnD1o= +github.com/aws/aws-sdk-go-v2/config v1.26.1/go.mod h1:ZB+CuKHRbb5v5F0oJtGdhFTelmrxd4iWO1lf0rQwSAg= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12 h1:v/WgB8NxprNvr5inKIiVVrXPuuTegM+K8nncFkr1usU= +github.com/aws/aws-sdk-go-v2/credentials v1.16.12/go.mod h1:X21k0FjEJe+/pauud82HYiQbEr9jRKY3kXEIQ4hXeTQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10 h1:w98BT5w+ao1/r5sUuiH6JkVzjowOKeOJRHERyy1vh58= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.10/go.mod h1:K2WGI7vUvkIv1HoNbfBA1bvIZ+9kL3YVmWxeKuLQsiw= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9 h1:v+HbZaCGmOwnTTVS86Fleq0vPzOd7tnJGbFhP0stNLs= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.9/go.mod h1:Xjqy+Nyj7VDLBtCMkQYOw1QYfAEZCVLrfI0ezve8wd4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9 h1:N94sVhRACtXyVcjXxrwK1SKFIJrA9pOJ5yu2eSHnmls= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.9/go.mod h1:hqamLz7g1/4EJP+GH5NBhcUMLjW+gKLQabgyz6/7WAU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 h1:GrSw8s0Gs/5zZ0SX+gX4zQjRnRsMJDJ2sLur1gRBhEM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/ecr v1.24.5 h1:wLPDAUFT50NEXGXpywRU3AA74pg35RJjWol/68ruvQQ= +github.com/aws/aws-sdk-go-v2/service/ecr v1.24.5/go.mod h1:AOHmGMoPtSY9Zm2zBuwUJQBisIvYAZeA1n7b6f4e880= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9 h1:Nf2sHxjMJR8CSImIVCONRi4g0Su3J+TSTbS7G0pUeMU= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.9/go.mod h1:idky4TER38YIjr2cADF1/ugFMKvZV7p//pVeV5LZbF0= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5 h1:ldSFWz9tEHAwHNmjx2Cvy1MjP5/L9kNoR0skc6wyOOM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.5/go.mod h1:CaFfXLYL376jgbP7VKC96uFcU8Rlavak0UlAwk1Dlhc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5 h1:2k9KmFawS63euAkY4/ixVNsYYwrwnd5fIvgEKkfZFNM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.5/go.mod h1:W+nd4wWDVkSUIox9bacmkBP5NMFQeTJ/xqNabpzSR38= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5 h1:5UYvv8JUvllZsRnfrcMQ+hJ9jNICmcgKPAO1CER25Wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.5/go.mod h1:XX5gh4CB7wAs4KhcF46G6C8a2i7eupU19dcAAE+EydU= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= 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/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/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -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/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= +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/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.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 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 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/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/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= -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-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -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/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -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-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -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/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/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.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -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/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +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 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.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -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/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/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -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.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= -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.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/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.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= -github.com/evanphx/json-patch v4.11.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/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/distribution/distribution/v3 v3.0.0-20230823142118-4f7424c8eb41 h1:vlGNYFw1NB27Gk5tIwJpiog1Ti6FKOUI+DoFZKoCDZo= +github.com/distribution/distribution/v3 v3.0.0-20230823142118-4f7424c8eb41/go.mod h1:WREzLx07iIFUGvbm6tBoqGt40zOC3whiM1qkcWOMFrs= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+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.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+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/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= +github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= +github.com/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.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.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.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.4.0 h1:hdGGRDCNphf9FRrk297lorhwHWcST74R7cGAOZTRtSU= -github.com/fluxcd/go-git-providers v0.4.0/go.mod h1:enIPrXnSOBxahS6rngohpG3d/QZ3yjjy/w+agbp97ZI= -github.com/fluxcd/helm-controller/api v0.14.0 h1:I6gyOaEcUUEk0cuXgKhvmvgN833LxdZ3dngTnMDii7w= -github.com/fluxcd/helm-controller/api v0.14.0/go.mod h1:3wDrDVSH/3yH31PzfXzYCCf8OP70eg82tlQU1+QGsS0= -github.com/fluxcd/image-automation-controller/api v0.18.0 h1:/FMwAJ31s4xk5XL7AKYWT0Z9f+GpLbd55e2I1m6jg/o= -github.com/fluxcd/image-automation-controller/api v0.18.0/go.mod h1:XqrRtH4aFOY1llgf4wx8VcSmzLsdV/xWcrPbdZjvvLg= -github.com/fluxcd/image-reflector-controller/api v0.14.0 h1:JlwwpQENkGSxtAC7VXykpOqObsupO61easXu30jpvb0= -github.com/fluxcd/image-reflector-controller/api v0.14.0/go.mod h1:ew7uL5XzPH3QTfNxt3BAklDh9ONrR5I2m3D7ST0zE9E= -github.com/fluxcd/kustomize-controller/api v0.18.1 h1:cssv85lh5RlDDEFvMv/4InR/7SzqX/+rVMYA3GqpnFI= -github.com/fluxcd/kustomize-controller/api v0.18.1/go.mod h1:xGHBIzVXepzm2/0iQJJSbCIRY0Ahq5AgbYsVojpRyX0= -github.com/fluxcd/notification-controller/api v0.19.0 h1:mSsDj30T4v/9aL2GoMY616p+6nIifD1nrZiBD/rUi8U= -github.com/fluxcd/notification-controller/api v0.19.0/go.mod h1:SkB3tLOXouLN6PAceNCsJrJaawHt+WiUVfUSIYcpwjs= -github.com/fluxcd/pkg/apis/acl v0.0.1 h1:biCgZMjpDSv3Q4mZPikUJILx3t2MuNXR4Oa5jRQxaNQ= -github.com/fluxcd/pkg/apis/acl v0.0.1/go.mod h1:y3qOXUFObVWk7jzOjubMnr/u18j1kCeSi6olycnxr/E= -github.com/fluxcd/pkg/apis/kustomize v0.2.0 h1:jhu2QHvs+j3Zo9rR6w8hkO3LSC6h3M37zY5ejufOmxY= -github.com/fluxcd/pkg/apis/kustomize v0.2.0/go.mod h1:gEl+W5cVykCC3RfrCaqe+Pz+j4lKl2aeR4dxsom/zII= -github.com/fluxcd/pkg/apis/meta v0.10.0/go.mod h1:CW9X9ijMTpNe7BwnokiUOrLl/h13miwVr/3abEQLbKE= -github.com/fluxcd/pkg/apis/meta v0.10.1 h1:zISenRlqNG7WK8TP3HxZTvv+1Z7JZOUIQvZrOr6pQ2w= -github.com/fluxcd/pkg/apis/meta v0.10.1/go.mod h1:yUblM2vg+X8TE3A2VvJfdhkGmg+uqBlSPkLk7dxi0UM= -github.com/fluxcd/pkg/runtime v0.12.2 h1:4iOpx2j/w15kNemDOnZrF6ugJ/rhSmRu7aI+xn23+BI= -github.com/fluxcd/pkg/runtime v0.12.2/go.mod h1:tuWdqpWPhgjQvYrSnojdZ4plyU8DRU1NDzsfOhnzl2g= -github.com/fluxcd/pkg/ssa v0.3.1 h1:lKjTRQmSWFEXpKJadK9Fu0GSLL8lv0k5muIcc+7hZIs= -github.com/fluxcd/pkg/ssa v0.3.1/go.mod h1:rFhWBX9/TfNwSFR+5NHOGnpl9OsWdaQrG5CggN+74EQ= -github.com/fluxcd/pkg/ssh v0.0.5 h1:rnbFZ7voy2JBlUfMbfyqArX2FYaLNpDhccGFC3qW83A= -github.com/fluxcd/pkg/ssh v0.0.5/go.mod h1:7jXPdXZpc0ttMNz2kD9QuMi3RNn/e0DOFbj0Tij/+Hs= -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.19.0 h1:D4hc/ROhcl7iJdgeVhmM6B7WkDqwtQKnvpl04n+LcNg= -github.com/fluxcd/source-controller/api v0.19.0/go.mod h1:rUqw0LmoCyGUoElmtLqHc8O35WAQUd8LdgdyDA+kNs4= -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/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= -github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= -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/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= -github.com/go-errors/errors v1.4.0 h1:2OA7MFw38+e9na72T1xgkomPb6GzZzzxvJ5U630FoRM= -github.com/go-errors/errors v1.4.0/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -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.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/cli-utils v0.36.0-flux.2 h1:7nlXfAJ7iaDF34IdbyId+wBf7beL2qvzDBLmVBJSDVo= +github.com/fluxcd/cli-utils v0.36.0-flux.2/go.mod h1:TQtgRf9OjQBzE5FJ9UDV6WNz9Po3pzAtk3NQmQEN5l8= +github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= +github.com/fluxcd/go-git-providers v0.19.2 h1:G/O0z4WUlgChV6o3YrZ1GP1ZgZSd8ZJWACIW0ICqxMo= +github.com/fluxcd/go-git-providers v0.19.2/go.mod h1:V44pmhFqoZ8heFJEBU1PS+OHTwLWOfoj0nKpFAqik6Y= +github.com/fluxcd/helm-controller/api v0.37.2 h1:tkLezpRdqPDz7HoKHFu92sV+ppOCVDxkjFTh8/lpff8= +github.com/fluxcd/helm-controller/api v0.37.2/go.mod h1:BuXZhAX9blQviil6yUN5zNM4RB753yhyBTJXxXff7Mo= +github.com/fluxcd/image-automation-controller/api v0.37.0 h1:0L8kzX3zaYfg4wQ2Mx3G56atLMNeDGjy5qMGP4tDKRs= +github.com/fluxcd/image-automation-controller/api v0.37.0/go.mod h1:B2EbmiY69mE2bGOENPNUXr6klwrVe7FwVoB2iE1q08E= +github.com/fluxcd/image-reflector-controller/api v0.31.1 h1:nc44G0JjLgSvqglJSiXQJZcrRw+eY01j7fHRUDB3FMw= +github.com/fluxcd/image-reflector-controller/api v0.31.1/go.mod h1:KopMbC92Cw2ypZZeMytzTLr3EfOj2hoL6MizrdpBDhc= +github.com/fluxcd/kustomize-controller/api v1.2.1 h1:+WgQOU7jpqz9bA4djPWmaeYAp9cG7c/TdcIYku3Jrzk= +github.com/fluxcd/kustomize-controller/api v1.2.1/go.mod h1:0Kgc4uYnr5jCm4H8JwArkR0v4WTmXeX/9KgoDbxluVc= +github.com/fluxcd/notification-controller/api v1.2.3 h1:vXVMg2PmTjmRC5+ULZfbmBEm/CsJaM9yjNJzRdI0JMs= +github.com/fluxcd/notification-controller/api v1.2.3/go.mod h1:A0VkH3mswQAeGKsmzq81jAUN+zNJt9SPXjwe8mvnnaw= +github.com/fluxcd/pkg/apis/acl v0.1.0 h1:EoAl377hDQYL3WqanWCdifauXqXbMyFuK82NnX6pH4Q= +github.com/fluxcd/pkg/apis/acl v0.1.0/go.mod h1:zfEZzz169Oap034EsDhmCAGgnWlcWmIObZjYMusoXS8= +github.com/fluxcd/pkg/apis/event v0.6.0 h1:AUaeee1CGWb65BLqVximHXG8Gcu6vWuYONIq6tVpjgo= +github.com/fluxcd/pkg/apis/event v0.6.0/go.mod h1:OEzWcX/oPbMmkCvC9QGoK27JXFvUZgBhLD+zgxZe47A= +github.com/fluxcd/pkg/apis/kustomize v1.2.0 h1:vkVs+OumxaWso0jNCqdgFFfMHdh+qtZhykTkjl7OgmA= +github.com/fluxcd/pkg/apis/kustomize v1.2.0/go.mod h1:VF7tR/WuVFeum+HaMTHwp+eCtsHiiQlY6ihgqtAnW/M= +github.com/fluxcd/pkg/apis/meta v1.2.0 h1:O766PzGAdMdQKybSflGL8oV0+GgCNIkdsxfalRyzeO8= +github.com/fluxcd/pkg/apis/meta v1.2.0/go.mod h1:fU/Az9AoVyIxC0oI4ihG0NVMNnvrcCzdEym3wxjIQsc= +github.com/fluxcd/pkg/git v0.16.0 h1:xgfMpgsVaxGLechKNaSUif9jnt2Ji/HkwIwxXeDoADk= +github.com/fluxcd/pkg/git v0.16.0/go.mod h1:ZsaxXDBHIUjNeRf+3qDGyHD22KpqbjuNPCbHy2Xid5U= +github.com/fluxcd/pkg/git/gogit v0.16.1 h1:byimVk7VLbERRxJDkVktithN03GC0y7fyc8Ur9Uka8U= +github.com/fluxcd/pkg/git/gogit v0.16.1/go.mod h1:nx3PumOFe5e3fMsh2HPLSlAeOk2wC+jVg7H9BTRKEmg= +github.com/fluxcd/pkg/gittestserver v0.9.0 h1:OthMahtKmmpwCRw8LmSGrceqVhHywGg/QiytxHGSgeY= +github.com/fluxcd/pkg/kustomize v1.5.0 h1:Q2kynQzF4coKlOJq/XaLM+gVmVloaInkoa+vsor6Hho= +github.com/fluxcd/pkg/kustomize v1.5.0/go.mod h1:QgFwpteTedb7oio5+yN+h+rhSgm253OIjmtoTow+a5c= +github.com/fluxcd/pkg/oci v0.33.3 h1:dj5IuF2O9/0r37tOWoOoKX/0emytjEiJ6sXy9FUkHww= +github.com/fluxcd/pkg/oci v0.33.3/go.mod h1:6+vIgdqP6AkFitvlyGx2W9f4s8q5Oguw9SNeN494MCY= +github.com/fluxcd/pkg/runtime v0.43.2 h1:xH2BvttUqJ7wS0zjuBETr2pLXG62QY6f0mdxg5UQKio= +github.com/fluxcd/pkg/runtime v0.43.2/go.mod h1:dhhNV45B3tekg7qPzATFTuWOulvUzMoO1bV+vc2pSts= +github.com/fluxcd/pkg/sourceignore v0.4.0 h1:99Ikoi8qMirlBK4yjnoKa5vx9YFQ/BSSK3Axi+yDg9s= +github.com/fluxcd/pkg/sourceignore v0.4.0/go.mod h1:j1BRQE+R0TJmPYPUnEd/0gm8KcpajlX6mDv3z7s8YFQ= +github.com/fluxcd/pkg/ssa v0.35.0 h1:8T3WY4P9SQWApa2hq1rU1u2WE8oqP3MMTsAiEWwhmfo= +github.com/fluxcd/pkg/ssa v0.35.0/go.mod h1:rhVh0EtYVUOznKXlz6E7JOSgdc8xWbIwA4L5HVtJRLA= +github.com/fluxcd/pkg/ssh v0.10.0 h1:JFz0u/CPEf3hXvmrEvUvXsc70eKh8xphqjXxZuSby9g= +github.com/fluxcd/pkg/ssh v0.10.0/go.mod h1:1lFTj3MhU9xQuaJ5PJJoh/FyRYzK54ll9NY/s2KqOZM= +github.com/fluxcd/pkg/tar v0.4.0 h1:SuXpfXBIcSJ5R/yqQi2CBxBmV/i/LH0agqNAh2PWBZg= +github.com/fluxcd/pkg/tar v0.4.0/go.mod h1:SyJBaQvuv2VA/rv4d1OHhCV6R8+9QKc9np193EzNHBc= +github.com/fluxcd/pkg/version v0.2.2 h1:ZpVXECeLA5hIQMft11iLp6gN3cKcz6UNuVTQPw/bRdI= +github.com/fluxcd/pkg/version v0.2.2/go.mod h1:NGnh/no8S6PyfCDxRFrPY3T5BUnqP48MxfxNRU0z8C0= +github.com/fluxcd/source-controller/api v1.2.3 h1:71mXv3Qg9HEhcpqOq1ObmoE+P/HuZNaAvxfI7dqZMo8= +github.com/fluxcd/source-controller/api v1.2.3/go.mod h1:5gaIVVH7hgb8p3HKFp8P6hGmZEC8fKSt4EcrG3g5vZI= +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.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +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/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= 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/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= -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 v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= -github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= -github.com/go-openapi/analysis v0.0.0-20180825180245-b006789cd277/go.mod h1:k70tL6pCuVxPJOHXQ+wIac1FUrvNkHolPie/cLEU6hI= -github.com/go-openapi/analysis v0.17.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.18.0/go.mod h1:IowGgpVeD0vNm45So8nr+IcQ3pxVtpRoBWb8PVZO0ik= -github.com/go-openapi/analysis v0.19.2/go.mod h1:3P1osvZa9jKjb8ed2TPng3f0i/UY9snX6gxi44djMjk= -github.com/go-openapi/analysis v0.19.5/go.mod h1:hkEAkxagaIvIP7VTn8ygJNkd4kAYON2rCu0v0ObL0AU= -github.com/go-openapi/errors v0.17.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.18.0/go.mod h1:LcZQpmvG4wyF5j4IhA73wkLFQg+QJXOQHVjmcZxhka0= -github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= -github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= -github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= -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.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= -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/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU= -github.com/go-openapi/loads v0.19.2/go.mod h1:QAskZPMX5V0C2gvfkGZzJlINuP7Hx/4+ix5jWFxsNPs= -github.com/go-openapi/loads v0.19.4/go.mod h1:zZVHonKd8DXyxyw4yfnVjPzBjIQcLt0CCsn0N0ZrQsk= -github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA= -github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64= -github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4= -github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= -github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= -github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.5 h1:Xm0Ao53uqnk9QE/LlYV5DEU09UAgpliA85QoT9LzqPw= -github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU= -github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY= -github.com/go-openapi/strfmt v0.19.3/go.mod h1:0yX7dbo8mKIvc3XSKp7MNfxw4JytCfCD6+bY1AVL9LU= -github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= -github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= -github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= -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-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= -github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= -github.com/go-openapi/validate v0.19.8/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 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/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0/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/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.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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/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.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/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= -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/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= +github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +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.12 h1:xwIyRbJcG9LgcDYys+HHLH9DqqHeQsUpS5CfBUeskbs= +github.com/gonvenience/neat v1.3.12/go.mod h1:8OljAIgPelN0uPPO94VBqxK+Kz98d6ZFwHDg5o/PfkE= +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.1.2 h1:xPKxNwL1HCguwyM+HlP/1CIuc9LRd7k8RodLwe9YTZA= +github.com/gonvenience/wrap v1.1.2/go.mod h1:GiryBSXoI3BAAhbWD1cZVj7RZmtiu0ERi/6R6eJfslI= +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.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.2/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/v35 v35.3.0 h1:fU+WBzuukn0VssbayTT+Zo3/ESKX9JYWjbZTLOTEyho= -github.com/google/go-github/v35 v35.3.0/go.mod h1:yWB7uCcVWaUbUP74Aq3whuMySRMatyRmq5U9FTNlbio= -github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= -github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +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.17.0 h1:5p+zYs/R4VGHkhyvgWurWrpJ2hW4Vv9fQI+GzdcwXLk= +github.com/google/go-containerregistry v0.17.0/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= +github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs= +github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw= +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 v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +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-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= 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.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.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.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -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/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -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.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.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.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +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/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/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -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.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 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-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-version v1.2.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/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru v0.5.4/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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/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/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +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.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +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.6.0 h1:AN+ikld0Fy+qx34YE7655b/bpWuxS6cL9k852pE2GUc= +github.com/homeport/dyff v1.6.0/go.mod h1:FlAOFYzeKvxmU5nTrnG+qrlJVWpsFew7pt8L99p5q8k= +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 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= +github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 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/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/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.28/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/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= -github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= -github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= -github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= -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/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/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/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/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/markbates/pkger v0.17.1/go.mod h1:0JoVlrol20BSywW79rN3kdFFsE5xYM+rSCQDXbLhiuI= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -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.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-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.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/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +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/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 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/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/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/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-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/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-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= -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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 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/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= -github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= -github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= -github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= -github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= -github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -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/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -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.7.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.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -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.12.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= -github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +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/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +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-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= -github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= -github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +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/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= 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/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +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-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 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/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= -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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -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.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= -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.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/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.1.0/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.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= -github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/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.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.2.0/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/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -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/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= -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.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/sergi/go-diff v1.1.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.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +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/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.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 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/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= -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/cast v1.3.0/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.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.1.3 h1:xghbfqPkxzxP3C/f3n5DdpAbdKLj4ZE4BWQI362l53M= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -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.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/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/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/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= -github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +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.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/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -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/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vdemeester/k8s-pkg-credentialprovider v1.18.1-0.20201019120933-f1d16962a4db/go.mod h1:grWy0bkr1XO6hqbaaCKaPXqkBVlMGHYG6PGykktwbJc= -github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= -github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= -github.com/xanzy/go-gitlab v0.51.1 h1:wWKLalwx4omxFoHh3PLs9zDgAD4GXDP/uoxwMRCSiWM= -github.com/xanzy/go-gitlab v0.51.1/go.mod h1:Q+hQhV508bDPoBijv7YjK/Lvlb4PhVhJdKqXVQrUoAE= -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/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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +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/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/xanzy/go-gitlab v0.95.1 h1:rQjcmX5Au2Lz9bc3QLTdtSK5ZHdTXLnmhz3CAB/G5So= +github.com/xanzy/go-gitlab v0.95.1/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.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/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.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -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 v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -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.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -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.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.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -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/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= -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/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.0 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/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-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -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-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/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-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 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.3.1-0.20200828183125-ce943fd02449/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-20181005035420-146acd28ed58/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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.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-20190125091013-d26f9f9a57f3/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-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/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-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-20200520004742-59133d7f0dd7/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-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-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-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -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 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/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 h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= 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-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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/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-20191220142924-d4481acd189f/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-20200116001909-b77594299b42/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-20200501052902-10377860bb8e/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-20200831180312-196b9ba8737a/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-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-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/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-20210426230700-d19ff857e887/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-20210616045830-e2b7044e8c71/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-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo= -golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/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-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.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 h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -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.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -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/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +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.9.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-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-20190125232054-d66bd3c5d5a6/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-20190617190820-da514acc4774/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-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/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-20200103221440-774c71fcf114/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-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/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.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.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 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.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= -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.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -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.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -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-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= -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-20200305110556-506484158171/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-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -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-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= -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.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/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/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= +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/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/evanphx/json-patch.v5 v5.7.0 h1:dGKGylPlZ/jus2g1YqhhyzfH0gPy2R8/MYUpW/OslTY= +gopkg.in/evanphx/json-patch.v5 v5.7.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk= 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/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.7/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/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -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-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -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= -k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY= -k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= -k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= -k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= -k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= -k8s.io/apiextensions-apiserver v0.22.2 h1:zK7qI8Ery7j2CaN23UCFaC1hj7dMiI87n01+nKuewd4= -k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA= -k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig= -k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= -k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= -k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= -k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= -k8s.io/apiserver v0.18.8/go.mod h1:12u5FuGql8Cc497ORNj79rhPdiXQC4bf53X/skR/1YM= -k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= -k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= -k8s.io/cli-runtime v0.21.1 h1:Oj/iZxa7LLXrhzShaLNF4rFJEIEBTDHj0dJw4ra2vX4= -k8s.io/cli-runtime v0.21.1/go.mod h1:TI9Bvl8lQWZB2KqE91QLCp9AZE4l29zNFnj/x4IX4Fw= -k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU= -k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= -k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= -k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= -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.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= -k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= -k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU= -k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= -k8s.io/component-base v0.22.2 h1:vNIvE0AIrLhjX8drH0BgCNJcR4QZxMXcJzBsDplDx9M= -k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllunGug= -k8s.io/component-helpers v0.21.1/go.mod h1:FtC1flbiQlosHQrLrRUulnKxE4ajgWCGy/67fT2GRlQ= -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/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.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -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-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/kubectl v0.21.1 h1:ySEusoeSgSDSiSBncDMsNrthSa3OSlXqT4R2rf1VFTw= -k8s.io/kubectl v0.21.1/go.mod h1:PMYR88MqESuysBM/MX+Vu4JbX/50nY4d4kny+SPEI2U= -k8s.io/legacy-cloud-providers v0.18.8/go.mod h1:tgp4xYf6lvjrWnjQwTOPvWQE9IVqSBGPF4on0IyICQE= -k8s.io/metrics v0.21.1/go.mod h1:pyDVLsLe++FIGDBFU80NcW4xMFsuiVTWL8Zfi7+PpNo= -k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210517184530-5a248b5acedc/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/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.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/cli-utils v0.26.0 h1:N1X1NVN19+daTihVimkSVNMgiVPIHG9OO3SJPh6SBQI= -sigs.k8s.io/cli-utils v0.26.0/go.mod h1:myCFn83XMe7vC1ZX5CEJJIY2cqsl6IxYI727mLW1mfE= -sigs.k8s.io/controller-runtime v0.9.0-beta.5.0.20210524185538-7181f1162e79/go.mod h1:rgf+cBz72pYlKXDRNhI1WFQv/S86EMUV4/ySmsEYgHk= -sigs.k8s.io/controller-runtime v0.10.1/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= -sigs.k8s.io/controller-runtime v0.10.2 h1:jW8qiY+yMnnPx6O9hu63tgcwaKzd1yLYui+mpvClOOc= -sigs.k8s.io/controller-runtime v0.10.2/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= -sigs.k8s.io/kustomize/api v0.8.8/go.mod h1:He1zoK0nk43Pc6NlV085xDXDXTNprtcyKZVm3swsdNY= -sigs.k8s.io/kustomize/api v0.8.10 h1:CqbdK/qT7JE+uVETkrVMk7pQf0fPFXk9+QQ//Q7sAtc= -sigs.k8s.io/kustomize/api v0.8.10/go.mod h1:ImeIkhUU7GIhamOtKPlkllt+fkBKL5f6/4NLhVwkinA= -sigs.k8s.io/kustomize/cmd/config v0.9.10/go.mod h1:Mrby0WnRH7hA6OwOYnYpfpiY0WJIMgYrEDfwOeFdMK0= -sigs.k8s.io/kustomize/kustomize/v4 v4.1.2/go.mod h1:PxBvo4WGYlCLeRPL+ziT64wBXqbgfcalOS/SXa/tcyo= -sigs.k8s.io/kustomize/kyaml v0.10.17/go.mod h1:mlQFagmkm1P+W4lZJbJ/yaxMd8PqMRSC4cPcfUVt5Hg= -sigs.k8s.io/kustomize/kyaml v0.10.20 h1:L9JNKvJfCBpmYFr4tP0igpfj/pXP7nW2aXOWNtF5k1g= -sigs.k8s.io/kustomize/kyaml v0.10.20/go.mod h1:TYWhGwW9vjoRh3rWqBwB/ZOXyEGRVWe7Ggc3+KZIO+c= -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.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/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= -sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +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= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU= +k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/cli-runtime v0.28.4 h1:IW3aqSNFXiGDllJF4KVYM90YX4cXPGxuCxCVqCD8X+Q= +k8s.io/cli-runtime v0.28.4/go.mod h1:MLGRB7LWTIYyYR3d/DOgtUC8ihsAPA3P8K8FDNIqJ0k= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= +k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8 h1:vzKzxN5uyJZLY8HL1/OovW7BJefnsBIWt8T7Gjh2boQ= +k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ= +k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c= +k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= +k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +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.16.0 h1:/zAR4FOQDCkgSDmVzV2uiFbuy9bhu3jEzthrHCuvm1g= +sigs.k8s.io/kustomize/api v0.16.0/go.mod h1:MnFZ7IP2YqVyVwMWoRxPtgl/5hpA+eCCrQR/866cm5c= +sigs.k8s.io/kustomize/kyaml v0.16.0 h1:6J33uKSoATlKZH16unr2XOhDI+otoe2sR3M8PDzW3K0= +sigs.k8s.io/kustomize/kyaml v0.16.0/go.mod h1:xOK/7i+vmE14N2FdFyugIshB8eF6ALpy7jI87Q2nRh4= +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 16d66ed7..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" - "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 - - kubeconfig string - kubecontext string - - 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.kubeconfig, b.kubecontext, kfile); err != nil { - return err - } - } else { - // Apply the CRDs and controllers - if _, err := utils.Apply(ctx, b.kubeconfig, b.kubecontext, 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.kubeconfig, b.kubecontext, 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.kubeconfig, b.kubecontext) - 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 b70cf8c3..00000000 --- a/internal/bootstrap/git/gogit/gogit_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// +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 new file mode 100644 index 00000000..201cc83b --- /dev/null +++ b/internal/build/build.go @@ -0,0 +1,577 @@ +/* +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 build + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + "sync" + "time" + + "github.com/fluxcd/pkg/ssa" + "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" + "sigs.k8s.io/kustomize/kyaml/filesys" + + "github.com/fluxcd/flux2/v2/internal/utils" +) + +const ( + 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 + +// Builder builds yaml manifests +// 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 + 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 +} + +// 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 + return nil + } +} + +func WithProgressBar() BuilderOptionFunc { + return func(b *Builder) error { + // Add a spiner + 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 + } +} + +// 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 + } +} + +// 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{ + name: name, + resourcesPath: resources, + } + + for _, opt := range opts { + if err := opt(b); err != nil { + return nil, err + } + } + + if b.timeout == 0 { + 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, + } + err := b.client.Get(ctx, namespacedName, liveKus) + if err != nil { + return nil, err + } + + 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() ([]*unstructured.Unstructured, error) { + m, err := b.build() + if err != nil { + return nil, err + } + + resources, err := m.AsYaml() + if err != nil { + return nil, fmt.Errorf("kustomize build failed: %w", err) + } + + objects, err := ssa.ReadObjects(bytes.NewReader(resources)) + if err != nil { + return nil, fmt.Errorf("kustomize build failed: %w", err) + } + + if m := b.kustomization.Spec.CommonMetadata; m != nil { + ssa.SetCommonMetadata(objects, m.Labels, m.Annotations) + } + + return objects, nil +} + +func (b *Builder) build() (m resmap.ResMap, err error) { + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + + // Get the kustomization object + 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 + } + + // store the kustomization object + b.kustomization = k + + // generate kustomization.yaml if needed + action, er := b.generate(*k, b.resourcesPath) + if er != nil { + errf := kustomize.CleanDirectory(b.resourcesPath, action) + err = fmt.Errorf("failed to generate kustomization.yaml: %w", fmt.Errorf("%v %v", er, errf)) + return + } + + b.action = action + + defer func() { + errf := b.Cancel() + if err == nil { + err = errf + } + }() + + // build the kustomization + m, err = b.do(ctx, *k, b.resourcesPath) + if err != nil { + return + } + + for _, res := range m.Resources() { + // set owner labels + err = b.setOwnerLabels(res) + if err != nil { + return + } + + // make sure secrets are masked + err = maskSopsData(res) + if err != nil { + return + } + } + + return + +} + +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 + } + + // a scanner will be used down the line to parse the list + // so we have to make sure to unclude 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() + + return gen.WriteFile(dirPath, kustomize.WithSaveOriginalKustomization()) +} + +func (b *Builder) do(ctx context.Context, kustomization kustomizev1.Kustomization, dirPath string) (resmap.ResMap, error) { + fs := filesys.MakeFsOnDisk() + + // acuire the lock + b.mu.Lock() + defer b.mu.Unlock() + + m, err := kustomize.Build(fs, dirPath) + if err != nil { + return nil, fmt.Errorf("kustomize build failed: %w", err) + } + + for _, res := range m.Resources() { + // run variable substitutions + if kustomization.Spec.PostBuild != nil { + data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&kustomization) + if err != nil { + return nil, err + } + outRes, err := kustomize.SubstituteVariables(ctx, b.client, unstructured.Unstructured{Object: data}, res, b.dryRun) + if err != nil { + return nil, fmt.Errorf("var substitution failed for '%s': %w", res.GetName(), err) + } + + if outRes != nil { + _, err = m.Replace(res) + if err != nil { + return nil, err + } + } + } + } + + return m, nil +} + +func (b *Builder) setOwnerLabels(res *resource.Resource) error { + labels := res.GetLabels() + + labels[controllerGroup+"/name"] = b.kustomization.GetName() + labels[controllerGroup+"/namespace"] = b.kustomization.GetNamespace() + + err := res.SetLabels(labels) + if err != nil { + return err + } + + return nil +} + +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() + 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) + } + + 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 +func (b *Builder) Cancel() error { + // acuire the lock + b.mu.Lock() + defer b.mu.Unlock() + + err := kustomize.CleanDirectory(b.resourcesPath, b.action) + if err != nil { + return err + } + + return nil +} diff --git a/internal/build/build_test.go b/internal/build/build_test.go new file mode 100644 index 00000000..be62abc5 --- /dev/null +++ b/internal/build/build_test.go @@ -0,0 +1,363 @@ +/* +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 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" +) + +func TestTrimSopsData(t *testing.T) { + testCases := []struct { + name string + yamlStr string + expected string + }{ + { + name: "secret with sops token", + yamlStr: `apiVersion: v1 +kind: Secret +metadata: + name: my-secret +type: Opaque +data: + token: | + ewoJImRhdGEiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpvQmU1UGxQbWZRQ1VVYzRzcUtJbW + p3PT0saXY6TUxMRVcxNVFDOWtSZFZWYWdKbnpMQ1NrMHhaR1dJcEFlVGZIenl4VDEwZz0s + dGFnOkszR2tCQ0dTK3V0NFRwazZuZGIwQ0E9PSx0eXBlOnN0cl0iLAoJInNvcHMiOiB7Cg + kJImttcyI6IG51bGwsCgkJImdjcF9rbXMiOiBudWxsLAoJCSJhenVyZV9rdiI6IG51bGws + CgkJImhjX3ZhdWx0IjogbnVsbCwKCQkiYWdlIjogWwoJCQl7CgkJCQkicmVjaXBpZW50Ij + ogImFnZTEwbGEyZ2Uwd3R2eDNxcjdkYXRxZjdyczR5bmd4c3pkYWw5MjdmczlydWthbXI4 + dTJwc2hzdnR6N2NlIiwKCQkJCSJlbmMiOiAiLS0tLS1CRUdJTiBBR0UgRU5DUllQVEVEIE + ZJTEUtLS0tLVxuWVdkbExXVnVZM0o1Y0hScGIyNHViM0puTDNZeENpMCtJRmd5TlRVeE9T + QTFMMlJwWkhScksxRlNWbVlyZDFWYVxuWTBoeFdGUXpTREJzVDFrM1dqTnRZbVUxUW1saW + FESnljWGxOQ25GMVlqZE5PVGhWYlZOdk1HOXJOUzlaVVhad1xuTW5WMGJuUlVNR050ZWpG + UGJ6TTRVMlV6V2tzemVWa0tMUzB0SUdKNlVHaHhNVVYzWW1WSlRIbEpTVUpwUlZSWlxuVm + pkMFJWUmFkVTh3ZWt4WFRISXJZVXBsWWtOMmFFRUswSS9NQ0V0WFJrK2IvTjJHMUpGM3ZI + UVQyNGRTaFdZRFxudytKSVVTQTNhTGYyc3YwenIyTWRVRWRWV0JKb004blQ0RDR4VmJCT1 + JEKzY2OVcrOW5EZVN3PT1cbi0tLS0tRU5EIEFHRSBFTkNSWVBURUQgRklMRS0tLS0tXG4i + CgkJCX0KCQldLAoJCSJsYXN0bW9kaWZpZWQiOiAiMjAyMS0xMS0yNlQxNjozNDo1MVoiLA + oJCSJtYWMiOiAiRU5DW0FFUzI1Nl9HQ00sZGF0YTpDT0d6ZjVZQ0hOTlA2ejRKYUVLcmpO + M004ZjUrUTF1S1VLVE1Id2ozODgvSUNtTHlpMnNTclRtajdQUCtYN005alRWd2E4d1ZnWV + RwTkxpVkp4K0xjeHF2SVhNMFR5bysvQ3UxenJmYW85OGFpQUNQOCtUU0VEaUZRTnRFdXMy + M0grZC9YMWhxTXdSSERJM2tRKzZzY2dFR25xWTU3cjNSRFNBM0U4RWhIcjQ9LGl2Okx4aX + RWSVltOHNyWlZxRnVlSmg5bG9DbEE0NFkyWjNYQVZZbXhlc01tT2c9LHRhZzpZOHFGRDhV + R2xEZndOU3Y3eGxjbjZBPT0sdHlwZTpzdHJdIiwKCQkicGdwIjogbnVsbCwKCQkidW5lbm + NyeXB0ZWRfc3VmZml4IjogIl91bmVuY3J5cHRlZCIsCgkJInZlcnNpb24iOiAiMy43LjEi + Cgl9Cn0= +`, + expected: `apiVersion: v1 +data: + token: KipTT1BTKio= +kind: Secret +metadata: + name: my-secret +type: Opaque +`, + }, + { + name: "secret with basic auth", + yamlStr: `apiVersion: v1 +data: + password: cGFzc3dvcmQK + username: YWRtaW4K +kind: Secret +metadata: + name: secret-basic-auth +type: kubernetes.io/basic-auth +`, + expected: `apiVersion: v1 +data: + password: cGFzc3dvcmQK + username: YWRtaW4K +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 +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + r, err := yaml.Parse(tc.yamlStr) + if err != nil { + t.Fatalf("unable to parse yaml: %v", err) + } + + resource := &resource.Resource{RNode: *r} + err = maskSopsData(resource) + if err != nil { + t.Fatalf("unable to trim sops data: %v", err) + } + + sYaml, err := resource.AsYAML() + if err != nil { + t.Fatalf("unable to convert sanitized resources to yaml: %v", err) + } + if diff := cmp.Diff(string(sYaml), tc.expected); diff != "" { + t.Errorf("unexpected sanitized resources: (-got +want)%v", diff) + } + }) + } +} + +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 new file mode 100644 index 00000000..b84037c2 --- /dev/null +++ b/internal/build/diff.go @@ -0,0 +1,338 @@ +/* +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 build + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/gonvenience/bunt" + "github.com/gonvenience/ytbx" + "github.com/google/go-cmp/cmp" + "github.com/homeport/dyff/pkg/dyff" + "github.com/lucasb-eyer/go-colorful" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "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" + + "github.com/fluxcd/flux2/v2/pkg/printers" +) + +func (b *Builder) Manager() (*ssa.ResourceManager, error) { + statusPoller := polling.NewStatusPoller(b.client, b.restMapper, polling.Options{}) + owner := ssa.Owner{ + Field: controllerName, + Group: controllerGroup, + } + + return ssa.NewResourceManager(b.client, statusPoller, owner), nil +} + +func (b *Builder) Diff() (string, bool, error) { + output := strings.Builder{} + createdOrDrifted := false + objects, err := b.Build() + if err != nil { + return "", createdOrDrifted, err + } + + err = ssa.SetNativeKindsDefaults(objects) + if err != nil { + return "", createdOrDrifted, err + } + + resourceManager, err := b.Manager() + if err != nil { + return "", createdOrDrifted, err + } + + ctx, cancel := context.WithTimeout(context.Background(), b.timeout) + defer cancel() + + 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 { + diffOptions := ssa.DiffOptions{ + Exclusions: map[string]string{ + "kustomize.toolkit.fluxcd.io/reconcile": "disabled", + }, + } + change, liveObject, mergedObject, err := resourceManager.Diff(ctx, obj, diffOptions) + if err != nil { + // 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 == ssa.ConfiguredAction { + diffSopsSecret(obj, liveObject, mergedObject, change) + } + + if change.Action == ssa.CreatedAction { + output.WriteString(writeString(fmt.Sprintf("► %s created\n", change.Subject), bunt.Green)) + createdOrDrifted = true + } + + 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 "", createdOrDrifted, err + } + + err = diff(liveFile, mergedFile, &output) + if err != nil { + cleanupDir(tmpDir) + return "", createdOrDrifted, err + } + + cleanupDir(tmpDir) + createdOrDrifted = true + } + + addObjectsToInventory(newInventory, change) + } + + 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 { + staleObjects, err := diffInventory(oldStatus.Inventory, newInventory) + if err != nil { + return "", createdOrDrifted, err + } + if len(staleObjects) > 0 { + createdOrDrifted = true + } + for _, object := range staleObjects { + output.WriteString(writeString(fmt.Sprintf("► %s deleted\n", ssa.FmtUnstructured(object)), bunt.OrangeRed)) + } + } + } + + 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) { + tmpDir, err := os.MkdirTemp("", "") + if err != nil { + return "", "", "", err + } + + liveYAML, _ := yaml.Marshal(liveObject) + liveFile := filepath.Join(tmpDir, "live.yaml") + 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, 0o600); err != nil { + return "", "", "", err + } + + return liveFile, mergedFile, tmpDir, nil +} + +func writeString(t string, color colorful.Color) string { + return bunt.Style( + t, + bunt.EachLine(), + bunt.Foreground(color), + ) +} + +func cleanupDir(dir string) error { + return os.RemoveAll(dir) +} + +func diff(liveFile, mergedFile string, output io.Writer) error { + from, to, err := ytbx.LoadFiles(liveFile, mergedFile) + if err != nil { + return fmt.Errorf("failed to load input files: %w", err) + } + + report, err := dyff.CompareInputFiles(from, to, + dyff.IgnoreOrderChanges(false), + dyff.KubernetesEntityDetection(true), + ) + if err != nil { + return fmt.Errorf("failed to compare input files: %w", err) + } + + printer := printers.NewDyffPrinter() + + printer.Print(output, report) + + return nil +} + +func diffSopsSecret(obj, liveObject, mergedObject *unstructured.Unstructured, change *ssa.ChangeSetEntry) { + // 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 = ssa.UnchangedAction + liveKeys, mergedKeys := sopsComparableByKeys(liveObject), sopsComparableByKeys(mergedObject) + if cmp.Diff(liveKeys, mergedKeys) != "" { + change.Action = ssa.ConfiguredAction + } + } + } + } +} + +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 +} + +// diffInventory returns the slice of objects that do not exist in the target inventory. +func diffInventory(inv *kustomizev1.ResourceInventory, target *kustomizev1.ResourceInventory) ([]*unstructured.Unstructured, error) { + versionOf := func(i *kustomizev1.ResourceInventory, objMetadata object.ObjMetadata) string { + for _, entry := range i.Entries { + if entry.ID == objMetadata.String() { + return entry.Version + } + } + return "" + } + + objects := make([]*unstructured.Unstructured, 0) + aList, err := listMetaInInventory(inv) + if err != nil { + return nil, err + } + + bList, err := listMetaInInventory(target) + if err != nil { + return nil, err + } + + list := aList.Diff(bList) + if len(list) == 0 { + return objects, nil + } + + for _, metadata := range list { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(schema.GroupVersionKind{ + Group: metadata.GroupKind.Group, + Kind: metadata.GroupKind.Kind, + Version: versionOf(inv, metadata), + }) + u.SetName(metadata.Name) + u.SetNamespace(metadata.Namespace) + objects = append(objects, u) + } + + sort.Sort(ssa.SortableUnstructureds(objects)) + return objects, nil +} + +// listMetaInInventory returns the inventory entries as object.ObjMetadata objects. +func listMetaInInventory(inv *kustomizev1.ResourceInventory) (object.ObjMetadataSet, error) { + var metas []object.ObjMetadata + for _, e := range inv.Entries { + m, err := object.ParseObjMetadata(e.ID) + if err != nil { + return metas, err + } + metas = append(metas, m) + } + + return metas, nil +} + +func newInventory() *kustomizev1.ResourceInventory { + return &kustomizev1.ResourceInventory{ + Entries: []kustomizev1.ResourceRef{}, + } +} + +// addObjectsToInventory extracts the metadata from the given objects and adds it to the inventory. +func addObjectsToInventory(inv *kustomizev1.ResourceInventory, entry *ssa.ChangeSetEntry) error { + if entry == nil { + return nil + } + + inv.Entries = append(inv.Entries, kustomizev1.ResourceRef{ + ID: entry.ObjMetadata.String(), + Version: entry.GroupVersion, + }) + + return nil +} 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..0c1e4fa6 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/v2beta2" - "github.com/fluxcd/flux2/internal/utils" + "github.com/fluxcd/flux2/v2/internal/utils" ) var supportedCRDsPolicies = []string{ diff --git a/internal/flags/crds_test.go b/internal/flags/crds_test.go index 9d20b095..e51362c1 100644 --- a/internal/flags/crds_test.go +++ b/internal/flags/crds_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* 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/decryption_provider_test.go b/internal/flags/decryption_provider_test.go index 86a0deba..2995a02d 100644 --- a/internal/flags/decryption_provider_test.go +++ b/internal/flags/decryption_provider_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* diff --git a/internal/flags/ecdsa_curve_test.go b/internal/flags/ecdsa_curve_test.go index e58c8b8b..e42b98fd 100644 --- a/internal/flags/ecdsa_curve_test.go +++ b/internal/flags/ecdsa_curve_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* 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/helm_chart_source.go b/internal/flags/helm_chart_source.go index c085d21f..1ec99408 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{sourcev1b2.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 62b34ea6..4590c3e9 100644 --- a/internal/flags/helm_chart_source_test.go +++ b/internal/flags/helm_chart_source_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* @@ -22,7 +23,7 @@ import ( "fmt" "testing" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" ) 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 8e392049..00000000 --- a/internal/flags/helm_release_values_test.go +++ /dev/null @@ -1,49 +0,0 @@ -// +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 db5062d9..68b81f54 100644 --- a/internal/flags/kustomization_source_test.go +++ b/internal/flags/kustomization_source_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* @@ -22,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/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/log_level_test.go b/internal/flags/log_level_test.go index a8ef1cae..8bb2e541 100644 --- a/internal/flags/log_level_test.go +++ b/internal/flags/log_level_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* diff --git a/internal/flags/public_key_algorithm_test.go b/internal/flags/public_key_algorithm_test.go index e2b69230..69b4bf46 100644 --- a/internal/flags/public_key_algorithm_test.go +++ b/internal/flags/public_key_algorithm_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* 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 e0b15e7c..7199be73 100644 --- a/internal/flags/rsa_key_bits_test.go +++ b/internal/flags/rsa_key_bits_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* @@ -31,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 8651436f..a9d5f3fe 100644 --- a/internal/flags/safe_relative_path_test.go +++ b/internal/flags/safe_relative_path_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* @@ -36,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 5ec82e35..2f3192fd 100644 --- a/internal/flags/source_bucket_provider_test.go +++ b/internal/flags/source_bucket_provider_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* @@ -21,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..fad06426 100644 --- a/internal/tree/tree.go +++ b/internal/tree/tree.go @@ -22,8 +22,8 @@ package tree import ( "strings" + "github.com/fluxcd/cli-utils/pkg/object" "github.com/fluxcd/pkg/ssa" - "sigs.k8s.io/cli-utils/pkg/object" ) const ( diff --git a/internal/utils/apply.go b/internal/utils/apply.go index cf87123d..94767623 100644 --- a/internal/utils/apply.go +++ b/internal/utils/apply.go @@ -23,67 +23,88 @@ import ( "fmt" "os" "path/filepath" + "time" - "github.com/fluxcd/pkg/ssa" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "sigs.k8s.io/cli-utils/pkg/kstatus/polling" + "k8s.io/cli-runtime/pkg/genericclioptions" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "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" + + "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, kubeConfigPath string, kubeContext string, manifestPath string) (string, error) { - cfg, err := KubeConfig(kubeConfigPath, kubeContext) +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 := apiutil.NewDynamicRESTMapper(cfg) - 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) - 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 ssa.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 } @@ -98,3 +119,52 @@ func readObjects(manifestPath string) ([]*unstructured.Unstructured, error) { return ssa.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 963b88e2..26d77ef8 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" @@ -37,26 +36,26 @@ import ( apiruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" sigyaml "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" "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/v2beta2" 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" + 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,29 +106,15 @@ func ExecKubectlCommand(ctx context.Context, mode ExecMode, kubeConfigPath strin return "", nil } -func ClientConfig(kubeConfigPath string, kubeContext string) clientcmd.ClientConfig { - configFiles := SplitKubeConfigPath(kubeConfigPath) - configOverrides := clientcmd.ConfigOverrides{} - - if len(kubeContext) > 0 { - configOverrides.CurrentContext = kubeContext - } - - return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - &clientcmd.ClientConfigLoadingRules{Precedence: configFiles}, - &configOverrides, - ) -} - -func KubeConfig(kubeConfigPath string, kubeContext string) (*rest.Config, error) { - cfg, err := ClientConfig(kubeConfigPath, kubeContext).ClientConfig() +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 } @@ -143,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(kubeConfigPath string, kubeContext string) (client.WithWatch, error) { - cfg, err := KubeConfig(kubeConfigPath, kubeContext) +func KubeClient(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) (client.WithWatch, error) { + cfg, err := rcg.ToRESTConfig() if err != nil { - return nil, fmt.Errorf("kubernetes client initialization failed: %w", err) + return nil, err } + cfg.QPS = opts.QPS + cfg.Burst = opts.Burst + scheme := NewScheme() kubeClient, err := client.NewWithWatch(cfg, client.Options{ Scheme: scheme, @@ -241,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 := "" @@ -253,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, }) @@ -261,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 2e5d6542..2cec902c 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* @@ -19,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) { @@ -87,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"}, @@ -134,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 27686332..099cd240 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.14.0/helm-controller.crds.yaml -- https://github.com/fluxcd/helm-controller/releases/download/v0.14.0/helm-controller.deployment.yaml +- https://github.com/fluxcd/helm-controller/releases/download/v0.37.2/helm-controller.crds.yaml +- https://github.com/fluxcd/helm-controller/releases/download/v0.37.2/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..79773699 100644 --- a/manifests/bases/helm-controller/patch.yaml +++ b/manifests/bases/helm-controller/patch.yaml @@ -1,6 +1,9 @@ - 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 diff --git a/manifests/bases/image-automation-controller/kustomization.yaml b/manifests/bases/image-automation-controller/kustomization.yaml index 79942e4a..a89e9847 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.18.0/image-automation-controller.crds.yaml -- https://github.com/fluxcd/image-automation-controller/releases/download/v0.18.0/image-automation-controller.deployment.yaml +- https://github.com/fluxcd/image-automation-controller/releases/download/v0.37.0/image-automation-controller.crds.yaml +- https://github.com/fluxcd/image-automation-controller/releases/download/v0.37.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..63d261ed 100644 --- a/manifests/bases/image-automation-controller/patch.yaml +++ b/manifests/bases/image-automation-controller/patch.yaml @@ -1,6 +1,6 @@ - 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 diff --git a/manifests/bases/image-reflector-controller/kustomization.yaml b/manifests/bases/image-reflector-controller/kustomization.yaml index 1d3e5eb3..f8aef1e6 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.14.0/image-reflector-controller.crds.yaml -- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.14.0/image-reflector-controller.deployment.yaml +- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.31.1/image-reflector-controller.crds.yaml +- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.31.1/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..1db56bf2 100644 --- a/manifests/bases/image-reflector-controller/patch.yaml +++ b/manifests/bases/image-reflector-controller/patch.yaml @@ -1,6 +1,6 @@ - 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 diff --git a/manifests/bases/kustomize-controller/kustomization.yaml b/manifests/bases/kustomize-controller/kustomization.yaml index f2b6d496..c25d9209 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.18.1/kustomize-controller.crds.yaml -- https://github.com/fluxcd/kustomize-controller/releases/download/v0.18.1/kustomize-controller.deployment.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.2.1/kustomize-controller.crds.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.2.1/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..6ce23139 100644 --- a/manifests/bases/kustomize-controller/patch.yaml +++ b/manifests/bases/kustomize-controller/patch.yaml @@ -1,6 +1,9 @@ - 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 diff --git a/manifests/bases/notification-controller/kustomization.yaml b/manifests/bases/notification-controller/kustomization.yaml index 38dbf1a0..af3abbc1 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.19.0/notification-controller.crds.yaml -- https://github.com/fluxcd/notification-controller/releases/download/v0.19.0/notification-controller.deployment.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.2.3/notification-controller.crds.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.2.3/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/source-controller/kustomization.yaml b/manifests/bases/source-controller/kustomization.yaml index 6c4e809c..538fc18b 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.19.0/source-controller.crds.yaml -- https://github.com/fluxcd/source-controller/releases/download/v0.19.0/source-controller.deployment.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.2.3/source-controller.crds.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.2.3/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..e55604ee 100644 --- a/manifests/bases/source-controller/patch.yaml +++ b/manifests/bases/source-controller/patch.yaml @@ -1,6 +1,9 @@ - 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 diff --git a/manifests/crds/kustomization.yaml b/manifests/crds/kustomization.yaml index 30cbe3ce..97e8cbcc 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.19.0/source-controller.crds.yaml -- https://github.com/fluxcd/kustomize-controller/releases/download/v0.18.1/kustomize-controller.crds.yaml -- https://github.com/fluxcd/helm-controller/releases/download/v0.14.0/helm-controller.crds.yaml -- https://github.com/fluxcd/notification-controller/releases/download/v0.19.0/notification-controller.crds.yaml -- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.14.0/image-reflector-controller.crds.yaml -- https://github.com/fluxcd/image-automation-controller/releases/download/v0.18.0/image-automation-controller.crds.yaml +- https://github.com/fluxcd/source-controller/releases/download/v1.2.3/source-controller.crds.yaml +- https://github.com/fluxcd/kustomize-controller/releases/download/v1.2.1/kustomize-controller.crds.yaml +- https://github.com/fluxcd/helm-controller/releases/download/v0.37.2/helm-controller.crds.yaml +- https://github.com/fluxcd/notification-controller/releases/download/v1.2.3/notification-controller.crds.yaml +- https://github.com/fluxcd/image-reflector-controller/releases/download/v0.31.1/image-reflector-controller.crds.yaml +- https://github.com/fluxcd/image-automation-controller/releases/download/v0.37.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/install/namespace.yaml b/manifests/install/namespace.yaml index c00a4321..c2b5961d 100644 --- a/manifests/install/namespace.yaml +++ b/manifests/install/namespace.yaml @@ -2,3 +2,6 @@ apiVersion: v1 kind: Namespace metadata: name: flux-system + labels: + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest 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 2aedd589..00000000 --- a/manifests/monitoring/grafana/dashboards/control-plane.json +++ /dev/null @@ -1,1430 +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": 1596541513301, - "links": [], - "panels": [ - { - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "custom": {}, - "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 - }, - "textMode": "value" - }, - "pluginVersion": "7.1.1", - "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": { - "custom": {}, - "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 - }, - "textMode": "auto" - }, - "pluginVersion": "7.1.1", - "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": { - "custom": {}, - "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 - }, - "pluginVersion": "7.1.1", - "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": { - "custom": { - "align": null - }, - "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 - }, - "textMode": "auto" - }, - "pluginVersion": "7.1.1", - "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" - }, - { - "collapsed": false, - "datasource": "${DS_PROMETHEUS}", - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 5 - }, - "id": 15, - "panels": [], - "title": "Resource Usage", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "description": "", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 6 - }, - "hiddenSeries": false, - "id": 8, - "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", - "percentage": false, - "pluginVersion": "7.1.1", - "pointradius": 2, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "histogram_quantile(0.50, sum(rate(rest_client_request_latency_seconds_bucket{namespace=\"$namespace\"}[5m])) by (le))", - "interval": "", - "legendFormat": "P50", - "refId": "A" - }, - { - "expr": "histogram_quantile(0.90, sum(rate(rest_client_request_latency_seconds_bucket{namespace=\"$namespace\"}[5m])) by (le))", - "hide": true, - "interval": "", - "legendFormat": "P90", - "refId": "B" - }, - { - "expr": "histogram_quantile(0.99, sum(rate(rest_client_request_latency_seconds_bucket{namespace=\"$namespace\"}[5m])) by (le))", - "hide": false, - "interval": "", - "legendFormat": "P99", - "refId": "C" - } - ], - "thresholds": [], - "timeFrom": null, - "timeRegions": [], - "timeShift": null, - "title": "Kubernetes API Requests 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": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "decimals": null, - "description": "", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 6 - }, - "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", - "percentage": false, - "pluginVersion": "7.1.1", - "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 - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_PROMETHEUS}", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "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", - "percentage": false, - "pluginVersion": "7.1.1", - "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}", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "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", - "percentage": false, - "pluginVersion": "7.1.1", - "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}", - "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}", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "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", - "percentage": false, - "pluginVersion": "7.1.1", - "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": "", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "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": "7.1.1", - "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": "", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "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": "7.1.1", - "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}", - "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}", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "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": "7.1.1", - "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": "", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "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": "7.1.1", - "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": "", - "fieldConfig": { - "defaults": { - "custom": {} - }, - "overrides": [] - }, - "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": "7.1.1", - "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": 26, - "style": "light", - "tags": [ - "flux" - ], - "templating": { - "list": [ - { - "current": { - "selected": false, - "text": "Prometheus", - "value": "Prometheus" - }, - "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", - "hide": 0, - "includeAll": false, - "label": null, - "multi": false, - "name": "namespace", - "options": [], - "query": "workqueue_work_duration_seconds_count", - "refresh": 2, - "regex": "/.*namespace=\"([^\"]*).*/", - "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 Control Plane", - "uid": "flux-control-plane", - "version": 1 -} 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 bf233078..00000000 --- a/manifests/monitoring/kube-prometheus-stack/release.yaml +++ /dev/null @@ -1,29 +0,0 @@ -apiVersion: helm.toolkit.fluxcd.io/v2beta1 -kind: HelmRelease -metadata: - name: kube-prometheus-stack -spec: - interval: 5m - chart: - spec: - 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/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/rbac/controller.yaml b/manifests/rbac/controller.yaml index 7fb181c5..c444c58e 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: 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 31babc7c..c8f242c8 100755 --- a/manifests/scripts/bundle.sh +++ b/manifests/scripts/bundle.sh @@ -16,8 +16,8 @@ set -e -IN_PATH=${1:-"$(git rev-parse --show-toplevel)/manifests"} -OUT_PATH=${2:-"$(git rev-parse --show-toplevel)/cmd/flux/manifests"} +IN_PATH=${1:-"$(realpath $(dirname "${BASH_SOURCE[0]}")/../..)/manifests"} +OUT_PATH=${2:-"$(realpath $(dirname "${BASH_SOURCE[0]}")/../..)/cmd/flux/manifests"} TAR=${3} info() { diff --git a/internal/bootstrap/bootstrap.go b/pkg/bootstrap/bootstrap.go similarity index 60% rename from internal/bootstrap/bootstrap.go rename to pkg/bootstrap/bootstrap.go index d3b79ed6..10fda941 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 @@ -171,34 +194,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 +243,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, sourcev1b2.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..ceb503bc --- /dev/null +++ b/pkg/bootstrap/bootstrap_plain_git.go @@ -0,0 +1,541 @@ +/* +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") + } + + 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 8f475b99..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") @@ -200,11 +213,33 @@ func (b *GitProviderBootstrapper) ReconcileSyncConfig(ctx context.Context, optio if err != nil { return err } + options.URL = syncURL } + 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. @@ -258,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. @@ -273,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) @@ -282,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 @@ -296,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) @@ -358,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) @@ -414,14 +474,11 @@ func (b *GitProviderBootstrapper) getOrganization(ctx context.Context, subOrgs [ func (b *GitProviderBootstrapper) getCloneURL(repository gitprovider.UserRepository, transport gitprovider.TransportType) (string, error) { var url string if cloner, ok := repository.(gitprovider.CloneableURL); ok { - return cloner.GetCloneURL("", transport), nil + url = cloner.GetCloneURL("", transport) + } else { + url = repository.Repository().GetCloneURL(transport) } - url = repository.Repository().GetCloneURL(transport) - // TODO(hidde): https://github.com/fluxcd/go-git-providers/issues/55 - if strings.HasPrefix(url, "https://https://") { - url = strings.TrimPrefix(url, "https://") - } var err error if transport == gitprovider.TransportTypeSSH && b.sshHostname != "" { if url, err = setHostname(url, b.sshHostname); err != nil { @@ -560,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 62% rename from internal/bootstrap/options.go rename to pkg/bootstrap/options.go index 2deb67db..27830f4a 100644 --- a/internal/bootstrap/options.go +++ b/pkg/bootstrap/options.go @@ -17,8 +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 { @@ -40,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) } @@ -90,21 +84,21 @@ func (o commitMessageAppendixOption) applyGitProvider(b *GitProviderBootstrapper o.applyGit(b.PlainGitBootstrapper) } -func WithKubeconfig(kubeconfig, kubecontext string) Option { +func WithKubeconfig(rcg genericclioptions.RESTClientGetter, opts *runclient.Options) Option { return kubeconfigOption{ - kubeconfig: kubeconfig, - kubecontext: kubecontext, + rcg: rcg, + opts: opts, } } type kubeconfigOption struct { - kubeconfig string - kubecontext string + rcg genericclioptions.RESTClientGetter + opts *runclient.Options } func (o kubeconfigOption) applyGit(b *PlainGitBootstrapper) { - b.kubeconfig = o.kubeconfig - b.kubecontext = o.kubecontext + b.restClientGetter = o.rcg + b.restClientOptions = o.opts } func (o kubeconfigOption) applyGitProvider(b *GitProviderBootstrapper) { @@ -127,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 } @@ -150,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/templates.go b/pkg/manifestgen/install/templates.go index 8cdce319..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: @@ -165,6 +163,9 @@ apiVersion: v1 kind: Namespace metadata: name: {{.Namespace}} + labels: + pod-security.kubernetes.io/warn: restricted + pod-security.kubernetes.io/warn-version: latest ` func execTemplate(obj interface{}, tmpl, filename string) error { 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..71b5f8bf 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,53 @@ 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" + + // 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 + + // 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 } func MakeDefaultOptions() Options { @@ -63,12 +88,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..f468fa72 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) @@ -128,29 +184,6 @@ 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 err != nil { - return nil, err - } - - return &ssh.KeyPair{ - PublicKey: cryptssh.MarshalAuthorizedKey(ppk.PublicKey()), - PrivateKey: b, - }, nil -} - func generateKeyPair(options Options) (*ssh.KeyPair, error) { var keyGen ssh.KeyPairGenerator switch options.PrivateKeyAlgorithm { @@ -170,14 +203,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 +222,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 b7e4e2c1..8eb619d4 100644 --- a/pkg/manifestgen/sourcesecret/sourcesecret_test.go +++ b/pkg/manifestgen/sourcesecret/sourcesecret_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* @@ -47,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 @@ -66,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 6dba4e35..5c22e26d 100644 --- a/pkg/manifestgen/sync/sync_test.go +++ b/pkg/manifestgen/sync/sync_test.go @@ -1,3 +1,4 @@ +//go:build !e2e // +build !e2e /* @@ -23,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 b2894052..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), - 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..220666ff --- /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/v2beta2" + autov1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + imagev1 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notificationv1 "github.com/fluxcd/notification-controller/api/v1" + notificationv1b3 "github.com/fluxcd/notification-controller/api/v1beta3" + 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 sourcev1b2.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 sourcev1b2.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 new file mode 100644 index 00000000..9f5f8cb7 --- /dev/null +++ b/rfcs/0001-authorization/README.md @@ -0,0 +1,166 @@ +# RFC-0001 Memorandum on Flux Authorization + +## Summary + +This RFC describes in detail, for [Flux version 0.24][] (Nov 2021), how Flux determines which +operations are allowed to proceed, and how this interacts with Kubernetes' access control. + +## Motivation + +To this point, the Flux project has provided [examples of how to make a multi-tenant +system](https://github.com/fluxcd/flux2-multi-tenancy/tree/v0.1.0), but not explained exactly how +they relate to Flux's authorization model; nor has the authorization model itself been +documented. Further work on support for multi-tenancy, among other things, requires a full account +of Flux's authorization model as a baseline. + +### Goals + +- Give a comprehensive account of Flux's authorization model + +### Non-Goals + +- Justify the model as it stands; this RFC simply records the state as at v0.24. + +## Flux's authorization model + +The Flux controllers undertake operations as specified by custom resources of the kinds defined in +the [Flux API][]. Most of the operations are through the Kubernetes API. Authorization for +operations on external systems is not accounted for here. + +Flux controllers defer to [Kubernetes' native RBAC][k8s-rbac] and [namespace isolation][k8s-ns] to +determine which operations are authorized, when processing the custom resources in the Flux API. + +In general, **Kubernetes API operations are constrained by the service account under which each +controller pod runs**. In the [default deployment of Flux][flux-rbac] each controller has its own +service account; and, the service accounts for the Kustomize controller and Helm controller have the +[`cluster-admin` cluster role][k8s-cluster-admin] bound to it. + +Both the Kustomize controller and the Helm controller create, update and delete arbitrary sets of +configuration that they take as user input. For example, a Kustomization object that references a +GitRepository is processed by taking whatever is in the specified Git repository and applying it to +the cluster. This is informally called "syncing", and these user-supplied configurations will be +called "sync configurations" in the following. + +There are five types of access that have a distinct treatment with respect to RBAC and namespace +isolation: + + - reading and writing the Flux API object to be processed + - accessing dependencies of a Flux API object; for example, a secret that holds a decryption key + - accessing Flux API objects related to the object being processed; for example, a GitRepository + referenced by a Kustomization + - creating, updating and deleting Flux API objects as part of processing; for example, each + `HelmRelease` object contains a template for a Helm chart spec, which the Helm controller uses to + create a `HelmChart` object + - creating, updating, deleting, and health-checking of arbitrary objects as specified by _sync + configurations_ (as mentioned above). + +This table summarises how these operations are subject to RBAC and namespace isolation. + +| Type of operation | Accessed via | Namespace isolation | +|------------------------------------------------|----------------------------|------------------------------| +| Reading and writing the object to be processed | Controller service account | N/A | +| Dependencies of object to be processed | Controller service account | Same namespace only | +| Access to related Flux API objects | Controller service account | Some cross-namespace refs[1] | +| CRUD of Flux API objects | Controller service account | Created in same namespace | +| CRUD and healthcheck of sync configurations | Impersonation[2] | As directed by spec[2] | + +[1] See "Cross-namespace references" below
+[2] See "Impersonation" below + +There are two related mechanisms that affect the service account used for the operations marked with +"Impersonation" above: "impersonation" and "remote apply". These are explained in the following +sections. + +### Impersonation + +The Kustomize controller and Helm controller both apply arbitrary sets of Kubernetes configuration +("_synced configuration_" as above) to a cluster. These controllers use the service account named in +the field `.spec.serviceAccountName` in the `Kustomization` and `HelmRelease` objects respectively, +while applying and health-checking the synced configuration. This mechanism is called +"impersonation". + +The `.spec.serviceAccountName` field is optional. If empty, the controller's service account is +used. + +### Remote apply + +The Kustomize controller and Helm controller are able to apply a set of configuration to a cluster +other than the cluster in which they run. If the `Kustomization` or `HelmRelease` object [refers to +a secret containing a "kubeconfig" file][kubeconfig], the controller will construct a client using +that kubeconfig, and the client is used to apply the prepared set of configuration. The effect of +this is that the configuration will be applied as the user given in the kubeconfig; often this is a +user with the `cluster-admin` role bound to it, but not necessarily so. + +All accesses that would use impersonation use the remote client instead. + +### Cross-namespace references + +Some Flux API kinds have fields which can refer to a Flux API object in another namespace. The Flux +controllers do not respect namespace isolation when dereferencing these fields. The following are +fields that are not restricted to the namespace of the containing object, listed by API kind. + +| API kind | field | explanation | +|----------|-------|-------------| +| **`kustomizations.kustomize.toolkit.fluxcd.io/v1beta2`** | `.spec.dependsOn` | Items are references that can include a namespace | +| | `.spec.healthChecks` | Items are references that can include a namespace (note: these are accessed using impersonation) | +| | `.spec.sourceRef` | This is a reference that can include a namespace | +| | `.spec.targetNamespace` | This sets or overrides the namespace given in the top-most `kustomization.yaml` | +| **`helmreleases.helm.toolkit.fluxcd/v2beta1`** | `.spec.dependsOn` | Items are references that can include a namespace | +| | `.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/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 +control for cross-namespace references" below. + +Note that the field `.spec.sourceRef` of **`imageupdateautomation.image.toolkit.fluxcd.io`** does +_not_ include a namespace. + +#### Access control for cross-namespace references + +In v0.24, an `ImagePolicy` object can refer to a `ImageRepository` object in another +namespace. Unlike most cross-namespace references, the controller processing `ImagePolicy` objects +applies additional access control, as given in the referenced `ImageRepository`: the field +[`.spec.accessFrom`][access-from-ref] grants access to the namespaces selected therein. Access is +denied unless granted. + +## Security considerations + +### Impersonation is optional + +Flux does not insist on a service account to be supplied in `Kustomization` and `HelmRelease` +specifications, and the default is to use the controller's service account. That means a user with +the ability to create either of those objects can trivially arrange for a configuration to be +applied with the controller service account, which in the default deployment of Flux will have +`cluster-admin` bound to it. This represents a privilege escalation vulnerability in the default +deployment of Flux. To guard against it, an admission controller can be used to make the +`.spec.serviceAccountName` field mandatory; an example which uses Kyverno is given in [the +multi-tenancy implementation][multi-tenancy-eg]. + +### Cross-namespace references side-step namespace isolation + +`HelmRelease` and `Kustomization` objects can refer to `GitRepository`, `HelmRepository`, or +`Bucket` (collectively "sources") in any other namespace. The referenced objects are accessed +through the controller's service account, which by default has `cluster-admin` bound to it. This +means all sources in a cluster are by default usable as a synced configuration, from any +namespace. To restrict access, an admission controller can be used to block cross-namespace +references; the [example using Kyverno][multi-tenancy-eg] from above also does this. + +## References + +- [CVE-2021-41254](https://github.com/fluxcd/kustomize-controller/security/advisories/GHSA-35rf-v2jv-gfg7) + "Privilege escalation to cluster admin on multi-tenant environments" was fixed in flux2 **v0.15.0**. + +[Flux version 0.24]: https://github.com/fluxcd/flux2/releases/tag/v0.24.0 +[serviceAccountName]: https://fluxcd.io/docs/components/kustomize/api/#kustomize.toolkit.fluxcd.io/v1beta2.KustomizationSpec +[kubeconfig]: https://fluxcd.io/docs/components/kustomize/api/#kustomize.toolkit.fluxcd.io/v1beta2.KubeConfig +[access-from-ref]: https://fluxcd.io/docs/components/image/imagerepositories/#allow-cross-namespace-references +[Flux API]: https://fluxcd.io/docs/components/ +[flux-rbac]: https://github.com/fluxcd/flux2/tree/v0.24.0/manifests/rbac +[k8s-ns]: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ +[k8s-rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ +[k8s-cluster-admin]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles +[multi-tenancy-eg]: https://github.com/fluxcd/flux2-multi-tenancy/blob/main/infrastructure/kyverno-policies/flux-multi-tenancy.yaml 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/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 index 80f99382..7a51fd5d 100644 --- a/tests/azure/README.md +++ b/tests/azure/README.md @@ -1,31 +1,35 @@ # 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. +The test suite goal is to verify that Flux integration with Azure services are working properly. ## 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. +The tests are run with the help of pre-configured Azure subscriptions and Azure DevOps organization. +Access to those 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) +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 [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 cannot 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 +a 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 [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 +Each test run is initiated by running `terraform apply` on the aks Terraform, it does this by using the library +[terraform-exec](https://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 offset to Terraform instead of requiring it to be implemented in the test. The following tests are currently implemented: @@ -41,11 +45,13 @@ The following tests are currently implemented: ## 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. +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 +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 index 04542659..889af1ad 100644 --- a/tests/azure/azure_test.go +++ b/tests/azure/azure_test.go @@ -22,6 +22,7 @@ import ( b64 "encoding/base64" "encoding/json" "fmt" + "io" "log" "os" "path/filepath" @@ -30,28 +31,34 @@ import ( "time" eventhub "github.com/Azure/azure-event-hubs-go/v3" + giturls "github.com/chainguard-dev/git-urls" + install "github.com/hashicorp/hc-install" + "github.com/hashicorp/hc-install/fs" + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/src" "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" + "k8s.io/klog/v2/klogr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + runtimeLog "sigs.k8s.io/controller-runtime/pkg/log" 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" + reflectorv1beta2 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1" + notiv1 "github.com/fluxcd/notification-controller/api/v1" + notiv1beta3 "github.com/fluxcd/notification-controller/api/v1beta3" + eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1" "github.com/fluxcd/pkg/apis/meta" - "github.com/fluxcd/pkg/runtime/events" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + extgogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" ) const ( @@ -106,10 +113,14 @@ func TestMain(m *testing.M) { func setup(m *testing.M) (exitVal int, err error) { ctx := context.TODO() - + runtimeLog.SetLogger(klogr.New()) // Setup Terraform binary and init state log.Println("Setting up Azure test infrastructure") - execPath, err := tfinstall.Find(ctx, &whichTerraform{}) + i := install.NewInstaller() + // Find Terraform binary path + execPath, err := i.Ensure(ctx, []src.Source{ + &fs.AnyVersion{Product: &product.Terraform}, + }) if err != nil { return 0, fmt.Errorf("terraform exec path not found: %v", err) } @@ -263,8 +274,10 @@ func TestAzureDevOpsCloning(t *testing.T) { } t.Log("Creating application sources") - repo, repoDir, err := getRepository(cfg.applicationRepository.http, branchName, true, cfg.azdoPat) + repo, _, err := getRepository(cfg.applicationRepository.http, branchName, true, cfg.azdoPat) require.NoError(t, err) + + files := make(map[string]io.Reader) for _, tt := range tests { manifest := fmt.Sprintf(` apiVersion: v1 @@ -273,12 +286,11 @@ func TestAzureDevOpsCloning(t *testing.T) { 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) + name := fmt.Sprintf("cloning-test/%s/configmap.yaml", tt.name) + files[name] = strings.NewReader(manifest) } - err = commitAndPushAll(repo, branchName, cfg.azdoPat) + + err = commitAndPushAll(repo, files, branchName) require.NoError(t, err) err = createTagAndPush(repo, branchName, tagName, cfg.azdoPat) require.NoError(t, err) @@ -329,8 +341,7 @@ func TestAzureDevOpsCloning(t *testing.T) { 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, + Reference: ref, SecretRef: &meta.LocalObjectReference{ Name: gitSecret.Name, }, @@ -407,11 +418,11 @@ func TestImageRepositoryACR(t *testing.T) { 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) + repo, _, err := getRepository(repoUrl, name, true, cfg.azdoPat) require.NoError(t, err) - err = commitAndPushAll(repo, name, cfg.azdoPat) + files := make(map[string]io.Reader) + files["podinfo.yaml"] = strings.NewReader(manifest) + err = commitAndPushAll(repo, files, name) require.NoError(t, err) err = setupNamespace(ctx, cfg.kubeClient, repoUrl, cfg.azdoPat, name) @@ -433,14 +444,14 @@ func TestImageRepositoryACR(t *testing.T) { return nil }) require.NoError(t, err) - imageRepository := reflectorv1beta1.ImageRepository{ + imageRepository := reflectorv1beta2.ImageRepository{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: name, }, } _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &imageRepository, func() error { - imageRepository.Spec = reflectorv1beta1.ImageRepositorySpec{ + imageRepository.Spec = reflectorv1beta2.ImageRepositorySpec{ Image: fmt.Sprintf("%s/container/podinfo", cfg.acr.url), Interval: metav1.Duration{ Duration: 1 * time.Minute, @@ -452,19 +463,19 @@ func TestImageRepositoryACR(t *testing.T) { return nil }) require.NoError(t, err) - imagePolicy := reflectorv1beta1.ImagePolicy{ + imagePolicy := reflectorv1beta2.ImagePolicy{ ObjectMeta: metav1.ObjectMeta{ Name: "podinfo", Namespace: name, }, } _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &imagePolicy, func() error { - imagePolicy.Spec = reflectorv1beta1.ImagePolicySpec{ + imagePolicy.Spec = reflectorv1beta2.ImagePolicySpec{ ImageRepositoryRef: meta.NamespacedObjectReference{ Name: imageRepository.Name, }, - Policy: reflectorv1beta1.ImagePolicyChoice{ - SemVer: &reflectorv1beta1.SemVerPolicy{ + Policy: reflectorv1beta2.ImagePolicyChoice{ + SemVer: &reflectorv1beta2.SemVerPolicy{ Range: "1.0.x", }, }, @@ -483,7 +494,7 @@ func TestImageRepositoryACR(t *testing.T) { Interval: metav1.Duration{ Duration: 1 * time.Minute, }, - SourceRef: automationv1beta1.SourceReference{ + SourceRef: automationv1beta1.CrossNamespaceSourceReference{ Kind: "GitRepository", Name: name, }, @@ -529,19 +540,25 @@ func TestKeyVaultSops(t *testing.T) { secretYaml := `apiVersion: v1 kind: Secret metadata: - name: "test" - namespace: "key-vault-sops" + name: "test" + namespace: "key-vault-sops" stringData: - foo: "bar"` + foo: "bar"` repo, tmpDir, err := getRepository(repoUrl, name, true, cfg.azdoPat) - err = runCommand(ctx, tmpDir, "mkdir -p ./key-vault-sops") + err = runCommand(ctx, 5*time.Minute, 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)) + err = runCommand(ctx, 5*time.Minute, 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)) + err = runCommand(ctx, 5*time.Minute, 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) + + r, err := os.Open(fmt.Sprintf("%s/key-vault-sops/secret.enc.yaml", tmpDir)) + require.NoError(t, err) + + files := make(map[string]io.Reader) + files["key-vault-sops/secret.enc.yaml"] = r + err = commitAndPushAll(repo, files, name) require.NoError(t, err) err = setupNamespace(ctx, cfg.kubeClient, repoUrl, cfg.azdoPat, name) @@ -551,7 +568,6 @@ stringData: 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, }, @@ -614,11 +630,13 @@ func TestAzureDevOpsCommitStatus(t *testing.T) { namespace: %s `, name) - repo, repoDir, err := getRepository(repoUrl, name, true, cfg.azdoPat) + c, _, 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) + + files := make(map[string]io.Reader) + files["configmap.yaml"] = strings.NewReader(manifest) + + err = commitAndPushAll(c, files, name) require.NoError(t, err) err = setupNamespace(ctx, cfg.kubeClient, repoUrl, cfg.azdoPat, name) @@ -663,14 +681,14 @@ func TestAzureDevOpsCommitStatus(t *testing.T) { } return nil }) - provider := notiv1beta1.Provider{ + provider := notiv1beta3.Provider{ ObjectMeta: metav1.ObjectMeta{ Name: "azuredevops", Namespace: name, }, } _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &provider, func() error { - provider.Spec = notiv1beta1.ProviderSpec{ + provider.Spec = notiv1beta3.ProviderSpec{ Type: "azuredevops", Address: repoUrl, SecretRef: &meta.LocalObjectReference{ @@ -680,18 +698,18 @@ func TestAzureDevOpsCommitStatus(t *testing.T) { return nil }) require.NoError(t, err) - alert := notiv1beta1.Alert{ + alert := notiv1beta3.Alert{ ObjectMeta: metav1.ObjectMeta{ Name: "azuredevops", Namespace: name, }, } _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &alert, func() error { - alert.Spec = notiv1beta1.AlertSpec{ + alert.Spec = notiv1beta3.AlertSpec{ ProviderRef: meta.LocalObjectReference{ Name: provider.Name, }, - EventSources: []notiv1beta1.CrossNamespaceObjectReference{ + EventSources: []notiv1.CrossNamespaceObjectReference{ { Kind: "Kustomization", Name: name, @@ -711,10 +729,14 @@ func TestAzureDevOpsCommitStatus(t *testing.T) { 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) + + repo, err := extgogit.PlainOpen(c.Path()) require.NoError(t, err) - commit, err := repo.LookupCommit(branch.Target()) - rev := commit.Id().String() + + ref, err := repo.Reference(plumbing.NewBranchReferenceName(name), false) + require.NoError(t, err) + + rev := ref.Hash().String() connection := azuredevops.NewPatConnection(orgUrl, cfg.azdoPat) client, err := git.NewClient(ctx, connection) require.NoError(t, err) @@ -766,8 +788,10 @@ func TestEventHubNotification(t *testing.T) { repo, repoDir, err := getRepository(repoUrl, name, true, cfg.azdoPat) require.NoError(t, err) err = addFile(repoDir, "configmap.yaml", manifest) + files := make(map[string]io.Reader) + files["configmap.yaml"] = strings.NewReader(manifest) require.NoError(t, err) - err = commitAndPushAll(repo, name, cfg.azdoPat) + err = commitAndPushAll(repo, files, name) require.NoError(t, err) err = setupNamespace(ctx, cfg.kubeClient, repoUrl, cfg.azdoPat, name) @@ -785,14 +809,14 @@ func TestEventHubNotification(t *testing.T) { } return nil }) - provider := notiv1beta1.Provider{ + provider := notiv1beta3.Provider{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: name, }, } _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &provider, func() error { - provider.Spec = notiv1beta1.ProviderSpec{ + provider.Spec = notiv1beta3.ProviderSpec{ Type: "azureeventhub", Address: repoUrl, SecretRef: &meta.LocalObjectReference{ @@ -802,24 +826,25 @@ func TestEventHubNotification(t *testing.T) { return nil }) require.NoError(t, err) - alert := notiv1beta1.Alert{ + alert := notiv1beta3.Alert{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: name, }, } _, err = controllerutil.CreateOrUpdate(ctx, cfg.kubeClient, &alert, func() error { - alert.Spec = notiv1beta1.AlertSpec{ + alert.Spec = notiv1beta3.AlertSpec{ ProviderRef: meta.LocalObjectReference{ Name: provider.Name, }, - EventSources: []notiv1beta1.CrossNamespaceObjectReference{ + EventSources: []notiv1.CrossNamespaceObjectReference{ { Kind: "Kustomization", Name: name, Namespace: name, }, }, + Summary: "cluster: test-1", } return nil }) @@ -856,7 +881,7 @@ func TestEventHubNotification(t *testing.T) { require.Eventually(t, func() bool { select { case eventJson := <-c: - event := &events.Event{} + event := &eventv1.Event{} err := json.Unmarshal([]byte(eventJson), event) if err != nil { t.Logf("the received event type does not match Flux format, error: %v", err) diff --git a/tests/azure/go.mod b/tests/azure/go.mod index d15050ce..98ee8663 100644 --- a/tests/azure/go.mod +++ b/tests/azure/go.mod @@ -1,25 +1,122 @@ module github.com/fluxcd/flux2/tests/azure -go 1.16 +go 1.20 + +// Fix CVE-2022-28948 +replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 require ( - github.com/Azure/azure-event-hubs-go/v3 v3.3.13 - github.com/fluxcd/helm-controller/api v0.14.0 - github.com/fluxcd/image-automation-controller/api v0.18.0 - github.com/fluxcd/image-reflector-controller/api v0.14.0 - github.com/fluxcd/kustomize-controller/api v0.18.1 - github.com/fluxcd/notification-controller/api v0.19.0 - github.com/fluxcd/pkg/apis/meta v0.10.1 - github.com/fluxcd/pkg/runtime v0.12.2 - github.com/fluxcd/source-controller/api v0.19.0 - github.com/hashicorp/terraform-exec v0.14.0 - github.com/libgit2/git2go/v31 v31.6.1 + github.com/Azure/azure-event-hubs-go/v3 v3.6.1 + github.com/chainguard-dev/git-urls v1.0.2 + github.com/fluxcd/helm-controller/api v0.37.2 + github.com/fluxcd/image-automation-controller/api v0.37.0 + github.com/fluxcd/image-reflector-controller/api v0.31.1 + github.com/fluxcd/kustomize-controller/api v1.2.1 + github.com/fluxcd/notification-controller/api v1.2.3 + github.com/fluxcd/pkg/apis/event v0.6.0 + github.com/fluxcd/pkg/apis/meta v1.2.0 + github.com/fluxcd/pkg/git v0.16.0 + github.com/fluxcd/pkg/git/gogit v0.16.1 + github.com/fluxcd/source-controller/api v1.2.3 + github.com/go-git/go-git/v5 v5.11.0 + github.com/hashicorp/hc-install v0.5.2 + github.com/hashicorp/terraform-exec v0.18.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.22.2 - k8s.io/apimachinery v0.22.2 - k8s.io/client-go v0.22.2 - sigs.k8s.io/controller-runtime v0.10.2 + github.com/stretchr/testify v1.8.4 + go.uber.org/multierr v1.11.0 + k8s.io/api v0.28.4 + k8s.io/apimachinery v0.28.4 + k8s.io/client-go v0.28.4 + k8s.io/klog/v2 v2.110.1 + sigs.k8s.io/controller-runtime v0.16.3 +) + +require ( + 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 v0.0.0-20231012073058-a7379d079e0e // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/devigned/tab v0.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanphx/json-patch/v5 v5.7.0 // indirect + github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect + github.com/fluxcd/pkg/apis/kustomize v1.2.0 // indirect + github.com/fluxcd/pkg/ssh v0.10.0 // indirect + github.com/fluxcd/pkg/version v0.2.2 // 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.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // 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.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/terraform-json v0.15.0 // indirect + github.com/imdario/mergo v0.3.15 // 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/mailru/easyjson v0.7.7 // 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/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/skeema/knownhosts v1.2.1 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/zclconf/go-cty v1.13.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.4.0 // indirect + golang.org/x/tools v0.16.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.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.28.4 // indirect + k8s.io/kube-openapi v0.0.0-20231113174909-778a5567bc1e // indirect + k8s.io/utils v0.0.0-20231127182322-b307cd553661 // 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/azure/go.sum b/tests/azure/go.sum index 68d1298c..095b8937 100644 --- a/tests/azure/go.sum +++ b/tests/azure/go.sum @@ -1,1061 +1,388 @@ -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 h1:3ithwDMr7/3vpAMXiH+ZQnYbuIsh+OPhUPMFC9enmn0= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -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 h1:xE3CPsOgttP4ACBePh79zTKALtXwn/Edhcr16R5hMWU= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0 h1:/May9ojXjRkPBNVrq+oWLqmWCkr4OU5uRY29bu0mRyQ= -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 h1:Lpy6hKgdcl7a3WGSfJIFmxmcdjSpP6OmBEfcOv1Y680= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -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 h1:UDpwYIwla4jHGzZJaEJYx1tOejbgSoNqsAfHAUYe2r8= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -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-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/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +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.1 h1:vSiMmn3tOwgiLyfnmhT5K6Of/3QWRLaaNZPI0hFvZyU= +github.com/Azure/azure-event-hubs-go/v3 v3.6.1/go.mod h1:i2NByb9Pr2na7y8wi/XefEVKkuA2CDUjCNoWQJtTsGo= +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.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.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -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.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 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.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/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.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 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -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/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 v0.0.0-20231012073058-a7379d079e0e h1:NfjGPY2A8SSRJvXny111ZPoB57LT5lWgX4XiUjW10eY= +github.com/ProtonMail/go-crypto v0.0.0-20231012073058-a7379d079e0e/go.mod h1:K4vciqCJaZ1Ghw/SvtJbEAM4soEtwDCNVqkdQIIujwU= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= 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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 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/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-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -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/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.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-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.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +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/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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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 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 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/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.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/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.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v4.11.0+incompatible h1:glyUF9yIYtMHzn8xaKw5rMhdWcwsYV8dZHIq5567/xs= -github.com/evanphx/json-patch v4.11.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.14.0 h1:I6gyOaEcUUEk0cuXgKhvmvgN833LxdZ3dngTnMDii7w= -github.com/fluxcd/helm-controller/api v0.14.0/go.mod h1:3wDrDVSH/3yH31PzfXzYCCf8OP70eg82tlQU1+QGsS0= -github.com/fluxcd/image-automation-controller/api v0.18.0 h1:/FMwAJ31s4xk5XL7AKYWT0Z9f+GpLbd55e2I1m6jg/o= -github.com/fluxcd/image-automation-controller/api v0.18.0/go.mod h1:XqrRtH4aFOY1llgf4wx8VcSmzLsdV/xWcrPbdZjvvLg= -github.com/fluxcd/image-reflector-controller/api v0.14.0 h1:JlwwpQENkGSxtAC7VXykpOqObsupO61easXu30jpvb0= -github.com/fluxcd/image-reflector-controller/api v0.14.0/go.mod h1:ew7uL5XzPH3QTfNxt3BAklDh9ONrR5I2m3D7ST0zE9E= -github.com/fluxcd/kustomize-controller/api v0.18.1 h1:cssv85lh5RlDDEFvMv/4InR/7SzqX/+rVMYA3GqpnFI= -github.com/fluxcd/kustomize-controller/api v0.18.1/go.mod h1:xGHBIzVXepzm2/0iQJJSbCIRY0Ahq5AgbYsVojpRyX0= -github.com/fluxcd/notification-controller/api v0.19.0 h1:mSsDj30T4v/9aL2GoMY616p+6nIifD1nrZiBD/rUi8U= -github.com/fluxcd/notification-controller/api v0.19.0/go.mod h1:SkB3tLOXouLN6PAceNCsJrJaawHt+WiUVfUSIYcpwjs= -github.com/fluxcd/pkg/apis/acl v0.0.1 h1:biCgZMjpDSv3Q4mZPikUJILx3t2MuNXR4Oa5jRQxaNQ= -github.com/fluxcd/pkg/apis/acl v0.0.1/go.mod h1:y3qOXUFObVWk7jzOjubMnr/u18j1kCeSi6olycnxr/E= -github.com/fluxcd/pkg/apis/kustomize v0.2.0 h1:jhu2QHvs+j3Zo9rR6w8hkO3LSC6h3M37zY5ejufOmxY= -github.com/fluxcd/pkg/apis/kustomize v0.2.0/go.mod h1:gEl+W5cVykCC3RfrCaqe+Pz+j4lKl2aeR4dxsom/zII= -github.com/fluxcd/pkg/apis/meta v0.10.0/go.mod h1:CW9X9ijMTpNe7BwnokiUOrLl/h13miwVr/3abEQLbKE= -github.com/fluxcd/pkg/apis/meta v0.10.1 h1:zISenRlqNG7WK8TP3HxZTvv+1Z7JZOUIQvZrOr6pQ2w= -github.com/fluxcd/pkg/apis/meta v0.10.1/go.mod h1:yUblM2vg+X8TE3A2VvJfdhkGmg+uqBlSPkLk7dxi0UM= -github.com/fluxcd/pkg/runtime v0.12.2 h1:4iOpx2j/w15kNemDOnZrF6ugJ/rhSmRu7aI+xn23+BI= -github.com/fluxcd/pkg/runtime v0.12.2/go.mod h1:tuWdqpWPhgjQvYrSnojdZ4plyU8DRU1NDzsfOhnzl2g= -github.com/fluxcd/source-controller/api v0.19.0 h1:D4hc/ROhcl7iJdgeVhmM6B7WkDqwtQKnvpl04n+LcNg= -github.com/fluxcd/source-controller/api v0.19.0/go.mod h1:rUqw0LmoCyGUoElmtLqHc8O35WAQUd8LdgdyDA+kNs4= -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/elazarl/goproxy v0.0.0-20231117061959-7cc037d33fb5 h1:m62nsMU279qRD9PQSWD1l66kmkXzuYcnVJqL4XLeV2M= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.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 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= +github.com/fluxcd/helm-controller/api v0.37.2 h1:tkLezpRdqPDz7HoKHFu92sV+ppOCVDxkjFTh8/lpff8= +github.com/fluxcd/helm-controller/api v0.37.2/go.mod h1:BuXZhAX9blQviil6yUN5zNM4RB753yhyBTJXxXff7Mo= +github.com/fluxcd/image-automation-controller/api v0.37.0 h1:0L8kzX3zaYfg4wQ2Mx3G56atLMNeDGjy5qMGP4tDKRs= +github.com/fluxcd/image-automation-controller/api v0.37.0/go.mod h1:B2EbmiY69mE2bGOENPNUXr6klwrVe7FwVoB2iE1q08E= +github.com/fluxcd/image-reflector-controller/api v0.31.1 h1:nc44G0JjLgSvqglJSiXQJZcrRw+eY01j7fHRUDB3FMw= +github.com/fluxcd/image-reflector-controller/api v0.31.1/go.mod h1:KopMbC92Cw2ypZZeMytzTLr3EfOj2hoL6MizrdpBDhc= +github.com/fluxcd/kustomize-controller/api v1.2.1 h1:+WgQOU7jpqz9bA4djPWmaeYAp9cG7c/TdcIYku3Jrzk= +github.com/fluxcd/kustomize-controller/api v1.2.1/go.mod h1:0Kgc4uYnr5jCm4H8JwArkR0v4WTmXeX/9KgoDbxluVc= +github.com/fluxcd/notification-controller/api v1.2.3 h1:vXVMg2PmTjmRC5+ULZfbmBEm/CsJaM9yjNJzRdI0JMs= +github.com/fluxcd/notification-controller/api v1.2.3/go.mod h1:A0VkH3mswQAeGKsmzq81jAUN+zNJt9SPXjwe8mvnnaw= +github.com/fluxcd/pkg/apis/acl v0.1.0 h1:EoAl377hDQYL3WqanWCdifauXqXbMyFuK82NnX6pH4Q= +github.com/fluxcd/pkg/apis/acl v0.1.0/go.mod h1:zfEZzz169Oap034EsDhmCAGgnWlcWmIObZjYMusoXS8= +github.com/fluxcd/pkg/apis/event v0.6.0 h1:AUaeee1CGWb65BLqVximHXG8Gcu6vWuYONIq6tVpjgo= +github.com/fluxcd/pkg/apis/event v0.6.0/go.mod h1:OEzWcX/oPbMmkCvC9QGoK27JXFvUZgBhLD+zgxZe47A= +github.com/fluxcd/pkg/apis/kustomize v1.2.0 h1:vkVs+OumxaWso0jNCqdgFFfMHdh+qtZhykTkjl7OgmA= +github.com/fluxcd/pkg/apis/kustomize v1.2.0/go.mod h1:VF7tR/WuVFeum+HaMTHwp+eCtsHiiQlY6ihgqtAnW/M= +github.com/fluxcd/pkg/apis/meta v1.2.0 h1:O766PzGAdMdQKybSflGL8oV0+GgCNIkdsxfalRyzeO8= +github.com/fluxcd/pkg/apis/meta v1.2.0/go.mod h1:fU/Az9AoVyIxC0oI4ihG0NVMNnvrcCzdEym3wxjIQsc= +github.com/fluxcd/pkg/git v0.16.0 h1:xgfMpgsVaxGLechKNaSUif9jnt2Ji/HkwIwxXeDoADk= +github.com/fluxcd/pkg/git v0.16.0/go.mod h1:ZsaxXDBHIUjNeRf+3qDGyHD22KpqbjuNPCbHy2Xid5U= +github.com/fluxcd/pkg/git/gogit v0.16.1 h1:byimVk7VLbERRxJDkVktithN03GC0y7fyc8Ur9Uka8U= +github.com/fluxcd/pkg/git/gogit v0.16.1/go.mod h1:nx3PumOFe5e3fMsh2HPLSlAeOk2wC+jVg7H9BTRKEmg= +github.com/fluxcd/pkg/gittestserver v0.9.0 h1:OthMahtKmmpwCRw8LmSGrceqVhHywGg/QiytxHGSgeY= +github.com/fluxcd/pkg/ssh v0.10.0 h1:JFz0u/CPEf3hXvmrEvUvXsc70eKh8xphqjXxZuSby9g= +github.com/fluxcd/pkg/ssh v0.10.0/go.mod h1:1lFTj3MhU9xQuaJ5PJJoh/FyRYzK54ll9NY/s2KqOZM= +github.com/fluxcd/pkg/version v0.2.2 h1:ZpVXECeLA5hIQMft11iLp6gN3cKcz6UNuVTQPw/bRdI= +github.com/fluxcd/pkg/version v0.2.2/go.mod h1:NGnh/no8S6PyfCDxRFrPY3T5BUnqP48MxfxNRU0z8C0= +github.com/fluxcd/source-controller/api v1.2.3 h1:71mXv3Qg9HEhcpqOq1ObmoE+P/HuZNaAvxfI7dqZMo8= +github.com/fluxcd/source-controller/api v1.2.3/go.mod h1:5gaIVVH7hgb8p3HKFp8P6hGmZEC8fKSt4EcrG3g5vZI= 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 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -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 v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= -github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= -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/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -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/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= -github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= -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/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/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +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/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 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/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/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/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/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.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/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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.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.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-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.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/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/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.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= 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.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= -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/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -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.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/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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-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/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/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.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0= +github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI= +github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4= +github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980= +github.com/hashicorp/terraform-json v0.15.0 h1:/gIyNtR6SFw6h5yzlbDbACyGvIhKtQi8mTsbkNd79lE= +github.com/hashicorp/terraform-json v0.15.0/go.mod h1:+L1RNzjDU5leLFZkHTFTbJXaoqUC6TqXlFgDoOXrtvk= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/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/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 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 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.7/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 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -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/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.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/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/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/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/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/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.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 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 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/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-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= -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 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/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/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= -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.11.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 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -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.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= -github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= -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/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/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +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 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 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -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.2.0/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/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 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/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 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/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -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 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/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/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -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/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +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.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-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/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= -github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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 v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= 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.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= -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.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.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/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 h1:mZQZefskPPCMIBCSEH0v2/iUqqLrYtaeqwD6FUGUnFE= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -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-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/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.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= 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.3.1-0.20200828183125-ce943fd02449/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/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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-20190404232315-eb5bcb51f2a3/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-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-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-20200520004742-59133d7f0dd7/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-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-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-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -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 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/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 h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0= +golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM= 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-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-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/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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= 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-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-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-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-20200519105757-fe76b779f299/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-20200831180312-196b9ba8737a/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-20201204225414-ed752295db88/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-20210225134936-a50acf3fe073/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-20210426230700-d19ff857e887/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-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo= -golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/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-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.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.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 h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= 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 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -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-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.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.9.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.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY= +golang.org/x/time v0.4.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-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-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-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-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/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-20201224043029-2b0845dc783e/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 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= 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.20.0 h1:jz2KixHX7EcCPiQrySzPdnYT7DbINAypCqKZ1Z7GM40= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= 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.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-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -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.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -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.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= -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/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 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= -k8s.io/api v0.22.2 h1:M8ZzAD0V6725Fjg53fKeTJxGsJvRbk4TEm/fexHMtfw= -k8s.io/api v0.22.2/go.mod h1:y3ydYpLJAaDI+BbSe2xmGcqxiWHmWjkEeIbiwHvnPR8= -k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= -k8s.io/apiextensions-apiserver v0.22.2 h1:zK7qI8Ery7j2CaN23UCFaC1hj7dMiI87n01+nKuewd4= -k8s.io/apiextensions-apiserver v0.22.2/go.mod h1:2E0Ve/isxNl7tWLSUDgi6+cmwHi5fQRdwGVCxbC+KFA= -k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= -k8s.io/apimachinery v0.21.2/go.mod h1:CdTY8fU/BlvAbJ2z/8kBwimGki5Zp8/fbVuLY8gJumM= -k8s.io/apimachinery v0.22.2 h1:ejz6y/zNma8clPVfNDLnPbleBo6MpoFy/HBiBqCouVk= -k8s.io/apimachinery v0.22.2/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0= -k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= -k8s.io/apiserver v0.22.2/go.mod h1:vrpMmbyjWrgdyOvZTSpsusQq5iigKNWv9o9KlDAbBHI= -k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= -k8s.io/client-go v0.22.2 h1:DaSQgs02aCC1QcwUdkKZWOeaVsQjYvWv8ZazcZ6JcHc= -k8s.io/client-go v0.22.2/go.mod h1:sAlhrkVDf50ZHx6z4K0S40wISNTarf1r800F+RlCF6U= -k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= -k8s.io/code-generator v0.22.2/go.mod h1:eV77Y09IopzeXOJzndrDyCI88UBok2h6WxAlBwpxa+o= -k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= -k8s.io/component-base v0.22.2/go.mod h1:5Br2QhI9OTe79p+TzPe9JKNQYvEKbq9rTJDWllunGug= -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/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.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.9.0 h1:D7HV+n1V57XeZ0m6tdRkfknthUaM06VFbWldOFh8kzM= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e h1:KLHHjkdQFomZy8+06csTWZ0m1343QqxZhR2LJ1OxCYM= -k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a h1:8dYfu/Fc9Gz2rNJKB9IQRGgQOh2clmRzNIPPY1xLY5g= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/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.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.22/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= -sigs.k8s.io/controller-runtime v0.10.2 h1:jW8qiY+yMnnPx6O9hu63tgcwaKzd1yLYui+mpvClOOc= -sigs.k8s.io/controller-runtime v0.10.2/go.mod h1:CQp8eyUQZ/Q7PJvnIrB6/hgfTC1kBkGylwsLgOQi1WY= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/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 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU= +k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231113174909-778a5567bc1e h1:snPmy96t93RredGRjKfMFt+gvxuVAncqSAyBveJtr4Q= +k8s.io/kube-openapi v0.0.0-20231113174909-778a5567bc1e/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= +k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +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/azure/terraform/aks/.terraform.lock.hcl b/tests/azure/terraform/aks/.terraform.lock.hcl index b5c9f96d..cc3c7846 100644 --- a/tests/azure/terraform/aks/.terraform.lock.hcl +++ b/tests/azure/terraform/aks/.terraform.lock.hcl @@ -2,77 +2,80 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azuread" { - version = "1.6.0" - constraints = "1.6.0" + version = "2.28.0" + constraints = "2.28.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", + "h1:22zcPLrP6T0FAGzhkx44Oc3SreGpzttng34JSYhoknE=", + "zh:0e8b008417d74f7d7f931effe48c0719f20789440c9c5932c2b1cf4110348f41", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:2a2e4408fc1dc902553ff6a5751924c5e9a59df30f0668b55aa6c07264537c03", + "zh:2ab09b735888a7402bdd8e74f75a053ac102e0a01b876b0608a0c240dff57b2e", + "zh:2ac1f45bb1597726ff6822e1f9a7bc7227179c10b0b51533849b44ab278a05ed", + "zh:601a7821c7fbef870a1a2165a684e4fb4f4c84f6b85e0ce51ef7783a581cf594", + "zh:7f8e4dd03a3d4259e06b498ed0b04c6911aa99cf5f01018e2092899cd135c6e5", + "zh:8408143a24baaf4ad527aeecfaf11dfcd0fb6f25648958f2c94464717f776206", + "zh:bc836c1389f7b01537eb71ec709ea9d1cb4180814b70992ce3004356ce28d173", + "zh:d4b5571c96c2bafdf79494265f508dbe569f6fb16a5ddc41f22da22e9be029e9", + "zh:f1c2a1a13fe3725ba84b57a418adb1bd8c93db09dd880658a468cbd4832f9224", + "zh:f39b090d45674395fecb39add1260dd4565661e38eb40c4017c3fd84c8af1717", ] } provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.76.0" - constraints = "2.76.0" + version = "3.20.0" + constraints = "3.20.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", + "h1:heH/4bYgajEFQ+fwSV9Zduvpyb7eTCQUv+gl201EFg8=", + "zh:0d534bb2fed67b5b58d3adb2b0be7a9986f62b34f40eae450dafc9454fb54db8", + "zh:19f6d5f196a35500e0f1ae9d9baee44f49b90858524338a7b8aaec06d3e3a047", + "zh:1d042648d2eaffde8858a8006b944374599c5e8c2f834ae74b97adedd1468142", + "zh:278ebac38cf3c1e6df4bc5de00e931bfc04298607f428aa84a932bbf26dee421", + "zh:48f29b802e2de7e6dd2452a012c633686fce5d7ad3eadb490a7b8c0967a9ebfa", + "zh:731bf2e97c4a519723682beb2e85e065bf0bf53b2f50e2ff7b15b39ea74e37ff", + "zh:7c8187ebca19ca8f6ef82d3d79a418ccfa6574bb99e63cc930fa46ff938a7921", + "zh:82fdb2052601f6fa925195e77506fb609ce8bb4a6f6e94cf6a5058252ef570d4", + "zh:995ca23bb3765a16c6b3138b468d920acff5742b22492324c836579e3344ea40", + "zh:a970131232ad41203382f6fa3f0014a22767cbfe28cd7562346184ea6e678d63", + "zh:bf5036675a7f0b8691fe393e2782a76c7943ba17eec7255e16a31c7547436a48", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" + version = "3.4.3" 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", + "h1:xZGZf18JjMS06pFa4NErzANI98qi59SEcBsOcS2P2yQ=", + "zh:41c53ba47085d8261590990f8633c8906696fa0a3c4b384ff6a7ecbf84339752", + "zh:59d98081c4475f2ad77d881c4412c5129c56214892f490adf11c7e7a5a47de9b", + "zh:686ad1ee40b812b9e016317e7f34c0d63ef837e084dea4a1f578f64a6314ad53", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:84103eae7251384c0d995f5a257c72b0096605048f757b749b7b62107a5dccb3", + "zh:8ee974b110adb78c7cd18aae82b2729e5124d8f115d484215fd5199451053de5", + "zh:9dd4561e3c847e45de603f17fa0c01ae14cae8c4b7b4e6423c9ef3904b308dda", + "zh:bb07bb3c2c0296beba0beec629ebc6474c70732387477a65966483b5efabdbc6", + "zh:e891339e96c9e5a888727b45b2e1bb3fcbdfe0fd7c5b4396e4695459b38c8cb1", + "zh:ea4739860c24dfeaac6c100b2a2e357106a89d18751f7693f3c31ecf6a996f8d", + "zh:f0c76ac303fd0ab59146c39bc121c5d7d86f878e9a69294e29444d4c653786f8", + "zh:f143a9a5af42b38fed328a161279906759ff39ac428ebcfe55606e05e1518b93", ] } provider "registry.terraform.io/microsoft/azuredevops" { - version = "0.1.7" - constraints = "0.1.7" + version = "0.2.2" + constraints = "0.2.2" 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", + "h1:oKfPQ5Tp9WNeacY08gMifP3G9I//o3LW6qTLsveJwi0=", + "zh:016142d26ec662949ba95b6c84672b243b54bbdca04cf8714fe0b4318783a72d", + "zh:0337b3c4e023bb56b23a5d2d9abe917f197eed378fa69803e9d0b11a36211e15", + "zh:240c9636660292eeb99bd892602eafe2e5c22b469b082de6963e31dab9e0092e", + "zh:439151590a489a7c0cde50ee701fdbf254e67bdbeaa2acd2a99d005c4051d518", + "zh:6086f5eab87662678eef7bc83041eab5667e92189eb3089b966aeb2cdb58d299", + "zh:94a64223905bb3cef2c38e163ae56ef841422e6511a79f8e60272edd7f8fc67f", + "zh:9d9545445607c5ba6482da0137464d5de4c3459ae1671e6ff94e337e5943c0eb", + "zh:a53bfdea73985ed31acbadd200b295745662a4a54e8c37f050faf71dab7deb8b", + "zh:aa6943db7093b2556fcc2ee5b8b5a8a48e625ded2b063183fbc5a52c94d133f2", + "zh:af4729e8fe8ec255e4c4ca0e6dd4cf43d855bfe4c45b2aa6e47d8c35be55813d", + "zh:bdf8752a6cd12ba3a33597bf7519825000a498b655c72be8c8df504bb9f70fe5", + "zh:c760fa7bc5c62d56c54ef41b4b03b0ae391149f46f36c8c8a55d2511e7f8e599", ] } diff --git a/tests/azure/terraform/aks/aks.tf b/tests/azure/terraform/aks/aks.tf index 1356be34..aab1a0cf 100644 --- a/tests/azure/terraform/aks/aks.tf +++ b/tests/azure/terraform/aks/aks.tf @@ -16,9 +16,7 @@ resource "azurerm_kubernetes_cluster" "this" { type = "SystemAssigned" } - role_based_access_control { - enabled = true - } + role_based_access_control_enabled = true network_profile { network_plugin = "kubenet" diff --git a/tests/azure/terraform/aks/main.tf b/tests/azure/terraform/aks/main.tf index 5dc44b99..0e70f4a1 100644 --- a/tests/azure/terraform/aks/main.tf +++ b/tests/azure/terraform/aks/main.tf @@ -6,20 +6,20 @@ terraform { key = "prod.terraform.tfstate" } - required_version = "1.0.7" + required_version = "1.2.8" required_providers { azurerm = { source = "hashicorp/azurerm" - version = "2.76.0" + version = "3.20.0" } azuread = { source = "hashicorp/azuread" - version = "1.6.0" + version = "2.28.0" } azuredevops = { source = "microsoft/azuredevops" - version = "0.1.7" + version = "0.2.2" } } } diff --git a/tests/azure/terraform/aks/outputs.tf b/tests/azure/terraform/aks/outputs.tf index 4833c70a..21a8f1de 100644 --- a/tests/azure/terraform/aks/outputs.tf +++ b/tests/azure/terraform/aks/outputs.tf @@ -5,18 +5,22 @@ output "aks_kube_config" { output "aks_host" { value = azurerm_kubernetes_cluster.this.kube_config[0].host + sensitive = true } output "aks_client_certificate" { value = base64decode(azurerm_kubernetes_cluster.this.kube_config[0].client_certificate) + sensitive = true } output "aks_client_key" { value = base64decode(azurerm_kubernetes_cluster.this.kube_config[0].client_key) + sensitive = true } output "aks_cluster_ca_certificate" { value = base64decode(azurerm_kubernetes_cluster.this.kube_config[0].cluster_ca_certificate) + sensitive = true } output "shared_pat" { diff --git a/tests/azure/terraform/shared/.terraform.lock.hcl b/tests/azure/terraform/shared/.terraform.lock.hcl index 749c3583..03a5e2fe 100644 --- a/tests/azure/terraform/shared/.terraform.lock.hcl +++ b/tests/azure/terraform/shared/.terraform.lock.hcl @@ -2,57 +2,60 @@ # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/azuread" { - version = "1.6.0" - constraints = "1.6.0" + version = "2.28.0" + constraints = "2.28.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", + "h1:22zcPLrP6T0FAGzhkx44Oc3SreGpzttng34JSYhoknE=", + "zh:0e8b008417d74f7d7f931effe48c0719f20789440c9c5932c2b1cf4110348f41", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:2a2e4408fc1dc902553ff6a5751924c5e9a59df30f0668b55aa6c07264537c03", + "zh:2ab09b735888a7402bdd8e74f75a053ac102e0a01b876b0608a0c240dff57b2e", + "zh:2ac1f45bb1597726ff6822e1f9a7bc7227179c10b0b51533849b44ab278a05ed", + "zh:601a7821c7fbef870a1a2165a684e4fb4f4c84f6b85e0ce51ef7783a581cf594", + "zh:7f8e4dd03a3d4259e06b498ed0b04c6911aa99cf5f01018e2092899cd135c6e5", + "zh:8408143a24baaf4ad527aeecfaf11dfcd0fb6f25648958f2c94464717f776206", + "zh:bc836c1389f7b01537eb71ec709ea9d1cb4180814b70992ce3004356ce28d173", + "zh:d4b5571c96c2bafdf79494265f508dbe569f6fb16a5ddc41f22da22e9be029e9", + "zh:f1c2a1a13fe3725ba84b57a418adb1bd8c93db09dd880658a468cbd4832f9224", + "zh:f39b090d45674395fecb39add1260dd4565661e38eb40c4017c3fd84c8af1717", ] } provider "registry.terraform.io/hashicorp/azurerm" { - version = "2.76.0" - constraints = "2.76.0" + version = "3.20.0" + constraints = "3.20.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", + "h1:heH/4bYgajEFQ+fwSV9Zduvpyb7eTCQUv+gl201EFg8=", + "zh:0d534bb2fed67b5b58d3adb2b0be7a9986f62b34f40eae450dafc9454fb54db8", + "zh:19f6d5f196a35500e0f1ae9d9baee44f49b90858524338a7b8aaec06d3e3a047", + "zh:1d042648d2eaffde8858a8006b944374599c5e8c2f834ae74b97adedd1468142", + "zh:278ebac38cf3c1e6df4bc5de00e931bfc04298607f428aa84a932bbf26dee421", + "zh:48f29b802e2de7e6dd2452a012c633686fce5d7ad3eadb490a7b8c0967a9ebfa", + "zh:731bf2e97c4a519723682beb2e85e065bf0bf53b2f50e2ff7b15b39ea74e37ff", + "zh:7c8187ebca19ca8f6ef82d3d79a418ccfa6574bb99e63cc930fa46ff938a7921", + "zh:82fdb2052601f6fa925195e77506fb609ce8bb4a6f6e94cf6a5058252ef570d4", + "zh:995ca23bb3765a16c6b3138b468d920acff5742b22492324c836579e3344ea40", + "zh:a970131232ad41203382f6fa3f0014a22767cbfe28cd7562346184ea6e678d63", + "zh:bf5036675a7f0b8691fe393e2782a76c7943ba17eec7255e16a31c7547436a48", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } provider "registry.terraform.io/hashicorp/random" { - version = "3.1.0" + version = "3.3.2" 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", + "h1:H5V+7iXol/EHB2+BUMzGlpIiCOdV74H8YjzCxnSAWcg=", + "zh:038293aebfede983e45ee55c328e3fde82ae2e5719c9bd233c324cfacc437f9c", + "zh:07eaeab03a723d83ac1cc218f3a59fceb7bbf301b38e89a26807d1c93c81cef8", + "zh:427611a4ce9d856b1c73bea986d841a969e4c2799c8ac7c18798d0cc42b78d32", + "zh:49718d2da653c06a70ba81fd055e2b99dfd52dcb86820a6aeea620df22cd3b30", + "zh:5574828d90b19ab762604c6306337e6cd430e65868e13ef6ddb4e25ddb9ad4c0", + "zh:7222e16f7833199dabf1bc5401c56d708ec052b2a5870988bc89ff85b68a5388", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:b1b2d7d934784d2aee98b0f8f07a8ccfc0410de63493ae2bf2222c165becf938", + "zh:b8f85b6a20bd264fcd0814866f415f0a368d1123cd7879c8ebbf905d370babc8", + "zh:c3813133acc02bbebddf046d9942e8ba5c35fc99191e3eb057957dafc2929912", + "zh:e7a41dbc919d1de800689a81c240c27eec6b9395564630764ebb323ea82ac8a9", + "zh:ee6d23208449a8eaa6c4f203e33f5176fa795b4b9ecf32903dffe6e2574732c2", ] } diff --git a/tests/azure/terraform/shared/acr.tf b/tests/azure/terraform/shared/acr.tf index 72a132c6..a339feed 100644 --- a/tests/azure/terraform/shared/acr.tf +++ b/tests/azure/terraform/shared/acr.tf @@ -2,5 +2,5 @@ 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" + sku = "Standard" } diff --git a/tests/azure/terraform/shared/main.tf b/tests/azure/terraform/shared/main.tf index 3be9d61f..36623f6b 100644 --- a/tests/azure/terraform/shared/main.tf +++ b/tests/azure/terraform/shared/main.tf @@ -6,16 +6,16 @@ terraform { key = "prod.terraform.tfstate" } - required_version = "1.0.7" + required_version = "1.2.8" required_providers { azurerm = { source = "hashicorp/azurerm" - version = "2.76.0" + version = "3.20.0" } azuread = { source = "hashicorp/azuread" - version = "1.6.0" + version = "2.28.0" } } } diff --git a/tests/azure/util_test.go b/tests/azure/util_test.go index 958b40fd..ededf2d9 100644 --- a/tests/azure/util_test.go +++ b/tests/azure/util_test.go @@ -19,13 +19,13 @@ package test import ( "context" "fmt" + "io" "os" "os/exec" "path/filepath" + "strings" "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" @@ -35,13 +35,23 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta1" + helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta2" 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" + reflectorv1beta2 "github.com/fluxcd/image-reflector-controller/api/v1beta2" + 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/v1beta1" + "github.com/fluxcd/pkg/git" + "github.com/fluxcd/pkg/git/gogit" + "github.com/fluxcd/pkg/git/repository" + sourcev1 "github.com/fluxcd/source-controller/api/v1" + sourcev1b2 "github.com/fluxcd/source-controller/api/v1beta2" + extgogit "github.com/go-git/go-git/v5" + gitconfig "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/http" ) const defaultBranch = "main" @@ -62,6 +72,10 @@ func getKubernetesCredentials(kubeconfig, aksHost, aksCert, aksKey, aksCa string CAData: []byte(aksCa), }, } + err = sourcev1b2.AddToScheme(scheme.Scheme) + if err != nil { + return "", nil, err + } err = sourcev1.AddToScheme(scheme.Scheme) if err != nil { return "", nil, err @@ -74,7 +88,7 @@ func getKubernetesCredentials(kubeconfig, aksHost, aksCert, aksKey, aksCa string if err != nil { return "", nil, err } - err = reflectorv1beta1.AddToScheme(scheme.Scheme) + err = reflectorv1beta2.AddToScheme(scheme.Scheme) if err != nil { return "", nil, err } @@ -82,7 +96,11 @@ func getKubernetesCredentials(kubeconfig, aksHost, aksCert, aksKey, aksCa string if err != nil { return "", nil, err } - err = notiv1beta1.AddToScheme(scheme.Scheme) + err = notiv1beta3.AddToScheme(scheme.Scheme) + if err != nil { + return "", nil, err + } + err = notiv1.AddToScheme(scheme.Scheme) if err != nil { return "", nil, err } @@ -103,17 +121,7 @@ func installFlux(ctx context.Context, kubeClient client.Client, kubeconfigPath, _, 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{ @@ -127,83 +135,68 @@ func installFlux(ctx context.Context, kubeClient client.Client, kubeconfigPath, 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") + //// Install Flux and push files to git repository + repo, _, err := getRepository(repoUrl, defaultBranch, true, azdoPat) if err != nil { - return err + return fmt.Errorf("error cloning repositoriy: %w", err) } + kustomizeYaml := ` - resources: - - gotk-components.yaml - - gotk-sync.yaml - patchesStrategicMerge: - - |- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: kustomize-controller - namespace: flux-system +resources: + - gotk-components.yaml + - gotk-sync.yaml +patchesStrategicMerge: + - |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: kustomize-controller + namespace: flux-system + spec: + template: spec: - template: - spec: - containers: - - name: manager - envFrom: - - secretRef: - name: azure-sp - - |- - apiVersion: apps/v1 - kind: Deployment - metadata: - name: source-controller - namespace: flux-system + containers: + - name: manager + envFrom: + - secretRef: + name: azure-sp + - |- + apiVersion: apps/v1 + kind: Deployment + metadata: + name: source-controller + namespace: flux-system + spec: + template: 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)) + containers: + - name: manager + envFrom: + - secretRef: + name: azure-sp +` + + files := make(map[string]io.Reader) + files["clusters/e2e/flux-system/kustomization.yaml"] = strings.NewReader(kustomizeYaml) + files["clusters/e2e/flux-system/gotk-components.yaml"] = strings.NewReader("") + files["clusters/e2e/flux-system/gotk-sync.yaml"] = strings.NewReader("") + err = commitAndPushAll(repo, files, defaultBranch) if err != nil { - return err + return fmt.Errorf("error committing and pushing manifests: %w", err) } - err = runCommand(ctx, repoDir, fmt.Sprintf("kubectl --kubeconfig=%s apply -k ./clusters/e2e/flux-system/", kubeconfigPath)) - if err != nil { - return err + + bootstrapCmd := fmt.Sprintf("flux bootstrap git --url=%s --password=%s --kubeconfig=%s"+ + " --token-auth --path=clusters/e2e --components-extra image-reflector-controller,image-automation-controller", + repoUrl, azdoPat, kubeconfigPath) + if err := runCommand(context.Background(), 10*time.Minute, "./", bootstrapCmd); err != nil { + return fmt.Errorf("error running bootstrap: %w", err) } + return nil } -func runCommand(ctx context.Context, dir, command string) error { - timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) +func runCommand(ctx context.Context, timeout time.Duration, dir, command string) error { + timeoutCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() cmd := exec.CommandContext(timeoutCtx, "bash", "-c", command) cmd.Dir = dir @@ -267,7 +260,6 @@ func setupNamespace(ctx context.Context, kubeClient client.Client, repoUrl, pass Interval: metav1.Duration{ Duration: 1 * time.Minute, }, - GitImplementation: sourcev1.LibGit2Implementation, Reference: &sourcev1.GitRepositoryRef{ Branch: name, }, @@ -302,7 +294,7 @@ func setupNamespace(ctx context.Context, kubeClient client.Client, repoUrl, pass return nil } -func getRepository(url, branchName string, overrideBranch bool, password string) (*git2go.Repository, string, error) { +func getRepository(repoURL, branchName string, overrideBranch bool, password string) (*gogit.Client, string, error) { checkoutBranch := defaultBranch if overrideBranch == false { checkoutBranch = branchName @@ -312,34 +304,30 @@ func getRepository(url, branchName string, overrideBranch bool, password string) 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, + c, err := gogit.NewClient(tmpDir, &git.AuthOptions{ + Transport: git.HTTPS, + Username: "git", + Password: password, }) 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()) + + _, err = c.Clone(context.Background(), repoURL, repository.CloneConfig{ + CheckoutStrategy: repository.CheckoutStrategy{ + Branch: checkoutBranch, + }, + }) if err != nil { return nil, "", err } - _, err = repo.CreateBranch(branchName, headCommit, true) + + err = c.SwitchBranch(context.Background(), branchName) if err != nil { return nil, "", err } - return repo, tmpDir, nil + + return c, tmpDir, nil } func addFile(dir, path, content string) error { @@ -350,114 +338,107 @@ func addFile(dir, path, content string) error { return nil } -func commitAndPushAll(repo *git2go.Repository, branchName, password string) error { - idx, err := repo.Index() +func commitAndPushAll(client *gogit.Client, files map[string]io.Reader, branchName string) error { + repo, err := extgogit.PlainOpen(client.Path()) 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()) + + wt, err := repo.Worktree() 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) + + err = wt.Checkout(&extgogit.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(branchName), + Force: true, + }) if err != nil { return err } - origin, err := repo.Remotes.Lookup("origin") + + f := repository.WithFiles(files) + _, err = client.Commit(git.Commit{ + Author: git.Signature{ + Name: "git", + Email: "test@example.com", + When: time.Now(), + }, + Message: "add file", + }, f) + if err != nil { return err } - err = origin.Push([]string{fmt.Sprintf("+refs/heads/%s", branchName)}, &git2go.PushOptions{ - RemoteCallbacks: git2go.RemoteCallbacks{ - CredentialsCallback: credentialCallback("git", password), - }, - }) + + err = client.Push(context.Background(), repository.PushConfig{}) if err != nil { return err } + return nil } -func createTagAndPush(repo *git2go.Repository, branchName, tag, password string) error { - branch, err := repo.LookupBranch(branchName, git2go.BranchAll) +func createTagAndPush(client *gogit.Client, branchName, newTag, password string) error { + repo, err := extgogit.PlainOpen(client.Path()) if err != nil { return err } - commit, err := repo.LookupCommit(branch.Target()) + + ref, err := repo.Reference(plumbing.NewBranchReferenceName(branchName), false) if err != nil { return err } - tags, err := repo.Tags.List() + tags, err := repo.TagObjects() if err != nil { return err } - for _, existingTag := range tags { - if existingTag == tag { - err = repo.Tags.Remove(tag) + + 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 err } - sig := &git2go.Signature{ + sig := &object.Signature{ Name: "git", Email: "test@example.com", When: time.Now(), } - _, err = repo.Tags.Create(tag, commit, sig, "create tag") + + _, err = repo.CreateTag(newTag, ref.Hash(), &extgogit.CreateTagOptions{ + Tagger: sig, + Message: "create tag", + }) if err != nil { return err } - origin, err := repo.Remotes.Lookup("origin") - if err != nil { - return err + + auth := &http.BasicAuth{ + Username: "git", + Password: password, } - err = origin.Push([]string{fmt.Sprintf("+refs/tags/%s", tag)}, &git2go.PushOptions{ - RemoteCallbacks: git2go.RemoteCallbacks{ - CredentialsCallback: credentialCallback("git", password), - }, - }) - if err != nil { + + po := &extgogit.PushOptions{ + RemoteName: "origin", + Progress: os.Stdout, + RefSpecs: []gitconfig.RefSpec{gitconfig.RefSpec("refs/tags/*:refs/tags/*")}, + Auth: auth, + } + if err := repo.Push(po); 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 - } + return nil } func getTestManifest(namespace string) string { diff --git a/tests/image-automation/auto.yaml b/tests/image-automation/auto.yaml index 321012c0..8f71c881 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 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..7b69149f --- /dev/null +++ b/tests/integration/go.mod @@ -0,0 +1,145 @@ +module github.com/fluxcd/flux2/tests/integration + +go 1.20 + +require ( + cloud.google.com/go/pubsub v1.31.0 + github.com/Azure/azure-event-hubs-go/v3 v3.6.0 + github.com/chainguard-dev/git-urls v1.0.2 + github.com/fluxcd/helm-controller/api v0.37.2 + github.com/fluxcd/image-automation-controller/api v0.37.0 + github.com/fluxcd/image-reflector-controller/api v0.31.1 + github.com/fluxcd/kustomize-controller/api v1.2.1 + github.com/fluxcd/notification-controller/api v1.2.3 + github.com/fluxcd/pkg/apis/event v0.6.0 + github.com/fluxcd/pkg/apis/meta v1.2.0 + github.com/fluxcd/pkg/git v0.16.0 + github.com/fluxcd/pkg/git/gogit v0.16.1 + github.com/fluxcd/pkg/runtime v0.43.2 + github.com/fluxcd/source-controller/api v1.2.3 + github.com/fluxcd/test-infra/tftestenv v0.0.0-20240108135005-b58e0c4e0cfa + github.com/go-git/go-git/v5 v5.11.0 + github.com/google/go-containerregistry v0.16.1 + github.com/hashicorp/terraform-exec v0.18.1 + github.com/hashicorp/terraform-json v0.16.0 + github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5 + github.com/onsi/gomega v1.30.0 + google.golang.org/grpc v1.56.3 + k8s.io/api v0.28.4 + k8s.io/apimachinery v0.28.4 + k8s.io/client-go v0.28.4 + sigs.k8s.io/controller-runtime v0.16.3 +) + +require ( + cloud.google.com/go v0.110.2 // indirect + cloud.google.com/go/compute v1.20.1 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/iam v1.0.1 // 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 v0.0.0-20231012073058-a7379d079e0e // indirect + github.com/apparentlymart/go-textseg/v13 v13.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.1 // indirect + github.com/devigned/tab v0.1.1 // indirect + github.com/docker/cli v24.0.0+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/docker/docker v24.0.7+incompatible // indirect + github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanphx/json-patch/v5 v5.7.0 // indirect + github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect + github.com/fluxcd/pkg/apis/kustomize v1.2.0 // indirect + github.com/fluxcd/pkg/ssh v0.10.0 // indirect + github.com/fluxcd/pkg/version v0.2.2 // 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.3.0 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.4 // 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.3 // 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.4 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/gax-go/v2 v2.11.0 // 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.5.0 // 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.1 // indirect + github.com/sirupsen/logrus v1.9.1 // indirect + github.com/skeema/knownhosts v1.2.1 // 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.13.0 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.15.0 // indirect + golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.16.0 // indirect + google.golang.org/api v0.126.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect + google.golang.org/protobuf v1.31.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.28.4 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8 // indirect + k8s.io/utils v0.0.0-20231127182322-b307cd553661 // 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..e3af981d --- /dev/null +++ b/tests/integration/go.sum @@ -0,0 +1,598 @@ +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.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= +cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU= +cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= +cloud.google.com/go/kms v1.10.2 h1:8UePKEypK3SQ6g+4mn/s/VgE5L7XOh+FwGGRUqvY3Hw= +cloud.google.com/go/pubsub v1.31.0 h1:aXdyyJz90kA+bor9+6+xHAciMD5mj8v15WqFZ5E0sek= +cloud.google.com/go/pubsub v1.31.0/go.mod h1:dYmJ3K97NCQ/e4OwZ20rD4Ym3Bu8Gu9m/aJdWQjdcks= +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-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic= +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.0 h1:UXRi5KewXYoTiekVjrj0gyGfbyGvtbYdot6/4IMf4I4= +github.com/Azure/azure-event-hubs-go/v3 v3.6.0/go.mod h1:UgyRnRU7H5e33igaLHJTqbkoNR1uj0j3MA/n7dABU24= +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/cli v0.3.1 h1:LXl088ZQlP0SBppGFsRZonW6hSvwgL5gRByMbvUbx8U= +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/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +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/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +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 v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= +github.com/ProtonMail/go-crypto v0.0.0-20231012073058-a7379d079e0e h1:NfjGPY2A8SSRJvXny111ZPoB57LT5lWgX4XiUjW10eY= +github.com/ProtonMail/go-crypto v0.0.0-20231012073058-a7379d079e0e/go.mod h1:K4vciqCJaZ1Ghw/SvtJbEAM4soEtwDCNVqkdQIIujwU= +github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +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 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +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/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +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/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/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/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qeXMx3O0wqM= +github.com/docker/cli v24.0.0+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.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= +github.com/docker/docker v24.0.7+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/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +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/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= +github.com/fluxcd/helm-controller/api v0.37.2 h1:tkLezpRdqPDz7HoKHFu92sV+ppOCVDxkjFTh8/lpff8= +github.com/fluxcd/helm-controller/api v0.37.2/go.mod h1:BuXZhAX9blQviil6yUN5zNM4RB753yhyBTJXxXff7Mo= +github.com/fluxcd/image-automation-controller/api v0.37.0 h1:0L8kzX3zaYfg4wQ2Mx3G56atLMNeDGjy5qMGP4tDKRs= +github.com/fluxcd/image-automation-controller/api v0.37.0/go.mod h1:B2EbmiY69mE2bGOENPNUXr6klwrVe7FwVoB2iE1q08E= +github.com/fluxcd/image-reflector-controller/api v0.31.1 h1:nc44G0JjLgSvqglJSiXQJZcrRw+eY01j7fHRUDB3FMw= +github.com/fluxcd/image-reflector-controller/api v0.31.1/go.mod h1:KopMbC92Cw2ypZZeMytzTLr3EfOj2hoL6MizrdpBDhc= +github.com/fluxcd/kustomize-controller/api v1.2.1 h1:+WgQOU7jpqz9bA4djPWmaeYAp9cG7c/TdcIYku3Jrzk= +github.com/fluxcd/kustomize-controller/api v1.2.1/go.mod h1:0Kgc4uYnr5jCm4H8JwArkR0v4WTmXeX/9KgoDbxluVc= +github.com/fluxcd/notification-controller/api v1.2.3 h1:vXVMg2PmTjmRC5+ULZfbmBEm/CsJaM9yjNJzRdI0JMs= +github.com/fluxcd/notification-controller/api v1.2.3/go.mod h1:A0VkH3mswQAeGKsmzq81jAUN+zNJt9SPXjwe8mvnnaw= +github.com/fluxcd/pkg/apis/acl v0.1.0 h1:EoAl377hDQYL3WqanWCdifauXqXbMyFuK82NnX6pH4Q= +github.com/fluxcd/pkg/apis/acl v0.1.0/go.mod h1:zfEZzz169Oap034EsDhmCAGgnWlcWmIObZjYMusoXS8= +github.com/fluxcd/pkg/apis/event v0.6.0 h1:AUaeee1CGWb65BLqVximHXG8Gcu6vWuYONIq6tVpjgo= +github.com/fluxcd/pkg/apis/event v0.6.0/go.mod h1:OEzWcX/oPbMmkCvC9QGoK27JXFvUZgBhLD+zgxZe47A= +github.com/fluxcd/pkg/apis/kustomize v1.2.0 h1:vkVs+OumxaWso0jNCqdgFFfMHdh+qtZhykTkjl7OgmA= +github.com/fluxcd/pkg/apis/kustomize v1.2.0/go.mod h1:VF7tR/WuVFeum+HaMTHwp+eCtsHiiQlY6ihgqtAnW/M= +github.com/fluxcd/pkg/apis/meta v1.2.0 h1:O766PzGAdMdQKybSflGL8oV0+GgCNIkdsxfalRyzeO8= +github.com/fluxcd/pkg/apis/meta v1.2.0/go.mod h1:fU/Az9AoVyIxC0oI4ihG0NVMNnvrcCzdEym3wxjIQsc= +github.com/fluxcd/pkg/git v0.16.0 h1:xgfMpgsVaxGLechKNaSUif9jnt2Ji/HkwIwxXeDoADk= +github.com/fluxcd/pkg/git v0.16.0/go.mod h1:ZsaxXDBHIUjNeRf+3qDGyHD22KpqbjuNPCbHy2Xid5U= +github.com/fluxcd/pkg/git/gogit v0.16.1 h1:byimVk7VLbERRxJDkVktithN03GC0y7fyc8Ur9Uka8U= +github.com/fluxcd/pkg/git/gogit v0.16.1/go.mod h1:nx3PumOFe5e3fMsh2HPLSlAeOk2wC+jVg7H9BTRKEmg= +github.com/fluxcd/pkg/gittestserver v0.9.0 h1:OthMahtKmmpwCRw8LmSGrceqVhHywGg/QiytxHGSgeY= +github.com/fluxcd/pkg/runtime v0.43.2 h1:xH2BvttUqJ7wS0zjuBETr2pLXG62QY6f0mdxg5UQKio= +github.com/fluxcd/pkg/runtime v0.43.2/go.mod h1:dhhNV45B3tekg7qPzATFTuWOulvUzMoO1bV+vc2pSts= +github.com/fluxcd/pkg/ssh v0.10.0 h1:JFz0u/CPEf3hXvmrEvUvXsc70eKh8xphqjXxZuSby9g= +github.com/fluxcd/pkg/ssh v0.10.0/go.mod h1:1lFTj3MhU9xQuaJ5PJJoh/FyRYzK54ll9NY/s2KqOZM= +github.com/fluxcd/pkg/version v0.2.2 h1:ZpVXECeLA5hIQMft11iLp6gN3cKcz6UNuVTQPw/bRdI= +github.com/fluxcd/pkg/version v0.2.2/go.mod h1:NGnh/no8S6PyfCDxRFrPY3T5BUnqP48MxfxNRU0z8C0= +github.com/fluxcd/source-controller/api v1.2.3 h1:71mXv3Qg9HEhcpqOq1ObmoE+P/HuZNaAvxfI7dqZMo8= +github.com/fluxcd/source-controller/api v1.2.3/go.mod h1:5gaIVVH7hgb8p3HKFp8P6hGmZEC8fKSt4EcrG3g5vZI= +github.com/fluxcd/test-infra/tftestenv v0.0.0-20240108135005-b58e0c4e0cfa h1:JdI+rVwGF5gBYt+UBijOVzXtq7aAU80vgksMNXSCCfU= +github.com/fluxcd/test-infra/tftestenv v0.0.0-20240108135005-b58e0c4e0cfa/go.mod h1:liFlLEXgambGVdWSJ4JzbIHf1Vjpp1HwUyPazPIVZug= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +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.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +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.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +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.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +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.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +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.5/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.16.1 h1:rUEt426sR6nyrL3gt+18ibRcvYpKYdpsa5ZW7MA08dQ= +github.com/google/go-containerregistry v0.16.1/go.mod h1:u0qB2l7mvtWVR5kNcbFIhFY1hLbf8eeGapA+vbFDCtQ= +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-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= +github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +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.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= +github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +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-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.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +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-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +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.5.0 h1:D9bl4KayIYKEeJ4vUDe9L5huqxZXczKaykSRcmQ0xY0= +github.com/hashicorp/hc-install v0.5.0/go.mod h1:JyzMfbzfSBSjoDCRPna1vi/24BEDxFaCPfdHtM5SCdo= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4= +github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980= +github.com/hashicorp/terraform-json v0.16.0 h1:UKkeWRWb23do5LNAFlh/K3N0ymn1qTOO8c+85Albo3s= +github.com/hashicorp/terraform-json v0.16.0/go.mod h1:v0Ufk9jJnk6tcIZvScHvetlKfiNTC+WS21mnXIlc0B0= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +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/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +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 v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/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/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +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/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +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/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +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.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/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/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +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.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +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.1.1/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.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/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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +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.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= +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.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= +github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/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-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= +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.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.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-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-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-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-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-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +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.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= +golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +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-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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.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-20180905080454-ebe1bf3edb33/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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-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-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-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-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.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.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.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +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.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.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.9.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.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +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= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= +google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +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.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +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-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= +google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= +google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +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.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +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.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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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-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/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.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.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-20210107192922-496545a6307b/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= +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.28.4 h1:8ZBrLjwosLl/NYgv1P7EQLqoO8MGQApnbgH8tu3BMzY= +k8s.io/api v0.28.4/go.mod h1:axWTGrY88s/5YE+JSt4uUi6NMM+gur1en2REMR7IRj0= +k8s.io/apiextensions-apiserver v0.28.4 h1:AZpKY/7wQ8n+ZYDtNHbAJBb+N4AXXJvyZx6ww6yAJvU= +k8s.io/apiextensions-apiserver v0.28.4/go.mod h1:pgQIZ1U8eJSMQcENew/0ShUTlePcSGFq6dxSxf2mwPM= +k8s.io/apimachinery v0.28.4 h1:zOSJe1mc+GxuMnFzD4Z/U1wst50X28ZNsn5bhgIIao8= +k8s.io/apimachinery v0.28.4/go.mod h1:wI37ncBvfAoswfq626yPTe6Bz1c22L7uaJ8dho83mgg= +k8s.io/client-go v0.28.4 h1:Np5ocjlZcTrkyRJ3+T3PkXDpe4UpatQxj85+xjaD2wY= +k8s.io/client-go v0.28.4/go.mod h1:0VDZFpgoZfelyP5Wqu0/r/TRYcLYuJ2U1KEeoaPa1N4= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8 h1:vzKzxN5uyJZLY8HL1/OovW7BJefnsBIWt8T7Gjh2boQ= +k8s.io/kube-openapi v0.0.0-20231206194836-bf4651e18aa8/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20231127182322-b307cd553661 h1:FepOBzJ0GXm8t0su67ln2wAZjbQ6RxQGZDnzuLcrUTI= +k8s.io/utils v0.0.0-20231127182322-b307cd553661/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +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..96abb1f3 --- /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" + + automationv1beta1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + reflectorv1beta2 "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 := reflectorv1beta2.ImageRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: testID, + }, + Spec: reflectorv1beta2.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 := reflectorv1beta2.ImagePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: testID, + }, + Spec: reflectorv1beta2.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: imageRepository.Name, + }, + Policy: reflectorv1beta2.ImagePolicyChoice{ + SemVer: &reflectorv1beta2.SemVerPolicy{ + Range: "6.0.x", + }, + }, + }, + } + g.Expect(testEnv.Create(ctx, &imagePolicy)).To(Succeed()) + defer testEnv.Delete(ctx, &imagePolicy) + + imageAutomation := automationv1beta1.ImageUpdateAutomation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "podinfo", + Namespace: testID, + }, + Spec: automationv1beta1.ImageUpdateAutomationSpec{ + Interval: metav1.Duration{ + Duration: 1 * time.Minute, + }, + SourceRef: automationv1beta1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: testID, + }, + GitSpec: &automationv1beta1.GitSpec{ + Checkout: &automationv1beta1.GitCheckoutSpec{ + Reference: sourcev1.GitRepositoryRef{ + Branch: branchName, + }, + }, + Commit: automationv1beta1.CommitSpec{ + Author: automationv1beta1.CommitUser{ + Email: "imageautomation@example.com", + Name: "imageautomation", + }, + }, + }, + Update: &automationv1beta1.UpdateStrategy{ + Path: testID, + Strategy: automationv1beta1.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..80d89af2 --- /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/v2beta2" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" +) + +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..c2052699 --- /dev/null +++ b/tests/integration/suite_test.go @@ -0,0 +1,338 @@ +/* +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" + + helmv2beta1 "github.com/fluxcd/helm-controller/api/v2beta2" + automationv1beta1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + reflectorv1beta2 "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" + + // 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(helmv2beta1.AddToScheme(scheme.Scheme)) + utilruntime.Must(reflectorv1beta2.AddToScheme(scheme.Scheme)) + utilruntime.Must(automationv1beta1.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)) + } + + 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/integration/terraform/azure/event-hub.tf b/tests/integration/terraform/azure/event-hub.tf new file mode 100644 index 00000000..cda95002 --- /dev/null +++ b/tests/integration/terraform/azure/event-hub.tf @@ -0,0 +1,27 @@ +resource "azurerm_eventhub_namespace" "this" { + 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 = local.name + namespace_name = azurerm_eventhub_namespace.this.name + resource_group_name = module.aks.resource_group + partition_count = 1 + message_retention = 1 +} + +resource "azurerm_eventhub_authorization_rule" "this" { + 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 + send = true + manage = false +} 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..2f6b6a66 --- /dev/null +++ b/tests/integration/util_test.go @@ -0,0 +1,413 @@ +/* +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("./build/flux bootstrap git --url=%s %s --kubeconfig=%s --path=clusters/e2e "+ + " --components-extra image-reflector-controller,image-automation-controller", + repoURL, bootstrapArgs, kubeconfigPath) + + return tftestenv.RunCommand(ctx, "./", bootstrapCmd, tftestenv.RunCommandOptions{ + Timeout: 15 * time.Minute, + }) +} + +func uninstallFlux(ctx context.Context) error { + uninstallCmd := fmt.Sprintf("./build/flux uninstall --kubeconfig %s -s", 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) +}