diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..a3aab7af --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file +# Ignore build and test binaries. +bin/ diff --git a/.github/workflows/pr-cc.yml b/.github/workflows/pr-cc.yml index 688d8868..a82bddcf 100644 --- a/.github/workflows/pr-cc.yml +++ b/.github/workflows/pr-cc.yml @@ -18,4 +18,4 @@ jobs: id: check-for-cc uses: ahmadnassri/action-commit-lint@v2 with: - config: conventional + config: ./commitlint.config.js diff --git a/.github/workflows/pr-code.yml b/.github/workflows/pr-code.yml index d856d3b6..1781f980 100644 --- a/.github/workflows/pr-code.yml +++ b/.github/workflows/pr-code.yml @@ -19,13 +19,13 @@ jobs: uses: actions/checkout@v4 - name: Set up Golang - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Run golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: version: latest only-new-issues: true @@ -39,22 +39,23 @@ jobs: name: Unit Testing strategy: matrix: - kubernetes-version: ['1.21', '1.22', '1.23', '1.24', '1.25', '1.26', '1.27', '1.28'] + kubernetes-version: ['1.24', '1.25', '1.26', '1.27', '1.28', '1.29'] runs-on: ubuntu-22.04 env: ENVTEST_K8S_VERSION: ${{ matrix.kubernetes-version }} + DOCKER_CLIENT_API_VERSION: '1.43' # 1.45 is not supported by ubuntu-22.04 GitHub image steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Set up Golang - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true - name: Restore cached Go modules - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/.cache/go-build @@ -70,7 +71,7 @@ jobs: go tool cover -html=cover.out -o coverage.html - name: Archive code coverage results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: code-coverage-report + name: code-coverage-report-${{ matrix.kubernetes-version }} path: coverage.html diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml index 5f37c904..9fde0bab 100644 --- a/.github/workflows/pr-security.yml +++ b/.github/workflows/pr-security.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v4 - name: Dependency Review - uses: actions/dependency-review-action@v3 + uses: actions/dependency-review-action@v4 vulnerability-scanner: name: Vulnerability Scanner @@ -31,7 +31,7 @@ jobs: uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner - uses: aquasecurity/trivy-action@0.16.0 + uses: aquasecurity/trivy-action@0.23.0 with: scan-type: 'fs' ignore-unfixed: true @@ -39,6 +39,6 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy scan results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v2 + uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b60ba4d9..3068e863 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,12 +73,14 @@ jobs: tests: name: Tests runs-on: ubuntu-22.04 + env: + DOCKER_CLIENT_API_VERSION: '1.43' # 1.45 is not supported by ubuntu-22.04 GitHub image steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Set up Golang - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: go-version-file: go.mod check-latest: true @@ -87,7 +89,7 @@ jobs: uses: helm/chart-testing-action@v2.6.1 - name: Run golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: version: latest args: --timeout 3m --verbose --issues-exit-code=0 @@ -148,7 +150,7 @@ jobs: ${{ env.QUAY_IMAGE }} - name: Build container images - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: repository platforms: linux/amd64,linux/arm64 @@ -161,7 +163,7 @@ jobs: ${{ env.HARBOR_URL }}/${{ env.HARBOR_REPO }}:${{ env.VERSION }} - name: Build alpine container images - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: repository platforms: linux/amd64,linux/arm64 @@ -173,8 +175,9 @@ jobs: labels: ${{ steps.meta.outputs.labels }} tags: | ${{ env.HARBOR_URL }}/${{ env.HARBOR_REPO }}:${{ env.VERSION }}-alpine - e2e: - name: Tests End-to-End on K8s + + e2e_install: + name: Tests e2e on K8s (Fresh install) needs: - build runs-on: ubuntu-22.04 @@ -187,26 +190,26 @@ jobs: strategy: max-parallel: 6 matrix: - k8sversion: ["v1.21.14", "v1.22.17", "v1.23.17", "v1.24.15", "v1.25.11", "v1.26.6", "v1.27.3", "v1.28.0"] + k8sversion: ["v1.24.17", "v1.25.16", "v1.26.14", "v1.27.11", "v1.28.7", "v1.29.2"] steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Setup KinD - uses: helm/kind-action@v1.8.0 + uses: helm/kind-action@v1.10.0 with: node_image: kindest/node:${{ matrix.k8sversion }} - name: Run cert-manager installation run: | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml - kubectl wait pods -n cert-manager -l app.kubernetes.io/instance=cert-manager --for condition=Ready --timeout=30s + kubectl wait pods -n cert-manager -l app.kubernetes.io/instance=cert-manager --for condition=Ready --timeout=90s - name: Set up chart-testing uses: helm/chart-testing-action@v2.6.1 - name: Set up helm - uses: azure/setup-helm@v3 + uses: azure/setup-helm@v4 with: version: '3.9.0' @@ -218,14 +221,6 @@ jobs: --chart-repos bitnami=https://charts.bitnami.com/bitnami \ --validate-maintainers=false --check-version-increment=false -# Need wait for the next release with flag --skip-clean-up -# - name: Run chart-testing (install) -# run: | -# set -euo pipefail -# ct install \ -# --charts helm/kube-image-keeper \ -# --helm-extra-set-args "--set controllers.image.tag=latest --set proxy.image.tag=latest" - - name: Run helm (install) run : | set -euo pipefail @@ -236,18 +231,168 @@ jobs: --set controllers.image.tag=$VERSION --set proxy.image.tag=$VERSION \ --set controllers.image.repository=$HARBOR_IMAGE --set proxy.image.repository=$HARBOR_IMAGE \ --set controllers.imagePullSecrets[0].name=harbor-secret --set proxy.image.imagePullSecrets[0].name=harbor-secret --debug + kubectl wait pods -n kuik-system -l app.kubernetes.io/instance=kube-image-keeper --for condition=Ready --timeout=90s + helm history kube-image-keeper -n kuik-system + + - name: Deploy test container + run: | + set -euo pipefail + kubectl create deploy nginx --image=nginx:stable-alpine --replicas=2 + kubectl rollout status deploy nginx + kubectl wait deployment nginx --for condition=Available=True --timeout=30s + echo "kubectl get cachedimage" + kubectl get cachedimages + echo "kubectl get repository" + kubectl get repository + + - name: Test cachedimage (CRD) + run: | + set -euo pipefail + ## Check if our test image is cached + if [ $(kubectl get cachedimages docker.io-library-nginx-stable-alpine -o json | jq ".status.isCached") ]; + then + if [ $(kubectl get cachedimages docker.io-library-nginx-stable-alpine -o json | jq ".status.usedBy.count") -eq 2 ]; + then + echo "Found cached image used by 2 pods" + else + echo "Error: pods count should be equal 2" + exit 1 + fi + else + echo "Error: image cached status is false" + exit 1 + fi + + - name: Test repository (CRD) + run: | + set -euo pipefail + ## Check repository status + if [ $(kubectl get repository docker.io-library-nginx -o json | jq '.status.phase') == '"Ready"' ] ; + then + echo "Found repository" + else + echo "Error: image repository status is not Ready" + exit 1 + fi + + - name: Test metrics endpoint + run: | + set -euo pipefail + ## Check for kuik's components metrics + for component in proxy controllers + do + echo "Testing $component metrics endpoint" + for ip in $(kubectl get po -l "app.kubernetes.io/component=$component" -n kuik-system -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}') + do + attempts=0 + success=false + while [[ $attempts -lt 3 && $success == false ]] + do + response=$(kubectl run curl-pod --image=curlimages/curl --rm -ti --quiet --restart=Never -- curl -s -o /dev/null -w "%{http_code}\n" http://$ip:8080/metrics) + if [[ -z "$response" ]]; then + echo "No HTTP response received from $ip" + elif [[ $response -ge 200 && $response -lt 300 ]]; then + echo "HTTP status code $response is valid for $ip" + success=true + else + echo "HTTP status code $response is not valid for $ip" + fi + attempts=$(( $attempts + 1 )) + sleep 3 + done + if [[ $success == false ]]; then + echo "Failed after 3 attempts for $ip" + exit 1 + fi + done + done + + e2e_upgrade: + name: Tests e2e on K8s (Upgrade) + needs: + - build + - e2e_install + runs-on: ubuntu-22.04 + env: + VERSION: ${{ github.run_id }} + HARBOR_IMAGE: "harbor.enix.io/kube-image-keeper/kube-image-keeper" + HARBOR_REGISTRY: "harbor.enix.io" + HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} + HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} + strategy: + max-parallel: 6 + matrix: + k8sversion: ["v1.24.17", "v1.25.16", "v1.26.14", "v1.27.11", "v1.28.7", "v1.29.2"] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup KinD + uses: helm/kind-action@v1.10.0 + with: + node_image: kindest/node:${{ matrix.k8sversion }} + + - name: Run cert-manager installation + run: | + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml + kubectl wait pods -n cert-manager -l app.kubernetes.io/instance=cert-manager --for condition=Ready --timeout=30s + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.6.1 + + - name: Set up helm + uses: azure/setup-helm@v4 + with: + version: '3.9.0' + + - name: Run chart-testing (lint) + run: | + set -euo pipefail + ct lint \ + --charts helm/kube-image-keeper \ + --chart-repos bitnami=https://charts.bitnami.com/bitnami \ + --validate-maintainers=false --check-version-increment=false + + - name: Run helm (install latest release) + run : | + set -euo pipefail + helm repo add enix https://charts.enix.io/ + helm repo update + helm upgrade --install kube-image-keeper -n kuik-system --create-namespace enix/kube-image-keeper --debug kubectl wait pods -n kuik-system -l app.kubernetes.io/instance=kube-image-keeper --for condition=Ready --timeout=30s + kubectl get po -n kuik-system - - name: Run end-to-end tests + - name: Run helm (upgrade) + run : | + set -euo pipefail + kubectl create secret docker-registry harbor-secret -n kuik-system --docker-server=${{ env.HARBOR_REGISTRY }} \ + --docker-username="$HARBOR_USERNAME" --docker-password="$HARBOR_PASSWORD" + helm upgrade --install kube-image-keeper -n kuik-system --create-namespace ./helm/kube-image-keeper \ + --set controllers.image.tag=$VERSION --set proxy.image.tag=$VERSION \ + --set controllers.image.repository=$HARBOR_IMAGE --set proxy.image.repository=$HARBOR_IMAGE \ + --set controllers.imagePullSecrets[0].name=harbor-secret --set proxy.image.imagePullSecrets[0].name=harbor-secret --wait --debug + kubectl rollout status deploy kube-image-keeper-controllers -n kuik-system + kubectl rollout status ds kube-image-keeper-proxy -n kuik-system + helm history kube-image-keeper -n kuik-system + + - name: Deploy test container run: | set -euo pipefail kubectl create deploy nginx --image=nginx:stable-alpine --replicas=2 + kubectl rollout status deploy nginx kubectl wait deployment nginx --for condition=Available=True --timeout=30s - echo "kubectl get cachedimages" + echo "kubectl get cachedimage" kubectl get cachedimages - if [ $(kubectl get cachedimages -o json | jq ".items[0].status.isCached") ]; + echo "kubectl get repository" + kubectl get repository + + - name: Test cachedimage (CRD) + run: | + set -euo pipefail + ## Check if our test image is cached + if [ $(kubectl get cachedimages docker.io-library-nginx-stable-alpine -o json | jq ".status.isCached") ]; then - if [ $(kubectl get cachedimages -o json | jq ".items[0].status.usedBy.count") -eq 2 ]; + if [ $(kubectl get cachedimages docker.io-library-nginx-stable-alpine -o json | jq ".status.usedBy.count") -eq 2 ]; then echo "Found cached image used by 2 pods" else @@ -259,10 +404,54 @@ jobs: exit 1 fi + - name: Test repository (CRD) + run: | + set -euo pipefail + ## Check repository status + if [ $(kubectl get repository docker.io-library-nginx -o json | jq '.status.phase') == '"Ready"' ] ; + then + echo "Found repository" + else + echo "Error: image repository status is not Ready" + exit 1 + fi + + - name: Test metrics endpoint + run: | + set -euo pipefail + ## Check for kuik's components metrics + for component in proxy controllers + do + echo "Testing $component metrics endpoint" + for ip in $(kubectl get po -l "app.kubernetes.io/component=$component" -n kuik-system -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}') + do + attempts=0 + success=false + while [[ $attempts -lt 3 && $success == false ]] + do + response=$(kubectl run curl-pod --image=curlimages/curl --rm -ti --quiet --restart=Never -- curl -s -o /dev/null -w "%{http_code}\n" http://$ip:8080/metrics) + if [[ -z "$response" ]]; then + echo "No HTTP response received from $ip" + elif [[ $response -ge 200 && $response -lt 300 ]]; then + echo "HTTP status code $response is valid for $ip" + success=true + else + echo "HTTP status code $response is not valid for $ip" + fi + attempts=$(( $attempts + 1 )) + sleep 3 + done + if [[ $success == false ]]; then + echo "Failed after 3 attempts for $ip" + exit 1 + fi + done + done + release: name: Release needs: - - e2e + - e2e_upgrade - semver runs-on: ubuntu-22.04 outputs: @@ -312,7 +501,7 @@ jobs: run: | from lib import * import os - + header('semantic-release job outputs') info('last = {}'.format(os.environ['SR_LAST'])) info('published = {}'.format(os.environ['SR_PUBLISHED'])) @@ -366,7 +555,7 @@ jobs: path: repository - name: Set up helm - uses: azure/setup-helm@v3 + uses: azure/setup-helm@v4 with: version: '3.9.0' @@ -376,10 +565,10 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Cache for chart-releaser id: cache-cr - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: bin/cr key: ${{ runner.os }}-cr-${{ env.CR_VERSION }} @@ -394,7 +583,7 @@ jobs: - name: Cache for helm-docs id: cache-helm-docs - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: bin/helm-docs key: ${{ runner.os }}-helm-docs-${{ env.HELM_DOCS_VERSION }} @@ -413,7 +602,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Login to GHCR + - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io @@ -437,7 +626,7 @@ jobs: ${{ env.GHCR_IMAGE }} - name: Build container images - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: repository platforms: linux/amd64,linux/arm64 @@ -452,7 +641,7 @@ jobs: ${{ env.GHCR_IMAGE }}:${{ env.VERSION }} - name: Push container images tag (Latest) - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 if: ${{ env.PRERELEASE != 'true' }} with: context: repository @@ -466,9 +655,9 @@ jobs: ${{ github.repository }}:latest ${{ env.QUAY_IMAGE }}:latest ${{ env.GHCR_IMAGE }}:latest - + - name: Push container images tag (Release) - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: repository platforms: linux/amd64,linux/arm64 @@ -483,7 +672,7 @@ jobs: ${{ env.GHCR_IMAGE }}:${{ env.VERSION }} - name: Push alpine container images tag (Release) - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: repository platforms: linux/amd64,linux/arm64 @@ -579,7 +768,7 @@ jobs: links.append({ 'name': 'GitHub issue #{}'.format(issue.group('id')), 'url': issue.group('url') - }) + }) entry = { 'kind': kind, @@ -619,7 +808,7 @@ jobs: run: | from lib import * import os, yaml, shutil - + chart_path = os.path.join(os.environ['WORKSPACE'], 'repository', 'helm', 'kube-image-keeper') os.chdir(chart_path) @@ -669,7 +858,7 @@ jobs: print(yaml.safe_load(open(yaml_file, 'r'))) header('release the chart') - + action('clone helm charts repository') charts_repo = os.path.join(os.environ['WORKSPACE'], 'enix-charts') run('git', 'clone', 'https://github.com/enix/helm-charts', charts_repo) @@ -695,6 +884,6 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Delete assets artifact - uses: geekyeggo/delete-artifact@v2 + uses: geekyeggo/delete-artifact@v5 with: name: binaries diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 9fef4f44..908fad6e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -25,7 +25,20 @@ jobs: - name: check-for-cc id: check-for-cc - uses: webiny/action-conventional-commits@v1.2.0 + uses: webiny/action-conventional-commits@v1.3.0 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.23.0 + with: + scan-type: 'fs' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -47,7 +60,7 @@ jobs: ${{ env.QUAY_IMAGE }} - name: Build container images - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: repository platforms: linux/amd64,linux/arm64 @@ -60,7 +73,7 @@ jobs: ${{ env.HARBOR_URL }}/${{ env.HARBOR_REPO }}:${{ env.VERSION }} - name: Build alpine container images - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: repository platforms: linux/amd64,linux/arm64 @@ -73,8 +86,8 @@ jobs: tags: | ${{ env.HARBOR_URL }}/${{ env.HARBOR_REPO }}:${{ env.VERSION }}-alpine - e2e: - name: Tests End-to-End on K8s + e2e_install: + name: Tests e2e on K8s (Fresh install) needs: - build runs-on: ubuntu-22.04 @@ -87,26 +100,26 @@ jobs: strategy: max-parallel: 6 matrix: - k8sversion: ["v1.21.14", "v1.22.17", "v1.23.17", "v1.24.15", "v1.25.11", "v1.26.6", "v1.27.3", "v1.28.0"] + k8sversion: ["v1.24.17", "v1.25.16", "v1.26.14", "v1.27.11", "v1.28.7", "v1.29.2"] steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Setup KinD - uses: helm/kind-action@v1.8.0 + uses: helm/kind-action@v1.10.0 with: node_image: kindest/node:${{ matrix.k8sversion }} - name: Run cert-manager installation run: | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml - kubectl wait pods -n cert-manager -l app.kubernetes.io/instance=cert-manager --for condition=Ready --timeout=30s + kubectl wait pods -n cert-manager -l app.kubernetes.io/instance=cert-manager --for condition=Ready --timeout=90s - name: Set up chart-testing uses: helm/chart-testing-action@v2.6.1 - name: Set up helm - uses: azure/setup-helm@v3 + uses: azure/setup-helm@v4 with: version: '3.9.0' @@ -136,18 +149,177 @@ jobs: --set controllers.image.tag=$VERSION --set proxy.image.tag=$VERSION \ --set controllers.image.repository=$HARBOR_IMAGE --set proxy.image.repository=$HARBOR_IMAGE \ --set controllers.imagePullSecrets[0].name=harbor-secret --set proxy.image.imagePullSecrets[0].name=harbor-secret --debug + kubectl wait pods -n kuik-system -l app.kubernetes.io/instance=kube-image-keeper --for condition=Ready --timeout=90s + helm history kube-image-keeper -n kuik-system + + - name: Deploy test container + run: | + set -euo pipefail + kubectl create deploy nginx --image=nginx:stable-alpine --replicas=2 + kubectl rollout status deploy nginx + kubectl wait deployment nginx --for condition=Available=True --timeout=30s + echo "kubectl get cachedimage" + kubectl get cachedimages + echo "kubectl get repository" + kubectl get repository + + - name: Test cachedimage (CRD) + run: | + set -euo pipefail + ## Check if our test image is cached + if [ $(kubectl get cachedimages docker.io-library-nginx-stable-alpine -o json | jq ".status.isCached") ]; + then + if [ $(kubectl get cachedimages docker.io-library-nginx-stable-alpine -o json | jq ".status.usedBy.count") -eq 2 ]; + then + echo "Found cached image used by 2 pods" + else + echo "Error: pods count should be equal 2" + exit 1 + fi + else + echo "Error: image cached status is false" + exit 1 + fi + + - name: Test repository (CRD) + run: | + set -euo pipefail + ## Check repository status + if [ $(kubectl get repository docker.io-library-nginx -o json | jq '.status.phase') == '"Ready"' ] ; + then + echo "Found repository" + else + echo "Error: image repository status is not Ready" + exit 1 + fi + + - name: Test metrics endpoint + run: | + set -euo pipefail + ## Check for kuik's components metrics + for component in proxy controllers + do + echo "Testing $component metrics endpoint" + for ip in $(kubectl get po -l "app.kubernetes.io/component=$component" -n kuik-system -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}') + do + attempts=0 + success=false + while [[ $attempts -lt 3 && $success == false ]] + do + response=$(kubectl run curl-pod --image=curlimages/curl --rm -ti --quiet --restart=Never -- curl -s -o /dev/null -w "%{http_code}\n" http://$ip:8080/metrics) + if [[ -z "$response" ]]; then + echo "No HTTP response received from $ip" + elif [[ $response -ge 200 && $response -lt 300 ]]; then + echo "HTTP status code $response is valid for $ip" + success=true + else + echo "HTTP status code $response is not valid for $ip" + fi + attempts=$(( $attempts + 1 )) + sleep 3 + done + if [[ $success == false ]]; then + echo "Failed after 3 attempts for $ip" + exit 1 + fi + done + done + + e2e_upgrade: + name: Tests e2e on K8s (Upgrade) + needs: + - build + - e2e_install + runs-on: ubuntu-22.04 + env: + VERSION: ${{ github.run_id }} + HARBOR_IMAGE: "harbor.enix.io/kube-image-keeper/kube-image-keeper" + HARBOR_REGISTRY: "harbor.enix.io" + HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }} + HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }} + strategy: + max-parallel: 6 + matrix: + k8sversion: ["v1.24.17", "v1.25.16", "v1.26.14", "v1.27.11", "v1.28.7", "v1.29.2"] + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup KinD + uses: helm/kind-action@v1.10.0 + with: + node_image: kindest/node:${{ matrix.k8sversion }} + + - name: Run cert-manager installation + run: | + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml + kubectl wait pods -n cert-manager -l app.kubernetes.io/instance=cert-manager --for condition=Ready --timeout=30s + + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.6.1 + + - name: Set up helm + uses: azure/setup-helm@v4 + with: + version: '3.9.0' + + - name: Run chart-testing (lint) + run: | + set -euo pipefail + ct lint \ + --charts helm/kube-image-keeper \ + --chart-repos bitnami=https://charts.bitnami.com/bitnami \ + --validate-maintainers=false --check-version-increment=false + +# Need wait for the next release with flash --skip-clean-up +# - name: Run chart-testing (install) +# run: | +# set -euo pipefail +# ct install \ +# --charts helm/cache-registry \ +# --helm-extra-set-args "--set controllers.image.tag=latest --set proxy.image.tag=latest" + + + - name: Run helm (install latest release) + run : | + set -euo pipefail + helm repo add enix https://charts.enix.io/ + helm repo update + helm upgrade --install kube-image-keeper -n kuik-system --create-namespace enix/kube-image-keeper --debug kubectl wait pods -n kuik-system -l app.kubernetes.io/instance=kube-image-keeper --for condition=Ready --timeout=30s + kubectl get po -n kuik-system + + - name: Run helm (upgrade) + run : | + set -euo pipefail + kubectl create secret docker-registry harbor-secret -n kuik-system --docker-server=${{ env.HARBOR_REGISTRY }} \ + --docker-username="$HARBOR_USERNAME" --docker-password="$HARBOR_PASSWORD" + helm upgrade --install kube-image-keeper -n kuik-system --create-namespace ./helm/kube-image-keeper \ + --set controllers.image.tag=$VERSION --set proxy.image.tag=$VERSION \ + --set controllers.image.repository=$HARBOR_IMAGE --set proxy.image.repository=$HARBOR_IMAGE \ + --set controllers.imagePullSecrets[0].name=harbor-secret --set proxy.image.imagePullSecrets[0].name=harbor-secret --wait --debug + kubectl rollout status deploy kube-image-keeper-controllers -n kuik-system + kubectl rollout status ds kube-image-keeper-proxy -n kuik-system + helm history kube-image-keeper -n kuik-system - - name: Run end-to-end tests + - name: Deploy test container run: | set -euo pipefail kubectl create deploy nginx --image=nginx:stable-alpine --replicas=2 + kubectl rollout status deploy nginx kubectl wait deployment nginx --for condition=Available=True --timeout=30s - echo "kubectl get cachedimages" + echo "kubectl get cachedimage" kubectl get cachedimages - if [ $(kubectl get cachedimages -o json | jq ".items[0].status.isCached") ]; + echo "kubectl get repository" + kubectl get repository + + - name: Test cachedimage (CRD) + run: | + set -euo pipefail + ## Check if our test image is cached + if [ $(kubectl get cachedimages docker.io-library-nginx-stable-alpine -o json | jq ".status.isCached") ]; then - if [ $(kubectl get cachedimages -o json | jq ".items[0].status.usedBy.count") -eq 2 ]; + if [ $(kubectl get cachedimages docker.io-library-nginx-stable-alpine -o json | jq ".status.usedBy.count") -eq 2 ]; then echo "Found cached image used by 2 pods" else @@ -158,6 +330,23 @@ jobs: echo "Error: image cached status is false" exit 1 fi + + - name: Test repository (CRD) + run: | + set -euo pipefail + ## Check repository status + if [ $(kubectl get repository docker.io-library-nginx -o json | jq '.status.phase') == '"Ready"' ] ; + then + echo "Found repository" + else + echo "Error: image repository status is not Ready" + exit 1 + fi + + - name: Test metrics endpoint + run: | + set -euo pipefail + ## Check for kuik's components metrics for component in proxy controllers do echo "Testing $component metrics endpoint" @@ -185,3 +374,4 @@ jobs: fi done done + diff --git a/.gitignore b/.gitignore index db6b2bfb..c17fc016 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ *.dylib bin testbin/* +Dockerfile.cross # Test binary, build with `go test -c` *.test @@ -20,6 +21,7 @@ zz_generated.* # editor and IDE paraphernalia .idea +.vscode *.swp *.swo *~ diff --git a/Dockerfile b/Dockerfile index ef58012f..8edff9c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # Build the manager binary -FROM --platform=${BUILDPLATFORM} golang:1.20-alpine3.17 AS builder +FROM --platform=${BUILDPLATFORM} golang:1.22-alpine3.19 AS builder WORKDIR /workspace -RUN go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.8.0 +RUN go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.15.0 # Copy the Go Modules manifests COPY go.mod go.mod @@ -13,9 +13,7 @@ COPY go.sum go.sum RUN go mod download # Copy the go source -COPY main.go main.go COPY api/ api/ -COPY controllers/ controllers/ COPY cmd/ cmd/ COPY internal/ internal/ @@ -33,13 +31,14 @@ ENV LD_FLAGS="\ -X 'github.com/enix/kube-image-keeper/internal/metrics.Revision=${REVISION}' \ -X 'github.com/enix/kube-image-keeper/internal/metrics.BuildDateTime=BUILD_DATE_TIME'" -RUN BUILD_DATE_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S") && \ +RUN --mount=type=cache,target="/root/.cache/go-build" \ + BUILD_DATE_TIME=$(date -u +"%Y-%m-%dT%H:%M:%S") && \ LD_FLAGS=$(/bin/ash -c "set -o pipefail && echo $LD_FLAGS | sed -e \"s/BUILD_DATE_TIME/$BUILD_DATE_TIME/g\"") && \ controller-gen object paths="./..." && \ - go build -a -ldflags="$LD_FLAGS" -o manager cmd/cache/main.go && \ - go build -a -ldflags="$LD_FLAGS" -o registry-proxy cmd/proxy/main.go + go build -ldflags="$LD_FLAGS" -o manager cmd/cache/main.go && \ + go build -ldflags="$LD_FLAGS" -o registry-proxy cmd/proxy/main.go -FROM alpine:3.17 AS alpine +FROM alpine:3.19 AS alpine COPY --from=builder /workspace/manager /usr/local/bin/ COPY --from=builder /workspace/registry-proxy /usr/local/bin/ diff --git a/Makefile b/Makefile index fa213ec2..343360f7 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # Image URL to use all building/pushing image targets IMG ?= controller:latest # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. -ENVTEST_K8S_VERSION ?= 1.21 +ENVTEST_K8S_VERSION ?= 1.27 # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) @@ -11,8 +11,13 @@ else GOBIN=$(shell go env GOBIN) endif +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + # Setting SHELL to bash allows bash commands to be executed by recipes. -# This is a requirement for 'setup-envtest.sh' in the test target. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec @@ -42,6 +47,8 @@ help: ## Display this help. .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases + tail -n+2 config/crd/bases/kuik.enix.io_cachedimages.yaml > helm/kube-image-keeper/crds/cachedimage-crd.yaml + tail -n+2 config/crd/bases/kuik.enix.io_repositories.yaml > helm/kube-image-keeper/crds/repository-crd.yaml .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. @@ -57,25 +64,45 @@ vet: ## Run go vet against code. .PHONY: test test: manifests generate fmt vet envtest ## Run tests. - KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -v ./... -covermode=count -coverprofile cover.out + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test -v ./... -covermode=count -coverprofile cover.out ##@ Build .PHONY: build -build: generate fmt vet ## Build manager binary. - go build -o bin/manager main.go +build: manifests generate fmt vet ## Build manager binary. + go build -o bin/manager cmd/cache/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./main.go + go run ./cmd/cache/main.go +# If you wish built the manager image targeting other platforms you can use the --platform flag. +# (i.e. docker build --platform linux/arm64 ). However, you must enable docker buildKit for it. +# More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build docker-build: test ## Build docker image with the manager. - docker build -t ${IMG} . + $(CONTAINER_TOOL) build -t ${IMG} . .PHONY: docker-push docker-push: ## Push docker image with the manager. - docker push ${IMG} + $(CONTAINER_TOOL) push ${IMG} + +# PLATFORMS defines the target platforms for the manager image be build to provide support to multiple +# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: +# - able to use docker buildx . More info: https://docs.docker.com/build/buildx/ +# - have enable BuildKit, More info: https://docs.docker.com/develop/develop-images/build_enhancements/ +# - be able to push the image for your registry (i.e. if you do not inform a valid value via IMG=> then the export will fail) +# To properly provided solutions that supports more than one platform you should use this option. +PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le +.PHONY: docker-buildx +docker-buildx: test ## Build and push docker image for the manager for cross-platform support + # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile + sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross + - $(CONTAINER_TOOL) buildx create --name project-v3-builder + $(CONTAINER_TOOL) buildx use project-v3-builder + - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . + - $(CONTAINER_TOOL) buildx rm project-v3-builder + rm Dockerfile.cross ##@ Deployment @@ -85,20 +112,20 @@ endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. - $(KUSTOMIZE) build config/crd | kubectl apply -f - + $(KUSTOMIZE) build config/crd | $(KUBECTL) apply -f - .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + $(KUSTOMIZE) build config/crd | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default | kubectl apply -f - + $(KUSTOMIZE) build config/default | $(KUBECTL) apply -f - .PHONY: undeploy undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + $(KUSTOMIZE) build config/default | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f - ##@ Build Dependencies @@ -108,29 +135,34 @@ $(LOCALBIN): mkdir -p $(LOCALBIN) ## Tool Binaries +KUBECTL ?= kubectl KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest ## Tool Versions -KUSTOMIZE_VERSION ?= v3.8.7 -CONTROLLER_TOOLS_VERSION ?= v0.8.0 +KUSTOMIZE_VERSION ?= v5.0.1 +CONTROLLER_TOOLS_VERSION ?= v0.15.0 -KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" .PHONY: kustomize -kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. +kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. If wrong version is installed, it will be removed before downloading. $(KUSTOMIZE): $(LOCALBIN) - curl -s $(KUSTOMIZE_INSTALL_SCRIPT) | bash -s -- $(subst v,,$(KUSTOMIZE_VERSION)) $(LOCALBIN) + @if test -x $(LOCALBIN)/kustomize && ! $(LOCALBIN)/kustomize version | grep -q $(KUSTOMIZE_VERSION); then \ + echo "$(LOCALBIN)/kustomize version is not expected $(KUSTOMIZE_VERSION). Removing it before installing."; \ + rm -rf $(LOCALBIN)/kustomize; \ + fi + test -s $(LOCALBIN)/kustomize || GOBIN=$(LOCALBIN) GO111MODULE=on go install sigs.k8s.io/kustomize/kustomize/v5@$(KUSTOMIZE_VERSION) .PHONY: controller-gen -controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. +controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. $(CONTROLLER_GEN): $(LOCALBIN) + test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) .PHONY: envtest envtest: $(ENVTEST) ## Download envtest-setup locally if necessary. $(ENVTEST): $(LOCALBIN) - GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest # This is not used for kubebuilder, but to generate the Helm chart template README. .PHONY: helm-docs @@ -140,4 +172,3 @@ helm-docs: printf "\n\n## License\n\n" ; \ cat LICENSE ; \ } > helm/kube-image-keeper/README.md.gotmpl - diff --git a/PROJECT b/PROJECT index da7eabe8..a47a5898 100644 --- a/PROJECT +++ b/PROJECT @@ -1,6 +1,11 @@ -domain: kuik.enix.io +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: enix.io layout: -- go.kubebuilder.io/v3 +- go.kubebuilder.io/v4 +multigroup: true projectName: kube-image-keeper repo: github.com/enix/kube-image-keeper resources: @@ -8,11 +13,14 @@ resources: crdVersion: v1 namespaced: true controller: true - domain: kuik.enix.io - group: kuik.enix.io + domain: enix.io + group: kuik kind: CachedImage - path: github.com/enix/kube-image-keeper/api/v1alpha1 + path: github.com/enix/kube-image-keeper/api/kuik/v1alpha1 version: v1alpha1 + webhooks: + defaulting: true + webhookVersion: v1 - controller: true group: core kind: Pod @@ -21,4 +29,13 @@ resources: webhooks: defaulting: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: enix.io + group: kuik + kind: Repository + path: github.com/enix/kube-image-keeper/api/kuik/v1alpha1 + version: v1alpha1 version: "3" diff --git a/README.md b/README.md index 0aae5b07..b7c8e265 100644 --- a/README.md +++ b/README.md @@ -10,20 +10,21 @@ It saves the container images used by your pods in its own local registry so tha ## Upgrading -### From 1.2.0 to 1.3.0 +### From 1.6.0 o 1.7.0 ***ACTION REQUIRED*** -In v1.3.0, we removed the finalizer `pod.kuik.enix.io/finalizer` from pods that were rewritten by kuik. +To follow Helm3 best pratices, we moved `cachedimage` and `repository` custom resources definition from the helm templates directory to the dedicated `crds` directory. +This will cause the `cachedimage` CRD to be deleted during the 1.7.0 upgrade. -To avoid having these pods stuck in `Terminating` state after a delete action or a rolling update, you will need to manually remove the finalizer from these pods once you upgrade to 1.3.0. -This can be achieved using the following command: +We advice you to uninstall your helm release, clean the remaining custom resources by removing their finalizer, then reinstall kuik in 1.7.0 +You may also recreate the custom resource definition right after the upgrade to 1.7.0 using ``` -kubectl get pods --all-namespaces -l kuik.enix.io/images-rewritten=true -o json | jq '.items[].metadata.finalizers=null' | kubectl replace -f - +kubectl apply -f https://raw.githubusercontent.com/enix/kube-image-keeper/main/helm/kube-image-keeper/crds/cachedimage-crd.yaml +kubectl apply -f https://raw.githubusercontent.com/enix/kube-image-keeper/main/helm/kube-image-keeper/crds/repository-crd.yaml ``` -***Ensure you run this command AFTER having fully upgraded to version 1.3.0. Otherwise, the kuik controllers will continuously re-add the finalizer to the rewritten pods.*** ## Why and when is it useful? @@ -67,19 +68,20 @@ We investigated other options, and we didn't find any that would quite fit our r ## Supported Kubernetes versions -kuik has been developed for, and tested with, Kubernetes 1.21 to 1.24; but the code doesn't use any deprecated (or new) feature or API, and should work with newer versions as well. (Community users have reported success with Kubernetes 1.26). +kuik has been developed for, and tested with, Kubernetes 1.24 to 1.28; but the code doesn't use any deprecated (or new) feature or API, and should work with newer versions as well. ## How it works -When a pod is created, kuik's **mutating webhook** rewrites its images on the fly, adding a `localhost:{port}/` prefix (the `port` is 7439 by default, and is configurable). +When a pod is created, kuik's **mutating webhook** rewrites its images on the fly to point to the local caching registry, adding a `localhost:{port}/` prefix (the `port` is 7439 by default, and is configurable). This means that you don't need to modify/rewrite the source registry url of your manifest/helm chart used to deploy your solution, kuik will take care of it. On `localhost:{port}`, there is an **image proxy** that serves images from kuik's **caching registry** (when the images have been cached) or directly from the original registry (when the images haven't been cached yet). One **controller** watches pods, and when it notices new images, it creates `CachedImage` custom resources for these images. -Another **controller** watches these `CachedImage` custom resources, and copies images from source registries to kuik's caching registry accordingly. +Another **controller** watches these `CachedImage` custom resources, and copies images from source registries to kuik's caching registry accordingly. When images come from a private registry, the controller will use the `imagePullSecrets` from the `CachedImage` spec, those are set from the pod that produced the `CachedImage`. Here is what our images look like when using kuik: + ```bash $ kubectl get pods -o custom-columns=NAME:metadata.name,IMAGES:spec.containers[*].image NAME IMAGES @@ -131,7 +133,6 @@ kube-image-keeper-proxy-54lzk 1m 19Mi Refer to the [dedicated documentation](https://github.com/enix/kube-image-keeper/blob/main/docs/metrics.md). - ## Installation 1. Make sure that you have cert-manager installed. If not, check its [installation page](https://cert-manager.io/docs/installation/) (it's fine to use the `kubectl apply` one-liner, and no further configuration is required). @@ -146,6 +147,10 @@ helm upgrade --install \ That's it! +Our container images are available across multiple registries for reliability. You can find them on [Github Container Registry](https://github.com/enix/kube-image-keeper/pkgs/container/kube-image-keeper), [Quay](https://quay.io/repository/enix/kube-image-keeper) and [DockerHub](https://hub.docker.com/r/enix/kube-image-keeper). + +CAUTION: If you use a storage backend that runs in the same cluster as kuik but in a different namespace, be sure to filter the storage backend's pods. Failure to do so may lead to interdependency issues, making it impossible to start both kuik and its storage backend if either encounters an issue. + ## Installation with plain YAML files @@ -184,12 +189,22 @@ helm upgrade --install \ There are 3 ways to tell kuik which pods it should manage (or, conversely, which ones it should ignore). - If a pod has the label `kube-image-keeper.enix.io/image-caching-policy=ignore`, kuik will ignore the pod (it will not rewrite its image references). -- If a pod is in an ignored Namespace, it will also be ignored. Namespaces can be ignored by setting the Helm value `controllers.webhook.ignoredNamespaces`, which defaults to `[kube-system]`. (Note: this feature relies on the [NamespaceDefaultLabelName](https://kubernetes.io/docs/concepts/services-networking/network-policies/#targeting-a-namespace-by-its-name) feature gate to work.) +- If a pod is in an ignored Namespace, it will also be ignored. Namespaces can be ignored by setting the Helm value `controllers.webhook.ignoredNamespaces` (`kube-system` and the kuik namespace will be ignored whatever the value of this parameter). (Note: this feature relies on the [NamespaceDefaultLabelName](https://kubernetes.io/docs/concepts/services-networking/network-policies/#targeting-a-namespace-by-its-name) feature gate to work.) - Finally, kuik will only work on pods matching a specific selector. By default, the selector is empty, which means "match all the pods". The selector can be set with the Helm value `controllers.webhook.objectSelector.matchExpressions`. This logic isn't implemented by the kuik controllers or webhook directly, but through Kubernetes' standard webhook object selectors. In other words, these parameters end up in the `MutatingWebhookConfiguration` template to filter which pods get presented to kuik's webhook. When the webhook rewrites the images for a pod, it adds a label to that pod, and the kuik controllers then rely on that label to know which `CachedImages` resources to create. -Keep in mind that kuik will ignore pods scheduled into its own namespace. +Keep in mind that kuik will ignore pods scheduled into its own namespace or in the `kube-system` namespace as recommended in the kubernetes documentation ([Avoiding operating on the kube-system namespace](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#avoiding-operating-on-the-kube-system-namespace)). + +> It is recommended to exclude the namespace where your webhook is running with a namespaceSelector. +> [...] +> Accidentally mutating or rejecting requests in the kube-system namespace may cause the control plane components to stop functioning or introduce unknown behavior. + +#### Image pull policy + +In the case of a container configured with `imagePullPolicy: Never`, the container will always be filtered out as it makes no sense to cache an image that would never be cached and always read from the disk. + +In the case of a container configured with `imagePullPolicy: Always`, or with the tag `latest`, or with no tag (defaulting to `latest`), by default, the container will be filtered out in order to keep the default behavior of kubernetes, which is to always pull the new version of the image (thus not using the cache of kuik). This can be disabled by setting the value `controllers.webhook.ignorePullPolicyAlways` to `false`. ### Cache persistence @@ -227,7 +242,7 @@ No manual action is required when migrating an amd64-only cluster from v1.3.0 to ### Corporate proxy -To configure kuik to work behind a corporate proxy, you can set the well known `http_proxy` and `https_proxy` environment variables (upper and lowercase variant both works) through helm values `.proxy.env` and `.controllers.env` like shown below: +To configure kuik to work behind a corporate proxy, you can set the well known `http_proxy` and `https_proxy` environment variables (upper and lowercase variant both works) through helm values `proxy.env` and `controllers.env` like shown below: ```yaml controllers: @@ -246,6 +261,27 @@ proxy: Be careful that both the proxy and the controllers need to access the kubernetes API, so you might need to define the `no_proxy` variable as well to ignore the kubernetes API in case it is not reachable from your proxy (which is true most of the time). +### Insecure registries & self-signed certificates + +In some cases, you may want to use images from self-hosted registries that are insecure (without TLS or with an invalid certificate for instance) or using a self-signed certificate. By default, kuik will not allow to cache images from those registries for security reasons, even though you configured your container runtime (e.g. Docker, containerd) to do so. However you can choose to trust a list of insecure registries to pull from using the helm value `insecureRegistries`. If you use a self-signed certificate you can store the root certificate authority in a secret and reference it with the helm value `rootCertificateAuthorities`. Here is an example of the use of those two values: + +```yaml +insecureRegistries: + - http://some-registry.com + - https://some-other-registry.com + +rootCertificateAuthorities: + secretName: some-secret + keys: + - root.pem +``` + +You can of course use as many insecure registries or root certificate authorities as you want. In the case of a self-signed certificate, you can either use the `insecureRegistries` or the `rootCertificateAuthorities` value, but trusting the root certificate will always be more secure than allowing insecure registries. + +### Registry UI + +For debugging reasons, it may be useful to be able to access the registry through an UI. This can be achieved by enabling the registry UI with the value `registryUI.enabled=true`. The UI will not be publicly available through an ingress, you will need to open a port-forward from port `80`. You can set a custom username and password with values `registryUI.auth.username` (default is `admin`) and `registryUI.auth.password` (empty by default). + ## Garbage collection and limitations When a CachedImage expires because it is not used anymore by the cluster, the image is deleted from the registry. However, since kuik uses [Docker's registry](https://docs.docker.com/registry/), this only deletes **reference files** like tags. It doesn't delete blobs, which account for most of the used disk space. [Garbage collection](https://docs.docker.com/registry/garbage-collection/) allows removing those blobs and free up space. The garbage collecting job can be configured to run thanks to the `registry.garbageCollectionSchedule` configuration in a cron-like format. It is disabled by default, because running garbage collection without persistence would just wipe out the cache registry. diff --git a/api/core/v1/pod_webhook.go b/api/core/v1/pod_webhook.go new file mode 100644 index 00000000..9a3ae133 --- /dev/null +++ b/api/core/v1/pod_webhook.go @@ -0,0 +1,196 @@ +package v1 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "regexp" + "strings" + + _ "crypto/sha256" + + "github.com/enix/kube-image-keeper/internal/controller/core" + "github.com/enix/kube-image-keeper/internal/registry" + "github.com/google/go-containerregistry/pkg/name" + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +//+kubebuilder:webhook:path=/mutate-core-v1-pod,mutating=true,failurePolicy=fail,sideEffects=None,groups=core,resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io,admissionReviewVersions=v1 + +var ( + errImageContainsDigests = errors.New("image contains a digest") + errPullPolicyAlways = errors.New("container is configured with imagePullPolicy: Always") + errPullPolicyNever = errors.New("container is configured with imagePullPolicy: Never") +) + +type ImageRewriter struct { + Client client.Client + IgnoreImages []*regexp.Regexp + IgnorePullPolicyAlways bool + ProxyPort int + Decoder *admission.Decoder +} + +type PodInitializer struct { + Client client.Client +} + +type RewrittenImage struct { + Original string + Rewritten string + NotRewrittenBecause string +} + +func (a *ImageRewriter) Handle(ctx context.Context, req admission.Request) admission.Response { + log := log. + FromContext(ctx). + WithName("webhook.pod") + + pod := &corev1.Pod{} + err := a.Decoder.Decode(req, pod) + if err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + + rewrittenImages := a.RewriteImages(pod, req.Operation == admissionv1.Create) + + log.Info("rewriting pod images", "rewrittenImages", rewrittenImages) + + marshaledPod, err := json.Marshal(pod) + if err != nil { + return admission.Errored(http.StatusInternalServerError, err) + } + + return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) +} + +func (a *ImageRewriter) RewriteImages(pod *corev1.Pod, isNewPod bool) []RewrittenImage { + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + + if pod.Labels == nil { + pod.Labels = map[string]string{} + } + + rewriteImages := pod.Annotations[core.AnnotationRewriteImagesName] == "true" || isNewPod + + pod.Labels[core.LabelManagedName] = "true" + pod.Annotations[core.AnnotationRewriteImagesName] = fmt.Sprintf("%t", rewriteImages) + + rewrittenImages := []RewrittenImage{} + + // Handle Containers + for i := range pod.Spec.Containers { + container := &pod.Spec.Containers[i] + rewrittenImage := a.handleContainer(pod, container, registry.ContainerAnnotationKey(container.Name, false), rewriteImages) + rewrittenImages = append(rewrittenImages, rewrittenImage) + } + + // Handle init containers + for i := range pod.Spec.InitContainers { + container := &pod.Spec.InitContainers[i] + rewrittenImage := a.handleContainer(pod, container, registry.ContainerAnnotationKey(container.Name, true), rewriteImages) + rewrittenImages = append(rewrittenImages, rewrittenImage) + } + + return rewrittenImages +} + +func (a *ImageRewriter) handleContainer(pod *corev1.Pod, container *corev1.Container, annotationKey string, rewriteImage bool) RewrittenImage { + if err := a.isImageRewritable(container); err != nil { + return RewrittenImage{ + Original: container.Image, + NotRewrittenBecause: err.Error(), + } + } + + re := regexp.MustCompile(`localhost:[0-9]+/`) + image := re.ReplaceAllString(container.Image, "") + + sourceRef, err := name.ParseReference(image, name.Insecure) + if err != nil { + return RewrittenImage{ + Original: container.Image, + NotRewrittenBecause: err.Error(), + } // ignore rewriting invalid images + } + + pod.Annotations[annotationKey] = image + + if !rewriteImage { + return RewrittenImage{ + Original: container.Image, + NotRewrittenBecause: "pod doesn't allow to rewrite its images", + } + } + + sanitizedRegistryName := strings.ReplaceAll(sourceRef.Context().RegistryStr(), ":", "-") + image = strings.ReplaceAll(image, sourceRef.Context().RegistryStr(), sanitizedRegistryName) + + originalImage := container.Image + container.Image = fmt.Sprintf("localhost:%d/%s", a.ProxyPort, image) + + return RewrittenImage{ + Original: originalImage, + Rewritten: container.Image, + } +} + +func (a *ImageRewriter) isImageRewritable(container *corev1.Container) error { + if strings.Contains(container.Image, "@") { + return errImageContainsDigests + } + + if container.ImagePullPolicy == corev1.PullNever { + return errPullPolicyNever + } + + if a.IgnorePullPolicyAlways { + pullAlways := container.ImagePullPolicy == corev1.PullAlways + isLatestWithoutPullPolicy := container.ImagePullPolicy == "" && (!strings.Contains(container.Image, ":") || strings.HasSuffix(container.Image, ":latest")) + if pullAlways || isLatestWithoutPullPolicy { + return errPullPolicyAlways + } + } + + for _, r := range a.IgnoreImages { + if r.MatchString(container.Image) { + return fmt.Errorf("image matches %s", r.String()) + } + } + + return nil +} + +func (p *PodInitializer) Start(ctx context.Context) error { + setupLog := ctrl.Log.WithName("setup.pods") + pods := corev1.PodList{} + err := p.Client.List(ctx, &pods) + if err != nil { + return err + } + + for _, pod := range pods.Items { + setupLog.Info("patching " + pod.Namespace + "/" + pod.Name) + err := p.Client.Patch(ctx, &pod, client.RawPatch(types.JSONPatchType, []byte("[]"))) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + } + + return nil +} + +func (t *PodInitializer) NeedLeaderElection() bool { + return true +} diff --git a/api/v1/pod_webhook_test.go b/api/core/v1/pod_webhook_test.go similarity index 65% rename from api/v1/pod_webhook_test.go rename to api/core/v1/pod_webhook_test.go index 22eb896e..a058d18b 100644 --- a/api/v1/pod_webhook_test.go +++ b/api/core/v1/pod_webhook_test.go @@ -2,15 +2,15 @@ package v1 import ( _ "crypto/sha256" + "errors" "regexp" "testing" - "github.com/enix/kube-image-keeper/controllers" + "github.com/enix/kube-image-keeper/internal/controller/core" "github.com/enix/kube-image-keeper/internal/registry" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) var podStub = corev1.Pod{ @@ -40,7 +40,11 @@ func TestRewriteImages(t *testing.T) { ir := ImageRewriter{ ProxyPort: 4242, } - ir.RewriteImages(&podStub) + + ir.RewriteImages(&podStub, false) + g.Expect(podStub.Annotations[core.AnnotationRewriteImagesName]).To(Equal("false")) + + ir.RewriteImages(&podStub, true) rewrittenInitContainers := []corev1.Container{ {Name: "a", Image: "localhost:4242/original-init"}, @@ -57,7 +61,7 @@ func TestRewriteImages(t *testing.T) { g.Expect(podStub.Spec.InitContainers).To(Equal(rewrittenInitContainers)) g.Expect(podStub.Spec.Containers).To(Equal(rewrittenContainers)) - g.Expect(podStub.Labels[controllers.LabelImageRewrittenName]).To(Equal("true")) + g.Expect(podStub.Labels[core.LabelManagedName]).To(Equal("true")) g.Expect(podStub.Annotations[registry.ContainerAnnotationKey("a", true)]).To(Equal("original-init")) g.Expect(podStub.Annotations[registry.ContainerAnnotationKey("b", false)]).To(Equal("original")) @@ -65,6 +69,9 @@ func TestRewriteImages(t *testing.T) { g.Expect(podStub.Annotations[registry.ContainerAnnotationKey("d", false)]).To(Equal("185.145.250.247:30042/alpine")) g.Expect(podStub.Annotations[registry.ContainerAnnotationKey("e", false)]).To(Equal("185.145.250.247:30042/alpine:latest")) g.Expect(podStub.Annotations[registry.ContainerAnnotationKey("f", false)]).To(Equal("")) + + ir.RewriteImages(&podStub, false) + g.Expect(podStub.Annotations[core.AnnotationRewriteImagesName]).To(Equal("true")) }) } @@ -80,7 +87,7 @@ func TestRewriteImagesWithIgnore(t *testing.T) { regexp.MustCompile("alpine:latest"), }, } - ir.RewriteImages(&podStub) + ir.RewriteImages(&podStub, true) rewrittenInitContainers := []corev1.Container{ {Name: "a", Image: "original-init"}, @@ -97,7 +104,7 @@ func TestRewriteImagesWithIgnore(t *testing.T) { g.Expect(podStub.Spec.InitContainers).To(Equal(rewrittenInitContainers)) g.Expect(podStub.Spec.Containers).To(Equal(rewrittenContainers)) - g.Expect(podStub.Labels[controllers.LabelImageRewrittenName]).To(Equal("true")) + g.Expect(podStub.Labels[core.LabelManagedName]).To(Equal("true")) g.Expect(podStub.Annotations[registry.ContainerAnnotationKey("a", true)]).To(Equal("")) g.Expect(podStub.Annotations[registry.ContainerAnnotationKey("b", false)]).To(Equal("")) @@ -108,21 +115,7 @@ func TestRewriteImagesWithIgnore(t *testing.T) { }) } -func TestInjectDecoder(t *testing.T) { - g := NewWithT(t) - t.Run("Inject decoder", func(t *testing.T) { - ir := ImageRewriter{} - decoder := &admission.Decoder{} - - g.Expect(ir.decoder).To(BeNil()) - err := ir.InjectDecoder(decoder) - g.Expect(err).To(Not(HaveOccurred())) - g.Expect(ir.decoder).To(Not(BeNil())) - g.Expect(ir.decoder).To(Equal(decoder)) - }) -} - -func Test_isImageIgnored(t *testing.T) { +func Test_isImageRewritable(t *testing.T) { emptyRegexps := []*regexp.Regexp{} someRegexps := []*regexp.Regexp{ regexp.MustCompile("alpine"), @@ -130,40 +123,74 @@ func Test_isImageIgnored(t *testing.T) { } tests := []struct { - name string - image string - regexps []*regexp.Regexp - ignored bool + name string + image string + imagePullPolicy corev1.PullPolicy + ignorePullPolicyAlways bool + regexps []*regexp.Regexp + err error }{ { name: "No regex", image: "alpine", regexps: emptyRegexps, - ignored: false, + err: nil, }, { name: "No regex with digest", image: "alpine:latest@sha256:5b161f051d017e55d358435f295f5e9a297e66158f136321d9b04520ec6c48a3", regexps: emptyRegexps, - ignored: true, + err: errImageContainsDigests, }, { name: "Match first regex", image: "alpine", regexps: someRegexps, - ignored: true, + err: errors.New("image matches alpine"), }, { name: "Match second regex", image: "nginx:latest", regexps: someRegexps, - ignored: true, + err: errors.New("image matches .*:latest"), }, { name: "Match no regex", image: "nginx", regexps: someRegexps, - ignored: false, + err: nil, + }, + { + name: "Ignore ImagePullPolicy: Always", + image: "nginx:1.0.0", + imagePullPolicy: corev1.PullAlways, + ignorePullPolicyAlways: true, + err: errPullPolicyAlways, + }, + { + name: "Ignore ImagePullPolicy: Always with tag latest", + image: "nginx:latest", + ignorePullPolicyAlways: true, + err: errPullPolicyAlways, + }, + { + name: "Ignore ImagePullPolicy: Always without any tag", + image: "nginx", + ignorePullPolicyAlways: true, + err: errPullPolicyAlways, + }, + { + name: "Ignore ImagePullPolicy: Always with tag latest but ImagePullPolicy: IfNotPresent", + image: "nginx:latest", + imagePullPolicy: corev1.PullIfNotPresent, + ignorePullPolicyAlways: true, + err: nil, + }, + { + name: "ImagePullPolicy: Never", + image: "nginx:1.0.0", + imagePullPolicy: corev1.PullNever, + err: errPullPolicyNever, }, } @@ -171,14 +198,21 @@ func Test_isImageIgnored(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { imageRewriter := ImageRewriter{ - IgnoreImages: tt.regexps, + IgnoreImages: tt.regexps, + IgnorePullPolicyAlways: tt.ignorePullPolicyAlways, } - ignored := imageRewriter.isImageIgnored(&corev1.Container{ - Image: tt.image, + err := imageRewriter.isImageRewritable(&corev1.Container{ + Image: tt.image, + ImagePullPolicy: tt.imagePullPolicy, }) - g.Expect(ignored).To(Equal(tt.ignored)) + if tt.err == nil { + g.Expect(err).To(BeNil()) + } else { + g.Expect(err).To(Equal(tt.err)) + } + }) } } diff --git a/api/v1alpha1/cachedimage_types.go b/api/kuik/v1alpha1/cachedimage_types.go similarity index 76% rename from api/v1alpha1/cachedimage_types.go rename to api/kuik/v1alpha1/cachedimage_types.go index b2edee75..9ddf41ef 100644 --- a/api/v1alpha1/cachedimage_types.go +++ b/api/kuik/v1alpha1/cachedimage_types.go @@ -12,9 +12,7 @@ type CachedImageSpec struct { // +optional ExpiresAt *metav1.Time `json:"expiresAt,omitempty"` // +optional - Retain bool `json:"retain,omitempty"` - PullSecretNames []string `json:"pullSecretNames,omitempty"` - PullSecretsNamespace string `json:"pullSecretsNamespace,omitempty"` + Retain bool `json:"retain,omitempty"` } type PodReference struct { @@ -31,12 +29,23 @@ type UsedBy struct { // CachedImageStatus defines the observed state of CachedImage type CachedImageStatus struct { IsCached bool `json:"isCached,omitempty"` - UsedBy UsedBy `json:"usedBy,omitempty" ` + Phase string `json:"phase,omitempty"` + UsedBy UsedBy `json:"usedBy,omitempty"` + + Digest string `json:"digest,omitempty"` + UpstreamDigest string `json:"upstreamDigest,omitempty"` + UpToDate bool `json:"upToDate,omitempty"` + LastSync metav1.Time `json:"lastSync,omitempty"` + LastSuccessfulPull metav1.Time `json:"lastSuccessfulPull,omitempty"` + + AvailableUpstream bool `json:"availableUpstream,omitempty"` + LastSeenUpstream metav1.Time `json:"lastSeenUpstream,omitempty"` } //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:scope=Cluster,shortName=ci +//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase" //+kubebuilder:printcolumn:name="Cached",type="boolean",JSONPath=".status.isCached" //+kubebuilder:printcolumn:name="Retain",type="boolean",JSONPath=".spec.retain" //+kubebuilder:printcolumn:name="Expires at",type="string",JSONPath=".spec.expiresAt" diff --git a/api/kuik/v1alpha1/cachedimage_utils.go b/api/kuik/v1alpha1/cachedimage_utils.go new file mode 100644 index 00000000..cd37905b --- /dev/null +++ b/api/kuik/v1alpha1/cachedimage_utils.go @@ -0,0 +1,41 @@ +package v1alpha1 + +import ( + "context" + + "github.com/distribution/reference" + "github.com/enix/kube-image-keeper/internal/registry" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (r *CachedImage) Repository() (reference.Named, error) { + named, err := reference.ParseNormalizedNamed(r.Spec.SourceImage) + if err != nil { + return nil, err + } + + return named, nil +} + +func (r *CachedImage) GetPullSecrets(apiReader client.Reader) ([]corev1.Secret, error) { + named, err := r.Repository() + if err != nil { + return nil, err + } + + repository := Repository{} + err = apiReader.Get(context.TODO(), types.NamespacedName{Name: registry.SanitizeName(named.Name())}, &repository) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + + pullSecrets, err := repository.GetPullSecrets(apiReader) + if err != nil { + return nil, err + } + + return pullSecrets, nil +} diff --git a/api/kuik/v1alpha1/cachedimage_webhook.go b/api/kuik/v1alpha1/cachedimage_webhook.go new file mode 100644 index 00000000..baa98b58 --- /dev/null +++ b/api/kuik/v1alpha1/cachedimage_webhook.go @@ -0,0 +1,37 @@ +package v1alpha1 + +import ( + "context" + + "github.com/distribution/reference" + "github.com/enix/kube-image-keeper/internal/registry" + runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" +) + +func (r *CachedImage) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + WithDefaulter(r). + For(r). + Complete() +} + +//+kubebuilder:webhook:path=/mutate-kuik-enix-io-v1alpha1-cachedimage,mutating=true,failurePolicy=fail,sideEffects=None,groups=kuik.enix.io,resources=cachedimages,verbs=create;update,versions=v1alpha1,name=mcachedimage.kb.io,admissionReviewVersions=v1 + +func (r *CachedImage) Default(ctx context.Context, obj runtime.Object) error { + cachedImage := obj.(*CachedImage) + + named, err := reference.ParseNormalizedNamed(cachedImage.Spec.SourceImage) + if err != nil { + return field.Invalid(field.NewPath("spec.sourceImage"), cachedImage.Spec.SourceImage, err.Error()) + } + + if cachedImage.Labels == nil { + cachedImage.Labels = map[string]string{} + } + + cachedImage.Labels[RepositoryLabelName] = registry.RepositoryLabel(named.Name()) + + return nil +} diff --git a/api/kuik/v1alpha1/cachedimage_webhook_test.go b/api/kuik/v1alpha1/cachedimage_webhook_test.go new file mode 100644 index 00000000..dc061f72 --- /dev/null +++ b/api/kuik/v1alpha1/cachedimage_webhook_test.go @@ -0,0 +1,54 @@ +package v1alpha1 + +import ( + "context" + "testing" + + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +func TestDefault(t *testing.T) { + cachedImageStub := CachedImage{} + + g := NewWithT(t) + tests := []struct { + name string + sourceImage string + expectedRepositoryLabel string + wantErr error + }{ + { + name: "Simple image name", + sourceImage: "alpine", + expectedRepositoryLabel: "docker.io-library-alpine", + }, + { + name: "Advanced image name", + sourceImage: "quay.io/jetstack/cert-manager-controller:v1.13.2", + expectedRepositoryLabel: "quay.io-jetstack-cert-manager-controller", + }, + { + name: "Invalid image name", + sourceImage: "@@@", + wantErr: field.Invalid(field.NewPath("spec.sourceImage"), "@@@", "invalid reference format"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cachedImage := cachedImageStub.DeepCopy() + cachedImage.Spec.SourceImage = tt.sourceImage + + err := (&CachedImage{}).Default(context.TODO(), cachedImage) + + if tt.wantErr == nil { + g.Expect(cachedImage.Labels).ToNot(BeNil()) + g.Expect(cachedImage.Labels[RepositoryLabelName]).To(Equal(tt.expectedRepositoryLabel)) + } else { + g.Expect(err).To(Equal(tt.wantErr)) + } + + }) + } +} diff --git a/api/v1alpha1/groupversion_info.go b/api/kuik/v1alpha1/groupversion_info.go similarity index 100% rename from api/v1alpha1/groupversion_info.go rename to api/kuik/v1alpha1/groupversion_info.go diff --git a/api/kuik/v1alpha1/repository_types.go b/api/kuik/v1alpha1/repository_types.go new file mode 100644 index 00000000..7b4528a1 --- /dev/null +++ b/api/kuik/v1alpha1/repository_types.go @@ -0,0 +1,56 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// RepositorySpec defines the desired state of Repository +type RepositorySpec struct { + Name string `json:"name"` + PullSecretNames []string `json:"pullSecretNames,omitempty"` + PullSecretsNamespace string `json:"pullSecretsNamespace,omitempty"` + UpdateInterval *metav1.Duration `json:"updateInterval,omitempty"` + UpdateFilters []string `json:"updateFilters,omitempty"` +} + +// RepositoryStatus defines the observed state of Repository +type RepositoryStatus struct { + Images int `json:"images,omitempty"` + Phase string `json:"phase,omitempty"` + LastUpdate metav1.Time `json:"lastUpdate,omitempty"` + //+listType=map + //+listMapKey=type + //+patchStrategy=merge + //+patchMergeKey=type + //+optional + Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:scope=Cluster,shortName=repo +//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.phase" +//+kubebuilder:printcolumn:name="Images",type="string",JSONPath=".status.images" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// Repository is the Schema for the repositories API +type Repository struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RepositorySpec `json:"spec,omitempty"` + Status RepositoryStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// RepositoryList contains a list of Repository +type RepositoryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Repository `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Repository{}, &RepositoryList{}) +} diff --git a/api/kuik/v1alpha1/repository_utils.go b/api/kuik/v1alpha1/repository_utils.go new file mode 100644 index 00000000..a057cc57 --- /dev/null +++ b/api/kuik/v1alpha1/repository_utils.go @@ -0,0 +1,32 @@ +package v1alpha1 + +import ( + "regexp" + + "github.com/enix/kube-image-keeper/internal/registry" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func (r *Repository) CompileUpdateFilters() ([]regexp.Regexp, error) { + regexps := make([]regexp.Regexp, len(r.Spec.UpdateFilters)) + + for i, updateFilter := range r.Spec.UpdateFilters { + r, err := regexp.Compile(updateFilter) + if err != nil { + return nil, err + } + regexps[i] = *r + } + + return regexps, nil +} + +func (r *Repository) GetPullSecrets(apiReader client.Reader) ([]corev1.Secret, error) { + pullSecrets, err := registry.GetPullSecrets(apiReader, r.Spec.PullSecretsNamespace, r.Spec.PullSecretNames) + if err != nil { + return nil, err + } + + return pullSecrets, nil +} diff --git a/api/v1/pod_webhook.go b/api/v1/pod_webhook.go deleted file mode 100644 index 7d024ce8..00000000 --- a/api/v1/pod_webhook.go +++ /dev/null @@ -1,108 +0,0 @@ -package v1 - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "regexp" - "strings" - - _ "crypto/sha256" - - "github.com/enix/kube-image-keeper/controllers" - "github.com/enix/kube-image-keeper/internal/registry" - "github.com/google/go-containerregistry/pkg/name" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -//+kubebuilder:webhook:path=/mutate-core-v1-pod,mutating=true,failurePolicy=fail,sideEffects=None,groups=core,resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io,admissionReviewVersions=v1 - -type ImageRewriter struct { - Client client.Client - IgnoreImages []*regexp.Regexp - ProxyPort int - decoder *admission.Decoder -} - -func (a *ImageRewriter) Handle(ctx context.Context, req admission.Request) admission.Response { - pod := &corev1.Pod{} - err := a.decoder.Decode(req, pod) - if err != nil { - return admission.Errored(http.StatusBadRequest, err) - } - - a.RewriteImages(pod) - - marshaledPod, err := json.Marshal(pod) - if err != nil { - return admission.Errored(http.StatusInternalServerError, err) - } - - return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod) -} - -func (a *ImageRewriter) RewriteImages(pod *corev1.Pod) { - if pod.Annotations == nil { - pod.Annotations = map[string]string{} - } - - if pod.Labels == nil { - pod.Labels = map[string]string{} - } - - pod.Labels[controllers.LabelImageRewrittenName] = "true" - - // Handle Containers - for i := range pod.Spec.Containers { - container := &pod.Spec.Containers[i] - a.handleContainer(pod, container, registry.ContainerAnnotationKey(container.Name, false)) - } - - // Handle init containers - for i := range pod.Spec.InitContainers { - container := &pod.Spec.InitContainers[i] - a.handleContainer(pod, container, registry.ContainerAnnotationKey(container.Name, true)) - } -} - -// InjectDecoder injects the decoder -func (a *ImageRewriter) InjectDecoder(d *admission.Decoder) error { - a.decoder = d - return nil -} - -func (a *ImageRewriter) handleContainer(pod *corev1.Pod, container *corev1.Container, annotationKey string) { - if a.isImageIgnored(container) { - return - } - - re := regexp.MustCompile(`localhost:[0-9]+/`) - image := re.ReplaceAllString(container.Image, "") - - sourceRef, err := name.ParseReference(image, name.Insecure) - if err != nil { - return // ignore rewriting invalid images - } - - pod.Annotations[annotationKey] = image - - sanitizedRegistryName := strings.ReplaceAll(sourceRef.Context().RegistryStr(), ":", "-") - image = strings.ReplaceAll(image, sourceRef.Context().RegistryStr(), sanitizedRegistryName) - - container.Image = fmt.Sprintf("localhost:%d/%s", a.ProxyPort, image) -} - -func (a *ImageRewriter) isImageIgnored(container *corev1.Container) (ignored bool) { - if strings.Contains(container.Image, "@") { - return true - } - for _, r := range a.IgnoreImages { - if r.MatchString(container.Image) { - return true - } - } - return -} diff --git a/cmd/cache/main.go b/cmd/cache/main.go index 83897589..2140bef6 100644 --- a/cmd/cache/main.go +++ b/cmd/cache/main.go @@ -15,10 +15,14 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - kuikenixiov1 "github.com/enix/kube-image-keeper/api/v1" - "github.com/enix/kube-image-keeper/controllers" + kuikenixiov1 "github.com/enix/kube-image-keeper/api/core/v1" + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" "github.com/enix/kube-image-keeper/internal" + kuikController "github.com/enix/kube-image-keeper/internal/controller" + "github.com/enix/kube-image-keeper/internal/controller/core" + "github.com/enix/kube-image-keeper/internal/controller/kuik" "github.com/enix/kube-image-keeper/internal/registry" "github.com/enix/kube-image-keeper/internal/scheme" //+kubebuilder:scaffold:imports @@ -35,6 +39,7 @@ func main() { var expiryDelay uint var proxyPort int var ignoreImages internal.RegexpArrayFlags + var ignorePullPolicyAlways bool var architectures internal.ArrayFlags var maxConcurrentCachedImageReconciles int var insecureRegistries internal.ArrayFlags @@ -47,6 +52,7 @@ func main() { flag.UintVar(&expiryDelay, "expiry-delay", 30, "The delay in days before deleting an unused CachedImage.") flag.IntVar(&proxyPort, "proxy-port", 8082, "The port on which the registry proxy accepts connections on each host.") flag.Var(&ignoreImages, "ignore-images", "Regex that represents images to be excluded (this flag can be used multiple times).") + flag.BoolVar(&ignorePullPolicyAlways, "ignore-pull-policy-always", true, "Ignore containers that are configured with imagePullPolicy: Always") flag.Var(&architectures, "arch", "Architecture of image to put in cache (this flag can be used multiple times).") flag.StringVar(®istry.Endpoint, "registry-endpoint", "kube-image-keeper-registry:5000", "The address of the registry where cached images are stored.") flag.IntVar(&maxConcurrentCachedImageReconciles, "max-concurrent-cached-image-reconciles", 3, "Maximum number of CachedImages that can be handled and reconciled at the same time (put or removed from cache).") @@ -82,7 +88,7 @@ func main() { os.Exit(1) } - if err = (&controllers.CachedImageReconciler{ + if err = (&kuik.CachedImageReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Recorder: mgr.GetEventRecorderFor("cachedimage-controller"), @@ -95,7 +101,7 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "CachedImage") os.Exit(1) } - if err = (&controllers.PodReconciler{ + if err = (&core.PodReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { @@ -103,30 +109,50 @@ func main() { os.Exit(1) } imageRewriter := kuikenixiov1.ImageRewriter{ - Client: mgr.GetClient(), - IgnoreImages: ignoreImages, - ProxyPort: proxyPort, + Client: mgr.GetClient(), + IgnoreImages: ignoreImages, + IgnorePullPolicyAlways: ignorePullPolicyAlways, + ProxyPort: proxyPort, + Decoder: admission.NewDecoder(mgr.GetScheme()), } mgr.GetWebhookServer().Register("/mutate-core-v1-pod", &webhook.Admission{Handler: &imageRewriter}) + if err = (&kuikv1alpha1.CachedImage{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "CachedImage") + os.Exit(1) + } + if err = (&kuik.RepositoryReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("epository-controller"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Repository") + os.Exit(1) + } //+kubebuilder:scaffold:builder - if err := mgr.AddHealthzCheck("healthz", controllers.MakeChecker(controllers.Healthz)); err != nil { + err = mgr.Add(&kuikenixiov1.PodInitializer{Client: mgr.GetClient()}) + if err != nil { + setupLog.Error(err, "unable to setup PodInitializer") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("healthz", kuikController.MakeChecker(kuikController.Healthz)); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) } - if err := mgr.AddReadyzCheck("readyz", controllers.MakeChecker(controllers.Readyz)); err != nil { + if err := mgr.AddReadyzCheck("readyz", kuikController.MakeChecker(kuikController.Readyz)); err != nil { setupLog.Error(err, "unable to set up ready check") os.Exit(1) } - controllers.SetLeader(false) + kuikController.SetLeader(false) go func() { <-mgr.Elected() - controllers.SetLeader(true) + kuikController.SetLeader(true) }() - controllers.ProbeAddr = probeAddr - controllers.RegisterMetrics(mgr.GetClient()) + kuikController.ProbeAddr = probeAddr + kuikController.RegisterMetrics(mgr.GetClient()) setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index 47b05097..7652bb28 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -21,6 +21,7 @@ import ( var ( kubeconfig string + proxyAddr string metricsAddr string rateLimitQPS int rateLimitBurst int @@ -33,8 +34,8 @@ func initFlags() { if err := flag.Set("logtostderr", "true"); err != nil { fmt.Fprint(os.Stderr, "could not enable logging to stderr") } + flag.StringVar(&proxyAddr, "bind-address", ":8082", "The address the proxy registry endpoint binds to.") flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") - flag.StringVar(&kubeconfig, "kubeconfig", "", "Absolute path to the kubeconfig file") flag.StringVar(®istry.Endpoint, "registry-endpoint", "kube-image-keeper-registry:5000", "The address of the registry where cached images are stored.") flag.IntVar(&rateLimitQPS, "kube-api-rate-limit-qps", 0, "Kubernetes API request rate limit") flag.IntVar(&rateLimitBurst, "kube-api-rate-limit-burst", 0, "Kubernetes API request burst") @@ -58,13 +59,17 @@ func main() { config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) } + if err != nil { + panic(err) + } + klog.Info("starting") + httpClient, err := rest.HTTPClientFor(config) if err != nil { panic(err) } - - restMapper, err := apiutil.NewDynamicRESTMapper(config, apiutil.WithLazyDiscovery) + restMapper, err := apiutil.NewDynamicRESTMapper(config, httpClient) if err != nil { panic(err) } @@ -88,5 +93,5 @@ func main() { panic(fmt.Errorf("could not load root certificate authorities: %s", err)) } - <-proxy.New(k8sClient, metricsAddr, []string(insecureRegistries), rootCAs).Run() + <-proxy.New(k8sClient, metricsAddr, []string(insecureRegistries), rootCAs).Run(proxyAddr) } diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 00000000..a8952fff --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,8 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'header-max-length': [0], + 'body-max-line-length': [0], + 'footer-max-line-length': [0], + }, +}; diff --git a/config/certmanager/certificate.yaml b/config/certmanager/certificate.yaml index 52d86618..cc679de9 100644 --- a/config/certmanager/certificate.yaml +++ b/config/certmanager/certificate.yaml @@ -4,6 +4,13 @@ apiVersion: cert-manager.io/v1 kind: Issuer metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: selfsigned-issuer namespace: system spec: @@ -12,13 +19,20 @@ spec: apiVersion: cert-manager.io/v1 kind: Certificate metadata: + labels: + app.kubernetes.io/name: certificate + app.kubernetes.io/instance: serving-cert + app.kubernetes.io/component: certificate + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: - # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize + # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize dnsNames: - - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc - - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local + - SERVICE_NAME.SERVICE_NAMESPACE.svc + - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer diff --git a/config/certmanager/kustomizeconfig.yaml b/config/certmanager/kustomizeconfig.yaml index 90d7c313..cf6f89e8 100644 --- a/config/certmanager/kustomizeconfig.yaml +++ b/config/certmanager/kustomizeconfig.yaml @@ -1,4 +1,4 @@ -# This configuration is for teaching kustomize how to update name ref and var substitution +# This configuration is for teaching kustomize how to update name ref substitution nameReference: - kind: Issuer group: cert-manager.io @@ -6,11 +6,3 @@ nameReference: - kind: Certificate group: cert-manager.io path: spec/issuerRef/name - -varReference: -- kind: Certificate - group: cert-manager.io - path: spec/commonName -- kind: Certificate - group: cert-manager.io - path: spec/dnsNames diff --git a/config/crd/bases/kuik.enix.io_cachedimages.yaml b/config/crd/bases/kuik.enix.io_cachedimages.yaml index 5e94b193..efbe1f37 100644 --- a/config/crd/bases/kuik.enix.io_cachedimages.yaml +++ b/config/crd/bases/kuik.enix.io_cachedimages.yaml @@ -3,8 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.15.0 name: cachedimages.kuik.enix.io spec: group: kuik.enix.io @@ -18,6 +17,9 @@ spec: scope: Cluster versions: - additionalPrinterColumns: + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .status.isCached name: Cached type: boolean @@ -39,14 +41,19 @@ spec: description: CachedImage is the Schema for the cachedimages 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' + 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' + 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 @@ -56,12 +63,6 @@ spec: expiresAt: format: date-time type: string - pullSecretNames: - items: - type: string - type: array - pullSecretsNamespace: - type: string retain: type: boolean sourceImage: @@ -72,13 +73,32 @@ spec: status: description: CachedImageStatus defines the observed state of CachedImage properties: + availableUpstream: + type: boolean + digest: + type: string isCached: type: boolean + lastSeenUpstream: + format: date-time + type: string + lastSuccessfulPull: + format: date-time + type: string + lastSync: + format: date-time + type: string + phase: + type: string + upToDate: + type: boolean + upstreamDigest: + type: string usedBy: properties: count: - description: jsonpath function .length() is not implemented, so - the count field is required to display pods count in additionalPrinterColumns + description: |- + jsonpath function .length() is not implemented, so the count field is required to display pods count in additionalPrinterColumns see https://github.com/kubernetes-sigs/controller-tools/issues/447 type: integer pods: @@ -95,9 +115,3 @@ spec: storage: true subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/config/crd/bases/kuik.enix.io_repositories.yaml b/config/crd/bases/kuik.enix.io_repositories.yaml new file mode 100644 index 00000000..0a72227c --- /dev/null +++ b/config/crd/bases/kuik.enix.io_repositories.yaml @@ -0,0 +1,158 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: repositories.kuik.enix.io +spec: + group: kuik.enix.io + names: + kind: Repository + listKind: RepositoryList + plural: repositories + shortNames: + - repo + singular: repository + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .status.images + name: Images + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Repository is the Schema for the repositories 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: RepositorySpec defines the desired state of Repository + properties: + name: + type: string + pullSecretNames: + items: + type: string + type: array + pullSecretsNamespace: + type: string + updateFilters: + items: + type: string + type: array + updateInterval: + type: string + required: + - name + type: object + status: + description: RepositoryStatus defines the observed state of Repository + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + images: + type: integer + lastUpdate: + format: date-time + type: string + phase: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 85835e6d..f7a5200b 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -3,17 +3,20 @@ # It should be run by config/default resources: - bases/kuik.enix.io_cachedimages.yaml +- bases/kuik.enix.io_repositories.yaml #+kubebuilder:scaffold:crdkustomizeresource -patchesStrategicMerge: [] +patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD -#- patches/webhook_in_cachedimages.yaml +#- path: patches/webhook_in_cachedimages.yaml +#- path: patches/webhook_in_repositories.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch -# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. +# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD -#- patches/cainjection_in_cachedimages.yaml +#- path: patches/cainjection_in_cachedimages.yaml +#- path: patches/cainjection_in_repositories.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_cachedimages.yaml b/config/crd/patches/cainjection_in_kuik_cachedimages.yaml similarity index 71% rename from config/crd/patches/cainjection_in_cachedimages.yaml rename to config/crd/patches/cainjection_in_kuik_cachedimages.yaml index 600fbec9..d5e7a3bc 100644 --- a/config/crd/patches/cainjection_in_cachedimages.yaml +++ b/config/crd/patches/cainjection_in_kuik_cachedimages.yaml @@ -3,5 +3,5 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME name: cachedimages.kuik.enix.io diff --git a/config/crd/patches/cainjection_in_kuik_repositories.yaml b/config/crd/patches/cainjection_in_kuik_repositories.yaml new file mode 100644 index 00000000..98171983 --- /dev/null +++ b/config/crd/patches/cainjection_in_kuik_repositories.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME + name: repositories.kuik.enix.io diff --git a/config/crd/patches/webhook_in_cachedimages.yaml b/config/crd/patches/webhook_in_kuik_cachedimages.yaml similarity index 100% rename from config/crd/patches/webhook_in_cachedimages.yaml rename to config/crd/patches/webhook_in_kuik_cachedimages.yaml diff --git a/config/crd/patches/webhook_in_kuik_repositories.yaml b/config/crd/patches/webhook_in_kuik_repositories.yaml new file mode 100644 index 00000000..ac9263a3 --- /dev/null +++ b/config/crd/patches/webhook_in_kuik_repositories.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: repositories.kuik.enix.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 4ef3ef84..822f8d02 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -1,26 +1,28 @@ # Adds namespace to all resources. -namespace: kuik-system +namespace: kube-image-keeper-system # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. -namePrefix: kuik- +namePrefix: kube-image-keeper- # Labels to add to all resources and selectors. -#commonLabels: -# someName: someValue +#labels: +#- includeSelectors: true +# pairs: +# someName: someValue -bases: +resources: - ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -- ../webhook +#- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. -- ../certmanager +#- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus @@ -28,47 +30,115 @@ patchesStrategicMerge: # Protect the /metrics endpoint by putting it behind auth. # If you want your controller-manager to expose the /metrics # endpoint w/o any authn/z, please comment the following line. -# - manager_auth_proxy_patch.yaml +- manager_auth_proxy_patch.yaml + -# Mount the controller config file for loading manager configurations -# through a ComponentConfig type -# - manager_config_patch.yaml # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml -- manager_webhook_patch.yaml +#- manager_webhook_patch.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. # 'CERTMANAGER' needs to be enabled to use ca injection -- webhookcainjection_patch.yaml +#- webhookcainjection_patch.yaml -# the following config is for teaching kustomize how to do var substitution -vars: # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. -- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR - objref: - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert # this name should match the one in certificate.yaml - fieldref: - fieldpath: metadata.namespace -- name: CERTIFICATE_NAME - objref: - kind: Certificate - group: cert-manager.io - version: v1 - name: serving-cert # this name should match the one in certificate.yaml -- name: SERVICE_NAMESPACE # namespace of the service - objref: - kind: Service - version: v1 - name: webhook-service - fieldref: - fieldpath: metadata.namespace -- name: SERVICE_NAME - objref: - kind: Service - version: v1 - name: webhook-service +# Uncomment the following replacements to add the cert-manager CA injection annotations +#replacements: +# - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.namespace # namespace of the certificate CR +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 0 +# create: true +# - source: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# name: serving-cert # this name should match the one in certificate.yaml +# fieldPath: .metadata.name +# targets: +# - select: +# kind: ValidatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: MutatingWebhookConfiguration +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - select: +# kind: CustomResourceDefinition +# fieldPaths: +# - .metadata.annotations.[cert-manager.io/inject-ca-from] +# options: +# delimiter: '/' +# index: 1 +# create: true +# - source: # Add cert-manager annotation to the webhook Service +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.name # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 0 +# create: true +# - source: +# kind: Service +# version: v1 +# name: webhook-service +# fieldPath: .metadata.namespace # namespace of the service +# targets: +# - select: +# kind: Certificate +# group: cert-manager.io +# version: v1 +# fieldPaths: +# - .spec.dnsNames.0 +# - .spec.dnsNames.1 +# options: +# delimiter: '.' +# index: 1 +# create: true diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml index a224be19..73fad2a6 100644 --- a/config/default/manager_auth_proxy_patch.yaml +++ b/config/default/manager_auth_proxy_patch.yaml @@ -10,15 +10,28 @@ spec: spec: containers: - name: kube-rbac-proxy - image: gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.1 args: - "--secure-listen-address=0.0.0.0:8443" - "--upstream=http://127.0.0.1:8080/" - "--logtostderr=true" - - "--v=10" + - "--v=0" ports: - containerPort: 8443 + protocol: TCP name: https + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 5m + memory: 64Mi - name: manager args: - "--health-probe-bind-address=:8081" diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml index 6c400155..f6f58916 100644 --- a/config/default/manager_config_patch.yaml +++ b/config/default/manager_config_patch.yaml @@ -8,13 +8,3 @@ spec: spec: containers: - name: manager - args: - - "--config=controller_manager_config.yaml" - volumeMounts: - - name: manager-config - mountPath: /controller_manager_config.yaml - subPath: controller_manager_config.yaml - volumes: - - name: manager-config - configMap: - name: manager-config diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml index f9012c9b..7322774b 100644 --- a/config/default/webhookcainjection_patch.yaml +++ b/config/default/webhookcainjection_patch.yaml @@ -1,15 +1,29 @@ # This patch add annotation to admission webhook config and -# the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. +# CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: + labels: + app.kubernetes.io/name: mutatingwebhookconfiguration + app.kubernetes.io/instance: mutating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: mutating-webhook-configuration annotations: - cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) -# --- -# apiVersion: admissionregistration.k8s.io/v1 -# kind: ValidatingWebhookConfiguration -# metadata: -# name: validating-webhook-configuration -# annotations: -# cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: validatingwebhookconfiguration + app.kubernetes.io/instance: validating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME diff --git a/config/manager/controller_manager_config.yaml b/config/manager/controller_manager_config.yaml deleted file mode 100644 index 4b924f28..00000000 --- a/config/manager/controller_manager_config.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 -kind: ControllerManagerConfig -health: - healthProbeBindAddress: :8081 -metrics: - bindAddress: 127.0.0.1:8080 -webhook: - port: 9443 -leaderElection: - leaderElect: true - resourceName: a046788b.kuik.enix.io diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 7bd1c63d..5c5f0b84 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,16 +1,2 @@ resources: - manager.yaml - -generatorOptions: - disableNameSuffixHash: true - -configMapGenerator: -- files: - - controller_manager_config.yaml - name: manager-config -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -images: -- name: controller - newName: enix/kube-image-keeper - newTag: latest diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index d1258b1a..07aadcd5 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -3,6 +3,12 @@ kind: Namespace metadata: labels: control-plane: controller-manager + app.kubernetes.io/name: namespace + app.kubernetes.io/instance: system + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: system --- apiVersion: apps/v1 @@ -12,6 +18,12 @@ metadata: namespace: system labels: control-plane: controller-manager + app.kubernetes.io/name: deployment + app.kubernetes.io/instance: controller-manager + app.kubernetes.io/component: manager + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize spec: selector: matchLabels: @@ -19,20 +31,52 @@ spec: replicas: 1 template: metadata: + annotations: + kubectl.kubernetes.io/default-container: manager labels: control-plane: controller-manager spec: + # TODO(user): Uncomment the following code to configure the nodeAffinity expression + # according to the platforms which are supported by your solution. + # It is considered best practice to support multiple architectures. You can + # build your manager image using the makefile target docker-buildx. + # affinity: + # nodeAffinity: + # requiredDuringSchedulingIgnoredDuringExecution: + # nodeSelectorTerms: + # - matchExpressions: + # - key: kubernetes.io/arch + # operator: In + # values: + # - amd64 + # - arm64 + # - ppc64le + # - s390x + # - key: kubernetes.io/os + # operator: In + # values: + # - linux securityContext: runAsNonRoot: true + # TODO(user): For common cases that do not require escalating privileges + # it is recommended to ensure that all your Pods/Containers are restrictive. + # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted + # Please uncomment the following code if your project does NOT have to work on old Kubernetes + # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). + # seccompProfile: + # type: RuntimeDefault containers: - command: - - manager + - /manager args: - --leader-elect image: controller:latest name: manager securityContext: allowPrivilegeEscalation: false + capabilities: + drop: + - "ALL" livenessProbe: httpGet: path: /healthz @@ -45,12 +89,14 @@ spec: port: 8081 initialDelaySeconds: 5 periodSeconds: 10 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: limits: - cpu: 100m - memory: 30Mi + cpu: 500m + memory: 128Mi requests: - cpu: 100m - memory: 20Mi + cpu: 10m + memory: 64Mi serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 diff --git a/config/prometheus/monitor.yaml b/config/prometheus/monitor.yaml index d19136ae..ddb1223b 100644 --- a/config/prometheus/monitor.yaml +++ b/config/prometheus/monitor.yaml @@ -5,6 +5,12 @@ kind: ServiceMonitor metadata: labels: control-plane: controller-manager + app.kubernetes.io/name: servicemonitor + app.kubernetes.io/instance: controller-manager-metrics-monitor + app.kubernetes.io/component: metrics + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-monitor namespace: system spec: diff --git a/config/rbac/auth_proxy_client_clusterrole.yaml b/config/rbac/auth_proxy_client_clusterrole.yaml index 51a75db4..5ef6205b 100644 --- a/config/rbac/auth_proxy_client_clusterrole.yaml +++ b/config/rbac/auth_proxy_client_clusterrole.yaml @@ -1,6 +1,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: metrics-reader + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: metrics-reader rules: - nonResourceURLs: diff --git a/config/rbac/auth_proxy_role.yaml b/config/rbac/auth_proxy_role.yaml index 80e1857c..028d1fc0 100644 --- a/config/rbac/auth_proxy_role.yaml +++ b/config/rbac/auth_proxy_role.yaml @@ -1,6 +1,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: proxy-role + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: proxy-role rules: - apiGroups: diff --git a/config/rbac/auth_proxy_role_binding.yaml b/config/rbac/auth_proxy_role_binding.yaml index ec7acc0a..f9d4a68f 100644 --- a/config/rbac/auth_proxy_role_binding.yaml +++ b/config/rbac/auth_proxy_role_binding.yaml @@ -1,6 +1,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: proxy-rolebinding + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: proxy-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io diff --git a/config/rbac/auth_proxy_service.yaml b/config/rbac/auth_proxy_service.yaml index 6cf656be..d2b8fc52 100644 --- a/config/rbac/auth_proxy_service.yaml +++ b/config/rbac/auth_proxy_service.yaml @@ -3,12 +3,19 @@ kind: Service metadata: labels: control-plane: controller-manager + app.kubernetes.io/name: service + app.kubernetes.io/instance: controller-manager-metrics-service + app.kubernetes.io/component: kube-rbac-proxy + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-service namespace: system spec: ports: - name: https port: 8443 + protocol: TCP targetPort: https selector: control-plane: controller-manager diff --git a/config/rbac/cachedimage_editor_role.yaml b/config/rbac/kuik_cachedimage_editor_role.yaml similarity index 56% rename from config/rbac/cachedimage_editor_role.yaml rename to config/rbac/kuik_cachedimage_editor_role.yaml index 3d5d057c..6e13e074 100644 --- a/config/rbac/cachedimage_editor_role.yaml +++ b/config/rbac/kuik_cachedimage_editor_role.yaml @@ -2,6 +2,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: cachedimage-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: cachedimage-editor-role rules: - apiGroups: diff --git a/config/rbac/cachedimage_viewer_role.yaml b/config/rbac/kuik_cachedimage_viewer_role.yaml similarity index 53% rename from config/rbac/cachedimage_viewer_role.yaml rename to config/rbac/kuik_cachedimage_viewer_role.yaml index 45de0f07..8311dcac 100644 --- a/config/rbac/cachedimage_viewer_role.yaml +++ b/config/rbac/kuik_cachedimage_viewer_role.yaml @@ -2,6 +2,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: cachedimage-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: cachedimage-viewer-role rules: - apiGroups: diff --git a/config/rbac/kuik_repository_editor_role.yaml b/config/rbac/kuik_repository_editor_role.yaml new file mode 100644 index 00000000..a8fe821a --- /dev/null +++ b/config/rbac/kuik_repository_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit repositories. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: repository-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize + name: repository-editor-role +rules: +- apiGroups: + - kuik.enix.io + resources: + - repositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - kuik.enix.io + resources: + - repositories/status + verbs: + - get diff --git a/config/rbac/kuik_repository_viewer_role.yaml b/config/rbac/kuik_repository_viewer_role.yaml new file mode 100644 index 00000000..e04769d4 --- /dev/null +++ b/config/rbac/kuik_repository_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view repositories. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: repository-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize + name: repository-viewer-role +rules: +- apiGroups: + - kuik.enix.io + resources: + - repositories + verbs: + - get + - list + - watch +- apiGroups: + - kuik.enix.io + resources: + - repositories/status + verbs: + - get diff --git a/config/rbac/leader_election_role.yaml b/config/rbac/leader_election_role.yaml index 4190ec80..ce0a4b48 100644 --- a/config/rbac/leader_election_role.yaml +++ b/config/rbac/leader_election_role.yaml @@ -2,6 +2,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: + labels: + app.kubernetes.io/name: role + app.kubernetes.io/instance: leader-election-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: - apiGroups: diff --git a/config/rbac/leader_election_role_binding.yaml b/config/rbac/leader_election_role_binding.yaml index 3ca63b6f..8b7f0f18 100644 --- a/config/rbac/leader_election_role_binding.yaml +++ b/config/rbac/leader_election_role_binding.yaml @@ -1,6 +1,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: + labels: + app.kubernetes.io/name: rolebinding + app.kubernetes.io/instance: leader-election-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: leader-election-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io @@ -9,4 +16,4 @@ roleRef: subjects: - kind: ServiceAccount name: controller-manager - namespace: default + namespace: system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5242819d..0869bff0 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -2,7 +2,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: manager-role rules: - apiGroups: @@ -72,3 +71,29 @@ rules: - get - patch - update +- apiGroups: + - kuik.enix.io + resources: + - repositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - kuik.enix.io + resources: + - repositories/finalizers + verbs: + - update +- apiGroups: + - kuik.enix.io + resources: + - repositories/status + verbs: + - get + - patch + - update diff --git a/config/rbac/role_binding.yaml b/config/rbac/role_binding.yaml index cddf957f..a17fc604 100644 --- a/config/rbac/role_binding.yaml +++ b/config/rbac/role_binding.yaml @@ -1,6 +1,13 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: + labels: + app.kubernetes.io/name: clusterrolebinding + app.kubernetes.io/instance: manager-rolebinding + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io @@ -9,4 +16,4 @@ roleRef: subjects: - kind: ServiceAccount name: controller-manager - namespace: default + namespace: system diff --git a/config/rbac/service_account.yaml b/config/rbac/service_account.yaml index 5ff6302f..a1bbbd45 100644 --- a/config/rbac/service_account.yaml +++ b/config/rbac/service_account.yaml @@ -1,5 +1,12 @@ apiVersion: v1 kind: ServiceAccount metadata: + labels: + app.kubernetes.io/name: serviceaccount + app.kubernetes.io/instance: controller-manager-sa + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: controller-manager - namespace: default + namespace: system diff --git a/config/samples/kuik.enix.io_v1alpha1_cachedimage.yaml b/config/samples/kuik.enix.io_v1alpha1_cachedimage.yaml deleted file mode 100644 index 7f02ba14..00000000 --- a/config/samples/kuik.enix.io_v1alpha1_cachedimage.yaml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: kuik.enix.io/v1alpha1 -kind: CachedImage -metadata: - name: cachedimage-sample -spec: - image: library/nginx:latest diff --git a/config/samples/kuik_v1alpha1_cachedimage.yaml b/config/samples/kuik_v1alpha1_cachedimage.yaml new file mode 100644 index 00000000..4041c88e --- /dev/null +++ b/config/samples/kuik_v1alpha1_cachedimage.yaml @@ -0,0 +1,12 @@ +apiVersion: kuik.enix.io/v1alpha1 +kind: CachedImage +metadata: + labels: + app.kubernetes.io/name: cachedimage + app.kubernetes.io/instance: cachedimage-sample + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: kube-image-keeper + name: cachedimage-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kuik_v1alpha1_repository.yaml b/config/samples/kuik_v1alpha1_repository.yaml new file mode 100644 index 00000000..5da5193e --- /dev/null +++ b/config/samples/kuik_v1alpha1_repository.yaml @@ -0,0 +1,12 @@ +apiVersion: kuik.enix.io/v1alpha1 +kind: Repository +metadata: + labels: + app.kubernetes.io/name: repository + app.kubernetes.io/instance: repository-sample + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: kube-image-keeper + name: repository-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml new file mode 100644 index 00000000..23d996bb --- /dev/null +++ b/config/samples/kustomization.yaml @@ -0,0 +1,5 @@ +## Append samples of your project ## +resources: +- kuik_v1alpha1_cachedimage.yaml +- kuik_v1alpha1_repository.yaml +#+kubebuilder:scaffold:manifestskustomizesamples diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml index 7405cf0f..206316e5 100644 --- a/config/webhook/kustomizeconfig.yaml +++ b/config/webhook/kustomizeconfig.yaml @@ -1,4 +1,4 @@ -# the following config is for teaching kustomize where to look at when substituting vars. +# the following config is for teaching kustomize where to look at when substituting nameReference. # It requires kustomize v2.1.0 or newer to work properly. nameReference: - kind: Service @@ -7,19 +7,16 @@ nameReference: - kind: MutatingWebhookConfiguration group: admissionregistration.k8s.io path: webhooks/clientConfig/service/name - # - kind: ValidatingWebhookConfiguration - # group: admissionregistration.k8s.io - # path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name namespace: - kind: MutatingWebhookConfiguration group: admissionregistration.k8s.io path: webhooks/clientConfig/service/namespace create: true -# - kind: ValidatingWebhookConfiguration -# group: admissionregistration.k8s.io -# path: webhooks/clientConfig/service/namespace -# create: true - -varReference: -- path: metadata/annotations +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index fdfd7496..59e235c1 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -2,9 +2,28 @@ apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: - creationTimestamp: null name: mutating-webhook-configuration webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-kuik-enix-io-v1alpha1-cachedimage + failurePolicy: Fail + name: mcachedimage.kb.io + rules: + - apiGroups: + - kuik.enix.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - cachedimages + sideEffects: None - admissionReviewVersions: - v1 clientConfig: diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml index 31e0f829..ed6966c5 100644 --- a/config/webhook/service.yaml +++ b/config/webhook/service.yaml @@ -2,11 +2,19 @@ apiVersion: v1 kind: Service metadata: + labels: + app.kubernetes.io/name: service + app.kubernetes.io/instance: webhook-service + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: kube-image-keeper + app.kubernetes.io/part-of: kube-image-keeper + app.kubernetes.io/managed-by: kustomize name: webhook-service namespace: system spec: ports: - port: 443 + protocol: TCP targetPort: 9443 selector: control-plane: controller-manager diff --git a/docs/high-availability.md b/docs/high-availability.md index ed214840..61dfb57b 100644 --- a/docs/high-availability.md +++ b/docs/high-availability.md @@ -11,7 +11,8 @@ The registry supports various storage solutions, some of which enable high avail | Name | HA-compatible | Enable | |---------------|---------------|-------------------------------------| | Tmpfs | No | by default | -| PVC | No | `registry.persistence.enabled=true` | +| PVC (RWO) | No | `registry.persistence.enabled=true` | +| PVC (RWX) | Yes | `registry.persistence.enabled=true`, `registry.persistence.accessModes='ReadWriteMany'` | | MinIO | Yes | `minio.enabled=true` | | S3-compatible | Yes | `registry.persistence.s3=...` | | GCS | Yes | `registry.persistence.gcs=...` | @@ -24,10 +25,16 @@ To enable HA, set `registry.replicas` to a value greater than `1` and make sure This is the default mode, the registry don't use a volume so the data isn't persistent. Garbage collection is disabled. In this mode, if the registry Pod fails, a new Pod can be created, but the registry cache will be empty and will need to be re-populated. -## PersistentVolumeClaim +## PersistentVolumeClaim (RWO) By setting the `registry.persistence.enabled` value to `true`, the kuik registry will use a PersistentVolumeClaim. If the PVC itself is backed by a local volume, this won't improve the durability of the registry in case of e.g. complete node failure. However, if the PVC is backed by a network or cloud volume, then the content of the registry cache won't be lost in case of node outage. But with most setups, a node outage might still take down the registry for an extended period of time, until the node is restored or the volume is detached from the node to be reattached to another (the exact procedure may depend on your specific cluster setup). Therefore, the PVC mode is *not* considered highly available here. +## PersistentVolumeClaim (RWX) + +By setting the `registry.persistence.enabled` value to `true` AND setting `registry.persistence.accessModes` to `ReadWriteMany` the kuik registry will use a PersistentVolumeClaim in ReadWriteMany mode. This allows multiple pods to use the same volume and therefore this approach is considered highly available. + +Only select few storage providers support ReadWriteMany an example of one which does is the [EFS CSI](https://docs.aws.amazon.com/eks/latest/userguide/efs-csi.html) if you are running EKS. This can be useful if you are running kuik within AWS and do not want to run MinIO or create S3 credentials ontop of the deployment. + ## S3-compatible Any S3-compatible service can be used as a storage backend for the registry, including but not limited to AWS S3 and MinIO. @@ -44,7 +51,7 @@ registry: bucket: registry ``` -Please refer to the [Docker registry S3 documentation](https://github.com/docker/docs/blob/main/registry/storage-drivers/s3.md) for more details. +Please refer to the [Docker registry S3 documentation](https://github.com/distribution/distribution/blob/main/docs/content/storage-drivers/s3.md) for more details. Note that when using AWS S3 buckets, you shouldn't prefix the bucket name with `s3://`: diff --git a/docs/pre-heat.md b/docs/pre-heat.md new file mode 100644 index 00000000..851d92da --- /dev/null +++ b/docs/pre-heat.md @@ -0,0 +1,37 @@ +# How to pre-heat to cache + +Sometimes, you may want to pre-heat the cache before deploying an application. In order to do this, you need to manually create corresponding `CachedImages`. For instance, if you need to use an nginx image, you could create the following `CachedImage`: + +```yaml +apiVersion: kuik.enix.io/v1alpha1 +kind: CachedImage +metadata: + name: nginx +spec: + sourceImage: nginx:1.25 + retain: true +``` + +## Retain flag / expiration + +If you plan to use the image in a long time (more days than configured in the value `cachedImagesExpiryDelay`) set the `spec.retain` flag to `true` to prevent the image from expiring. + +## Naming convention + +`CachedImage` follow a strict naming convention that is `--`. Giving any name that doesn't match this convention will make the controller recreate the `CachedImage` with the right name. For instance here, it will be renamed `docker.io-library-nginx-1.25`. The reason why the controller do this is to ensure that only one `CachedImage` exists for a particular image and to ensure consistency. + +## Pull secrets + +If the image you want to put in cache needs a pull secret, you will have to configure it in the corresponding `Repository` (following the same naming convention than a `CachedImage`). For instance: + +```yaml +apiVersion: kuik.enix.io/v1alpha1 +kind: Repository +metadata: + name: docker.io-library-nginx +spec: + name: docker.io/library/nginx + pullSecretsNamespace: secret-namespace + pullSecretNames: + - secret-name +``` diff --git a/go.mod b/go.mod index 2b02d1d1..6f0027d7 100644 --- a/go.mod +++ b/go.mod @@ -1,128 +1,174 @@ module github.com/enix/kube-image-keeper -go 1.20 +go 1.22 require ( github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230519004202-7f2db5bd753e - github.com/docker/cli v24.0.7+incompatible - github.com/docker/distribution v2.8.3+incompatible - github.com/docker/docker v24.0.6+incompatible - github.com/docker/go-connections v0.4.0 - github.com/gin-gonic/gin v1.9.1 - github.com/go-logr/logr v1.3.0 - github.com/google/go-containerregistry v0.16.1 - github.com/onsi/ginkgo v1.16.5 - github.com/onsi/gomega v1.30.0 - github.com/prometheus/client_golang v1.17.0 + github.com/distribution/reference v0.6.0 + github.com/docker/cli v26.1.4+incompatible + github.com/docker/docker v27.0.0+incompatible + github.com/docker/go-connections v0.5.0 + github.com/gin-gonic/gin v1.10.0 + github.com/go-logr/logr v1.4.2 + github.com/google/go-containerregistry v0.19.2 + github.com/onsi/ginkgo/v2 v2.19.0 + github.com/onsi/gomega v1.33.1 + github.com/prometheus/client_golang v1.19.1 go.uber.org/automaxprocs v1.5.3 - go.uber.org/zap v1.26.0 - golang.org/x/exp v0.0.0-20231006140011-7918f672742d - k8s.io/api v0.23.5 - k8s.io/apimachinery v0.23.5 - k8s.io/client-go v0.23.5 - k8s.io/klog/v2 v2.110.1 - k8s.io/utils v0.0.0-20211116205334-6203023598ed - sigs.k8s.io/controller-runtime v0.11.2 + go.uber.org/zap v1.27.0 + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f + k8s.io/api v0.27.13 + k8s.io/apimachinery v0.27.13 + k8s.io/client-go v0.27.13 + k8s.io/klog/v2 v2.130.1 + k8s.io/kubernetes v1.27.13 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b + sigs.k8s.io/controller-runtime v0.15.0 ) require ( - cloud.google.com/go/compute v1.19.3 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest v0.11.18 // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.13 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/aws/aws-sdk-go-v2 v1.18.0 // indirect - github.com/aws/aws-sdk-go-v2/config v1.18.25 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.24 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect - github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.16.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect - github.com/aws/smithy-go v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.27.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect + github.com/aws/smithy-go v1.20.2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/bytedance/sonic v1.9.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/distribution/reference v0.5.0 // indirect - github.com/docker/docker-credential-helpers v0.7.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.8.1 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect - github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect - github.com/fsnotify/fsnotify v1.5.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-logr/zapr v1.2.0 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.6.0 // indirect - github.com/google/gofuzz v1.1.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect github.com/imdario/mergo v0.3.12 // 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/klauspost/compress v1.16.5 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/nxadm/tail v1.4.8 // 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/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect - github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.11.1 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - github.com/sirupsen/logrus v1.9.2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect github.com/vbatts/tar-split v0.11.3 // indirect - go.uber.org/multierr v1.10.0 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/term v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect - golang.org/x/tools v0.14.0 // indirect - gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/otel v1.26.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/sdk v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/oauth2 v0.16.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.21.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.23.5 // indirect - k8s.io/component-base v0.23.5 // indirect - k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect + k8s.io/apiextensions-apiserver v0.27.13 // indirect + k8s.io/apiserver v0.27.13 // indirect + k8s.io/component-base v0.27.13 // indirect + k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // 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.3.0 // indirect ) + +replace ( + k8s.io/api => k8s.io/api v0.27.13 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.27.13 + k8s.io/apimachinery => k8s.io/apimachinery v0.27.13 + k8s.io/apiserver => k8s.io/apiserver v0.27.13 + k8s.io/cli-runtime => k8s.io/cli-runtime v0.27.13 + k8s.io/client-go => k8s.io/client-go v0.27.13 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.27.13 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.27.13 + k8s.io/code-generator => k8s.io/code-generator v0.27.13 + k8s.io/component-base => k8s.io/component-base v0.27.13 + k8s.io/component-helpers => k8s.io/component-helpers v0.27.13 + k8s.io/controller-manager => k8s.io/controller-manager v0.27.13 + k8s.io/cri-api => k8s.io/cri-api v0.27.13 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.27.13 + k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.27.13 + k8s.io/endpointslice => k8s.io/endpointslice v0.27.13 + k8s.io/kms => k8s.io/kms v0.27.13 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.27.13 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.27.13 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.27.13 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.27.13 + k8s.io/kubectl => k8s.io/kubectl v0.27.13 + k8s.io/kubelet => k8s.io/kubelet v0.27.13 + k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.27.13 + k8s.io/metrics => k8s.io/metrics v0.27.13 + k8s.io/mount-utils => k8s.io/mount-utils v0.27.13 + k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.27.13 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.27.13 + k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.27.13 + k8s.io/sample-controller => k8s.io/sample-controller v0.27.13 +) diff --git a/go.sum b/go.sum index e6aef125..8df2fe75 100644 --- a/go.sum +++ b/go.sum @@ -1,1117 +1,469 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= -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/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/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.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.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.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 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -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/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/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 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/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/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/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/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= -github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/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/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY= github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q= +github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA= +github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM= github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4= -github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0= +github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA= +github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE= github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs= +github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc= -github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11 h1:wlTgmb/sCmVRJrN5De3CiHj4v/bTCgL5+qpdEd0CPtw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= github.com/aws/aws-sdk-go-v2/service/ecr v1.18.11/go.mod h1:Ce1q2jlNm8BVpjLaOnwnm5v2RClAbK6txwPljFzyW6c= -github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.16.2 h1:yflJrGmi1pXtP9lOpOeaNZyc0vXnJTuP2sor3nJcGGo= +github.com/aws/aws-sdk-go-v2/service/ecr v1.27.4 h1:Qr9W21mzWT3RhfYn9iAux7CeRIdbnTAqmiOlASqQgZI= +github.com/aws/aws-sdk-go-v2/service/ecr v1.27.4/go.mod h1:if7ybzzjOmDB8pat9FE35AHTY6ZxlYSy3YviSmFZv8c= github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.16.2/go.mod h1:uHtRE7aqXNmpeYL+7Ec7LacH5zC9+w2T5MBOeEKDdu0= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.4 h1:aNuiieMaS2IHxqAsTdM/pjHyY1aoaDLBGLqpNnFMMqk= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.23.4/go.mod h1:8pvvNAklmq+hKmqyvFoMRg0bwg9sdGOvdwximmKiKP0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk= github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk= -github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak= github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8= -github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q= +github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230519004202-7f2db5bd753e h1:hli0IOU73/tNWARHav2a41uMg7arHx0Qbhgcm4bDKXI= github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20230519004202-7f2db5bd753e/go.mod h1:cheRroDS4qmOzi+Ue/oMHG4AV6n9F52W5QFdEKU59a0= -github.com/benbjohnson/clock v1.0.3/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= -github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= -github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -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/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/datadriven v0.0.0-20200714090401-bf6692d28da5/go.mod h1:h6jFvWxBdQXxjopDMZyH2UVceIRfR84bdzbkoKrsWNo= -github.com/cockroachdb/errors v1.2.4/go.mod h1:rQD95gz6FARkaKkQXUksEje/d9a6wBJoCr5oaCLELYA= -github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f/go.mod h1:i/u985jwjWRlyHXQbwatDASoW0RMlZ/3i9yJHE2xLkI= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= -github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= -github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/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/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= 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/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/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1xfI36MSkFg= -github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v26.1.4+incompatible h1:I8PHdc0MtxEADqYJZvhBrW9bo8gawKwwenxRM7/rLu8= +github.com/docker/cli v26.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= -github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= +github.com/docker/docker v27.0.0+incompatible h1:JRugTYuelmWlW0M3jakcIadDx2HUoUO6+Tf2C5jVfwA= +github.com/docker/docker v27.0.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= +github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= -github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= -github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.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.0 h1:n4JnPI1T3Qq1SFEi/F8rwLrZERp2bso19PJZDB9dayk= -github.com/go-logr/zapr v1.2.0/go.mod h1:Qa4Bsj2Vb+FAVeAKsLD8RLQ+YRJB8YDmOAKxaBQf7Ro= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= -github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.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.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= +github.com/go-openapi/jsonreference v0.20.1/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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/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/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/cel-go v0.9.0/go.mod h1:U7ayypeSkw23szu4GaQTPJGx66c20mx8JklMSxrmI1w= -github.com/google/cel-spec v0.6.0/go.mod h1:Nwjgxy5CbjlPrtCWjeDjUyKMl8w41YBYGjsyDdqk0xA= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w= +github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= 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/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/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-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= -github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -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-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= -github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= -github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= 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/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 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/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 v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/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/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= -github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.16.5 h1:IFV2oUNUzZaz+XyusxpLzpzS8Pt5rh0Z16For/djlyI= github.com/klauspost/compress v1.16.5/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/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/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -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/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= -github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= -github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/term v0.0.0-20210610120745-9d4ed1856297/go.mod h1:vgPCkQMyxTZ7IDy8SXRufE172gr8+K/JE/7hHFxHW3A= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/term v0.0.0-20221205130635-1aeaba878587/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-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +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/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0Ys52cJydRwBkb8= github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -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/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= -github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -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/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -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-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.28.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= -github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= -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/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/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/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.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/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= -github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= -github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 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/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= -go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= -go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0= -go.etcd.io/etcd/pkg/v3 v3.5.0/go.mod h1:UzJGatBQ1lXChBkQF0AuAtkRQMYnHubxAEYIrC3MSsE= -go.etcd.io/etcd/raft/v3 v3.5.0/go.mod h1:UFOHSIvO/nKwd4lhkwabrTD3cqW5yVyYYf/KlD00Szc= -go.etcd.io/etcd/server/v3 v3.5.0/go.mod h1:3Ah5ruV+M+7RZr0+Y/5mNLwC+eQlni+mQmOVdCRJoS4= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/contrib v0.20.0/go.mod h1:G/EtFaa6qaN7+LxqfIAT3GiZa7Wv5DTBUzl5H4LY0Kc= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0/go.mod h1:oVGt1LRbBOBq1A5BQLlUg9UaU/54aiHw8cgjV3aWZ/E= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4= -go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM= -go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= -go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE= -go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE= -go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= +go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= -go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -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/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 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-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 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/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -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/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= +golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-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-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/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-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -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.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= +golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= -gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +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.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201102152239-715cce707fb0/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20230525234025-438c736192d0 h1:x1vNwUhVOcsYoKyEGCZBH694SBmmBjA2EfauFVEI2+M= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= +google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/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= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +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/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= -gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= -gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/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/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.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.23.5 h1:zno3LUiMubxD/V1Zw3ijyKO3wxrhbUF1Ck+VjBvfaoA= -k8s.io/api v0.23.5/go.mod h1:Na4XuKng8PXJ2JsploYYrivXrINeTaycCGcYgF91Xm8= -k8s.io/apiextensions-apiserver v0.23.5 h1:5SKzdXyvIJKu+zbfPc3kCbWpbxi+O+zdmAJBm26UJqI= -k8s.io/apiextensions-apiserver v0.23.5/go.mod h1:ntcPWNXS8ZPKN+zTXuzYMeg731CP0heCTl6gYBxLcuQ= -k8s.io/apimachinery v0.23.5 h1:Va7dwhp8wgkUPWsEXk6XglXWU4IKYLKNlv8VkX7SDM0= -k8s.io/apimachinery v0.23.5/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/apiserver v0.23.5/go.mod h1:7wvMtGJ42VRxzgVI7jkbKvMbuCbVbgsWFT7RyXiRNTw= -k8s.io/client-go v0.23.5 h1:zUXHmEuqx0RY4+CsnkOn5l0GU+skkRXKGJrhmE2SLd8= -k8s.io/client-go v0.23.5/go.mod h1:flkeinTO1CirYgzMPRWxUCnV0G4Fbu2vLhYCObnt/r4= -k8s.io/code-generator v0.23.5/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk= -k8s.io/component-base v0.23.5 h1:8qgP5R6jG1BBSXmRYW+dsmitIrpk8F/fPEvgDenMCCE= -k8s.io/component-base v0.23.5/go.mod h1:c5Nq44KZyt1aLl0IpHX82fhsn84Sb0jjzwjpcA42bY0= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -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-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= -k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +k8s.io/api v0.27.13 h1:d49LYs1dh+JMMDNYQSu8FhEzCjc2TNpYvDWoSGAKs80= +k8s.io/api v0.27.13/go.mod h1:W3lYMPs34i0XQA+cmKfejve+HwbRZjy67fL05RyJUTo= +k8s.io/apiextensions-apiserver v0.27.13 h1:it32SCkrjzhimZasL++nsshG66m2O570y56R+xj1/WE= +k8s.io/apiextensions-apiserver v0.27.13/go.mod h1:LkAz0+pjqr/92kPigX/B2sjsPhGCuG+hi8GyyjUNNsE= +k8s.io/apimachinery v0.27.13 h1:xDAnOWaRVNSkaKdfB0Ab11hixH90KGTbLwEHMloMjFM= +k8s.io/apimachinery v0.27.13/go.mod h1:TWo+8wOIz3CytsrlI9k/LBWXLRr9dqf5hRSCbbggMAg= +k8s.io/apiserver v0.27.13 h1:Yf69zVdbuQVIMpz7N4dtntWsUklKpcFXGAdVh7vKOH4= +k8s.io/apiserver v0.27.13/go.mod h1:XHth2MKAUdcLvdhPOwvDPbSyOrMev2vRqE05oUEC5Hk= +k8s.io/client-go v0.27.13 h1:SfUbIukb6BSqaadlYRX0AzMoN6+e+9FZGEKqfisidho= +k8s.io/client-go v0.27.13/go.mod h1:I9SBaI28r6ii465Fb0dTpf5O3adOnDwNBoeqlDNbbFg= +k8s.io/component-base v0.27.13 h1:JuDLZqD8L+Wu9nUNv9msjQjkh172Ag6crphzASEvlKo= +k8s.io/component-base v0.27.13/go.mod h1:QKur/xRE4R25PhScEe3lAhrVSwEuZPGlFPyEDOaWCgU= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= +k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= +k8s.io/kubernetes v1.27.13 h1:5NBz3aNy5Jwcgi+w2+rP4x5B0Wa21NXUJxDJwdUtxlY= +k8s.io/kubernetes v1.27.13/go.mod h1:T4toI2XSWG5FJoq/H8q9eFYPymAxe/k4UnaC00uPnMs= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= -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.30/go.mod h1:fEO7lRTdivWO2qYVCVG7dEADOMo/MLDCVr8So2g88Uw= -sigs.k8s.io/controller-runtime v0.11.2 h1:H5GTxQl0Mc9UjRJhORusqfJCIjBO8UtUxGggCwL1rLA= -sigs.k8s.io/controller-runtime v0.11.2/go.mod h1:P6QCzrEjLaZGqHsfd+os7JQ+WFZhvB8MRFsn4dWF7O4= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/controller-runtime v0.15.0 h1:ML+5Adt3qZnMSYxZ7gAverBLNPSMQEibtzAgp0UPojU= +sigs.k8s.io/controller-runtime v0.15.0/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= +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.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/helm/kube-image-keeper/Chart.lock b/helm/kube-image-keeper/Chart.lock index de126879..4a055a61 100644 --- a/helm/kube-image-keeper/Chart.lock +++ b/helm/kube-image-keeper/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: minio repository: https://charts.bitnami.com/bitnami - version: 12.1.4 -digest: sha256:568849941ea1910370be9ac8fdae8500441454ce976927714b927fee3c557aa9 -generated: "2023-02-17T18:38:01.769288012+01:00" + version: 13.2.0 +digest: sha256:deb5af1d98c80ea52289c771f4cae41c7ef73fbb231c86f8eda553a9d4d53cc8 +generated: "2024-01-23T16:17:31.822508041+01:00" diff --git a/helm/kube-image-keeper/Chart.yaml b/helm/kube-image-keeper/Chart.yaml index 47e953ff..202ddd4a 100644 --- a/helm/kube-image-keeper/Chart.yaml +++ b/helm/kube-image-keeper/Chart.yaml @@ -23,11 +23,12 @@ maintainers: url: https://github.com/enixsas - name: Paul Laffitte email: paul.laffitte@enix.fr + url: https://github.com/paullaffitte - name: David Donchez email: david.donchez@enix.fr url: https://github.com/donch dependencies: - name: minio - version: "12.1.4" + version: "13.2.0" repository: "https://charts.bitnami.com/bitnami" condition: minio.enabled diff --git a/helm/kube-image-keeper/README.md.gotmpl b/helm/kube-image-keeper/README.md.gotmpl index a4ee585e..91c9598d 100644 --- a/helm/kube-image-keeper/README.md.gotmpl +++ b/helm/kube-image-keeper/README.md.gotmpl @@ -10,20 +10,21 @@ It saves the container images used by your pods in its own local registry so tha ## Upgrading -### From 1.2.0 to 1.3.0 +### From 1.6.0 o 1.7.0 ***ACTION REQUIRED*** -In v1.3.0, we removed the finalizer `pod.kuik.enix.io/finalizer` from pods that were rewritten by kuik. +To follow Helm3 best pratices, we moved `cachedimage` and `repository` custom resources definition from the helm templates directory to the dedicated `crds` directory. +This will cause the `cachedimage` CRD to be deleted during the 1.7.0 upgrade. -To avoid having these pods stuck in `Terminating` state after a delete action or a rolling update, you will need to manually remove the finalizer from these pods once you upgrade to 1.3.0. -This can be achieved using the following command: +We advice you to uninstall your helm release, clean the remaining custom resources by removing their finalizer, then reinstall kuik in 1.7.0 +You may also recreate the custom resource definition right after the upgrade to 1.7.0 using ``` -kubectl get pods --all-namespaces -l kuik.enix.io/images-rewritten=true -o json | jq '.items[].metadata.finalizers=null' | kubectl replace -f - +kubectl apply -f https://raw.githubusercontent.com/enix/kube-image-keeper/main/helm/kube-image-keeper/crds/cachedimage-crd.yaml +kubectl apply -f https://raw.githubusercontent.com/enix/kube-image-keeper/main/helm/kube-image-keeper/crds/repository-crd.yaml ``` -***Ensure you run this command AFTER having fully upgraded to version 1.3.0. Otherwise, the kuik controllers will continuously re-add the finalizer to the rewritten pods.*** ## Why and when is it useful? @@ -67,19 +68,20 @@ We investigated other options, and we didn't find any that would quite fit our r ## Supported Kubernetes versions -kuik has been developed for, and tested with, Kubernetes 1.21 to 1.24; but the code doesn't use any deprecated (or new) feature or API, and should work with newer versions as well. (Community users have reported success with Kubernetes 1.26). +kuik has been developed for, and tested with, Kubernetes 1.24 to 1.28; but the code doesn't use any deprecated (or new) feature or API, and should work with newer versions as well. ## How it works -When a pod is created, kuik's **mutating webhook** rewrites its images on the fly, adding a `localhost:{port}/` prefix (the `port` is 7439 by default, and is configurable). +When a pod is created, kuik's **mutating webhook** rewrites its images on the fly to point to the local caching registry, adding a `localhost:{port}/` prefix (the `port` is 7439 by default, and is configurable). This means that you don't need to modify/rewrite the source registry url of your manifest/helm chart used to deploy your solution, kuik will take care of it. On `localhost:{port}`, there is an **image proxy** that serves images from kuik's **caching registry** (when the images have been cached) or directly from the original registry (when the images haven't been cached yet). One **controller** watches pods, and when it notices new images, it creates `CachedImage` custom resources for these images. -Another **controller** watches these `CachedImage` custom resources, and copies images from source registries to kuik's caching registry accordingly. +Another **controller** watches these `CachedImage` custom resources, and copies images from source registries to kuik's caching registry accordingly. When images come from a private registry, the controller will use the `imagePullSecrets` from the `CachedImage` spec, those are set from the pod that produced the `CachedImage`. Here is what our images look like when using kuik: + ```bash $ kubectl get pods -o custom-columns=NAME:metadata.name,IMAGES:spec.containers[*].image NAME IMAGES @@ -131,7 +133,6 @@ kube-image-keeper-proxy-54lzk 1m 19Mi Refer to the [dedicated documentation](https://github.com/enix/kube-image-keeper/blob/main/docs/metrics.md). - ## Installation 1. Make sure that you have cert-manager installed. If not, check its [installation page](https://cert-manager.io/docs/installation/) (it's fine to use the `kubectl apply` one-liner, and no further configuration is required). @@ -146,6 +147,10 @@ helm upgrade --install \ That's it! +Our container images are available across multiple registries for reliability. You can find them on [Github Container Registry](https://github.com/enix/kube-image-keeper/pkgs/container/kube-image-keeper), [Quay](https://quay.io/repository/enix/kube-image-keeper) and [DockerHub](https://hub.docker.com/r/enix/kube-image-keeper). + +CAUTION: If you use a storage backend that runs in the same cluster as kuik but in a different namespace, be sure to filter the storage backend's pods. Failure to do so may lead to interdependency issues, making it impossible to start both kuik and its storage backend if either encounters an issue. + {{ template "chart.valuesSection" . }} ## Installation with plain YAML files @@ -184,19 +189,99 @@ helm upgrade --install \ There are 3 ways to tell kuik which pods it should manage (or, conversely, which ones it should ignore). - If a pod has the label `kube-image-keeper.enix.io/image-caching-policy=ignore`, kuik will ignore the pod (it will not rewrite its image references). -- If a pod is in an ignored Namespace, it will also be ignored. Namespaces can be ignored by setting the Helm value `controllers.webhook.ignoredNamespaces`, which defaults to `[kube-system]`. (Note: this feature relies on the [NamespaceDefaultLabelName](https://kubernetes.io/docs/concepts/services-networking/network-policies/#targeting-a-namespace-by-its-name) feature gate to work.) +- If a pod is in an ignored Namespace, it will also be ignored. Namespaces can be ignored by setting the Helm value `controllers.webhook.ignoredNamespaces` (`kube-system` and the kuik namespace will be ignored whatever the value of this parameter). (Note: this feature relies on the [NamespaceDefaultLabelName](https://kubernetes.io/docs/concepts/services-networking/network-policies/#targeting-a-namespace-by-its-name) feature gate to work.) - Finally, kuik will only work on pods matching a specific selector. By default, the selector is empty, which means "match all the pods". The selector can be set with the Helm value `controllers.webhook.objectSelector.matchExpressions`. This logic isn't implemented by the kuik controllers or webhook directly, but through Kubernetes' standard webhook object selectors. In other words, these parameters end up in the `MutatingWebhookConfiguration` template to filter which pods get presented to kuik's webhook. When the webhook rewrites the images for a pod, it adds a label to that pod, and the kuik controllers then rely on that label to know which `CachedImages` resources to create. -Keep in mind that kuik will ignore pods scheduled into its own namespace. +Keep in mind that kuik will ignore pods scheduled into its own namespace or in the `kube-system` namespace as recommended in the kubernetes documentation ([Avoiding operating on the kube-system namespace](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#avoiding-operating-on-the-kube-system-namespace)). + +> It is recommended to exclude the namespace where your webhook is running with a namespaceSelector. +> [...] +> Accidentally mutating or rejecting requests in the kube-system namespace may cause the control plane components to stop functioning or introduce unknown behavior. -### Cache persistence & garbage collection +#### Image pull policy + +In the case of a container configured with `imagePullPolicy: Never`, the container will always be filtered out as it makes no sense to cache an image that would never be cached and always read from the disk. + +In the case of a container configured with `imagePullPolicy: Always`, or with the tag `latest`, or with no tag (defaulting to `latest`), by default, the container will be filtered out in order to keep the default behavior of kubernetes, which is to always pull the new version of the image (thus not using the cache of kuik). This can be disabled by setting the value `controllers.webhook.ignorePullPolicyAlways` to `false`. + +### Cache persistence Persistence is disabled by default. You can enable it by setting the Helm value `registry.persistence.enabled=true`. This will create a PersistentVolumeClaim with a default size of 20 GiB. You can change that size by setting the value `registry.persistence.size`. Keep in mind that enabling persistence isn't enough to provide high availability of the registry! If you want kuik to be highly available, please refer to the [high availability guide](https://github.com/enix/kube-image-keeper/blob/main/docs/high-availability.md). Note that persistence requires your cluster to have some PersistentVolumes. If you don't have PersistentVolumes, kuik's registry Pod will remain `Pending` and your images won't be cached (but they will still be served transparently by kuik's image proxy). +### Retain policy + +Sometimes, you want images to stay cached even when they are not used anymore (for instance when you run a workload for a fixed amount of time, stop it, and run it again later). You can choose to prevent `CachedImages` from expiring by manually setting the `spec.retain` flag to `true` like shown below: + +```yaml +apiVersion: kuik.enix.io/v1alpha1 +kind: CachedImage +metadata: + name: docker.io-library-nginx-1.25 +spec: + retain: true # here + sourceImage: nginx:1.25 +``` + +### Multi-arch cluster / Non-amd64 architectures + +By default, kuik only caches the `amd64` variant of an image. To cache more/other architectures, you need to set the `architectures` field in your helm values. + +Example: + +```yaml +architectures: [amd64, arm] +``` + +Kuik will only cache available architectures for an image, but will not crash if the architecture doesn't exist. + +No manual action is required when migrating an amd64-only cluster from v1.3.0 to v1.4.0. + +### Corporate proxy + +To configure kuik to work behind a corporate proxy, you can set the well known `http_proxy` and `https_proxy` environment variables (upper and lowercase variant both works) through helm values `proxy.env` and `controllers.env` like shown below: + +```yaml +controllers: + env: + - name: http_proxy + value: https://proxy.mycompany.org:3128 + - name: https_proxy + value: https://proxy.mycompany.org:3128 +proxy: + env: + - name: http_proxy + value: https://proxy.mycompany.org:3128 + - name: https_proxy + value: https://proxy.mycompany.org:3128 +``` + +Be careful that both the proxy and the controllers need to access the kubernetes API, so you might need to define the `no_proxy` variable as well to ignore the kubernetes API in case it is not reachable from your proxy (which is true most of the time). + +### Insecure registries & self-signed certificates + +In some cases, you may want to use images from self-hosted registries that are insecure (without TLS or with an invalid certificate for instance) or using a self-signed certificate. By default, kuik will not allow to cache images from those registries for security reasons, even though you configured your container runtime (e.g. Docker, containerd) to do so. However you can choose to trust a list of insecure registries to pull from using the helm value `insecureRegistries`. If you use a self-signed certificate you can store the root certificate authority in a secret and reference it with the helm value `rootCertificateAuthorities`. Here is an example of the use of those two values: + +```yaml +insecureRegistries: + - http://some-registry.com + - https://some-other-registry.com + +rootCertificateAuthorities: + secretName: some-secret + keys: + - root.pem +``` + +You can of course use as many insecure registries or root certificate authorities as you want. In the case of a self-signed certificate, you can either use the `insecureRegistries` or the `rootCertificateAuthorities` value, but trusting the root certificate will always be more secure than allowing insecure registries. + +### Registry UI + +For debugging reasons, it may be useful to be able to access the registry through an UI. This can be achieved by enabling the registry UI with the value `registryUI.enabled=true`. The UI will not be publicly available through an ingress, you will need to open a port-forward from port `80`. You can set a custom username and password with values `registryUI.auth.username` (default is `admin`) and `registryUI.auth.password` (empty by default). + ## Garbage collection and limitations When a CachedImage expires because it is not used anymore by the cluster, the image is deleted from the registry. However, since kuik uses [Docker's registry](https://docs.docker.com/registry/), this only deletes **reference files** like tags. It doesn't delete blobs, which account for most of the used disk space. [Garbage collection](https://docs.docker.com/registry/garbage-collection/) allows removing those blobs and free up space. The garbage collecting job can be configured to run thanks to the `registry.garbageCollectionSchedule` configuration in a cron-like format. It is disabled by default, because running garbage collection without persistence would just wipe out the cache registry. @@ -231,6 +316,7 @@ controllers: ### Private images are a bit less private Imagine the following scenario: + - pods A and B use a private image, `example.com/myimage:latest` - pod A correctly references `imagePullSecrets, but pod B does not @@ -242,11 +328,14 @@ Howevever, when using kuik, once an image has been pulled and stored in kuik's r With kuik, all image pulls (except in the namespaces excluded from kuik) go through kuik's registry proxy, which runs on each node thanks to a DaemonSet. When a node gets added to a Kubernetes cluster (for instance, by the cluster autoscaler), a kuik registry proxy Pod gets scheduled on that node, but it will take a brief moment to start. During that time, all other image pulls will fail. Thanks to Kubernetes automatic retry mechanisms, they will eventually succeed, but on new nodes, you may see Pods in `ErrImagePull` or `ImagePullBackOff` status for a minute before everything works correctly. If you are using cluster autoscaling and try to achieve very fast scale-up times, this is something that you might want to keep in mind. - ### Garbage collection issue We use Docker Distribution in Kuik, along with the integrated garbage collection tool. There is a bug that occurs when untagged images are pushed into the registry, causing it to crash. It's possible to end up in a situation where the registry is in read-only mode and becomes unusable. Until a permanent solution is found, we advise keeping the value `registry.garbageCollection.deleteUntagged` set to false. +### Images with digest + +As of today, there is no way to manage container images based on a digest. The rational behind this limitation is that a digest is an image manifest hash, and the manifest contains the registry URL associated with the image. Thus, pushing the image to another registry (our cache registry) changes its digest and as a consequence, it is not anymore referenced by its original digest. Digest validation prevent from pushing a manifest with an invalid digest. Therefore, we currently ignore all images based on a digest, those images will not be rewritten nor put in cache to prevent malfunctionning of kuik. + ## License diff --git a/helm/kube-image-keeper/templates/cachedimage-crd.yaml b/helm/kube-image-keeper/crds/cachedimage-crd.yaml similarity index 57% rename from helm/kube-image-keeper/templates/cachedimage-crd.yaml rename to helm/kube-image-keeper/crds/cachedimage-crd.yaml index bd9de492..cb9a882e 100644 --- a/helm/kube-image-keeper/templates/cachedimage-crd.yaml +++ b/helm/kube-image-keeper/crds/cachedimage-crd.yaml @@ -1,7 +1,8 @@ -{{- if .Values.installCRD -}} apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 name: cachedimages.kuik.enix.io spec: group: kuik.enix.io @@ -15,6 +16,9 @@ spec: scope: Cluster versions: - additionalPrinterColumns: + - jsonPath: .status.phase + name: Status + type: string - jsonPath: .status.isCached name: Cached type: boolean @@ -36,14 +40,19 @@ spec: description: CachedImage is the Schema for the cachedimages 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' + 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' + 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 @@ -53,12 +62,6 @@ spec: expiresAt: format: date-time type: string - pullSecretNames: - items: - type: string - type: array - pullSecretsNamespace: - type: string retain: type: boolean sourceImage: @@ -69,13 +72,32 @@ spec: status: description: CachedImageStatus defines the observed state of CachedImage properties: + availableUpstream: + type: boolean + digest: + type: string isCached: type: boolean + lastSeenUpstream: + format: date-time + type: string + lastSuccessfulPull: + format: date-time + type: string + lastSync: + format: date-time + type: string + phase: + type: string + upToDate: + type: boolean + upstreamDigest: + type: string usedBy: properties: count: - description: jsonpath function .length() is not implemented, so - the count field is required to display pods count in additionalPrinterColumns + description: |- + jsonpath function .length() is not implemented, so the count field is required to display pods count in additionalPrinterColumns see https://github.com/kubernetes-sigs/controller-tools/issues/447 type: integer pods: @@ -92,10 +114,3 @@ spec: storage: true subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] -{{- end -}} diff --git a/helm/kube-image-keeper/crds/repository-crd.yaml b/helm/kube-image-keeper/crds/repository-crd.yaml new file mode 100644 index 00000000..5bf51081 --- /dev/null +++ b/helm/kube-image-keeper/crds/repository-crd.yaml @@ -0,0 +1,157 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + name: repositories.kuik.enix.io +spec: + group: kuik.enix.io + names: + kind: Repository + listKind: RepositoryList + plural: repositories + shortNames: + - repo + singular: repository + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.phase + name: Status + type: string + - jsonPath: .status.images + name: Images + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Repository is the Schema for the repositories 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: RepositorySpec defines the desired state of Repository + properties: + name: + type: string + pullSecretNames: + items: + type: string + type: array + pullSecretsNamespace: + type: string + updateFilters: + items: + type: string + type: array + updateInterval: + type: string + required: + - name + type: object + status: + description: RepositoryStatus defines the observed state of Repository + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource.\n---\nThis struct is intended for + direct use as an array at the field path .status.conditions. For + example,\n\n\n\ttype FooStatus struct{\n\t // Represents the + observations of a foo's current state.\n\t // Known .status.conditions.type + are: \"Available\", \"Progressing\", and \"Degraded\"\n\t // + +patchMergeKey=type\n\t // +patchStrategy=merge\n\t // +listType=map\n\t + \ // +listMapKey=type\n\t Conditions []metav1.Condition `json:\"conditions,omitempty\" + patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"`\n\n\n\t + \ // other fields\n\t}" + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + images: + type: integer + lastUpdate: + format: date-time + type: string + phase: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/helm/kube-image-keeper/templates/NOTES.txt b/helm/kube-image-keeper/templates/NOTES.txt new file mode 100644 index 00000000..8f5f6b08 --- /dev/null +++ b/helm/kube-image-keeper/templates/NOTES.txt @@ -0,0 +1 @@ +CAUTION: If you use a storage backend that runs in the same cluster as kuik but in a different namespace, be sure to filter the storage backend's pods. Failure to do so may lead to interdependency issues, making it impossible to start both kuik and its storage backend if either encounters an issue. diff --git a/helm/kube-image-keeper/templates/clusterrole.yaml b/helm/kube-image-keeper/templates/clusterrole.yaml index 084978b9..e7626ddc 100644 --- a/helm/kube-image-keeper/templates/clusterrole.yaml +++ b/helm/kube-image-keeper/templates/clusterrole.yaml @@ -78,6 +78,32 @@ rules: - get - patch - update + - apiGroups: + - kuik.enix.io + resources: + - repositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - kuik.enix.io + resources: + - repositories/finalizers + verbs: + - update + - apiGroups: + - kuik.enix.io + resources: + - repositories/status + verbs: + - get + - patch + - update {{- if .Values.psp.create }} - apiGroups: - policy diff --git a/helm/kube-image-keeper/templates/controller-deployment.yaml b/helm/kube-image-keeper/templates/controller-deployment.yaml index 19fc98ac..7df329e2 100644 --- a/helm/kube-image-keeper/templates/controller-deployment.yaml +++ b/helm/kube-image-keeper/templates/controller-deployment.yaml @@ -44,6 +44,7 @@ spec: - -registry-endpoint={{ include "kube-image-keeper.fullname" . }}-registry:5000 - -max-concurrent-cached-image-reconciles={{ .Values.controllers.maxConcurrentCachedImageReconciles }} - -zap-log-level={{ .Values.controllers.verbosity }} + - -ignore-pull-policy-always={{- .Values.controllers.webhook.ignorePullPolicyAlways }} {{- range .Values.controllers.webhook.ignoredImages }} - -ignore-images={{- . }} {{- end }} @@ -90,6 +91,10 @@ spec: readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.controllers.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} {{- with .Values.controllers.resources }} resources: {{- toYaml . | nindent 12 }} diff --git a/helm/kube-image-keeper/templates/garbage-collection-cron-job.yaml b/helm/kube-image-keeper/templates/garbage-collection-cron-job.yaml index 07a4ec8c..10f8ba06 100644 --- a/helm/kube-image-keeper/templates/garbage-collection-cron-job.yaml +++ b/helm/kube-image-keeper/templates/garbage-collection-cron-job.yaml @@ -22,7 +22,8 @@ spec: restartPolicy: Never containers: - name: kubectl - image: bitnami/kubectl + image: "{{ .Values.registry.garbageCollection.image.repository }}:{{ .Values.registry.garbageCollection.image.tag }}" + imagePullPolicy: {{ .Values.registry.garbageCollection.image.pullPolicy }} command: - bash - -c diff --git a/helm/kube-image-keeper/templates/mutatingwebhookconfiguration.yaml b/helm/kube-image-keeper/templates/mutatingwebhookconfiguration.yaml index 20051899..dc5f2ba4 100644 --- a/helm/kube-image-keeper/templates/mutatingwebhookconfiguration.yaml +++ b/helm/kube-image-keeper/templates/mutatingwebhookconfiguration.yaml @@ -20,6 +20,7 @@ webhooks: - key: kubernetes.io/metadata.name operator: NotIn values: + - kube-system - {{ .Release.Namespace }} {{- if .Values.controllers.webhook.ignoredNamespaces }} {{- range .Values.controllers.webhook.ignoredNamespaces }} @@ -47,3 +48,23 @@ webhooks: resources: - pods sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: {{ include "kube-image-keeper.fullname" . }}-webhook + namespace: {{ .Release.Namespace }} + path: /mutate-kuik-enix-io-v1alpha1-cachedimage + failurePolicy: Fail + name: mcachedimage.kb.io + rules: + - apiGroups: + - kuik.enix.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - cachedimages + sideEffects: None diff --git a/helm/kube-image-keeper/templates/proxy-daemonset.yaml b/helm/kube-image-keeper/templates/proxy-daemonset.yaml index 71f78a89..427b8965 100644 --- a/helm/kube-image-keeper/templates/proxy-daemonset.yaml +++ b/helm/kube-image-keeper/templates/proxy-daemonset.yaml @@ -25,6 +25,10 @@ spec: {{- if .Values.proxy.priorityClassName }} priorityClassName: {{ .Values.proxy.priorityClassName | quote }} {{- end }} + {{- if .Values.proxy.hostNetwork }} + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + {{- end }} securityContext: {{- toYaml .Values.proxy.podSecurityContext | nindent 8 }} containers: @@ -34,13 +38,23 @@ spec: image: "{{ .Values.proxy.image.repository }}:{{ .Values.proxy.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.proxy.image.pullPolicy }} ports: - - containerPort: 8082 + {{- if .Values.proxy.hostNetwork }} + - containerPort: {{ .Values.proxy.hostPort }} + hostPort: {{ .Values.proxy.hostPort }} + protocol: TCP + - containerPort: {{ .Values.proxy.metricsPort }} + hostPort: {{ .Values.proxy.metricsPort }} + name: metrics + protocol: TCP + {{- else }} + - containerPort: {{ .Values.proxy.hostPort }} hostIP: {{ .Values.proxy.hostIp }} hostPort: {{ .Values.proxy.hostPort }} protocol: TCP - containerPort: 8080 name: metrics protocol: TCP + {{- end }} command: - registry-proxy - -v={{ .Values.proxy.verbosity }} @@ -57,19 +71,39 @@ spec: - -root-certificate-authorities=/etc/ssl/certs/registry-certificate-authorities/{{- . }} {{- end }} {{- end }} - {{- if .Values.rootCertificateAuthorities }} - {{- with .Values.proxy.env }} + {{- if .Values.proxy.hostNetwork }} + - -bind-address={{ .Values.proxy.hostIp }}:{{ .Values.proxy.hostPort }} + - -metrics-bind-address={{ .Values.proxy.hostIp }}:{{ .Values.proxy.metricsPort }} + {{- else }} + - -bind-address=:{{ .Values.proxy.hostPort }} + {{- end }} env: + {{- with .Values.proxy.env }} {{- toYaml . | nindent 12 }} {{- end }} + - name: GIN_MODE + value: release + {{- if .Values.rootCertificateAuthorities }} volumeMounts: - mountPath: /etc/ssl/certs/registry-certificate-authorities name: registry-certificate-authorities readOnly: true {{- end }} + {{- $readinessProbe := deepCopy .Values.proxy.readinessProbe }} + {{- if .Values.proxy.hostNetwork }} + {{- $readinessProbe := merge $readinessProbe.httpGet (dict "host" "localhost") }} + {{- end }} {{- with .Values.proxy.readinessProbe }} readinessProbe: - {{- toYaml . | nindent 12 }} + {{- $readinessProbe | toYaml | nindent 12 }} + {{- end }} + {{- $livenessProbe := deepCopy .Values.proxy.livenessProbe }} + {{- if .Values.proxy.hostNetwork }} + {{- $livenessProbe := merge $livenessProbe.httpGet (dict "host" "localhost") }} + {{- end }} + {{- with .Values.proxy.livenessProbe }} + livenessProbe: + {{- $livenessProbe | toYaml | nindent 12 }} {{- end }} {{- with .Values.proxy.resources }} resources: diff --git a/helm/kube-image-keeper/templates/registry-deployment.yaml b/helm/kube-image-keeper/templates/registry-deployment.yaml index e0964c9e..11833d62 100644 --- a/helm/kube-image-keeper/templates/registry-deployment.yaml +++ b/helm/kube-image-keeper/templates/registry-deployment.yaml @@ -114,6 +114,10 @@ spec: readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.registry.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} {{- with .Values.registry.persistence.gcsExistingSecret }} volumes: - name: gcs-key diff --git a/helm/kube-image-keeper/templates/registry-pvc.yaml b/helm/kube-image-keeper/templates/registry-pvc.yaml new file mode 100644 index 00000000..9eb7a899 --- /dev/null +++ b/helm/kube-image-keeper/templates/registry-pvc.yaml @@ -0,0 +1,14 @@ +{{- if and (eq (include "kube-image-keeper.registry-stateless-mode" .) "false") (.Values.registry.persistence.enabled) (eq .Values.registry.persistence.accessModes "ReadWriteMany") }} + +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "kube-image-keeper.fullname" . }}-registry-pvc +spec: + accessModes: + - {{ .Values.registry.persistence.accessModes }} + storageClassName: {{ .Values.registry.persistence.storageClass }} + resources: + requests: + storage: {{ .Values.registry.persistence.size }} +{{- end }} diff --git a/helm/kube-image-keeper/templates/registry-statefulset.yaml b/helm/kube-image-keeper/templates/registry-statefulset.yaml index 3d39d5a9..6077b5f6 100644 --- a/helm/kube-image-keeper/templates/registry-statefulset.yaml +++ b/helm/kube-image-keeper/templates/registry-statefulset.yaml @@ -1,7 +1,7 @@ {{- if eq (include "kube-image-keeper.registry-stateless-mode" .) "false" }} -{{- if gt (int .Values.registry.replicas) 1 -}} -{{ fail "registry needs a configured S3 endpoint to enable HA mode (>1 replicas), please enable minio or configure an external S3 endpoint" }} +{{- if and (gt (int .Values.registry.replicas) 1) (ne .Values.registry.persistence.accessModes "ReadWriteMany") -}} +{{ fail "registry needs a configured S3 endpoint or a PVC which supports ReadWriteMany to enable HA mode (>1 replicas), please enable minio or configure an external S3 endpoint" }} {{- end }} apiVersion: apps/v1 @@ -11,6 +11,7 @@ metadata: labels: {{- include "kube-image-keeper.registry-labels" . | nindent 4 }} spec: + replicas: {{ .Values.registry.replicas }} serviceName: {{ include "kube-image-keeper.fullname" . }}-registry selector: matchLabels: @@ -96,6 +97,10 @@ spec: readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.registry.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} {{- with .Values.registry.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} @@ -108,15 +113,22 @@ spec: tolerations: {{- toYaml . | nindent 8 }} {{- end }} - {{- if .Values.registry.persistence.enabled }} + {{- if and (.Values.registry.persistence.enabled) (ne .Values.registry.persistence.accessModes "ReadWriteMany") }} volumeClaimTemplates: - metadata: name: data spec: - accessModes: [ "ReadWriteOnce" ] + accessModes: + - {{ .Values.registry.persistence.accessModes }} storageClassName: {{ .Values.registry.persistence.storageClass }} resources: requests: storage: {{ .Values.registry.persistence.size }} {{- end }} + {{- if and (.Values.registry.persistence.enabled) (eq .Values.registry.persistence.accessModes "ReadWriteMany") }} + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "kube-image-keeper.fullname" . }}-registry-pvc + {{- end }} {{- end }} diff --git a/helm/kube-image-keeper/values.yaml b/helm/kube-image-keeper/values.yaml index 9534a57e..e118a36e 100644 --- a/helm/kube-image-keeper/values.yaml +++ b/helm/kube-image-keeper/values.yaml @@ -4,8 +4,6 @@ # -- Delay in days before deleting an unused CachedImage cachedImagesExpiryDelay: 30 -# -- If true, install the CRD -installCRD: true # -- List of architectures to put in cache architectures: [amd64] # -- Insecure registries to allow to cache and proxify images from @@ -22,7 +20,7 @@ controllers: replicas: 2 image: # -- Controller image repository. Also available: `quay.io/enix/kube-image-keeper` - repository: enix/kube-image-keeper + repository: ghcr.io/enix/kube-image-keeper # -- Controller image pull policy pullPolicy: IfNotPresent # -- Controller image tag. Default chart appVersion @@ -59,6 +57,11 @@ controllers: httpGet: path: /readyz port: 8081 + # -- Liveness probe definition for the controllers pod + livenessProbe: + httpGet: + path: /healthz + port: 8081 resources: requests: # -- Cpu requests for the controller pod @@ -72,10 +75,11 @@ controllers: memory: "512Mi" webhook: # -- Don't enable image caching for pods scheduled into these namespaces - ignoredNamespaces: - - kube-system + ignoredNamespaces: [] # -- Don't enable image caching if the image match the following regexes ignoredImages: [] + # -- Don't enable image caching if the image is configured with imagePullPolicy: Always + ignorePullPolicyAlways: true # -- If true, create the issuer used to issue the webhook certificate createCertificateIssuer: true # -- Issuer reference to issue the webhook certificate, ignored if createCertificateIssuer is true @@ -100,15 +104,19 @@ controllers: proxy: image: # -- Proxy image repository. Also available: `quay.io/enix/kube-image-keeper` - repository: enix/kube-image-keeper + repository: ghcr.io/enix/kube-image-keeper # -- Proxy image pull policy pullPolicy: IfNotPresent # -- Proxy image tag. Default chart appVersion tag: "" + # -- whether to run the proxy daemonset in hostNetwork mode + hostNetwork: false # -- hostPort used for the proxy pod hostPort: 7439 # -- hostIp used for the proxy pod hostIp: "127.0.0.1" + # -- metricsPort used for the proxy pod (to expose prometheus metrics) + metricsPort: 8080 # -- Verbosity level for the proxy pod verbosity: 1 # -- Specify secrets to be used when pulling proxy image @@ -159,8 +167,13 @@ proxy: # -- Readiness probe definition for the proxy pod readinessProbe: httpGet: - path: /v2/ - port: 8082 + path: /readyz + port: 7439 + # -- Liveness probe definition for the proxy pod + livenessProbe: + httpGet: + path: /healthz + port: 7439 resources: requests: # -- Cpu requests for the proxy pod @@ -195,10 +208,12 @@ registry: # -- Registry image pull policy pullPolicy: IfNotPresent # -- Registry image tag - tag: "2.8.3" + tag: "2.8" # -- Number of replicas for the registry pod replicas: 1 persistence: + # -- AccessMode for persistent volume + accessModes: ReadWriteOnce # -- If true, enable persistent storage (ignored when using minio or S3) enabled: false # -- StorageClass for persistent volume @@ -219,6 +234,13 @@ registry: schedule: "0 0 * * 0" # -- If true, delete untagged manifests. Default to false since there is a known bug in **docker distribution** garbage collect job. deleteUntagged: false + image: + # -- Cronjob image repository + repository: bitnami/kubectl + # -- Cronjob image pull policy + pullPolicy: IfNotPresent + # -- Cronjob image tag. Default 'latest' + tag: "latest" service: # -- Registry service type type: ClusterIP @@ -231,6 +253,11 @@ registry: httpGet: path: /v2/ port: 5000 + # -- Liveness probe definition for the proxy pod + livenessProbe: + httpGet: + path: /v2/ + port: 5000 resources: requests: # -- Cpu requests for the registry pod diff --git a/controllers/collector.go b/internal/controller/collector.go similarity index 93% rename from controllers/collector.go rename to internal/controller/collector.go index ca4a3735..345ce05a 100644 --- a/controllers/collector.go +++ b/internal/controller/collector.go @@ -1,10 +1,10 @@ -package controllers +package controller import ( "context" "strconv" - kuikenixiov1alpha1 "github.com/enix/kube-image-keeper/api/v1alpha1" + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" kuikMetrics "github.com/enix/kube-image-keeper/internal/metrics" "github.com/prometheus/client_golang/prometheus" "sigs.k8s.io/controller-runtime/pkg/client" @@ -70,7 +70,7 @@ func RegisterMetrics(client client.Client) { ) } -func cachedImagesWithLabelValues(gaugeVec *prometheus.GaugeVec, cachedImage *kuikenixiov1alpha1.CachedImage) prometheus.Gauge { +func cachedImagesWithLabelValues(gaugeVec *prometheus.GaugeVec, cachedImage *kuikv1alpha1.CachedImage) prometheus.Gauge { return gaugeVec.WithLabelValues(strconv.FormatBool(cachedImage.Status.IsCached), strconv.FormatBool(cachedImage.Spec.ExpiresAt != nil)) } @@ -83,7 +83,7 @@ func (c *ControllerCollector) Describe(ch chan<- *prometheus.Desc) { } func (c *ControllerCollector) Collect(ch chan<- prometheus.Metric) { - cachedImageList := &kuikenixiov1alpha1.CachedImageList{} + cachedImageList := &kuikv1alpha1.CachedImageList{} if err := c.List(context.Background(), cachedImageList); err == nil { cachedImageGaugeVec := prometheus.NewGaugeVec( prometheus.GaugeOpts{ diff --git a/controllers/pod_controller.go b/internal/controller/core/pod_controller.go similarity index 64% rename from controllers/pod_controller.go rename to internal/controller/core/pod_controller.go index 11b85320..15839a6c 100644 --- a/controllers/pod_controller.go +++ b/internal/controller/core/pod_controller.go @@ -1,17 +1,18 @@ -package controllers +package core import ( "context" _ "crypto/sha256" "strings" + "golang.org/x/exp/maps" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/selection" "k8s.io/apimachinery/pkg/types" - "github.com/docker/distribution/reference" - kuikenixiov1alpha1 "github.com/enix/kube-image-keeper/api/v1alpha1" + "github.com/distribution/reference" + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" "github.com/enix/kube-image-keeper/internal/registry" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -20,15 +21,16 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" ) -const cachedImageOwnerKey = ".metadata.podOwner" -const LabelImageRewrittenName = "kuik.enix.io/images-rewritten" +const CachedImageOwnerKey = ".metadata.podOwner" +const LabelManagedName = "kuik.enix.io/managed" +const AnnotationRewriteImagesName = "kuik.enix.io/rewrite-images" // PodReconciler reconciles a Pod object type PodReconciler struct { @@ -48,7 +50,7 @@ type PodReconciler struct { // the user. // // For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) @@ -59,21 +61,38 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R log.Info("reconciling pod") - cachedImages := desiredCachedImages(ctx, &pod) - serviceAccountImagePullSecrets, err := r.imagePullSecretNamesFromPodServiceAccount(ctx, &pod) - if err != nil { - return ctrl.Result{}, err - } - // On pod deletion if !pod.DeletionTimestamp.IsZero() { log.Info("pod is deleting") return ctrl.Result{}, nil } + cachedImages := DesiredCachedImages(ctx, &pod) + repositories, err := r.desiredRepositories(ctx, &pod, cachedImages) + if err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + // On pod creation and update + for _, repository := range repositories { + repo := repository.DeepCopy() + + operation, err := controllerutil.CreateOrPatch(ctx, r.Client, repo, func() error { + repo.Spec.Name = repository.Spec.Name + repo.Spec.PullSecretNames = repository.Spec.PullSecretNames + repo.Spec.PullSecretsNamespace = repository.Spec.PullSecretsNamespace + return nil + }) + + if err != nil { + return ctrl.Result{}, err + } + + log.Info("repository reconcilied", "repository", klog.KObj(&repository), "operation", operation) + } + for _, cachedImage := range cachedImages { - var ci kuikenixiov1alpha1.CachedImage + var ci kuikv1alpha1.CachedImage err := r.Get(ctx, client.ObjectKeyFromObject(&cachedImage), &ci) if err != nil && !apierrors.IsNotFound(err) { return ctrl.Result{}, err @@ -85,8 +104,6 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R continue } - cachedImage.Spec.PullSecretNames = append(cachedImage.Spec.PullSecretNames, serviceAccountImagePullSecrets...) - // Create or update CachedImage depending on weather it already exists or not if apierrors.IsNotFound(err) { err = r.Create(ctx, &cachedImage) @@ -96,8 +113,6 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R } else { patch := client.MergeFrom(ci.DeepCopy()) - ci.Spec.PullSecretNames = cachedImage.Spec.PullSecretNames - ci.Spec.PullSecretsNamespace = cachedImage.Spec.PullSecretsNamespace ci.Spec.SourceImage = cachedImage.Spec.SourceImage if err = r.Patch(ctx, &ci, patch); err != nil { @@ -108,7 +123,7 @@ func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.R log.Info("cachedimage patched", "cachedImage", klog.KObj(&cachedImage), "sourceImage", cachedImage.Spec.SourceImage) } - log.Info("reconciled pod") + log.Info("pod reconciled") return ctrl.Result{}, nil } @@ -122,35 +137,35 @@ func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&corev1.Pod{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { - _, ok := object.GetLabels()[LabelImageRewrittenName] + _, ok := object.GetLabels()[LabelManagedName] return ok }))). Watches( - &source.Kind{Type: &kuikenixiov1alpha1.CachedImage{}}, + &kuikv1alpha1.CachedImage{}, handler.EnqueueRequestsFromMapFunc(r.podsWithDeletingCachedImages), builder.WithPredicates(p), ). Complete(r) } -func (r *PodReconciler) podsWithDeletingCachedImages(obj client.Object) []ctrl.Request { +func (r *PodReconciler) podsWithDeletingCachedImages(ctx context.Context, obj client.Object) []ctrl.Request { log := log. - FromContext(context.Background()). + FromContext(ctx). WithName("controller-runtime.manager.controller.pod.deletingCachedImages"). WithValues("cachedImage", klog.KObj(obj)) - cachedImage := obj.(*kuikenixiov1alpha1.CachedImage) - var currentCachedImage kuikenixiov1alpha1.CachedImage + cachedImage := obj.(*kuikv1alpha1.CachedImage) + var currentCachedImage kuikv1alpha1.CachedImage // wait for the CachedImage to be really deleted - if err := r.Get(context.Background(), types.NamespacedName{Name: cachedImage.Name}, ¤tCachedImage); err == nil || !apierrors.IsNotFound(err) { + if err := r.Get(ctx, client.ObjectKeyFromObject(cachedImage), ¤tCachedImage); err == nil || !apierrors.IsNotFound(err) { return make([]ctrl.Request, 0) } var podList corev1.PodList - podRequirements, _ := labels.NewRequirement(LabelImageRewrittenName, selection.Equals, []string{"true"}) + podRequirements, _ := labels.NewRequirement(LabelManagedName, selection.Equals, []string{"true"}) selector := labels.NewSelector() selector = selector.Add(*podRequirements) - if err := r.List(context.Background(), &podList, &client.ListOptions{ + if err := r.List(ctx, &podList, &client.ListOptions{ LabelSelector: selector, }); err != nil { log.Error(err, "could not list pods") @@ -163,8 +178,7 @@ func (r *PodReconciler) podsWithDeletingCachedImages(obj client.Object) []ctrl.R if cachedImage.Spec.SourceImage == value { log.Info("image in use", "pod", pod.Namespace+"/"+pod.Name) res := make([]ctrl.Request, 1) - res[0].Name = pod.Name - res[0].Namespace = pod.Namespace + res[0].NamespacedName = client.ObjectKeyFromObject(&pod) return res } } @@ -173,27 +187,44 @@ func (r *PodReconciler) podsWithDeletingCachedImages(obj client.Object) []ctrl.R return make([]ctrl.Request, 0) } -func desiredCachedImages(ctx context.Context, pod *corev1.Pod) []kuikenixiov1alpha1.CachedImage { - pullSecretNames := []string{} +func (r *PodReconciler) desiredRepositories(ctx context.Context, pod *corev1.Pod, cachedImages []kuikv1alpha1.CachedImage) ([]kuikv1alpha1.Repository, error) { + repositories := map[string]kuikv1alpha1.Repository{} - for _, pullSecret := range pod.Spec.ImagePullSecrets { - pullSecretNames = append(pullSecretNames, pullSecret.Name) + pullSecretNames, err := r.imagePullSecretNamesFromPod(ctx, pod) + if err != nil { + return nil, err } - cachedImages := desiredCachedImagesForContainers(ctx, pod.Spec.Containers, pod.Annotations, false) - cachedImages = append(cachedImages, desiredCachedImagesForContainers(ctx, pod.Spec.InitContainers, pod.Annotations, true)...) - - for i := range cachedImages { - cachedImages[i].Spec.PullSecretNames = pullSecretNames - cachedImages[i].Spec.PullSecretsNamespace = pod.Namespace + for _, cachedImage := range cachedImages { + named, err := cachedImage.Repository() + if err != nil { + return nil, err + } + repositoryName := named.Name() + repositories[repositoryName] = kuikv1alpha1.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: registry.SanitizeName(repositoryName), + }, + Spec: kuikv1alpha1.RepositorySpec{ + Name: repositoryName, + PullSecretNames: pullSecretNames, + PullSecretsNamespace: pod.Namespace, + }, + } } + return maps.Values(repositories), nil +} + +func DesiredCachedImages(ctx context.Context, pod *corev1.Pod) []kuikv1alpha1.CachedImage { + cachedImages := desiredCachedImagesForContainers(ctx, pod.Spec.Containers, pod.Annotations, false) + cachedImages = append(cachedImages, desiredCachedImagesForContainers(ctx, pod.Spec.InitContainers, pod.Annotations, true)...) return cachedImages } -func desiredCachedImagesForContainers(ctx context.Context, containers []corev1.Container, annotations map[string]string, initContainer bool) []kuikenixiov1alpha1.CachedImage { +func desiredCachedImagesForContainers(ctx context.Context, containers []corev1.Container, annotations map[string]string, initContainer bool) []kuikv1alpha1.CachedImage { log := log.FromContext(ctx) - cachedImages := []kuikenixiov1alpha1.CachedImage{} + cachedImages := []kuikv1alpha1.CachedImage{} for _, container := range containers { annotationKey := registry.ContainerAnnotationKey(container.Name, initContainer) @@ -218,7 +249,7 @@ func desiredCachedImagesForContainers(ctx context.Context, containers []corev1.C return cachedImages } -func cachedImageFromSourceImage(sourceImage string) (*kuikenixiov1alpha1.CachedImage, error) { +func cachedImageFromSourceImage(sourceImage string) (*kuikv1alpha1.CachedImage, error) { ref, err := reference.ParseAnyReference(sourceImage) if err != nil { return nil, err @@ -228,20 +259,13 @@ func cachedImageFromSourceImage(sourceImage string) (*kuikenixiov1alpha1.CachedI if !strings.Contains(sourceImage, ":") { sanitizedName += "-latest" } - named, err := reference.ParseNormalizedNamed(ref.String()) - if err != nil { - return nil, err - } - cachedImage := kuikenixiov1alpha1.CachedImage{ - TypeMeta: metav1.TypeMeta{APIVersion: kuikenixiov1alpha1.GroupVersion.String(), Kind: "CachedImage"}, + cachedImage := kuikv1alpha1.CachedImage{ + TypeMeta: metav1.TypeMeta{APIVersion: kuikv1alpha1.GroupVersion.String(), Kind: "CachedImage"}, ObjectMeta: metav1.ObjectMeta{ Name: sanitizedName, - Labels: map[string]string{ - kuikenixiov1alpha1.RepositoryLabelName: registry.RepositoryLabel(named.Name()), - }, }, - Spec: kuikenixiov1alpha1.CachedImageSpec{ + Spec: kuikv1alpha1.CachedImageSpec{ SourceImage: sourceImage, }, } @@ -249,20 +273,21 @@ func cachedImageFromSourceImage(sourceImage string) (*kuikenixiov1alpha1.CachedI return &cachedImage, nil } -func (r *PodReconciler) imagePullSecretNamesFromPodServiceAccount(ctx context.Context, pod *corev1.Pod) ([]string, error) { +func (r *PodReconciler) imagePullSecretNamesFromPod(ctx context.Context, pod *corev1.Pod) ([]string, error) { if pod.Spec.ServiceAccountName == "" { return []string{}, nil } var serviceAccount corev1.ServiceAccount serviceAccountNamespacedName := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Spec.ServiceAccountName} - if err := r.Get(ctx, serviceAccountNamespacedName, &serviceAccount); err != nil { + if err := r.Get(ctx, serviceAccountNamespacedName, &serviceAccount); err != nil && !apierrors.IsNotFound(err) { return []string{}, err } - imagePullSecretNames := make([]string, len(serviceAccount.ImagePullSecrets)) + imagePullSecrets := append(pod.Spec.ImagePullSecrets, serviceAccount.ImagePullSecrets...) + imagePullSecretNames := make([]string, len(imagePullSecrets)) - for i, imagePullSecret := range serviceAccount.ImagePullSecrets { + for i, imagePullSecret := range imagePullSecrets { imagePullSecretNames[i] = imagePullSecret.Name } diff --git a/controllers/pod_controller_test.go b/internal/controller/core/pod_controller_test.go similarity index 58% rename from controllers/pod_controller_test.go rename to internal/controller/core/pod_controller_test.go index fc5fe899..54216cf1 100644 --- a/controllers/pod_controller_test.go +++ b/internal/controller/core/pod_controller_test.go @@ -1,14 +1,13 @@ -package controllers +package core import ( "context" "testing" "time" - "github.com/enix/kube-image-keeper/api/v1alpha1" - kuikenixiov1alpha1 "github.com/enix/kube-image-keeper/api/v1alpha1" + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" "github.com/enix/kube-image-keeper/internal/registry" - . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,7 +23,7 @@ var podStub = corev1.Pod{ registry.ContainerAnnotationKey("c", false): "busybox", }, Labels: map[string]string{ - LabelImageRewrittenName: "true", + LabelManagedName: "true", }, }, Spec: corev1.PodSpec{ @@ -38,17 +37,6 @@ var podStub = corev1.Pod{ }, } -var serviceAccountStub = corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: podStub.Namespace, - }, - ImagePullSecrets: []corev1.LocalObjectReference{ - {Name: "service-account-pull-secret"}, - {Name: "service-account-pull-secret-2"}, - }, -} - var podStubNotRewritten = corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", @@ -69,19 +57,19 @@ func TestDesiredCachedImages(t *testing.T) { tests := []struct { name string pod corev1.Pod - cachedImages []v1alpha1.CachedImage + cachedImages []kuikv1alpha1.CachedImage }{ { name: "basic", pod: podStub, - cachedImages: []v1alpha1.CachedImage{ - {Spec: kuikenixiov1alpha1.CachedImageSpec{ + cachedImages: []kuikv1alpha1.CachedImage{ + {Spec: kuikv1alpha1.CachedImageSpec{ SourceImage: "nginx", }}, - {Spec: kuikenixiov1alpha1.CachedImageSpec{ + {Spec: kuikv1alpha1.CachedImageSpec{ SourceImage: "busybox", }}, - {Spec: kuikenixiov1alpha1.CachedImageSpec{ + {Spec: kuikv1alpha1.CachedImageSpec{ SourceImage: "alpine", }}, }, @@ -91,18 +79,10 @@ func TestDesiredCachedImages(t *testing.T) { g := NewWithT(t) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cachedImages := desiredCachedImages(context.Background(), &tt.pod) + cachedImages := DesiredCachedImages(context.Background(), &tt.pod) g.Expect(cachedImages).To(HaveLen(len(tt.cachedImages))) for i, cachedImage := range cachedImages { g.Expect(cachedImage.Spec.SourceImage).To(Equal(tt.cachedImages[i].Spec.SourceImage)) - g.Expect(cachedImage.Spec.PullSecretsNamespace).To(Equal(tt.pod.Namespace)) - - pullSecretNames := []string{} - for _, pullSecret := range tt.pod.Spec.ImagePullSecrets { - pullSecretNames = append(pullSecretNames, pullSecret.Name) - } - g.Expect(cachedImage.Spec.PullSecretNames).To(ConsistOf(pullSecretNames)) - } }) } @@ -144,11 +124,6 @@ func Test_cachedImageFromSourceImage(t *testing.T) { g.Expect(cachedImage.Name).To(Equal(tt.expectedName)) g.Expect(cachedImage.Spec.SourceImage).To(Equal(tt.sourceImage)) g.Expect(cachedImage.Spec.ExpiresAt).To(BeNil()) - g.Expect(cachedImage.Spec.PullSecretNames).To(BeEmpty()) - g.Expect(cachedImage.Spec.PullSecretsNamespace).To(BeEmpty()) - g.Expect(cachedImage.Labels).To(Equal(map[string]string{ - kuikenixiov1alpha1.RepositoryLabelName: registry.RepositoryLabel(tt.expectedRepository), - })) }) } } @@ -159,7 +134,6 @@ var _ = Describe("Pod Controller", func() { const interval = time.Second * 1 BeforeEach(func() { - // Add any setup steps that needs to be executed before each test }) AfterEach(func() { @@ -172,7 +146,7 @@ var _ = Describe("Pod Controller", func() { podStubNotRewritten.ResourceVersion = "" By("Deleting all cached images") - Expect(k8sClient.DeleteAllOf(context.Background(), &kuikenixiov1alpha1.CachedImage{})).Should(Succeed()) + Expect(k8sClient.DeleteAllOf(context.Background(), &kuikv1alpha1.CachedImage{})).Should(Succeed()) }) Context("Pod with containers and init containers", func() { @@ -180,8 +154,8 @@ var _ = Describe("Pod Controller", func() { By("Creating a pod") Expect(k8sClient.Create(context.Background(), &podStub)).Should(Succeed()) - fetched := &kuikenixiov1alpha1.CachedImageList{} - Eventually(func() []kuikenixiov1alpha1.CachedImage { + fetched := &kuikv1alpha1.CachedImageList{} + Eventually(func() []kuikv1alpha1.CachedImage { _ = k8sClient.List(context.Background(), fetched) return fetched.Items }, timeout, interval).Should(HaveLen(len(podStub.Spec.Containers) + len(podStub.Spec.InitContainers))) @@ -198,49 +172,16 @@ var _ = Describe("Pod Controller", func() { By("Deleting previously created pod") Expect(k8sClient.Delete(context.Background(), &podStub)).Should(Succeed()) - - Eventually(func() []kuikenixiov1alpha1.CachedImage { - expiringCachedImages := []kuikenixiov1alpha1.CachedImage{} - _ = k8sClient.List(context.Background(), fetched) - for _, cachedImage := range fetched.Items { - if cachedImage.Spec.ExpiresAt != nil { - expiringCachedImages = append(expiringCachedImages, cachedImage) - } - } - return expiringCachedImages - }, timeout, interval).Should(HaveLen(len(podStub.Spec.Containers) + len(podStub.Spec.InitContainers))) }) It("Should not create CachedImages", func() { By("Creating a pod without rewriting images") Expect(k8sClient.Create(context.Background(), &podStubNotRewritten)).Should(Succeed()) - fetched := &kuikenixiov1alpha1.CachedImageList{} - Eventually(func() []kuikenixiov1alpha1.CachedImage { + fetched := &kuikv1alpha1.CachedImageList{} + Eventually(func() []kuikv1alpha1.CachedImage { _ = k8sClient.List(context.Background(), fetched) return fetched.Items }, timeout, interval).Should(HaveLen(0)) }) - It("Should create CachedImages with imagePullSecrets from Pod's ServiceAccount", func() { - By("Creating a Pod with a ServiceAccount") - Expect(k8sClient.Create(context.Background(), &serviceAccountStub)).Should(Succeed()) - - podStub.Spec.ServiceAccountName = serviceAccountStub.Name - Expect(k8sClient.Create(context.Background(), &podStub)).Should(Succeed()) - - fetched := &kuikenixiov1alpha1.CachedImageList{} - Eventually(func() []kuikenixiov1alpha1.CachedImage { - _ = k8sClient.List(context.Background(), fetched) - return fetched.Items - }, timeout, interval).Should(HaveLen(len(podStub.Spec.Containers) + len(podStub.Spec.InitContainers))) - - imagePullSecretNames := make([]string, len(serviceAccountStub.ImagePullSecrets)) - for i, imagePullSecretName := range serviceAccountStub.ImagePullSecrets { - imagePullSecretNames[i] = imagePullSecretName.Name - } - - for _, cachedImage := range fetched.Items { - Expect(cachedImage.Spec.PullSecretNames).Should(ContainElements(imagePullSecretNames)) - } - }) }) }) diff --git a/internal/controller/core/suite_test.go b/internal/controller/core/suite_test.go new file mode 100644 index 00000000..21cedb86 --- /dev/null +++ b/internal/controller/core/suite_test.go @@ -0,0 +1,93 @@ +package core + +import ( + "context" + "os" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + corev1 "k8s.io/api/core/v1" + + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(os.Stdout), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = kuikv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + err = corev1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + err = (&PodReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/internal/controller/force_name.go b/internal/controller/force_name.go new file mode 100644 index 00000000..1d583607 --- /dev/null +++ b/internal/controller/force_name.go @@ -0,0 +1,54 @@ +package controller + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func ForceName(c client.Client, ctx context.Context, newName string, obj client.Object, finalizerName string) error { + log := log.FromContext(ctx) + kind := obj.GetObjectKind().GroupVersionKind().Kind + + if newName != obj.GetName() { + newObj := obj.DeepCopyObject().(client.Object) + oldObj := obj.DeepCopyObject().(client.Object) + newObj.SetName(newName) + + if err := c.Get(ctx, types.NamespacedName{Name: newName}, oldObj); err != nil { + if apierrors.IsNotFound(err) { + log.Info(fmt.Sprintf("recreating %s with an appropriate name", kind), "newName", newName) + newObj.SetResourceVersion("") + newObj.SetUID("") + if err := c.Create(ctx, newObj); err != nil { + return err + } + } else { + return err + } + } else { + log.Info(fmt.Sprintf("updating %s from %s with an invalid name", kind, kind), "newName", newName) + newObj.SetResourceVersion(oldObj.GetResourceVersion()) + newObj.SetUID(oldObj.GetUID()) + newObj.SetDeletionGracePeriodSeconds(oldObj.GetDeletionGracePeriodSeconds()) + newObj.SetDeletionTimestamp(oldObj.GetDeletionTimestamp()) + if err := c.Patch(ctx, newObj, client.MergeFrom(oldObj)); err != nil { + return err + } + } + log.Info(fmt.Sprintf("removing finalizer and deleting %s with an invalid name", kind)) + controllerutil.RemoveFinalizer(obj, finalizerName) + if err := c.Update(ctx, obj); err != nil { + return err + } + if err := c.Delete(ctx, obj); err != nil { + return err + } + } + return nil +} diff --git a/controllers/healthz.go b/internal/controller/healthz.go similarity index 92% rename from controllers/healthz.go rename to internal/controller/healthz.go index fb1e3505..88320f47 100644 --- a/controllers/healthz.go +++ b/internal/controller/healthz.go @@ -1,4 +1,4 @@ -package controllers +package controller import ( "net/http" diff --git a/controllers/cachedimage_controller.go b/internal/controller/kuik/cachedimage_controller.go similarity index 54% rename from controllers/cachedimage_controller.go rename to internal/controller/kuik/cachedimage_controller.go index 700988a9..3f82cf9d 100644 --- a/controllers/cachedimage_controller.go +++ b/internal/controller/kuik/cachedimage_controller.go @@ -1,12 +1,15 @@ -package controllers +package kuik import ( "context" "crypto/x509" "net/http" + "strings" "time" + "github.com/distribution/reference" "github.com/go-logr/logr" + "github.com/google/go-containerregistry/pkg/v1/remote" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -24,15 +27,26 @@ import ( "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" - "github.com/enix/kube-image-keeper/api/v1alpha1" - kuikenixiov1alpha1 "github.com/enix/kube-image-keeper/api/v1alpha1" + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" + kuikController "github.com/enix/kube-image-keeper/internal/controller" + "github.com/enix/kube-image-keeper/internal/controller/core" "github.com/enix/kube-image-keeper/internal/registry" ) -// https://book.kubebuilder.io/reference/using-finalizers.html -const cachedImageFinalizerName = "cachedimage.kuik.enix.io/finalizer" +const ( + cachedImageFinalizerName = "cachedimage.kuik.enix.io/finalizer" + cachedImageAnnotationForceUpdateName = "cachedimage.kuik.enix.io/forceUpdate" + repositoryOwnerKey = ".metadata.repositoryOwner" +) + +const ( + cachedImagePhaseSynchronizing = "Synchronizing" + cachedImagePhasePulling = "Pulling" + cachedImagePhaseErrImagePull = "ErrImagePull" + cachedImagePhaseReady = "Ready" + cachedImagePhaseTerminating = "Terminating" +) // CachedImageReconciler reconciles a CachedImage object type CachedImageReconciler struct { @@ -60,21 +74,65 @@ type CachedImageReconciler struct { // the user. // // For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile func (r *CachedImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := log. - FromContext(ctx) + log := log.FromContext(ctx) - var cachedImage kuikenixiov1alpha1.CachedImage + var cachedImage kuikv1alpha1.CachedImage if err := r.Get(ctx, req.NamespacedName, &cachedImage); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } log.Info("reconciling cachedimage") + // Handle images with an invalid name + sanitizedName, err := getSanitizedName(&cachedImage) + if err != nil { + return ctrl.Result{}, err + } + + if err := kuikController.ForceName(r.Client, ctx, sanitizedName, &cachedImage, cachedImageFinalizerName); err != nil { + return ctrl.Result{}, err + } + + // Create or patch related repository + named, err := cachedImage.Repository() + if err != nil { + return ctrl.Result{}, err + } + + repositoryName := named.Name() + repository := kuikv1alpha1.Repository{ObjectMeta: metav1.ObjectMeta{Name: registry.SanitizeName(repositoryName)}} + operation, err := controllerutil.CreateOrPatch(ctx, r.Client, &repository, func() error { + repository.Spec.Name = repositoryName + return nil + }) + + if err != nil { + return ctrl.Result{}, err + } + + log.Info("repository updated", "repository", klog.KObj(&repository), "operation", operation) + + // Set owner reference + owner := &kuikv1alpha1.Repository{} + if err := r.Get(ctx, client.ObjectKeyFromObject(&repository), owner); err != nil { + return ctrl.Result{}, err + } + if err := controllerutil.SetOwnerReference(owner, &cachedImage, r.Scheme); err != nil { + return ctrl.Result{}, err + } + if err := r.Update(ctx, &cachedImage); err != nil { + return ctrl.Result{}, err + } + // Remove image from registry when CachedImage is being deleted, finalizer is removed after it if !cachedImage.ObjectMeta.DeletionTimestamp.IsZero() { if controllerutil.ContainsFinalizer(&cachedImage, cachedImageFinalizerName) { + if err := r.patchPhase(&cachedImage, cachedImagePhaseTerminating); err != nil { + return ctrl.Result{}, err + } + log.Info("deleting image from cache") r.Recorder.Eventf(&cachedImage, "Normal", "CleaningUp", "Removing image %s from cache", cachedImage.Spec.SourceImage) if err := registry.DeleteImage(cachedImage.Spec.SourceImage); err != nil { @@ -82,7 +140,7 @@ func (r *CachedImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) return ctrl.Result{}, err } r.Recorder.Eventf(&cachedImage, "Normal", "CleanedUp", "Image %s successfully removed from cache", cachedImage.Spec.SourceImage) - imageRemovedFromCache.Inc() + // imageRemovedFromCache.Inc() log.Info("removing finalizer") controllerutil.RemoveFinalizer(&cachedImage, cachedImageFinalizerName) @@ -133,6 +191,60 @@ func (r *CachedImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) if err != nil && !apierrors.IsNotFound(err) { return ctrl.Result{}, err } + if err := r.Get(ctx, req.NamespacedName, &cachedImage); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + } + + // Removing forceUpdate annotation + forceUpdate := cachedImage.Annotations[cachedImageAnnotationForceUpdateName] == "true" + patch := client.MergeFrom(cachedImage.DeepCopy()) + if forceUpdate { + delete(cachedImage.Annotations, cachedImageAnnotationForceUpdateName) + } + err = r.Patch(ctx, &cachedImage, patch) + if err != nil { + return ctrl.Result{}, err + } + + isCached, err := registry.ImageIsCached(cachedImage.Spec.SourceImage) + if err != nil { + return ctrl.Result{}, err + } + + err = updateStatusRaw(r.Client, &cachedImage, func(status *kuikv1alpha1.CachedImageStatus) { + cachedImage.Status.IsCached = isCached + }) + if err != nil { + return ctrl.Result{}, err + } + + // Adding image to registry + putImageInCache := true + if isCached && !forceUpdate { + putImageInCache = false + } + if putImageInCache { + r.Recorder.Eventf(&cachedImage, "Normal", "Caching", "Start caching image %s", cachedImage.Spec.SourceImage) + err = r.cacheImage(&cachedImage) + if err != nil { + log.Error(err, "failed to cache image") + r.Recorder.Eventf(&cachedImage, "Warning", "CacheFailed", "Failed to cache image %s, reason: %s", cachedImage.Spec.SourceImage, err) + if phaseErr := r.patchPhase(&cachedImage, cachedImagePhaseErrImagePull); phaseErr != nil { + return ctrl.Result{}, phaseErr + } + return ctrl.Result{}, err + } else { + log.Info("image cached") + r.Recorder.Eventf(&cachedImage, "Normal", "Cached", "Successfully cached image %s", cachedImage.Spec.SourceImage) + // imagePutInCache.Inc() + } + } else { + log.Info("image already present in cache, ignoring") + } + + if err := r.patchPhase(&cachedImage, cachedImagePhaseReady); err != nil { + return ctrl.Result{}, err } // Delete expired CachedImage and schedule deletion for expiring ones @@ -152,54 +264,115 @@ func (r *CachedImageReconciler) Reconcile(ctx context.Context, req ctrl.Request) } } - // Adding image to registry - log.Info("caching image") - isCached, err := registry.ImageIsCached(cachedImage.Spec.SourceImage) - if err != nil { - log.Error(err, "could not determine if the image present in cache") - return ctrl.Result{}, err - } + log.Info("cachedimage reconciled") + return ctrl.Result{}, nil +} - if !isCached { - r.Recorder.Eventf(&cachedImage, "Normal", "Caching", "Start caching image %s", cachedImage.Spec.SourceImage) - keychain := registry.NewKubernetesKeychain(r.ApiReader, cachedImage.Spec.PullSecretsNamespace, cachedImage.Spec.PullSecretNames) - if err := registry.CacheImage(cachedImage.Spec.SourceImage, keychain, r.Architectures, r.InsecureRegistries, r.RootCAs); err != nil { - log.Error(err, "failed to cache image") - r.Recorder.Eventf(&cachedImage, "Warning", "CacheFailed", "Failed to cache image %s, reason: %s", cachedImage.Spec.SourceImage, err) - return ctrl.Result{}, err +func updateStatus(c client.Client, cachedImage *kuikv1alpha1.CachedImage, upstreamDescriptor *remote.Descriptor, update func(*kuikv1alpha1.CachedImageStatus)) error { + return updateStatusRaw(c, cachedImage, func(status *kuikv1alpha1.CachedImageStatus) { + cachedImage.Status.AvailableUpstream = upstreamDescriptor != nil + cachedImage.Status.LastSync = metav1.NewTime(time.Now()) + + update(&cachedImage.Status) + + if upstreamDescriptor != nil { + cachedImage.Status.UpstreamDigest = upstreamDescriptor.Digest.Hex + cachedImage.Status.UpToDate = cachedImage.Status.Digest == upstreamDescriptor.Digest.Hex } else { - log.Info("image cached") - r.Recorder.Eventf(&cachedImage, "Normal", "Cached", "Successfully cached image %s", cachedImage.Spec.SourceImage) - imagePutInCache.Inc() - if err := r.Get(ctx, req.NamespacedName, &cachedImage); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } + cachedImage.Status.UpstreamDigest = "" + cachedImage.Status.UpToDate = false } - } else { - log.Info("image already present in cache, ignoring") + }) +} + +func updateStatusRaw(c client.Client, cachedImage *kuikv1alpha1.CachedImage, update func(*kuikv1alpha1.CachedImageStatus)) error { + patch := client.MergeFrom(cachedImage.DeepCopy()) + update(&cachedImage.Status) + return c.Status().Patch(context.Background(), cachedImage, patch) +} + +func getSanitizedName(cachedImage *kuikv1alpha1.CachedImage) (string, error) { + ref, err := reference.ParseAnyReference(cachedImage.Spec.SourceImage) + if err != nil { + return "", err + } + + sanitizedName := registry.SanitizeName(ref.String()) + if !strings.Contains(cachedImage.Spec.SourceImage, ":") { + sanitizedName += "-latest" + } + + return sanitizedName, nil +} + +func (r *CachedImageReconciler) cacheImage(cachedImage *kuikv1alpha1.CachedImage) error { + if err := r.patchPhase(cachedImage, cachedImagePhaseSynchronizing); err != nil { + return err } - // Update CachedImage IsCached status - log.Info("updating CachedImage status") - cachedImage.Status.IsCached = true - err = r.Status().Update(context.Background(), &cachedImage) + pullSecrets, err := cachedImage.GetPullSecrets(r.ApiReader) if err != nil { - if statusErr, ok := err.(*errors.StatusError); ok && statusErr.Status().Code == http.StatusConflict { - return ctrl.Result{Requeue: true}, nil + return err + } + + desc, err := registry.GetDescriptor(cachedImage.Spec.SourceImage, pullSecrets, r.InsecureRegistries, r.RootCAs) + + statusErr := updateStatus(r.Client, cachedImage, desc, func(status *kuikv1alpha1.CachedImageStatus) { + _, err := registry.GetLocalDescriptor(cachedImage.Spec.SourceImage) + cachedImage.Status.IsCached = err == nil + + if cachedImage.Status.AvailableUpstream { + cachedImage.Status.LastSeenUpstream = metav1.NewTime(time.Now()) } - return ctrl.Result{}, err + }) + + if err != nil { + return err + } + if statusErr != nil { + return statusErr } - log.Info("reconciled cachedimage") - return ctrl.Result{}, nil + if cachedImage.Status.UpToDate { + return nil + } + + if err = r.patchPhase(cachedImage, cachedImagePhasePulling); err != nil { + return err + } + + err = registry.CacheImage(cachedImage.Spec.SourceImage, desc, r.Architectures) + + statusErr = updateStatus(r.Client, cachedImage, desc, func(status *kuikv1alpha1.CachedImageStatus) { + if err == nil { + cachedImage.Status.IsCached = true + cachedImage.Status.Digest = desc.Digest.Hex + cachedImage.Status.LastSuccessfulPull = metav1.NewTime(time.Now()) + } + }) + + if err != nil { + return err + } + if statusErr != nil { + return statusErr + } + + return nil +} + +func (r *CachedImageReconciler) patchPhase(cachedImage *kuikv1alpha1.CachedImage, phase string) error { + patch := client.MergeFrom(cachedImage.DeepCopy()) + cachedImage.Status.Phase = phase + return r.Status().Patch(context.Background(), cachedImage, patch) } // SetupWithManager sets up the controller with the Manager. func (r *CachedImageReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconciles int) error { // Create an index to list Pods by CachedImage - if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Pod{}, cachedImageOwnerKey, func(rawObj client.Object) []string { + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Pod{}, core.CachedImageOwnerKey, func(rawObj client.Object) []string { pod := rawObj.(*corev1.Pod) - if _, ok := pod.Labels[LabelImageRewrittenName]; !ok { + if _, ok := pod.Labels[core.LabelManagedName]; !ok { return []string{} } @@ -208,7 +381,7 @@ func (r *CachedImageReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrent WithValues("pod", klog.KObj(pod)) ctx := logr.NewContext(context.Background(), logger) - cachedImages := desiredCachedImages(ctx, pod) + cachedImages := core.DesiredCachedImages(ctx, pod) cachedImageNames := make([]string, len(cachedImages)) for _, cachedImage := range cachedImages { @@ -221,9 +394,9 @@ func (r *CachedImageReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrent } return ctrl.NewControllerManagedBy(mgr). - For(&kuikenixiov1alpha1.CachedImage{}). + For(&kuikv1alpha1.CachedImage{}). Watches( - &source.Kind{Type: &corev1.Pod{}}, + &corev1.Pod{}, handler.EnqueueRequestsFromMapFunc(r.cachedImagesRequestFromPod), builder.WithPredicates(predicate.Funcs{ // GenericFunc: func(e event.GenericEvent) bool { @@ -239,7 +412,7 @@ func (r *CachedImageReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrent }), ). Watches( - &source.Kind{Type: &corev1.Pod{}}, + &corev1.Pod{}, handler.EnqueueRequestsFromMapFunc(r.cachedImagesRequestFromPod), builder.WithPredicates(predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { @@ -250,30 +423,36 @@ func (r *CachedImageReconciler) SetupWithManager(mgr ctrl.Manager, maxConcurrent WithOptions(controller.Options{ MaxConcurrentReconciles: maxConcurrentReconciles, }). + // prevent from reenquing after status update (produced a infinite loop between ErrImagePull and Synchronizing phases) + WithEventFilter(predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.LabelChangedPredicate{}, + predicate.AnnotationChangedPredicate{}, + )). Complete(r) } // updatePodCount update CachedImage UsedBy status -func (r *CachedImageReconciler) updatePodCount(ctx context.Context, cachedImage *kuikenixiov1alpha1.CachedImage) (requeue bool, err error) { +func (r *CachedImageReconciler) updatePodCount(ctx context.Context, cachedImage *kuikv1alpha1.CachedImage) (requeue bool, err error) { var podsList corev1.PodList - if err = r.List(ctx, &podsList, client.MatchingFields{cachedImageOwnerKey: cachedImage.Name}); err != nil && !apierrors.IsNotFound(err) { + if err = r.List(ctx, &podsList, client.MatchingFields{core.CachedImageOwnerKey: cachedImage.Name}); err != nil && !apierrors.IsNotFound(err) { return } - pods := []v1alpha1.PodReference{} + pods := []kuikv1alpha1.PodReference{} for _, pod := range podsList.Items { if !pod.DeletionTimestamp.IsZero() { continue } - pods = append(pods, v1alpha1.PodReference{NamespacedName: pod.Namespace + "/" + pod.Name}) + pods = append(pods, kuikv1alpha1.PodReference{NamespacedName: pod.Namespace + "/" + pod.Name}) } - cachedImage.Status.UsedBy = v1alpha1.UsedBy{ + cachedImage.Status.UsedBy = kuikv1alpha1.UsedBy{ Pods: pods, Count: len(pods), } - err = r.Status().Update(context.Background(), cachedImage) + err = r.Status().Update(ctx, cachedImage) if err != nil { if statusErr, ok := err.(*errors.StatusError); ok && statusErr.Status().Code == http.StatusConflict { requeue = true @@ -284,15 +463,9 @@ func (r *CachedImageReconciler) updatePodCount(ctx context.Context, cachedImage return } -func (r *CachedImageReconciler) cachedImagesRequestFromPod(obj client.Object) []ctrl.Request { - log := log. - FromContext(context.Background()). - WithName("controller-runtime.manager.controller.cachedImage.deletingPods"). - WithValues("pod", klog.KObj(obj)) - +func (r *CachedImageReconciler) cachedImagesRequestFromPod(ctx context.Context, obj client.Object) []ctrl.Request { pod := obj.(*corev1.Pod) - ctx := logr.NewContext(context.Background(), log) - cachedImages := desiredCachedImages(ctx, pod) + cachedImages := core.DesiredCachedImages(ctx, pod) res := []ctrl.Request{} for _, cachedImage := range cachedImages { diff --git a/internal/controller/kuik/cachedimage_controller_test.go b/internal/controller/kuik/cachedimage_controller_test.go new file mode 100644 index 00000000..ad847fba --- /dev/null +++ b/internal/controller/kuik/cachedimage_controller_test.go @@ -0,0 +1,71 @@ +package kuik + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("CachedImage Controller", func() { + + const timeout = time.Second * 30 + const interval = time.Second * 1 + + Context("When creating CachedImages", func() { + It("Should expire image that are not retained only", func() { + fetched := &kuikv1alpha1.CachedImageList{} + + By("Creating an image without the retain flag", func() { + Expect(k8sClient.Create(context.Background(), &kuikv1alpha1.CachedImage{ + ObjectMeta: v1.ObjectMeta{ + Name: "nginx", + }, + Spec: kuikv1alpha1.CachedImageSpec{ + SourceImage: "nginx", + }, + })).Should(Succeed()) + + Eventually(func() []kuikv1alpha1.CachedImage { + expiringCachedImages := []kuikv1alpha1.CachedImage{} + _ = k8sClient.List(context.Background(), fetched) + for _, cachedImage := range fetched.Items { + if cachedImage.Spec.ExpiresAt != nil { + expiringCachedImages = append(expiringCachedImages, cachedImage) + } + } + return expiringCachedImages + }, timeout, interval).Should(HaveLen(1)) + }) + + By("Creating an expiring image with the retain flag", func() { + Expect(k8sClient.Create(context.Background(), &kuikv1alpha1.CachedImage{ + ObjectMeta: v1.ObjectMeta{ + Name: "alpine", + }, + Spec: kuikv1alpha1.CachedImageSpec{ + SourceImage: "alpine", + Retain: true, + ExpiresAt: &v1.Time{Time: time.Now().Add(time.Hour)}, + }, + })).Should(Succeed()) + + Eventually(func() []kuikv1alpha1.CachedImage { + expiringCachedImages := []kuikv1alpha1.CachedImage{} + _ = k8sClient.List(context.Background(), fetched) + for _, cachedImage := range fetched.Items { + if cachedImage.Spec.ExpiresAt != nil { + expiringCachedImages = append(expiringCachedImages, cachedImage) + } + } + return expiringCachedImages + }, timeout, interval).Should(HaveLen(1)) + }) + + }) + }) +}) diff --git a/internal/controller/kuik/repository_controller.go b/internal/controller/kuik/repository_controller.go new file mode 100644 index 00000000..7b9d3300 --- /dev/null +++ b/internal/controller/kuik/repository_controller.go @@ -0,0 +1,296 @@ +package kuik + +import ( + "context" + "regexp" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" + kuikController "github.com/enix/kube-image-keeper/internal/controller" + "github.com/enix/kube-image-keeper/internal/registry" +) + +const ( + repositoryFinalizerName = "repository.kuik.enix.io/finalizer" + typeReadyRepository = "Ready" +) + +// RepositoryReconciler reconciles a Repository object +type RepositoryReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder +} + +//+kubebuilder:rbac:groups=kuik.enix.io,resources=repositories,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=kuik.enix.io,resources=repositories/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=kuik.enix.io,resources=repositories/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Repository object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *RepositoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + var repository kuikv1alpha1.Repository + if err := r.Get(ctx, req.NamespacedName, &repository); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + log.Info("reconciling repository") + + // Handle repositories with an invalid name + sanitizedName := registry.SanitizeName(repository.Spec.Name) + + if err := kuikController.ForceName(r.Client, ctx, sanitizedName, &repository, repositoryFinalizerName); err != nil { + return ctrl.Result{}, err + } + + var cachedImageList kuikv1alpha1.CachedImageList + if err := r.List(ctx, &cachedImageList, client.MatchingFields{repositoryOwnerKey: repository.Name}); err != nil && !apierrors.IsNotFound(err) { + return ctrl.Result{}, err + } + repository.Status.Images = len(cachedImageList.Items) + + if !repository.ObjectMeta.DeletionTimestamp.IsZero() { + if repository.Status.Phase != "Terminating" { + r.Recorder.Eventf(&repository, "Normal", "Terminating", "Waiting for cached images to be deleted") + err := r.UpdateStatus(ctx, &repository, []metav1.Condition{{ + Type: typeReadyRepository, + Status: metav1.ConditionFalse, + Reason: "Terminating", + Message: "Repository has been asked to be deleted", + }}) + if err != nil { + return ctrl.Result{}, err + } + } + + if controllerutil.ContainsFinalizer(&repository, repositoryFinalizerName) { + log.Info("repository is deleting", "cachedImages", len(cachedImageList.Items)) + if len(cachedImageList.Items) > 0 { + return ctrl.Result{}, nil + } + + log.Info("removing finalizer") + controllerutil.RemoveFinalizer(&repository, repositoryFinalizerName) + if err := r.Update(ctx, &repository); err != nil { + return ctrl.Result{}, err + } + } + + return ctrl.Result{}, nil + } + + pullingCount := 0 + errImagePullCount := 0 + for _, cachedImage := range cachedImageList.Items { + if cachedImage.Status.Phase == cachedImagePhasePulling || cachedImage.Status.Phase == cachedImagePhaseSynchronizing { + pullingCount++ + } else if cachedImage.Status.Phase == cachedImagePhaseErrImagePull { + errImagePullCount++ + } + } + + if errImagePullCount > 0 { + err := r.UpdateStatus(ctx, &repository, []metav1.Condition{{ + Type: typeReadyRepository, + Status: metav1.ConditionFalse, + Reason: "ErrImagePull", + Message: "Some images in pull error", + }}) + if err != nil { + return ctrl.Result{}, err + } + } else if pullingCount > 0 { + err := r.UpdateStatus(ctx, &repository, []metav1.Condition{{ + Type: typeReadyRepository, + Status: metav1.ConditionFalse, + Reason: "Pulling", + Message: "Some images are being cached", + }}) + if err != nil { + return ctrl.Result{}, err + } + } else { + if repository.Status.Phase != "Ready" { + r.Recorder.Eventf(&repository, "Normal", "UpToDate", "All images have been cached") + } + err := r.UpdateStatus(ctx, &repository, []metav1.Condition{{ + Type: typeReadyRepository, + Status: metav1.ConditionTrue, + Reason: "UpToDate", + Message: "All images have been cached", + }}) + if err != nil { + return ctrl.Result{}, err + } + } + + if repository.Spec.UpdateInterval != nil { + nextUpdate := repository.Status.LastUpdate.Add(repository.Spec.UpdateInterval.Duration) + if time.Now().After(nextUpdate) { + log.Info("updating repository") + + regexps, err := repository.CompileUpdateFilters() + if err != nil { + return ctrl.Result{}, err + } + + for _, cachedImage := range cachedImageList.Items { + if !isImageFilteredForUpdate(cachedImage.Spec.SourceImage, regexps) { + continue + } + patch := client.MergeFrom(cachedImage.DeepCopy()) + if cachedImage.Annotations == nil { + cachedImage.Annotations = map[string]string{} + } + cachedImage.Annotations[cachedImageAnnotationForceUpdateName] = "true" + r.Patch(ctx, &cachedImage, patch) + } + + repository.Status.LastUpdate = metav1.NewTime(time.Now()) + } + } + + // Add finalizer to keep the Repository during image removal from registry on deletion + if !controllerutil.ContainsFinalizer(&repository, repositoryFinalizerName) { + log.Info("adding finalizer") + controllerutil.AddFinalizer(&repository, repositoryFinalizerName) + if err := r.Update(ctx, &repository); err != nil { + return ctrl.Result{}, err + } + } + + if repository.Spec.UpdateInterval != nil { + return ctrl.Result{RequeueAfter: repository.Spec.UpdateInterval.Duration}, nil + } + + return ctrl.Result{}, nil +} + +func (r *RepositoryReconciler) UpdateStatus(ctx context.Context, repository *kuikv1alpha1.Repository, conditions []metav1.Condition) error { + log := log.FromContext(ctx) + + for _, condition := range conditions { + meta.SetStatusCondition(&repository.Status.Conditions, condition) + } + + conditionReady := meta.FindStatusCondition(repository.Status.Conditions, typeReadyRepository) + if conditionReady.Status == metav1.ConditionTrue { + repository.Status.Phase = "Ready" + } else if conditionReady.Status == metav1.ConditionFalse { + repository.Status.Phase = conditionReady.Reason + } else { + repository.Status.Phase = "" + } + + if err := r.Status().Update(ctx, repository); err != nil { + log.Error(err, "Failed to update Repository status") + return err + } + + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *RepositoryReconciler) SetupWithManager(mgr ctrl.Manager) error { + // Create an index to list CachedImage by Repository + if err := mgr.GetFieldIndexer().IndexField(context.Background(), &kuikv1alpha1.CachedImage{}, repositoryOwnerKey, func(rawObj client.Object) []string { + cachedImage := rawObj.(*kuikv1alpha1.CachedImage) + + owners := cachedImage.GetOwnerReferences() + for _, owner := range owners { + if owner.APIVersion != kuikv1alpha1.GroupVersion.String() || owner.Kind != "Repository" { + return nil + } + + return []string{owner.Name} + } + + return []string{} + }); err != nil { + return err + } + + return ctrl.NewControllerManagedBy(mgr). + For(&kuikv1alpha1.Repository{}). + Watches( + &kuikv1alpha1.CachedImage{}, + handler.EnqueueRequestsFromMapFunc(r.repositoryWithDeletingCachedImages), + builder.WithPredicates(predicate.Funcs{ + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + }), + ). + Watches( + &kuikv1alpha1.CachedImage{}, + handler.EnqueueRequestsFromMapFunc(requestRepositoryFromCachedImage), + builder.WithPredicates(predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return true + }, + }), + ). + Complete(r) +} + +func (r *RepositoryReconciler) repositoryWithDeletingCachedImages(ctx context.Context, obj client.Object) []ctrl.Request { + cachedImage := obj.(*kuikv1alpha1.CachedImage) + var currentCachedImage kuikv1alpha1.CachedImage + // wait for the CachedImage to be really deleted + if err := r.Get(ctx, client.ObjectKeyFromObject(cachedImage), ¤tCachedImage); err == nil || !apierrors.IsNotFound(err) { + return nil + } + + return requestRepositoryFromCachedImage(ctx, cachedImage) +} + +func requestRepositoryFromCachedImage(ctx context.Context, obj client.Object) []ctrl.Request { + cachedImage := obj.(*kuikv1alpha1.CachedImage) + repositoryName, ok := cachedImage.Labels[kuikv1alpha1.RepositoryLabelName] + if !ok { + return nil + } + + return []ctrl.Request{{NamespacedName: types.NamespacedName{Name: repositoryName}}} +} + +func isImageFilteredForUpdate(imageName string, regexps []regexp.Regexp) bool { + if len(regexps) == 0 { + return true + } + + for _, regexp := range regexps { + if regexp.Match([]byte(imageName)) { + return true + } + } + + return false +} diff --git a/controllers/suite_test.go b/internal/controller/kuik/suite_test.go similarity index 72% rename from controllers/suite_test.go rename to internal/controller/kuik/suite_test.go index 444067c5..85e6ed2a 100644 --- a/controllers/suite_test.go +++ b/internal/controller/kuik/suite_test.go @@ -1,4 +1,20 @@ -package controllers +/* +Copyright 2024. + +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 kuik import ( "context" @@ -8,12 +24,13 @@ import ( "testing" "time" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + dockerClient "github.com/docker/docker/client" "github.com/docker/go-connections/nat" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -22,9 +39,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - corev1 "k8s.io/api/core/v1" - - kuikenixiov1alpha1 "github.com/enix/kube-image-keeper/api/v1alpha1" + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" "github.com/enix/kube-image-keeper/internal/registry" //+kubebuilder:scaffold:imports ) @@ -38,20 +53,15 @@ var testEnv *envtest.Environment var ctx context.Context var cancel context.CancelFunc var registryContainerId string - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecs(t, "Controller Suite") -} +var dockerClientApiVersion = os.Getenv("DOCKER_CLIENT_API_VERSION") func setupRegistry() { - client, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv) + client, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithVersion(dockerClientApiVersion)) Expect(err).NotTo(HaveOccurred()) // Pull image ctx := context.Background() - reader, err := client.ImagePull(ctx, "registry", types.ImagePullOptions{}) + reader, err := client.ImagePull(ctx, "registry", image.PullOptions{}) Expect(err).NotTo(HaveOccurred()) _, err = io.Copy(os.Stdout, reader) Expect(err).NotTo(HaveOccurred()) @@ -72,7 +82,7 @@ func setupRegistry() { registryContainerId = resp.ID // Start container - err = client.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}) + err = client.ContainerStart(ctx, resp.ID, container.StartOptions{}) Expect(err).NotTo(HaveOccurred()) // Configure registry endpoint @@ -92,23 +102,29 @@ func setupRegistry() { } func removeRegistry() { - client, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv) + client, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithVersion(dockerClientApiVersion)) Expect(err).NotTo(HaveOccurred()) - err = client.ContainerRemove(context.Background(), registryContainerId, types.ContainerRemoveOptions{ + err = client.ContainerRemove(context.Background(), registryContainerId, container.RemoveOptions{ Force: true, }) Expect(err).NotTo(HaveOccurred()) } +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(os.Stdout), zap.UseDevMode(true))) + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } @@ -118,10 +134,7 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) - err = kuikenixiov1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) - - err = corev1.AddToScheme(scheme.Scheme) + err = kuikv1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme @@ -133,7 +146,8 @@ var _ = BeforeSuite(func() { Expect(k8sClient).NotTo(BeNil()) k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme.Scheme, + Scheme: scheme.Scheme, + MetricsBindAddress: ":8081", }) Expect(err).ToNot(HaveOccurred()) @@ -146,17 +160,12 @@ var _ = BeforeSuite(func() { }).SetupWithManager(k8sManager, 3) Expect(err).ToNot(HaveOccurred()) - err = (&PodReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - }).SetupWithManager(k8sManager) - Expect(err).ToNot(HaveOccurred()) - go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) Expect(err).ToNot(HaveOccurred(), "failed to run manager") }() + }) var _ = AfterSuite(func() { diff --git a/internal/proxy/server.go b/internal/proxy/server.go index 4596324f..d68d85a9 100644 --- a/internal/proxy/server.go +++ b/internal/proxy/server.go @@ -12,14 +12,18 @@ import ( "regexp" "strings" - "github.com/docker/distribution/reference" - kuikenixiov1alpha1 "github.com/enix/kube-image-keeper/api/v1alpha1" + "github.com/distribution/reference" + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" "github.com/enix/kube-image-keeper/internal/metrics" "github.com/enix/kube-image-keeper/internal/registry" "github.com/gin-gonic/gin" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote/transport" "golang.org/x/exp/slices" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -37,7 +41,7 @@ func New(k8sClient client.Client, metricsAddr string, insecureRegistries []strin collector := NewCollector() return &Proxy{ k8sClient: k8sClient, - engine: gin.Default(), + engine: gin.New(), collector: collector, exporter: metrics.New(collector, metricsAddr), insecureRegistries: insecureRegistries, @@ -55,14 +59,24 @@ func NewWithEngine(k8sClient client.Client, engine *gin.Engine) *Proxy { func (p *Proxy) Serve() *Proxy { r := p.engine - r.Use(recoveryMiddleware()) - r.Use(func(c *gin.Context) { - c.Next() - registry := c.Param("originRegistry") - if registry == "" { - return - } - p.collector.IncHTTPCall(registry, c.Writer.Status(), c.GetBool("cacheHit")) + r.Use( + gin.LoggerWithWriter(gin.DefaultWriter, "/readyz", "/healthz"), + recoveryMiddleware(), + func(c *gin.Context) { + c.Next() + registry := c.Param("originRegistry") + if registry == "" { + return + } + p.collector.IncHTTPCall(registry, c.Writer.Status(), c.GetBool("cacheHit")) + }, + ) + + r.GET("/readyz", func(c *gin.Context) { + c.Status(http.StatusOK) + }) + r.GET("/healthz", func(c *gin.Context) { + c.Status(http.StatusOK) }) v2 := r.Group("/v2") @@ -78,7 +92,7 @@ func (p *Proxy) Serve() *Proxy { subMatches := pathRegex.FindStringSubmatch(subPath) if subMatches == nil { - c.Status(404) + c.Status(http.StatusNotFound) return } @@ -108,11 +122,11 @@ func (p *Proxy) Serve() *Proxy { return p } -func (p *Proxy) Run() chan struct{} { +func (p *Proxy) Run(proxyAddr string) chan struct{} { p.Serve() finished := make(chan struct{}) go func() { - if err := p.engine.Run(":8082"); err != nil { + if err := p.engine.Run(proxyAddr); err != nil { panic(err) } finished <- struct{}{} @@ -130,19 +144,31 @@ func (p *Proxy) Run() chan struct{} { // https://distribution.github.io/distribution/spec/api/#api-version-check func (p *Proxy) v2Endpoint(c *gin.Context) { - c.JSON(200, map[string]string{}) + c.Header("Docker-Distribution-Api-Version", "registry/2.0") + c.Header("X-Content-Type-Options", "nosniff") + c.JSON(http.StatusOK, map[string]string{}) } func (p *Proxy) routeProxy(c *gin.Context) { - repository := c.Param("repository") + repositoryName := c.Param("repository") originRegistry := c.Param("originRegistry") - klog.InfoS("proxying request", "repository", repository, "originRegistry", originRegistry) + klog.InfoS("proxying request", "repository", repositoryName, "originRegistry", originRegistry) if err := p.proxyRegistry(c, registry.Protocol+registry.Endpoint, false, nil); err != nil { klog.InfoS("cached image is not available, proxying origin", "originRegistry", originRegistry, "error", err) - transport, err := p.getAuthentifiedTransport(originRegistry, repository) + repository, err := p.getRepository(originRegistry, repositoryName) + if err != nil { + if statusError, isStatus := err.(*apierrors.StatusError); isStatus && statusError.ErrStatus.Code != 0 { + _ = c.AbortWithError(int(statusError.ErrStatus.Code), err) + } else { + _ = c.AbortWithError(http.StatusInternalServerError, err) + } + return + } + + transport, err := p.getAuthentifiedTransport(repository, "https://"+originRegistry) if err != nil { _ = c.AbortWithError(http.StatusUnauthorized, err) return @@ -156,8 +182,8 @@ func (p *Proxy) routeProxy(c *gin.Context) { if err != nil { klog.Errorf("could not proxy registry: %s", err) _ = c.AbortWithError(http.StatusInternalServerError, err) - return } + return } @@ -225,45 +251,72 @@ func (p *Proxy) proxyRegistry(c *gin.Context, endpoint string, endpointIsOrigin return proxyError } -func (p *Proxy) getAuthentifiedTransport(registryDomain string, repository string) (http.RoundTripper, error) { - repositoryLabel := registry.RepositoryLabel(registryDomain + "/" + repository) - cachedImages := &kuikenixiov1alpha1.CachedImageList{} +func (p *Proxy) getRepository(registryDomain string, repositoryName string) (*kuikv1alpha1.Repository, error) { + sanitizedName := registry.SanitizeName(registryDomain + "/" + repositoryName) - klog.InfoS("listing CachedImages", "repositoryLabel", repositoryLabel) - if err := p.k8sClient.List(context.Background(), cachedImages, client.MatchingLabels{ - kuikenixiov1alpha1.RepositoryLabelName: repositoryLabel, - }, client.Limit(1)); err != nil { + repository := &kuikv1alpha1.Repository{} + if err := p.k8sClient.Get(context.Background(), types.NamespacedName{Name: sanitizedName}, repository); err != nil { return nil, err } - if len(cachedImages.Items) == 0 { - return nil, errors.New("no CachedImage found for this repository") - } + return repository, nil +} - cachedImage := cachedImages.Items[0] // Images from the same repository should need the same pull-secret - if len(cachedImage.Spec.PullSecretNames) == 0 { - return nil, nil // Not an error since not all images requires authentication to be pulled +func (p *Proxy) getKeychains(repository *kuikv1alpha1.Repository) ([]authn.Keychain, error) { + pullSecrets, err := repository.GetPullSecrets(p.k8sClient) + if err != nil { + return nil, err } - keychain := registry.NewKubernetesKeychain(p.k8sClient, cachedImage.Spec.PullSecretsNamespace, cachedImage.Spec.PullSecretNames) + return registry.GetKeychains(repository.Spec.Name, pullSecrets) +} + +func (p *Proxy) getAuthentifiedTransport(repository *kuikv1alpha1.Repository, originRegistry string) (http.RoundTripper, error) { + imageRef, err := name.ParseReference(repository.Spec.Name) + if err != nil { + return nil, err + } - ref, err := name.ParseReference(cachedImage.Spec.SourceImage) + keychains, err := p.getKeychains(repository) if err != nil { return nil, err } - auth, err := keychain.Resolve(ref.Context()) + var proxyErrors []error + for _, keychain := range keychains { + transport, err := p.getAuthentifiedTransportWithKeychain(imageRef.Context(), keychain) + if err != nil { + proxyErrors = append(proxyErrors, err) + continue + } + + client := &http.Client{Transport: transport} + + // if :latest doesn't exist, it will respond with 404 so we still can know that this transport is well authentified (!= 401) + resp, err := client.Head(originRegistry + "/v2/" + imageRef.Context().RepositoryStr() + "/manifests/latest") + if err != nil { + proxyErrors = append(proxyErrors, err) + } else if resp.StatusCode != http.StatusUnauthorized { + return transport, nil + } + } + + return nil, utilerrors.NewAggregate(proxyErrors) +} + +func (p *Proxy) getAuthentifiedTransportWithKeychain(repository name.Repository, keychain authn.Keychain) (http.RoundTripper, error) { + auth, err := keychain.Resolve(repository) if err != nil { return nil, err } originalTransport := http.DefaultTransport.(*http.Transport).Clone() originalTransport.TLSClientConfig = &tls.Config{RootCAs: p.rootCAs} - if slices.Contains(p.insecureRegistries, ref.Context().Registry.RegistryStr()) { + if slices.Contains(p.insecureRegistries, repository.Registry.RegistryStr()) { originalTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} } - return transport.NewWithContext(context.Background(), ref.Context().Registry, auth, originalTransport, []string{ref.Scope(transport.PullScope)}) + return transport.NewWithContext(context.Background(), repository.Registry, auth, originalTransport, []string{repository.Scope(transport.PullScope)}) } // See https://github.com/golang/go/issues/28239, https://github.com/golang/go/issues/23643 and https://github.com/golang/go/issues/56228 diff --git a/internal/proxy/server_test.go b/internal/proxy/server_test.go index 4c3b80ce..3478c8b4 100644 --- a/internal/proxy/server_test.go +++ b/internal/proxy/server_test.go @@ -78,14 +78,14 @@ func Test_handleOriginRegistryPort(t *testing.T) { }, { name: "Domain name with number + port", - originRegistry: "regsitry-2.enix.io-5000", - expectedOutput: "regsitry-2.enix.io:5000", + originRegistry: "registry-2.enix.io-5000", + expectedOutput: "registry-2.enix.io:5000", }, { name: "Domain name with number + port with same value", - originRegistry: "regsitry-5000.enix.io-5000", - expectedOutput: "regsitry-5000.enix.io:5000", + originRegistry: "registry-5000.enix.io-5000", + expectedOutput: "registry-5000.enix.io:5000", }, } diff --git a/internal/registry/keychain.go b/internal/registry/keychain.go index c83e65f2..6a7226a9 100644 --- a/internal/registry/keychain.go +++ b/internal/registry/keychain.go @@ -1,110 +1,80 @@ package registry import ( - "bytes" "context" "fmt" - "sync" ecrLogin "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" - "github.com/docker/cli/cli/config" - dockerCliTypes "github.com/docker/cli/cli/config/types" + "github.com/distribution/reference" "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" + "k8s.io/kubernetes/pkg/credentialprovider" + credentialprovidersecrets "k8s.io/kubernetes/pkg/credentialprovider/secrets" "sigs.k8s.io/controller-runtime/pkg/client" ) -const ( - // DefaultAuthKey is the key used for dockerhub in config files, which - // is hardcoded for historical reasons. - DefaultAuthKey = "https://" + name.DefaultRegistry + "/v1/" -) - -type kubernetesKeychain struct { - client client.Reader - mu sync.Mutex - namespace string - pullSecret string +type authConfigKeychain struct { + authn.AuthConfig } -func NewKubernetesKeychain(client client.Reader, namespace string, pullSecrets []string) authn.Keychain { - keychains := []authn.Keychain{} - for _, pullSecret := range pullSecrets { - keychains = append(keychains, &kubernetesKeychain{ - client: client, - namespace: namespace, - pullSecret: pullSecret, - }) - } - - // Add ECR Login Helper - keychains = append(keychains, authn.NewKeychainFromHelper(ecrLogin.NewECRHelper())) - - return authn.NewMultiKeychain(keychains...) +func (a *authConfigKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) { + return authn.FromConfig(a.AuthConfig), nil } -// Resolve implements Keychain. -func (k *kubernetesKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) { - k.mu.Lock() - defer k.mu.Unlock() - - var secret corev1.Secret - err := k.client.Get(context.TODO(), types.NamespacedName{ - Namespace: k.namespace, - Name: k.pullSecret, - }, &secret) +func GetKeychains(repositoryName string, pullSecrets []corev1.Secret) ([]authn.Keychain, error) { + defaultKeyring := &credentialprovider.BasicDockerKeyring{} + keyring, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets, defaultKeyring) if err != nil { - if apierrors.IsNotFound(err) { - return authn.Anonymous, nil - } else { - return nil, err - } + return nil, err } - secretKey := "" - if secret.Type == corev1.SecretTypeDockerConfigJson { - secretKey = corev1.DockerConfigJsonKey - } else if secret.Type == corev1.SecretTypeDockercfg { - secretKey = corev1.DockerConfigKey - } else { - return nil, fmt.Errorf("invalid secret type (%s)", secret.Type) - } - dockerConfigJson, ok := secret.Data[secretKey] - if !ok { - return nil, fmt.Errorf("invalid secret: missing %s key", secretKey) - } - cf, err := config.LoadFromReader(bytes.NewReader(dockerConfigJson)) + keychains := []authn.Keychain{} + + named, err := reference.ParseNormalizedNamed(repositoryName) if err != nil { - return nil, err + return nil, fmt.Errorf("couldn't parse image name: %v", err) } - // See: - // https://github.com/google/ko/issues/90 - // https://github.com/moby/moby/blob/fc01c2b481097a6057bec3cd1ab2d7b4488c50c4/registry/config.go#L397-L404 - authKey := target.RegistryStr() - if authKey == name.DefaultRegistry { - authKey = DefaultAuthKey + creds, _ := keyring.Lookup(named.Name()) + for _, cred := range creds { + keychains = append(keychains, &authConfigKeychain{ + AuthConfig: authn.AuthConfig{ + Username: cred.Username, + Password: cred.Password, + Auth: cred.Auth, + IdentityToken: cred.IdentityToken, + RegistryToken: cred.RegistryToken, + }, + }) } - cfg, err := cf.GetAuthConfig(authKey) - if err != nil { - return nil, err - } + keychains = append(keychains, authn.NewKeychainFromHelper(ecrLogin.NewECRHelper())) + + return keychains, nil +} + +func GetPullSecrets(apiReader client.Reader, namespace string, pullSecretNames []string) ([]corev1.Secret, error) { + pullSecrets := []corev1.Secret{} + for _, pullSecretName := range pullSecretNames { + var pullSecret corev1.Secret + err := apiReader.Get(context.TODO(), types.NamespacedName{ + Namespace: namespace, + Name: pullSecretName, + }, &pullSecret) + + if err != nil { + if apierrors.IsNotFound(err) { + continue + } else { + return nil, err + } + } - empty := dockerCliTypes.AuthConfig{} - if cfg == empty { - return authn.Anonymous, nil + pullSecrets = append(pullSecrets, pullSecret) } - return authn.FromConfig(authn.AuthConfig{ - Username: cfg.Username, - Password: cfg.Password, - Auth: cfg.Auth, - IdentityToken: cfg.IdentityToken, - RegistryToken: cfg.RegistryToken, - }), nil + return pullSecrets, nil } diff --git a/internal/registry/keychain_test.go b/internal/registry/keychain_test.go index b6baae97..85c8d103 100644 --- a/internal/registry/keychain_test.go +++ b/internal/registry/keychain_test.go @@ -3,13 +3,13 @@ package registry import ( "bytes" "context" + "crypto/rand" "errors" - "fmt" "testing" + ecrLogin "github.com/awslabs/amazon-ecr-credential-helper/ecr-login" "github.com/docker/cli/cli/config" "github.com/google/go-containerregistry/pkg/authn" - "github.com/google/go-containerregistry/pkg/name" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -20,6 +20,7 @@ import ( type mockClient struct { client.Client produceError bool + namespace string } var pullSecrets = map[string]corev1.Secret{ @@ -38,193 +39,289 @@ var pullSecrets = map[string]corev1.Secret{ "invalidJson": { Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ - ".dockerconfigjson": []byte("invalid"), + corev1.DockerConfigJsonKey: []byte("invalid"), }, }, "invalidConfigurationFile": { Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ - ".dockerconfigjson": []byte("{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"00000000\"}}}"), + corev1.DockerConfigJsonKey: []byte("{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"00000000\"}}}"), }, }, "foo": { Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ - ".dockerconfigjson": []byte("{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"bG9naW46cGFzc3dvcmQ=\"}}}"), + corev1.DockerConfigJsonKey: []byte("{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"bG9naW46cGFzc3dvcmQ=\"}}}"), }, }, "bar": { Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ - ".dockerconfigjson": []byte("{\"auths\":{\"localhost:5000\":{\"auth\":\"bG9jYWxsb2dpbjpsb2NhbHBhc3N3b3Jk\"}}}"), + corev1.DockerConfigJsonKey: []byte("{\"auths\":{\"localhost:5000\":{\"auth\":\"bG9jYWxsb2dpbjpsb2NhbHBhc3N3b3Jk\"}}}"), }, }, "foobar": { Type: corev1.SecretTypeDockerConfigJson, Data: map[string][]byte{ - ".dockerconfigjson": []byte("{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"bG9naW46cGFzc3dvcmQ=\"},\"localhost:5000\":{\"auth\":\"bG9jYWxsb2dpbjpsb2NhbHBhc3N3b3Jk\"}}}"), + corev1.DockerConfigJsonKey: []byte("{\"auths\":{\"https://index.docker.io/v1/\":{\"auth\":\"bG9naW46cGFzc3dvcmQ=\"},\"localhost:5000\":{\"auth\":\"bG9jYWxsb2dpbjpsb2NhbHBhc3N3b3Jk\"}}}"), }, }, "dockercfg": { Type: corev1.SecretTypeDockercfg, Data: map[string][]byte{ - ".dockercfg": []byte("{\"auths\":{\"https://index.docker.io/v1/\":{\"username\":\"login\",\"password\":\"password\"}}}"), + corev1.DockerConfigKey: []byte("{\"https://index.docker.io/v1/\":{\"username\":\"login\",\"password\":\"password\"}}"), }, }, } var clientError = errors.New("an error occurred") var _, invalidJsonError = config.LoadFromReader(bytes.NewReader([]byte("invalid"))) -var defaultAuthenticator = authn.FromConfig(authn.AuthConfig{ - Username: "login", - Password: "password", -}) -var localAuthenticator = authn.FromConfig(authn.AuthConfig{ - Username: "locallogin", - Password: "localpassword", -}) - -func (m mockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error { + +func (m mockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { if m.produceError { return clientError } + if key.Namespace != m.namespace { + return apierrors.NewNotFound(schema.GroupResource{}, key.String()) + } + secret := obj.(*corev1.Secret) s, ok := pullSecrets[key.Name] if !ok { - return apierrors.NewNotFound(schema.GroupResource{}, key.Name) + return apierrors.NewNotFound(schema.GroupResource{}, key.String()) } *secret = s + return nil } func TestResolve(t *testing.T) { + expectedAuthConfig := authn.AuthConfig{ + Username: "username", + Password: "password", + Auth: "auth", + IdentityToken: "identityToken", + RegistryToken: "registryToken", + } + keychain := authConfigKeychain{ + AuthConfig: expectedAuthConfig, + } + + g := NewWithT(t) + auth, err := keychain.Resolve(nil) + g.Expect(err).To(BeNil()) + + authConfig, err := auth.Authorization() + g.Expect(*authConfig).To(Equal(expectedAuthConfig)) + g.Expect(err).To(BeNil()) +} + +func TestGetKeychains(t *testing.T) { + ecrHelper := authn.NewKeychainFromHelper(ecrLogin.NewECRHelper()) + defaultKeychains := []authn.Keychain{ + ecrHelper, + } + dockerHubKeychains := []authn.Keychain{ + ecrHelper, + &authConfigKeychain{ + AuthConfig: authn.AuthConfig{ + Username: "login", + Password: "password", + }, + }, + } + localKeychains := []authn.Keychain{ + ecrHelper, + &authConfigKeychain{ + AuthConfig: authn.AuthConfig{ + Username: "locallogin", + Password: "localpassword", + }, + }, + } + tests := []struct { - name string - pullSecrets []string - imageName string - expectedAuthenticator authn.Authenticator - clientProduceError bool - wantErr error + name string + repositoryName string + pullSecrets []corev1.Secret + expectedKeychains []authn.Keychain + wantErr error }{ { - name: "Empty", - pullSecrets: []string{}, + name: "Empty", + expectedKeychains: defaultKeychains, }, { - name: "Missing secret", - pullSecrets: []string{ - "missing", + name: "Empty bis", + pullSecrets: []corev1.Secret{{}, {}, {}}, + expectedKeychains: defaultKeychains, + }, + { + name: "Multiple secrets (dockerhub)", + pullSecrets: []corev1.Secret{ + pullSecrets["foo"], + pullSecrets["bar"], }, + expectedKeychains: dockerHubKeychains, }, { - name: "Client returns an error", - pullSecrets: []string{ - "foo", + name: "Multiple secrets (localhost)", + repositoryName: "localhost:5000/alpine", + pullSecrets: []corev1.Secret{ + pullSecrets["foo"], + pullSecrets["bar"], }, - clientProduceError: true, - wantErr: clientError, + expectedKeychains: localKeychains, }, { name: "Missing .dockerconfigjson", - pullSecrets: []string{ - "missing_.dockerconfigjson", + pullSecrets: []corev1.Secret{ + pullSecrets["missing_.dockerconfigjson"], }, - wantErr: errors.New("invalid secret: missing .dockerconfigjson key"), + expectedKeychains: defaultKeychains, }, { name: "Missing .dockercfg", - pullSecrets: []string{ - "missing_.dockercfg", + pullSecrets: []corev1.Secret{ + pullSecrets["missing_.dockercfg"], }, - wantErr: errors.New("invalid secret: missing .dockercfg key"), + expectedKeychains: defaultKeychains, }, { name: "Invalid secret type", - pullSecrets: []string{ - "invalidSecretType", + pullSecrets: []corev1.Secret{ + pullSecrets["invalidSecretType"], + }, + expectedKeychains: defaultKeychains, + }, + { + name: "Multiple secrets in one .dockerconfigjson (dockerhub)", + pullSecrets: []corev1.Secret{ + pullSecrets["foobar"], + }, + expectedKeychains: dockerHubKeychains, + }, + { + name: "Multiple secrets in one .dockerconfigjson (localhost)", + repositoryName: "localhost:5000/alpine", + pullSecrets: []corev1.Secret{ + pullSecrets["foobar"], }, - wantErr: fmt.Errorf("invalid secret type (%s)", corev1.SecretTypeBasicAuth), + expectedKeychains: localKeychains, + }, + { + name: ".dockercfg format", + pullSecrets: []corev1.Secret{ + pullSecrets["dockercfg"], + }, + expectedKeychains: dockerHubKeychains, }, { name: "Invalid json", - pullSecrets: []string{ - "invalidJson", + pullSecrets: []corev1.Secret{ + pullSecrets["invalidJson"], }, wantErr: invalidJsonError, }, { - name: "Default registry", - pullSecrets: []string{ - "foo", + name: "Invalid configuration file", + pullSecrets: []corev1.Secret{ + pullSecrets["invalidConfigurationFile"], }, - expectedAuthenticator: defaultAuthenticator, + wantErr: errors.New("unable to parse auth field, must be formatted as base64(username:password)"), }, { - name: "Local registry", - pullSecrets: []string{ - "bar", - }, - imageName: "localhost:5000/alpine", - expectedAuthenticator: localAuthenticator, + name: "Invalid image name", + repositoryName: ":::://::", + wantErr: errors.New("couldn't parse image name: invalid reference format"), + }, + } + + g := NewWithT(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.repositoryName == "" { + tt.repositoryName = "alpine" + } + + keychains, err := GetKeychains(tt.repositoryName, tt.pullSecrets) + + if tt.wantErr == nil { + g.Expect(err).To(Succeed()) + g.Expect(keychains).To(ConsistOf(tt.expectedKeychains)) + } else { + g.Expect(err).To(MatchError(tt.wantErr)) + g.Expect(keychains).To(BeNil()) + } + }) + } +} + +func TestGetPullSecrets(t *testing.T) { + tests := []struct { + name string + pullSecrets []string + clientProduceError bool + wantErr error + }{ + { + name: "Empty", }, { - name: "Multiple secrets", + name: "Existing secrets", pullSecrets: []string{ "foo", "bar", }, - expectedAuthenticator: defaultAuthenticator, }, { - name: "Multiple secrets with local registry", + name: "Missing secret", pullSecrets: []string{ - "foo", - "bar", + "missing", }, - imageName: "localhost:5000/alpine", - expectedAuthenticator: localAuthenticator, }, { - name: "Multiple secrets in one .dockerconfigjson", + name: "Some missing, some existing secrets", pullSecrets: []string{ - "foobar", + "foo", + "missing", + "bar", + "not_existing", }, - expectedAuthenticator: defaultAuthenticator, }, { - name: ".dockercfg format", + name: "Client returns an error", pullSecrets: []string{ - "dockercfg", + "foo", }, - expectedAuthenticator: defaultAuthenticator, + clientProduceError: true, + wantErr: clientError, }, } g := NewWithT(t) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - keychain := NewKubernetesKeychain(mockClient{produceError: tt.clientProduceError}, "namespace", tt.pullSecrets) - if tt.imageName == "" { - tt.imageName = "alpine" - } - ref, err := name.ParseReference(tt.imageName) - g.Expect(err).To(Succeed()) + namespace := make([]byte, 10) + rand.Read(namespace) - auth, err := keychain.Resolve(ref.Context()) + apiReader := mockClient{produceError: tt.clientProduceError, namespace: string(namespace)} + secrets, err := GetPullSecrets(apiReader, string(namespace), tt.pullSecrets) + + expectedSecrets := []corev1.Secret{} + for _, pullSecretName := range tt.pullSecrets { + if secret, ok := pullSecrets[pullSecretName]; ok { + expectedSecrets = append(expectedSecrets, secret) + } + } if tt.wantErr == nil { g.Expect(err).To(Succeed()) - g.Expect(auth).ToNot(BeNil()) - - if tt.expectedAuthenticator == nil { - g.Expect(auth).To(Equal(authn.Anonymous)) - } else { - g.Expect(auth).To(Equal(tt.expectedAuthenticator)) - } + g.Expect(secrets).To(ConsistOf(expectedSecrets)) } else { g.Expect(err).To(MatchError(tt.wantErr)) - g.Expect(auth).To(BeNil()) + g.Expect(secrets).To(BeNil()) } }) } diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 5296a45a..3c187601 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -18,12 +18,16 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/google/go-containerregistry/pkg/v1/types" + corev1 "k8s.io/api/core/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/utils/strings/slices" ) var Endpoint = "" var Protocol = "http://" +var ErrNotFound = errors.New("could not find source image") + // See https://github.com/kubernetes/apimachinery/blob/v0.20.6/pkg/util/validation/validation.go#L198 var sanitizeNameRegex = regexp.MustCompile(`[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*`) @@ -69,13 +73,32 @@ func parseLocalReference(imageName string) (name.Reference, error) { return name.ParseReference(destName, name.Insecure) } +func options(ref name.Reference, keychain authn.Keychain, insecureRegistries []string, rootCAs *x509.CertPool) []remote.Option { + auth := remote.WithAuthFromKeychain(keychain) + opts := []remote.Option{auth} + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = &tls.Config{RootCAs: rootCAs} + + if slices.Contains(insecureRegistries, ref.Context().Registry.RegistryStr()) { + transport.TLSClientConfig.InsecureSkipVerify = true + } + + opts = append(opts, remote.WithTransport(transport)) + + return opts +} + func ImageIsCached(imageName string) (bool, error) { reference, err := parseLocalReference(imageName) if err != nil { return false, err } - return imageExists(reference) + exists, err := imageExists(reference) + if err != nil { + err = fmt.Errorf("could not determine if the image present in cache: %w", err) + } + return exists, err } func DeleteImage(imageName string) error { @@ -100,34 +123,11 @@ func DeleteImage(imageName string) error { return remote.Delete(digest) } -func CacheImage(imageName string, keychain authn.Keychain, architectures []string, insecureRegistries []string, rootCAs *x509.CertPool) error { +func CacheImage(imageName string, desc *remote.Descriptor, architectures []string) error { destRef, err := parseLocalReference(imageName) if err != nil { return err } - sourceRef, err := name.ParseReference(imageName) - if err != nil { - return err - } - - auth := remote.WithAuthFromKeychain(keychain) - opts := []remote.Option{auth} - transport := http.DefaultTransport.(*http.Transport).Clone() - transport.TLSClientConfig = &tls.Config{RootCAs: rootCAs} - - if slices.Contains(insecureRegistries, sourceRef.Context().Registry.RegistryStr()) { - transport.TLSClientConfig.InsecureSkipVerify = true - } - - opts = append(opts, remote.WithTransport(transport)) - - desc, err := remote.Get(sourceRef, opts...) - if err != nil { - if errIsImageNotFound(err) { - return errors.New("could not find source image") - } - return err - } switch desc.MediaType { case types.OCIImageIndex, types.DockerManifestList: @@ -161,6 +161,47 @@ func CacheImage(imageName string, keychain authn.Keychain, architectures []strin return nil } +func GetLocalDescriptor(imageName string) (*remote.Descriptor, error) { + ref, err := parseLocalReference(imageName) + if err != nil { + return nil, err + } + + descriptor, err := remote.Get(ref) + if err != nil { + return nil, err + } + + return descriptor, nil +} + +func GetDescriptor(imageName string, pullSecrets []corev1.Secret, insecureRegistries []string, rootCAs *x509.CertPool) (*remote.Descriptor, error) { + keychains, err := GetKeychains(imageName, pullSecrets) + if err != nil { + return nil, err + } + + sourceRef, err := name.ParseReference(imageName) + if err != nil { + return nil, err + } + + var cacheErrors []error + for _, keychain := range keychains { + opts := options(sourceRef, keychain, insecureRegistries, rootCAs) + desc, err := remote.Get(sourceRef, opts...) + + if err == nil { // stops at the first success + return desc, nil + } else if errIsImageNotFound(err) { + err = ErrNotFound + } + cacheErrors = append(cacheErrors, err) + } + + return nil, utilerrors.NewAggregate(cacheErrors) +} + func SanitizeName(image string) string { return strings.Join(sanitizeNameRegex.FindAllString(strings.ToLower(image), -1), "-") } diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go index 7e85b07f..a7635388 100644 --- a/internal/registry/registry_test.go +++ b/internal/registry/registry_test.go @@ -10,6 +10,7 @@ import ( "testing" "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/onsi/gomega" "github.com/onsi/gomega/ghttp" @@ -144,6 +145,10 @@ func Test_ImageIsCached(t *testing.T) { Endpoint = server.Addr() isCached, err := ImageIsCached(tt.image) if tt.wantErr != "" { + err2 := errors.Unwrap(err) + if err2 != nil { + err = err2 + } g.Expect(err).To(BeAssignableToTypeOf(tt.errType)) g.Expect(err).To(MatchError(ContainSubstring(tt.wantErr))) } else { @@ -235,27 +240,6 @@ func Test_CacheImage(t *testing.T) { name: "Basic", image: "alpine", }, - { - name: "Basic", - image: "*****", - wantErr: "could not parse reference", - errType: &name.ErrBadName{}, - }, - { - name: "Image not found", - image: "image-not-found", - httpStatus: http.StatusNotFound, - wantErr: "could not find source image", - errType: errors.New(""), - }, - { - name: "Unauthorized", - image: "alpine", - httpStatus: http.StatusUnauthorized, - httpResponse: "unauthorized", - wantErr: "unauthorized", - errType: &transport.Error{}, - }, { name: "Could not write", image: "alpine", @@ -328,8 +312,15 @@ func Test_CacheImage(t *testing.T) { ) Endpoint = cacheRegistry.Addr() - keychain := NewKubernetesKeychain(nil, "default", []string{}) - err := CacheImage(originRegistry.Addr()+"/"+tt.image, keychain, []string{"amd64"}, []string{}, nil) + imageName := originRegistry.Addr() + "/" + tt.image + + sourceRef, err := name.ParseReference(imageName) + g.Expect(err).To(BeNil()) + + desc, err := remote.Get(sourceRef) + g.Expect(err).To(BeNil()) + + err = CacheImage(imageName, desc, []string{"amd64"}) if tt.wantErr != "" { g.Expect(err).To(BeAssignableToTypeOf(tt.errType)) g.Expect(err).To(MatchError(ContainSubstring(tt.wantErr))) diff --git a/internal/scheme/scheme.go b/internal/scheme/scheme.go index 706601d0..a685106d 100644 --- a/internal/scheme/scheme.go +++ b/internal/scheme/scheme.go @@ -10,7 +10,7 @@ import ( utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - kuikenixiov1alpha1 "github.com/enix/kube-image-keeper/api/v1alpha1" + kuikv1alpha1 "github.com/enix/kube-image-keeper/api/kuik/v1alpha1" //+kubebuilder:scaffold:imports ) @@ -19,7 +19,7 @@ func NewScheme() *runtime.Scheme { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(kuikenixiov1alpha1.AddToScheme(scheme)) + utilruntime.Must(kuikv1alpha1.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme return scheme diff --git a/main.go b/main.go deleted file mode 120000 index 285c239a..00000000 --- a/main.go +++ /dev/null @@ -1 +0,0 @@ -cmd/cache/main.go \ No newline at end of file