diff --git a/.github/ISSUE_TEMPLATE/release.md b/.github/ISSUE_TEMPLATE/release.md index dd24ed32aee77f..b43b91a0e05ce9 100644 --- a/.github/ISSUE_TEMPLATE/release.md +++ b/.github/ISSUE_TEMPLATE/release.md @@ -9,12 +9,6 @@ assignees: '' Target RC1 date: ___. __, ____ Target GA date: ___. __, ____ - - [ ] Create new section in the [Release Planning doc](https://docs.google.com/document/d/1trJIomcgXcfvLw0aYnERrFWfPjQOfYMDJOCh1S8nMBc/edit?usp=sharing) - - [ ] Schedule a Release Planning meeting roughly two weeks before the scheduled Release freeze date by adding it to the community calendar (or delegate this task to someone with write access to the community calendar) - - [ ] Include Zoom link in the invite - - [ ] Post in #argo-cd and #argo-contributors one week before the meeting - - [ ] Post again one hour before the meeting - - [ ] At the meeting, remove issues/PRs from the project's column for that release which have not been “claimed” by at least one Approver (add it to the next column if Approver requests that) - [ ] 1wk before feature freeze post in #argo-contributors that PRs must be merged by DD-MM-YYYY to be included in the release - ask approvers to drop items from milestone they can’t merge - [ ] At least two days before RC1 date, draft RC blog post and submit it for review (or delegate this task) - [ ] Cut RC1 (or delegate this task to an Approver and coordinate timing) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 406306bbeca2ea..c1a3f42508aaad 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -13,11 +13,12 @@ Checklist: * [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them. * [ ] Does this PR require documentation updates? * [ ] I've updated documentation as required by this PR. -* [ ] Optional. My organization is added to USERS.md. * [ ] I have signed off all my commits as required by [DCO](https://github.com/argoproj/argoproj/blob/master/community/CONTRIBUTING.md#legal) * [ ] I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged. * [ ] My build is green ([troubleshooting builds](https://argo-cd.readthedocs.io/en/latest/developer-guide/ci/)). * [ ] My new feature complies with the [feature status](https://github.com/argoproj/argoproj/blob/master/community/feature-status.md) guidelines. * [ ] I have added a brief description of why this PR is necessary and/or what this PR solves. +* [ ] Optional. My organization is added to USERS.md. +* [ ] Optional. For bug fixes, I've indicated what older releases this fix should be cherry-picked into (this may or may not happen depending on risk/complexity). diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index 3a596a9552d707..b5f5a752e0a467 100644 --- a/.github/workflows/ci-build.yaml +++ b/.github/workflows/ci-build.yaml @@ -1,5 +1,5 @@ name: Integration tests -on: +on: push: branches: - 'master' @@ -23,9 +23,32 @@ permissions: contents: read jobs: + changes: + runs-on: ubuntu-latest + outputs: + backend: ${{ steps.filter.outputs.backend_any_changed }} + frontend: ${{ steps.filter.outputs.frontend_any_changed }} + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: tj-actions/changed-files@90a06d6ba9543371ab4df8eeca0be07ca6054959 # v42.0.2 + id: filter + with: + # Any file which is not under docs/, ui/ or is not a markdown file is counted as a backend file + files_yaml: | + backend: + - '!ui/**' + - '!**.md' + - '!**/*.md' + - '!docs/**' + frontend: + - 'ui/**' + - Dockerfile check-go: name: Ensure Go modules synchronicity + if: ${{ needs.changes.outputs.backend == 'true' }} runs-on: ubuntu-22.04 + needs: + - changes steps: - name: Checkout code uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 @@ -36,14 +59,17 @@ jobs: - name: Download all Go modules run: | go mod download - - name: Check for tidyness of go.mod and go.sum + - name: Check for tidiness of go.mod and go.sum run: | go mod tidy git diff --exit-code -- . build-go: name: Build & cache Go code + if: ${{ needs.changes.outputs.backend == 'true' }} runs-on: ubuntu-22.04 + needs: + - changes steps: - name: Checkout code uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 @@ -67,7 +93,10 @@ jobs: contents: read # for actions/checkout to fetch code pull-requests: read # for golangci/golangci-lint-action to fetch pull requests name: Lint Go code + if: ${{ needs.changes.outputs.backend == 'true' }} runs-on: ubuntu-22.04 + needs: + - changes steps: - name: Checkout code uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 @@ -83,12 +112,14 @@ jobs: test-go: name: Run unit tests for Go packages + if: ${{ needs.changes.outputs.backend == 'true' }} runs-on: ubuntu-22.04 needs: - build-go + - changes env: GITHUB_TOKEN: ${{ secrets.E2E_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITLAB_TOKEN: ${{ secrets.E2E_TEST_GITLAB_TOKEN }} + GITLAB_TOKEN: ${{ secrets.E2E_TEST_GITLAB_TOKEN }} steps: - name: Create checkout directory run: mkdir -p ~/go/src/github.com/argoproj @@ -150,12 +181,14 @@ jobs: test-go-race: name: Run unit tests with -race for Go packages + if: ${{ needs.changes.outputs.backend == 'true' }} runs-on: ubuntu-22.04 needs: - build-go + - changes env: GITHUB_TOKEN: ${{ secrets.E2E_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITLAB_TOKEN: ${{ secrets.E2E_TEST_GITLAB_TOKEN }} + GITLAB_TOKEN: ${{ secrets.E2E_TEST_GITLAB_TOKEN }} steps: - name: Create checkout directory run: mkdir -p ~/go/src/github.com/argoproj @@ -212,7 +245,10 @@ jobs: codegen: name: Check changes to generated code + if: ${{ needs.changes.outputs.backend == 'true' }} runs-on: ubuntu-22.04 + needs: + - changes steps: - name: Checkout code uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 @@ -260,14 +296,17 @@ jobs: build-ui: name: Build, test & lint UI code + if: ${{ needs.changes.outputs.frontend == 'true' }} runs-on: ubuntu-22.04 + needs: + - changes steps: - name: Checkout code uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - name: Setup NodeJS uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 with: - node-version: '20.7.0' + node-version: '21.6.1' - name: Restore node dependency cache id: cache-dependencies uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 @@ -292,10 +331,12 @@ jobs: analyze: name: Process & analyze test artifacts + if: ${{ needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true' }} runs-on: ubuntu-22.04 needs: - test-go - build-ui + - changes env: sonar_secret: ${{ secrets.SONAR_TOKEN }} steps: @@ -315,7 +356,7 @@ jobs: - name: Create test-results directory run: | mkdir -p test-results - - name: Get code coverage artifiact + - name: Get code coverage artifact uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: name: code-coverage @@ -336,35 +377,37 @@ jobs: SCANNER_PATH: /tmp/cache/scanner OS: linux run: | - # We do not use the provided action, because it does contain an old - # version of the scanner, and also takes time to build. - set -e - mkdir -p ${SCANNER_PATH} - export SONAR_USER_HOME=${SCANNER_PATH}/.sonar - if [[ ! -x "${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner" ]]; then - curl -Ol https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip - unzip -qq -o sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip -d ${SCANNER_PATH} - fi - - chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner - chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/jre/bin/java - - # Explicitly set NODE_MODULES - export NODE_MODULES=${PWD}/ui/node_modules - export NODE_PATH=${PWD}/ui/node_modules - - ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner + # We do not use the provided action, because it does contain an old + # version of the scanner, and also takes time to build. + set -e + mkdir -p ${SCANNER_PATH} + export SONAR_USER_HOME=${SCANNER_PATH}/.sonar + if [[ ! -x "${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner" ]]; then + curl -Ol https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip + unzip -qq -o sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip -d ${SCANNER_PATH} + fi + + chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner + chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/jre/bin/java + + # Explicitly set NODE_MODULES + export NODE_MODULES=${PWD}/ui/node_modules + export NODE_PATH=${PWD}/ui/node_modules + + ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner if: env.sonar_secret != '' test-e2e: name: Run end-to-end tests + if: ${{ needs.changes.outputs.backend == 'true' }} runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: - k3s-version: [v1.28.2, v1.27.6, v1.26.9, v1.25.14] - needs: + k3s-version: [v1.29.1, v1.28.6, v1.27.10, v1.26.13, v1.25.16] + needs: - build-go + - changes env: GOPATH: /home/runner/go ARGOCD_FAKE_IN_CLUSTER: "true" @@ -377,7 +420,7 @@ jobs: ARGOCD_APPLICATION_NAMESPACES: "argocd-e2e-external,argocd-e2e-external-2" ARGOCD_SERVER: "127.0.0.1:8088" GITHUB_TOKEN: ${{ secrets.E2E_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - GITLAB_TOKEN: ${{ secrets.E2E_TEST_GITLAB_TOKEN }} + GITLAB_TOKEN: ${{ secrets.E2E_TEST_GITLAB_TOKEN }} steps: - name: Checkout code uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 @@ -427,7 +470,7 @@ jobs: git config --global user.email "john.doe@example.com" - name: Pull Docker image required for tests run: | - docker pull ghcr.io/dexidp/dex:v2.37.0 + docker pull ghcr.io/dexidp/dex:v2.38.0 docker pull argoproj/argo-cd-ci-builder:v1.0.0 docker pull redis:7.0.14-alpine - name: Create target directory for binaries in the build-process @@ -462,3 +505,26 @@ jobs: name: e2e-server-k8s${{ matrix.k3s-version }}.log path: /tmp/e2e-server.log if: ${{ failure() }} + + # workaround for status checks -- check this one job instead of each individual E2E job in the matrix + # this allows us to skip the entire matrix when it doesn't need to run while still having accurate status checks + # see: + # https://github.com/argoproj/argo-workflows/pull/12006 + # https://github.com/orgs/community/discussions/9141#discussioncomment-2296809 + # https://github.com/orgs/community/discussions/26822#discussioncomment-3305794 + test-e2e-composite-result: + name: E2E Tests - Composite result + if: ${{ always() }} + needs: + - test-e2e + - changes + runs-on: ubuntu-22.04 + steps: + - run: | + result="${{ needs.test-e2e.result }}" + # mark as successful even if skipped + if [[ $result == "success" || $result == "skipped" ]]; then + exit 0 + else + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 58426890abcbf7..2311d43925bb79 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,10 +27,15 @@ jobs: # CodeQL runs on ubuntu-latest and windows-latest runs-on: ubuntu-22.04 - steps: - name: Checkout repository uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + + # Use correct go version. https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 + - name: Setup Golang + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.0.0 + with: + go-version-file: go.mod # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/image-reuse.yaml b/.github/workflows/image-reuse.yaml index 898becc2815f43..0838f38e4230de 100644 --- a/.github/workflows/image-reuse.yaml +++ b/.github/workflows/image-reuse.yaml @@ -74,9 +74,9 @@ jobs: go-version: ${{ inputs.go-version }} - name: Install cosign - uses: sigstore/cosign-installer@11086d25041f77fe8fe7b9ea4e48e3b9192b8f19 # v3.1.2 + uses: sigstore/cosign-installer@1fc5bd396d372bee37d608f955b336615edf79c8 # v3.2.0 with: - cosign-release: 'v2.0.2' + cosign-release: 'v2.2.1' - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 - uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 @@ -145,7 +145,7 @@ jobs: - name: Build and push container image id: image - uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 #v4.1.1 + uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 #v5.1.0 with: context: . platforms: ${{ inputs.platforms }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ae5174659cf406..567ab8a23ab31c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -87,6 +87,14 @@ jobs: echo "KUBECTL_VERSION=$(go list -m k8s.io/client-go | head -n 1 | rev | cut -d' ' -f1 | rev)" >> $GITHUB_ENV echo "GIT_TREE_STATE=$(if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)" >> $GITHUB_ENV + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@4d9e71b726748f254fe64fa44d273194bd18ec91 + with: + large-packages: false + docker-images: false + swap-storage: false + tool-cache: false + - name: Run GoReleaser uses: goreleaser/goreleaser-action@7ec5c2b0c6cdda6e8bbb49444bc797dd33d74dd8 # v5.0.0 id: run-goreleaser diff --git a/CODEOWNERS b/CODEOWNERS index ec72eccbf416e3..83bb38871d96d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,9 +2,10 @@ ** @argoproj/argocd-approvers # Docs -/docs/** @argoproj/argocd-approvers @argoproj/argocd-approvers-docs -/USERS.md @argoproj/argocd-approvers @argoproj/argocd-approvers-docs -/mkdocs.yml @argoproj/argocd-approvers @argoproj/argocd-approvers-docs +/docs/** @argoproj/argocd-approvers @argoproj/argocd-approvers-docs +/USERS.md @argoproj/argocd-approvers @argoproj/argocd-approvers-docs +/README.md @argoproj/argocd-approvers @argoproj/argocd-approvers-docs +/mkdocs.yml @argoproj/argocd-approvers @argoproj/argocd-approvers-docs # CI /.github/** @argoproj/argocd-approvers @argoproj/argocd-approvers-ci diff --git a/Dockerfile b/Dockerfile index 2c31b5077f67ee..44202104d356bc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,7 @@ RUN groupadd -g $ARGOCD_USER_ID argocd && \ apt-get update && \ apt-get dist-upgrade -y && \ apt-get install -y \ - git git-lfs tini gpg tzdata && \ + git git-lfs tini gpg tzdata connect-proxy && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* @@ -83,7 +83,7 @@ WORKDIR /home/argocd #################################################################################################### # Argo CD UI stage #################################################################################################### -FROM --platform=$BUILDPLATFORM docker.io/library/node:20.6.1@sha256:14bd39208dbc0eb171cbfb26ccb9ac09fa1b2eba04ccd528ab5d12983fd9ee24 AS argocd-ui +FROM --platform=$BUILDPLATFORM docker.io/library/node:21.6.2@sha256:65998e325b06014d4f1417a8a6afb1540d1ac66521cca76f2221a6953947f9ee AS argocd-ui WORKDIR /src COPY ["ui/package.json", "ui/yarn.lock", "./"] diff --git a/Makefile b/Makefile index d4df041fa58e12..84282a8b25bf79 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,8 @@ DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd ARGOCD_PROCFILE?=Procfile -# Strict mode has been disabled in latest versions of mkdocs-material. -# Thus pointing to the older image of mkdocs-material matching the version used by argo-cd. -MKDOCS_DOCKER_IMAGE?=squidfunk/mkdocs-material:4.1.1 +# pointing to python 3.7 to match https://github.com/argoproj/argo-cd/blob/master/.readthedocs.yml +MKDOCS_DOCKER_IMAGE?=python:3.7-alpine MKDOCS_RUN_ARGS?= # Configuration for building argocd-test-tools image @@ -49,7 +48,7 @@ ARGOCD_E2E_DEX_PORT?=5556 ARGOCD_E2E_YARN_HOST?=localhost ARGOCD_E2E_DISABLE_AUTH?= -ARGOCD_E2E_TEST_TIMEOUT?=60m +ARGOCD_E2E_TEST_TIMEOUT?=90m ARGOCD_IN_CI?=false ARGOCD_TEST_E2E?=true @@ -175,29 +174,21 @@ endif .PHONY: all all: cli image -# We have some legacy requirements for being checked out within $GOPATH. -# The ensure-gopath target can be used as dependency to ensure we are running -# within these boundaries. -.PHONY: ensure-gopath -ensure-gopath: -ifneq ("$(PWD)","$(LEGACY_PATH)") - @echo "Due to legacy requirements for codegen, repository needs to be checked out within \$$GOPATH" - @echo "Location of this repo should be '$(LEGACY_PATH)' but is '$(PWD)'" - @exit 1 -endif - .PHONY: gogen -gogen: ensure-gopath +gogen: export GO111MODULE=off go generate ./util/argo/... .PHONY: protogen -protogen: ensure-gopath mod-vendor-local +protogen: mod-vendor-local protogen-fast + +.PHONY: protogen-fast +protogen-fast: export GO111MODULE=off ./hack/generate-proto.sh .PHONY: openapigen -openapigen: ensure-gopath +openapigen: export GO111MODULE=off ./hack/update-openapi.sh @@ -212,19 +203,22 @@ notification-docs: .PHONY: clientgen -clientgen: ensure-gopath +clientgen: export GO111MODULE=off ./hack/update-codegen.sh .PHONY: clidocsgen -clidocsgen: ensure-gopath +clidocsgen: go run tools/cmd-docs/main.go .PHONY: codegen-local -codegen-local: ensure-gopath mod-vendor-local gogen protogen clientgen openapigen clidocsgen manifests-local notification-docs notification-catalog +codegen-local: mod-vendor-local gogen protogen clientgen openapigen clidocsgen manifests-local notification-docs notification-catalog rm -rf vendor/ +.PHONY: codegen-local-fast +codegen-local-fast: gogen protogen-fast clientgen openapigen clidocsgen manifests-local notification-docs notification-catalog + .PHONY: codegen codegen: test-tools-image $(call run-in-test-client,make codegen-local) @@ -492,6 +486,7 @@ start-local: mod-vendor-local dep-ui-local cli-local ARGOCD_ZJWT_FEATURE_FLAG=always \ ARGOCD_IN_CI=false \ ARGOCD_GPG_ENABLED=$(ARGOCD_GPG_ENABLED) \ + BIN_MODE=$(ARGOCD_BIN_MODE) \ ARGOCD_E2E_TEST=false \ ARGOCD_APPLICATION_NAMESPACES=$(ARGOCD_APPLICATION_NAMESPACES) \ goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START} @@ -525,7 +520,7 @@ build-docs-local: .PHONY: build-docs build-docs: - docker run ${MKDOCS_RUN_ARGS} --rm -it -v ${CURRENT_DIR}:/docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs build' + docker run ${MKDOCS_RUN_ARGS} --rm -it -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs build' .PHONY: serve-docs-local serve-docs-local: diff --git a/OWNERS b/OWNERS index d8532c550005aa..56e037e282a0a5 100644 --- a/OWNERS +++ b/OWNERS @@ -5,6 +5,7 @@ owners: approvers: - alexec - alexmt +- gdsoumya - jannfis - jessesuen - jgwest @@ -30,4 +31,3 @@ reviewers: - zachaller - 34fathombelow - alexef -- gdsoumya diff --git a/Procfile b/Procfile index 2bb26a086fb1dc..4862b0230062f3 100644 --- a/Procfile +++ b/Procfile @@ -1,4 +1,4 @@ -controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''}" +controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "HOSTNAME=testappcontroller-1 FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --server-side-diff-enabled=${ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF:-'false'}" api-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-server $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''}" dex: sh -c "ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/v2/cmd gendexcfg -o `pwd`/dist/dex.yaml && (test -f dist/dex.yaml || { echo 'Failed to generate dex configuration'; exit 1; }) && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:$(grep "image: ghcr.io/dexidp/dex" manifests/base/dex/argocd-dex-server-deployment.yaml | cut -d':' -f3) dex serve /dex.yaml" redis: bash -c "if [ \"$ARGOCD_REDIS_LOCAL\" = 'true' ]; then redis-server --save '' --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}; else docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} docker.io/library/redis:$(grep "image: redis" manifests/base/redis/argocd-redis-deployment.yaml | cut -d':' -f3) --save '' --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}; fi" @@ -9,4 +9,5 @@ git-server: test/fixture/testrepos/start-git.sh helm-registry: test/fixture/testrepos/start-helm-registry.sh dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} applicationset-controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-applicationset-controller $COMMAND --loglevel debug --metrics-addr localhost:12345 --probe-addr localhost:12346 --argocd-repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}" -notification: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_BINARY_NAME=argocd-notifications $COMMAND --loglevel debug" +notification: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_BINARY_NAME=argocd-notifications $COMMAND --loglevel debug --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --self-service-notification-enabled=${ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED:-'false'}" + diff --git a/README.md b/README.md index ef5664de5b5b77..707848191c8305 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ **Social:** [![Twitter Follow](https://img.shields.io/twitter/follow/argoproj?style=social)](https://twitter.com/argoproj) [![Slack](https://img.shields.io/badge/slack-argoproj-brightgreen.svg?logo=slack)](https://argoproj.github.io/community/join-slack) +[![LinkedIn](https://img.shields.io/badge/LinkedIn-argoproj-blue.svg?logo=linkedin)](https://www.linkedin.com/company/argoproj/) # Argo CD - Declarative Continuous Delivery for Kubernetes @@ -85,4 +86,5 @@ Participation in the Argo CD project is governed by the [CNCF Code of Conduct](h 1. [Getting Started with ArgoCD for GitOps Deployments](https://youtu.be/AvLuplh1skA) 1. [Using Argo CD & Datree for Stable Kubernetes CI/CD Deployments](https://youtu.be/17894DTru2Y) 1. [How to create Argo CD Applications Automatically using ApplicationSet? "Automation of GitOps"](https://amralaayassen.medium.com/how-to-create-argocd-applications-automatically-using-applicationset-automation-of-the-gitops-59455eaf4f72) +1. [Progressive Delivery with Service Mesh – Argo Rollouts with Istio](https://www.cncf.io/blog/2022/12/16/progressive-delivery-with-service-mesh-argo-rollouts-with-istio/) diff --git a/USERS.md b/USERS.md index 0f660ebb5f4d6f..6a09724c7d7b73 100644 --- a/USERS.md +++ b/USERS.md @@ -20,6 +20,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Allianz Direct](https://www.allianzdirect.de/) 1. [Amadeus IT Group](https://amadeus.com/) 1. [Ambassador Labs](https://www.getambassador.io/) +1. [Ancestry](https://www.ancestry.com/) 1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/) 1. [Ant Group](https://www.antgroup.com/) 1. [AppDirect](https://www.appdirect.com) @@ -40,17 +41,18 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Boozt](https://www.booztgroup.com/) 1. [Boticario](https://www.boticario.com.br/) 1. [Bulder Bank](https://bulderbank.no) +1. [CAM](https://cam-inc.co.jp) 1. [Camptocamp](https://camptocamp.com) 1. [Candis](https://www.candis.io) 1. [Capital One](https://www.capitalone.com) -1. [CARFAX](https://www.carfax.com) 1. [CARFAX Europe](https://www.carfax.eu) +1. [CARFAX](https://www.carfax.com) 1. [Carrefour Group](https://www.carrefour.com) 1. [Casavo](https://casavo.com) 1. [Celonis](https://www.celonis.com/) 1. [CERN](https://home.cern/) -1. [Chargetrip](https://chargetrip.com) 1. [Chainnodes](https://chainnodes.org) +1. [Chargetrip](https://chargetrip.com) 1. [Chime](https://www.chime.com) 1. [Cisco ET&I](https://eti.cisco.com/) 1. [Cloud Posse](https://www.cloudposse.com/) @@ -93,6 +95,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Fave](https://myfave.com) 1. [Flexport](https://www.flexport.com/) 1. [Flip](https://flip.id) +1. [Fly Security](https://www.flysecurity.com.br/) 1. [Fonoa](https://www.fonoa.com/) 1. [Fortra](https://www.fortra.com) 1. [freee](https://corp.freee.co.jp/en/company/) @@ -111,8 +114,8 @@ Currently, the following organizations are **officially** using Argo CD: 1. [GlueOps](https://glueops.dev) 1. [GMETRI](https://gmetri.com/) 1. [Gojek](https://www.gojek.io/) -1. [GoTo](https://www.goto.com/) 1. [GoTo Financial](https://gotofinancial.com/) +1. [GoTo](https://www.goto.com/) 1. [Greenpass](https://www.greenpass.com.br/) 1. [Gridfuse](https://gridfuse.com/) 1. [Groww](https://groww.in) @@ -125,9 +128,11 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Hiya](https://hiya.com) 1. [Honestbank](https://honestbank.com) 1. [Hostinger](https://www.hostinger.com) +1. [IABAI](https://www.iab.ai) 1. [IBM](https://www.ibm.com/) 1. [Ibotta](https://home.ibotta.com) 1. [IITS-Consulting](https://iits-consulting.de) +1. [IllumiDesk](https://www.illumidesk.com) 1. [imaware](https://imaware.health) 1. [Indeed](https://indeed.com) 1. [Index Exchange](https://www.indexexchange.com/) @@ -148,6 +153,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Kinguin](https://www.kinguin.net/) 1. [KintoHub](https://www.kintohub.com/) 1. [KompiTech GmbH](https://www.kompitech.com/) +1. [Kong Inc.](https://konghq.com/) 1. [KPMG](https://kpmg.com/uk) 1. [KubeSphere](https://github.com/kubesphere) 1. [Kurly](https://www.kurly.com/) @@ -184,6 +190,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Natura &Co](https://naturaeco.com/) 1. [Nethopper](https://nethopper.io) 1. [New Relic](https://newrelic.com/) +1. [Nextbasket](https://nextbasket.com) 1. [Nextdoor](https://nextdoor.com/) 1. [Nikkei](https://www.nikkei.co.jp/nikkeiinfo/en/) 1. [Nitro](https://gonitro.com) @@ -210,6 +217,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [PagerDuty](https://www.pagerduty.com/) 1. [Pandosearch](https://www.pandosearch.com/en/home) 1. [Patreon](https://www.patreon.com/) +1. [PayIt](https://payitgov.com/) 1. [PayPay](https://paypay.ne.jp/) 1. [Peloton Interactive](https://www.onepeloton.com/) 1. [Percona](https://percona.com/) @@ -217,6 +225,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Pigment](https://www.gopigment.com/) 1. [Pipefy](https://www.pipefy.com/) 1. [Pismo](https://pismo.io/) +1. [PITS Globale Datenrettungsdienste](https://www.pitsdatenrettung.de/) 1. [Platform9 Systems](https://platform9.com/) 1. [Polarpoint.io](https://polarpoint.io) 1. [PostFinance](https://github.com/postfinance) @@ -238,11 +247,14 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Reenigne Cloud](https://reenigne.ca) 1. [reev.com](https://www.reev.com/) 1. [RightRev](https://rightrev.com/) +1. [Rijkswaterstaat](https://www.rijkswaterstaat.nl/en) 1. [Rise](https://www.risecard.eu/) 1. [Riskified](https://www.riskified.com/) 1. [Robotinfra](https://www.robotinfra.com) +1. [Rocket.Chat](https://rocket.chat) 1. [Rubin Observatory](https://www.lsst.org) 1. [Saildrone](https://www.saildrone.com/) +1. [Salad Technologies](https://salad.com/) 1. [Saloodo! GmbH](https://www.saloodo.com) 1. [Sap Labs](http://sap.com) 1. [Sauce Labs](https://saucelabs.com/) @@ -264,6 +276,8 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Spendesk](https://spendesk.com/) 1. [Splunk](https://splunk.com/) 1. [Spores Labs](https://spores.app) +1. [Statsig](https://statsig.com) +1. [SternumIOT](https://sternumiot.com) 1. [StreamNative](https://streamnative.io) 1. [Stuart](https://stuart.com/) 1. [Sumo Logic](https://sumologic.com/) @@ -277,6 +291,7 @@ Currently, the following organizations are **officially** using Argo CD: 1. [Tamkeen Technologies](https://tamkeentech.sa/) 1. [Techcombank](https://www.techcombank.com.vn/trang-chu) 1. [Technacy](https://www.technacy.it/) +1. [Telavita](https://www.telavita.com.br/) 1. [Tesla](https://tesla.com/) 1. [The Scale Factory](https://www.scalefactory.com/) 1. [ThousandEyes](https://www.thousandeyes.com/) diff --git a/applicationset/controllers/applicationset_controller.go b/applicationset/controllers/applicationset_controller.go index be73c81ca0c3f9..4f5ac66fc016dc 100644 --- a/applicationset/controllers/applicationset_controller.go +++ b/applicationset/controllers/applicationset_controller.go @@ -108,6 +108,16 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque // Do not attempt to further reconcile the ApplicationSet if it is being deleted. if applicationSetInfo.ObjectMeta.DeletionTimestamp != nil { + deleteAllowed := utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete() + if !deleteAllowed { + if err := r.removeOwnerReferencesOnDeleteAppSet(ctx, applicationSetInfo); err != nil { + return ctrl.Result{}, err + } + controllerutil.RemoveFinalizer(&applicationSetInfo, argov1alpha1.ResourcesFinalizerName) + if err := r.Update(ctx, &applicationSetInfo); err != nil { + return ctrl.Result{}, err + } + } return ctrl.Result{}, nil } @@ -512,6 +522,7 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli for _, p := range a.Params { app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) + if err != nil { logCtx.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). Error("error generating application from params") @@ -522,6 +533,24 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli } continue } + + if applicationSetInfo.Spec.TemplatePatch != nil { + patchedApplication, err := r.applyTemplatePatch(app, applicationSetInfo, p) + + if err != nil { + log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). + Error("error generating application from params") + + if firstError == nil { + firstError = err + applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError + } + continue + } + + app = patchedApplication + } + res = append(res, *app) } } @@ -533,6 +562,16 @@ func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, appli return res, applicationSetReason, firstError } +func (r *ApplicationSetReconciler) applyTemplatePatch(app *argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet, params map[string]interface{}) (*argov1alpha1.Application, error) { + replacedTemplate, err := r.Renderer.Replace(*applicationSetInfo.Spec.TemplatePatch, params, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) + + if err != nil { + return nil, fmt.Errorf("error replacing values in templatePatch: %w", err) + } + + return applyTemplatePatch(app, replacedTemplate) +} + func ignoreNotAllowedNamespaces(namespaces []string) predicate.Predicate { return predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { @@ -607,6 +646,8 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context, var firstError error // Creates or updates the application in appList for _, generatedApp := range desiredApplications { + // The app's namespace must be the same as the AppSet's namespace to preserve the appsets-in-any-namespace + // security boundary. generatedApp.Namespace = applicationSet.Namespace appLog := logCtx.WithFields(log.Fields{"app": generatedApp.QualifiedName()}) @@ -731,10 +772,9 @@ func (r *ApplicationSetReconciler) createInCluster(ctx context.Context, logCtx * return r.createOrUpdateInCluster(ctx, logCtx, applicationSet, createApps) } -func (r *ApplicationSetReconciler) getCurrentApplications(_ context.Context, applicationSet argov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error) { - // TODO: Should this use the context param? +func (r *ApplicationSetReconciler) getCurrentApplications(ctx context.Context, applicationSet argov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error) { var current argov1alpha1.ApplicationList - err := r.Client.List(context.Background(), ¤t, client.MatchingFields{".metadata.controller": applicationSet.Name}, client.InNamespace(applicationSet.Namespace)) + err := r.Client.List(ctx, ¤t, client.MatchingFields{".metadata.controller": applicationSet.Name}, client.InNamespace(applicationSet.Namespace)) if err != nil { return nil, fmt.Errorf("error retrieving applications: %w", err) @@ -875,6 +915,23 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte return nil } +func (r *ApplicationSetReconciler) removeOwnerReferencesOnDeleteAppSet(ctx context.Context, applicationSet argov1alpha1.ApplicationSet) error { + applications, err := r.getCurrentApplications(ctx, applicationSet) + if err != nil { + return err + } + + for _, app := range applications { + app.SetOwnerReferences([]metav1.OwnerReference{}) + err := r.Client.Update(ctx, &app) + if err != nil { + return err + } + } + + return nil +} + func (r *ApplicationSetReconciler) performProgressiveSyncs(ctx context.Context, logCtx *log.Entry, appset argov1alpha1.ApplicationSet, applications []argov1alpha1.Application, desiredApplications []argov1alpha1.Application, appMap map[string]argov1alpha1.Application) (map[string]bool, error) { appDependencyList, appStepMap, err := r.buildAppDependencyList(logCtx, appset, desiredApplications) diff --git a/applicationset/controllers/applicationset_controller_test.go b/applicationset/controllers/applicationset_controller_test.go index add37780e70774..81fbad95ac50b0 100644 --- a/applicationset/controllers/applicationset_controller_test.go +++ b/applicationset/controllers/applicationset_controller_test.go @@ -86,6 +86,12 @@ func (g *generatorMock) GenerateParams(appSetGenerator *v1alpha1.ApplicationSetG return args.Get(0).([]map[string]interface{}), args.Error(1) } +func (g *generatorMock) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) { + args := g.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions) + + return args.Get(0).(string), args.Error(1) +} + type rendererMock struct { mock.Mock } @@ -107,6 +113,12 @@ func (r *rendererMock) RenderTemplateParams(tmpl *v1alpha1.Application, syncPoli } +func (r *rendererMock) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) { + args := r.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions) + + return args.Get(0).(string), args.Error(1) +} + func TestExtractApplications(t *testing.T) { scheme := runtime.NewScheme() err := v1alpha1.AddToScheme(scheme) @@ -1593,6 +1605,81 @@ func TestRemoveFinalizerOnInvalidDestination_DestinationTypes(t *testing.T) { } } +func TestRemoveOwnerReferencesOnDeleteAppSet(t *testing.T) { + scheme := runtime.NewScheme() + err := v1alpha1.AddToScheme(scheme) + assert.Nil(t, err) + + err = v1alpha1.AddToScheme(scheme) + assert.Nil(t, err) + + for _, c := range []struct { + // name is human-readable test name + name string + }{ + { + name: "ownerReferences cleared", + }, + } { + t.Run(c.name, func(t *testing.T) { + appSet := v1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + Finalizers: []string{v1alpha1.ResourcesFinalizerName}, + }, + Spec: v1alpha1.ApplicationSetSpec{ + Template: v1alpha1.ApplicationSetTemplate{ + Spec: v1alpha1.ApplicationSpec{ + Project: "project", + }, + }, + }, + } + + app := v1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1", + Namespace: "namespace", + }, + Spec: v1alpha1.ApplicationSpec{ + Project: "project", + Source: &v1alpha1.ApplicationSource{Path: "path", TargetRevision: "revision", RepoURL: "repoURL"}, + Destination: v1alpha1.ApplicationDestination{ + Namespace: "namespace", + Server: "https://kubernetes.default.svc", + }, + }, + } + + err := controllerutil.SetControllerReference(&appSet, &app, scheme) + assert.NoError(t, err, "Unexpected error") + + initObjs := []crtclient.Object{&app, &appSet} + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build() + + r := ApplicationSetReconciler{ + Client: client, + Scheme: scheme, + Recorder: record.NewFakeRecorder(10), + KubeClientset: nil, + Cache: &fakeCache{}, + } + + err = r.removeOwnerReferencesOnDeleteAppSet(context.Background(), appSet) + assert.NoError(t, err, "Unexpected error") + + retrievedApp := v1alpha1.Application{} + err = client.Get(context.Background(), crtclient.ObjectKeyFromObject(&app), &retrievedApp) + assert.NoError(t, err, "Unexpected error") + + ownerReferencesRemoved := len(retrievedApp.OwnerReferences) == 0 + assert.True(t, ownerReferencesRemoved) + }) + } +} + func TestCreateApplications(t *testing.T) { scheme := runtime.NewScheme() @@ -2740,17 +2827,24 @@ func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) { { name: "Generate an application from a go template application set manifest using a pull request generator", params: []map[string]interface{}{{ - "number": "1", - "branch": "branch1", - "branch_slug": "branchSlug1", - "head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958", - "head_short_sha": "089d92cb", - "labels": []string{"label1"}}}, + "number": "1", + "branch": "branch1", + "branch_slug": "branchSlug1", + "head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958", + "head_short_sha": "089d92cb", + "branch_slugify_default": "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature", + "branch_slugify_smarttruncate_disabled": "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature", + "branch_slugify_smarttruncate_enabled": "feat/testwithsmarttruncateenabledramdomlonglistofcharacters", + "labels": []string{"label1"}}, + }, template: v1alpha1.ApplicationSetTemplate{ ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ Name: "AppSet-{{.branch}}-{{.number}}", Labels: map[string]string{ - "app1": "{{index .labels 0}}", + "app1": "{{index .labels 0}}", + "branch-test1": "AppSet-{{.branch_slugify_default | slugify }}", + "branch-test2": "AppSet-{{.branch_slugify_smarttruncate_disabled | slugify 49 false }}", + "branch-test3": "AppSet-{{.branch_slugify_smarttruncate_enabled | slugify 50 true }}", }, }, Spec: v1alpha1.ApplicationSpec{ @@ -2769,7 +2863,10 @@ func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "AppSet-branch1-1", Labels: map[string]string{ - "app1": "label1", + "app1": "label1", + "branch-test1": "AppSet-feat-a-really-long-pull-request-name-to-test-argo", + "branch-test2": "AppSet-feat-areallylongpullrequestnametotestargoslugific", + "branch-test3": "AppSet-feat", }, }, Spec: v1alpha1.ApplicationSpec{ diff --git a/applicationset/controllers/templatePatch.go b/applicationset/controllers/templatePatch.go new file mode 100644 index 00000000000000..f8efd9f376996b --- /dev/null +++ b/applicationset/controllers/templatePatch.go @@ -0,0 +1,46 @@ +package controllers + +import ( + "encoding/json" + "fmt" + + "k8s.io/apimachinery/pkg/util/strategicpatch" + + "github.com/argoproj/argo-cd/v2/applicationset/utils" + appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" +) + +func applyTemplatePatch(app *appv1.Application, templatePatch string) (*appv1.Application, error) { + + appString, err := json.Marshal(app) + if err != nil { + return nil, fmt.Errorf("error while marhsalling Application %w", err) + } + + convertedTemplatePatch, err := utils.ConvertYAMLToJSON(templatePatch) + + if err != nil { + return nil, fmt.Errorf("error while converting template to json %q: %w", convertedTemplatePatch, err) + } + + if err := json.Unmarshal([]byte(convertedTemplatePatch), &appv1.Application{}); err != nil { + return nil, fmt.Errorf("invalid templatePatch %q: %w", convertedTemplatePatch, err) + } + + data, err := strategicpatch.StrategicMergePatch(appString, []byte(convertedTemplatePatch), appv1.Application{}) + + if err != nil { + return nil, fmt.Errorf("error while applying templatePatch template to json %q: %w", convertedTemplatePatch, err) + } + + finalApp := appv1.Application{} + err = json.Unmarshal(data, &finalApp) + if err != nil { + return nil, fmt.Errorf("error while unmarhsalling patched application: %w", err) + } + + // Prevent changes to the `project` field. This helps prevent malicious template patches + finalApp.Spec.Project = app.Spec.Project + + return &finalApp, nil +} diff --git a/applicationset/controllers/templatePatch_test.go b/applicationset/controllers/templatePatch_test.go new file mode 100644 index 00000000000000..c1a794077c8eeb --- /dev/null +++ b/applicationset/controllers/templatePatch_test.go @@ -0,0 +1,249 @@ +package controllers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" +) + +func Test_ApplyTemplatePatch(t *testing.T) { + testCases := []struct { + name string + appTemplate *appv1.Application + templatePatch string + expectedApp *appv1.Application + }{ + { + name: "patch with JSON", + appTemplate: &appv1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-guestbook", + Namespace: "namespace", + Finalizers: []string{"resources-finalizer.argocd.argoproj.io"}, + }, + Spec: appv1.ApplicationSpec{ + Project: "default", + Source: &appv1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + }, + Destination: appv1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "guestbook", + }, + }, + }, + templatePatch: `{ + "metadata": { + "annotations": { + "annotation-some-key": "annotation-some-value" + } + }, + "spec": { + "source": { + "helm": { + "valueFiles": [ + "values.test.yaml", + "values.big.yaml" + ] + } + }, + "syncPolicy": { + "automated": { + "prune": true + } + } + } + }`, + expectedApp: &appv1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-guestbook", + Namespace: "namespace", + Finalizers: []string{"resources-finalizer.argocd.argoproj.io"}, + Annotations: map[string]string{ + "annotation-some-key": "annotation-some-value", + }, + }, + Spec: appv1.ApplicationSpec{ + Project: "default", + Source: &appv1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + Helm: &appv1.ApplicationSourceHelm{ + ValueFiles: []string{ + "values.test.yaml", + "values.big.yaml", + }, + }, + }, + Destination: appv1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "guestbook", + }, + SyncPolicy: &appv1.SyncPolicy{ + Automated: &appv1.SyncPolicyAutomated{ + Prune: true, + }, + }, + }, + }, + }, + { + name: "patch with YAML", + appTemplate: &appv1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-guestbook", + Namespace: "namespace", + Finalizers: []string{"resources-finalizer.argocd.argoproj.io"}, + }, + Spec: appv1.ApplicationSpec{ + Project: "default", + Source: &appv1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + }, + Destination: appv1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "guestbook", + }, + }, + }, + templatePatch: ` +metadata: + annotations: + annotation-some-key: annotation-some-value +spec: + source: + helm: + valueFiles: + - values.test.yaml + - values.big.yaml + syncPolicy: + automated: + prune: true`, + expectedApp: &appv1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-guestbook", + Namespace: "namespace", + Finalizers: []string{"resources-finalizer.argocd.argoproj.io"}, + Annotations: map[string]string{ + "annotation-some-key": "annotation-some-value", + }, + }, + Spec: appv1.ApplicationSpec{ + Project: "default", + Source: &appv1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + Helm: &appv1.ApplicationSourceHelm{ + ValueFiles: []string{ + "values.test.yaml", + "values.big.yaml", + }, + }, + }, + Destination: appv1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "guestbook", + }, + SyncPolicy: &appv1.SyncPolicy{ + Automated: &appv1.SyncPolicyAutomated{ + Prune: true, + }, + }, + }, + }, + }, + { + name: "project field isn't overwritten", + appTemplate: &appv1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-guestbook", + Namespace: "namespace", + }, + Spec: appv1.ApplicationSpec{ + Project: "default", + Source: &appv1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + }, + Destination: appv1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "guestbook", + }, + }, + }, + templatePatch: ` +spec: + project: my-project`, + expectedApp: &appv1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: "Application", + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-guestbook", + Namespace: "namespace", + }, + Spec: appv1.ApplicationSpec{ + Project: "default", + Source: &appv1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + }, + Destination: appv1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "guestbook", + }, + }, + }, + }, + } + + for _, tc := range testCases { + tcc := tc + t.Run(tcc.name, func(t *testing.T) { + result, err := applyTemplatePatch(tcc.appTemplate, tcc.templatePatch) + require.NoError(t, err) + assert.Equal(t, *tcc.expectedApp, *result) + }) + } +} + +func TestError(t *testing.T) { + app := &appv1.Application{} + + result, err := applyTemplatePatch(app, "hello world") + require.Error(t, err) + require.Nil(t, result) +} diff --git a/applicationset/generators/git.go b/applicationset/generators/git.go index 07c1b11849cd09..57fe2835b8df09 100644 --- a/applicationset/generators/git.go +++ b/applicationset/generators/git.go @@ -56,12 +56,14 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic return nil, EmptyAppSetGeneratorError } + noRevisionCache := appSet.RefreshRequired() + var err error var res []map[string]interface{} if len(appSetGenerator.Git.Directories) != 0 { - res, err = g.generateParamsForGitDirectories(appSetGenerator, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions) + res, err = g.generateParamsForGitDirectories(appSetGenerator, noRevisionCache, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions) } else if len(appSetGenerator.Git.Files) != 0 { - res, err = g.generateParamsForGitFiles(appSetGenerator, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions) + res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions) } else { return nil, EmptyAppSetGeneratorError } @@ -72,10 +74,10 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic return res, nil } -func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) { +func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache bool, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) { // Directories, not files - allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision) + allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, noRevisionCache) if err != nil { return nil, fmt.Errorf("error getting directories from repo: %w", err) } @@ -98,12 +100,12 @@ func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoproj return res, nil } -func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) { +func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache bool, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) { // Get all files that match the requested path string, removing duplicates allFiles := make(map[string][]byte) for _, requestedPath := range appSetGenerator.Git.Files { - files, err := g.repos.GetFiles(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, requestedPath.Path) + files, err := g.repos.GetFiles(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, requestedPath.Path, noRevisionCache) if err != nil { return nil, err } diff --git a/applicationset/generators/git_test.go b/applicationset/generators/git_test.go index f0d1d29bca6ecc..d3fd4965057f8c 100644 --- a/applicationset/generators/git_test.go +++ b/applicationset/generators/git_test.go @@ -317,7 +317,7 @@ func TestGitGenerateParamsFromDirectories(t *testing.T) { argoCDServiceMock := mocks.Repos{} - argoCDServiceMock.On("GetDirectories", mock.Anything, mock.Anything, mock.Anything).Return(testCaseCopy.repoApps, testCaseCopy.repoError) + argoCDServiceMock.On("GetDirectories", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCaseCopy.repoApps, testCaseCopy.repoError) var gitGenerator = NewGitGenerator(&argoCDServiceMock) applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ @@ -613,7 +613,7 @@ func TestGitGenerateParamsFromDirectoriesGoTemplate(t *testing.T) { argoCDServiceMock := mocks.Repos{} - argoCDServiceMock.On("GetDirectories", mock.Anything, mock.Anything, mock.Anything).Return(testCaseCopy.repoApps, testCaseCopy.repoError) + argoCDServiceMock.On("GetDirectories", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(testCaseCopy.repoApps, testCaseCopy.repoError) var gitGenerator = NewGitGenerator(&argoCDServiceMock) applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ @@ -972,7 +972,7 @@ cluster: t.Parallel() argoCDServiceMock := mocks.Repos{} - argoCDServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + argoCDServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(testCaseCopy.repoFileContents, testCaseCopy.repoPathsError) var gitGenerator = NewGitGenerator(&argoCDServiceMock) @@ -1322,7 +1322,7 @@ cluster: t.Parallel() argoCDServiceMock := mocks.Repos{} - argoCDServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + argoCDServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(testCaseCopy.repoFileContents, testCaseCopy.repoPathsError) var gitGenerator = NewGitGenerator(&argoCDServiceMock) diff --git a/applicationset/generators/matrix_test.go b/applicationset/generators/matrix_test.go index 35748b98bcf196..21e88710ae618d 100644 --- a/applicationset/generators/matrix_test.go +++ b/applicationset/generators/matrix_test.go @@ -1108,7 +1108,7 @@ func TestGitGenerator_GenerateParams_list_x_git_matrix_generator(t *testing.T) { } repoServiceMock := &mocks.Repos{} - repoServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string][]byte{ + repoServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string][]byte{ "some/path.json": []byte("test: content"), }, nil) gitGenerator := NewGitGenerator(repoServiceMock) diff --git a/applicationset/services/mocks/Repos.go b/applicationset/services/mocks/Repos.go index 776b104cae2845..b7620b22f08bb2 100644 --- a/applicationset/services/mocks/Repos.go +++ b/applicationset/services/mocks/Repos.go @@ -13,25 +13,25 @@ type Repos struct { mock.Mock } -// GetDirectories provides a mock function with given fields: ctx, repoURL, revision -func (_m *Repos) GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) { - ret := _m.Called(ctx, repoURL, revision) +// GetDirectories provides a mock function with given fields: ctx, repoURL, revision, noRevisionCache +func (_m *Repos) GetDirectories(ctx context.Context, repoURL string, revision string, noRevisionCache bool) ([]string, error) { + ret := _m.Called(ctx, repoURL, revision, noRevisionCache) var r0 []string var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]string, error)); ok { - return rf(ctx, repoURL, revision) + if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) ([]string, error)); ok { + return rf(ctx, repoURL, revision, noRevisionCache) } - if rf, ok := ret.Get(0).(func(context.Context, string, string) []string); ok { - r0 = rf(ctx, repoURL, revision) + if rf, ok := ret.Get(0).(func(context.Context, string, string, bool) []string); ok { + r0 = rf(ctx, repoURL, revision, noRevisionCache) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]string) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { - r1 = rf(ctx, repoURL, revision) + if rf, ok := ret.Get(1).(func(context.Context, string, string, bool) error); ok { + r1 = rf(ctx, repoURL, revision, noRevisionCache) } else { r1 = ret.Error(1) } @@ -39,25 +39,25 @@ func (_m *Repos) GetDirectories(ctx context.Context, repoURL string, revision st return r0, r1 } -// GetFiles provides a mock function with given fields: ctx, repoURL, revision, pattern -func (_m *Repos) GetFiles(ctx context.Context, repoURL string, revision string, pattern string) (map[string][]byte, error) { - ret := _m.Called(ctx, repoURL, revision, pattern) +// GetFiles provides a mock function with given fields: ctx, repoURL, revision, pattern, noRevisionCache +func (_m *Repos) GetFiles(ctx context.Context, repoURL string, revision string, pattern string, noRevisionCache bool) (map[string][]byte, error) { + ret := _m.Called(ctx, repoURL, revision, pattern, noRevisionCache) var r0 map[string][]byte var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) (map[string][]byte, error)); ok { - return rf(ctx, repoURL, revision, pattern) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) (map[string][]byte, error)); ok { + return rf(ctx, repoURL, revision, pattern, noRevisionCache) } - if rf, ok := ret.Get(0).(func(context.Context, string, string, string) map[string][]byte); ok { - r0 = rf(ctx, repoURL, revision, pattern) + if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool) map[string][]byte); ok { + r0 = rf(ctx, repoURL, revision, pattern, noRevisionCache) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(map[string][]byte) } } - if rf, ok := ret.Get(1).(func(context.Context, string, string, string) error); ok { - r1 = rf(ctx, repoURL, revision, pattern) + if rf, ok := ret.Get(1).(func(context.Context, string, string, string, bool) error); ok { + r1 = rf(ctx, repoURL, revision, pattern, noRevisionCache) } else { r1 = ret.Error(1) } diff --git a/applicationset/services/pull_request/azure_devops_test.go b/applicationset/services/pull_request/azure_devops_test.go index 15ac1c8233d892..5ed8f4de78b9df 100644 --- a/applicationset/services/pull_request/azure_devops_test.go +++ b/applicationset/services/pull_request/azure_devops_test.go @@ -206,9 +206,9 @@ func TestBuildURL(t *testing.T) { }, { name: "Provided custom URL and organization", - url: "https://azuredevops.mycompany.com/", + url: "https://azuredevops.example.com/", organization: "myorganization", - expected: "https://azuredevops.mycompany.com/myorganization", + expected: "https://azuredevops.example.com/myorganization", }, } diff --git a/applicationset/services/repo_service.go b/applicationset/services/repo_service.go index 8ad261fda11cdd..64fedc34390b8b 100644 --- a/applicationset/services/repo_service.go +++ b/applicationset/services/repo_service.go @@ -11,6 +11,8 @@ import ( "github.com/argoproj/argo-cd/v2/util/io" ) +//go:generate go run github.com/vektra/mockery/v2@v2.25.1 --name=RepositoryDB + // RepositoryDB Is a lean facade for ArgoDB, // Using a lean interface makes it easier to test the functionality of the git generator type RepositoryDB interface { @@ -25,13 +27,15 @@ type argoCDService struct { newFileGlobbingEnabled bool } +//go:generate go run github.com/vektra/mockery/v2@v2.25.1 --name=Repos + type Repos interface { // GetFiles returns content of files (not directories) within the target repo - GetFiles(ctx context.Context, repoURL string, revision string, pattern string) (map[string][]byte, error) + GetFiles(ctx context.Context, repoURL string, revision string, pattern string, noRevisionCache bool) (map[string][]byte, error) // GetDirectories returns a list of directories (not files) within the target repo - GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) + GetDirectories(ctx context.Context, repoURL string, revision string, noRevisionCache bool) ([]string, error) } func NewArgoCDService(db db.ArgoDB, submoduleEnabled bool, repoClientset apiclient.Clientset, newFileGlobbingEnabled bool) (Repos, error) { @@ -43,7 +47,7 @@ func NewArgoCDService(db db.ArgoDB, submoduleEnabled bool, repoClientset apiclie }, nil } -func (a *argoCDService) GetFiles(ctx context.Context, repoURL string, revision string, pattern string) (map[string][]byte, error) { +func (a *argoCDService) GetFiles(ctx context.Context, repoURL string, revision string, pattern string, noRevisionCache bool) (map[string][]byte, error) { repo, err := a.repositoriesDB.GetRepository(ctx, repoURL) if err != nil { return nil, fmt.Errorf("error in GetRepository: %w", err) @@ -55,6 +59,7 @@ func (a *argoCDService) GetFiles(ctx context.Context, repoURL string, revision s Revision: revision, Path: pattern, NewGitFileGlobbingEnabled: a.newFileGlobbingEnabled, + NoRevisionCache: noRevisionCache, } closer, client, err := a.repoServerClientSet.NewRepoServerClient() if err != nil { @@ -69,7 +74,7 @@ func (a *argoCDService) GetFiles(ctx context.Context, repoURL string, revision s return fileResponse.GetMap(), nil } -func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revision string) ([]string, error) { +func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revision string, noRevisionCache bool) ([]string, error) { repo, err := a.repositoriesDB.GetRepository(ctx, repoURL) if err != nil { return nil, fmt.Errorf("error in GetRepository: %w", err) @@ -79,6 +84,7 @@ func (a *argoCDService) GetDirectories(ctx context.Context, repoURL string, revi Repo: repo, SubmoduleEnabled: a.submoduleEnabled, Revision: revision, + NoRevisionCache: noRevisionCache, } closer, client, err := a.repoServerClientSet.NewRepoServerClient() diff --git a/applicationset/services/repo_service_test.go b/applicationset/services/repo_service_test.go index 62f8c11c172d00..040fe57f969585 100644 --- a/applicationset/services/repo_service_test.go +++ b/applicationset/services/repo_service_test.go @@ -25,9 +25,10 @@ func TestGetDirectories(t *testing.T) { repoServerClientFuncs []func(*repo_mocks.RepoServerServiceClient) } type args struct { - ctx context.Context - repoURL string - revision string + ctx context.Context + repoURL string + revision string + noRevisionCache bool } tests := []struct { name string @@ -88,11 +89,11 @@ func TestGetDirectories(t *testing.T) { submoduleEnabled: tt.fields.submoduleEnabled, repoServerClientSet: &repo_mocks.Clientset{RepoServerServiceClient: mockRepoClient}, } - got, err := a.GetDirectories(tt.args.ctx, tt.args.repoURL, tt.args.revision) - if !tt.wantErr(t, err, fmt.Sprintf("GetDirectories(%v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision)) { + got, err := a.GetDirectories(tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.noRevisionCache) + if !tt.wantErr(t, err, fmt.Sprintf("GetDirectories(%v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.noRevisionCache)) { return } - assert.Equalf(t, tt.want, got, "GetDirectories(%v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision) + assert.Equalf(t, tt.want, got, "GetDirectories(%v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.noRevisionCache) }) } } @@ -105,10 +106,11 @@ func TestGetFiles(t *testing.T) { repoServerClientFuncs []func(*repo_mocks.RepoServerServiceClient) } type args struct { - ctx context.Context - repoURL string - revision string - pattern string + ctx context.Context + repoURL string + revision string + pattern string + noRevisionCache bool } tests := []struct { name string @@ -175,11 +177,11 @@ func TestGetFiles(t *testing.T) { submoduleEnabled: tt.fields.submoduleEnabled, repoServerClientSet: &repo_mocks.Clientset{RepoServerServiceClient: mockRepoClient}, } - got, err := a.GetFiles(tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern) - if !tt.wantErr(t, err, fmt.Sprintf("GetFiles(%v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern)) { + got, err := a.GetFiles(tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, tt.args.noRevisionCache) + if !tt.wantErr(t, err, fmt.Sprintf("GetFiles(%v, %v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, tt.args.noRevisionCache)) { return } - assert.Equalf(t, tt.want, got, "GetFiles(%v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern) + assert.Equalf(t, tt.want, got, "GetFiles(%v, %v, %v, %v, %v)", tt.args.ctx, tt.args.repoURL, tt.args.revision, tt.args.pattern, tt.args.noRevisionCache) }) } } diff --git a/applicationset/services/scm_provider/gitlab.go b/applicationset/services/scm_provider/gitlab.go index f4b92b3ed9e5f9..ca174de540887f 100644 --- a/applicationset/services/scm_provider/gitlab.go +++ b/applicationset/services/scm_provider/gitlab.go @@ -100,12 +100,20 @@ func (g *GitlabProvider) ListRepos(ctx context.Context, cloneProtocol string) ([ return nil, fmt.Errorf("unknown clone protocol for Gitlab %v", cloneProtocol) } + var repoLabels []string + if len(gitlabRepo.Topics) == 0 { + // fallback to for gitlab prior to 14.5 + repoLabels = gitlabRepo.TagList + } else { + repoLabels = gitlabRepo.Topics + } + repos = append(repos, &Repository{ Organization: gitlabRepo.Namespace.FullPath, Repository: gitlabRepo.Path, URL: url, Branch: gitlabRepo.DefaultBranch, - Labels: gitlabRepo.TagList, + Labels: repoLabels, RepositoryId: gitlabRepo.ID, }) } diff --git a/applicationset/services/scm_provider/gitlab_test.go b/applicationset/services/scm_provider/gitlab_test.go index 11b21cb6da6d43..b93616fa8367f3 100644 --- a/applicationset/services/scm_provider/gitlab_test.go +++ b/applicationset/services/scm_provider/gitlab_test.go @@ -1063,6 +1063,16 @@ func TestGitlabListRepos(t *testing.T) { proto: "ssh", url: "git@gitlab.com:test-argocd-proton/argocd.git", }, + { + name: "labelmatch", + proto: "ssh", + url: "git@gitlab.com:test-argocd-proton/argocd.git", + filters: []v1alpha1.SCMProviderGeneratorFilter{ + { + LabelMatch: strp("test-topic"), + }, + }, + }, { name: "https protocol", proto: "https", diff --git a/applicationset/utils/utils.go b/applicationset/utils/utils.go index 089a6ff1031004..2d128eb81a16cf 100644 --- a/applicationset/utils/utils.go +++ b/applicationset/utils/utils.go @@ -16,6 +16,7 @@ import ( "unsafe" "github.com/Masterminds/sprig/v3" + "github.com/gosimple/slug" "github.com/valyala/fasttemplate" "sigs.k8s.io/yaml" @@ -32,6 +33,7 @@ func init() { delete(sprigFuncMap, "expandenv") delete(sprigFuncMap, "getHostByName") sprigFuncMap["normalize"] = SanitizeName + sprigFuncMap["slugify"] = SlugifyName sprigFuncMap["toYaml"] = toYAML sprigFuncMap["fromYaml"] = fromYAML sprigFuncMap["fromYamlArray"] = fromYAMLArray @@ -39,6 +41,7 @@ func init() { type Renderer interface { RenderTemplateParams(tmpl *argoappsv1.Application, syncPolicy *argoappsv1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*argoappsv1.Application, error) + Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) } type Render struct { @@ -434,6 +437,54 @@ func NormalizeBitbucketBasePath(basePath string) string { return basePath } +// SlugifyName generates a URL-friendly slug from the provided name and additional options. +// The slug is generated in accordance with the following rules: +// 1. The generated slug will be URL-safe and suitable for use in URLs. +// 2. The maximum length of the slug can be specified using the `maxSize` argument. +// 3. Smart truncation can be enabled or disabled using the `EnableSmartTruncate` argument. +// 4. The input name can be any string value that needs to be converted into a slug. +// +// Args: +// - args: A variadic number of arguments where: +// - The first argument (if provided) is an integer specifying the maximum length of the slug. +// - The second argument (if provided) is a boolean indicating whether smart truncation is enabled. +// - The last argument (if provided) is the input name that needs to be slugified. +// If no name is provided, an empty string will be used. +// +// Returns: +// - string: The generated URL-friendly slug based on the input name and options. +func SlugifyName(args ...interface{}) string { + // Default values for arguments + maxSize := 50 + EnableSmartTruncate := true + name := "" + + // Process the arguments + for idx, arg := range args { + switch idx { + case len(args) - 1: + name = arg.(string) + case 0: + maxSize = arg.(int) + case 1: + EnableSmartTruncate = arg.(bool) + default: + log.Errorf("Bad 'slugify' arguments.") + } + } + + sanitizedName := SanitizeName(name) + + // Configure slug generation options + slug.EnableSmartTruncate = EnableSmartTruncate + slug.MaxLength = maxSize + + // Generate the slug from the input name + urlSlug := slug.Make(sanitizedName) + + return urlSlug +} + func getTlsConfigWithCACert(scmRootCAPath string) *tls.Config { tlsConfig := &tls.Config{} diff --git a/applicationset/utils/utils_test.go b/applicationset/utils/utils_test.go index a1c58769160cc5..3b4702bc35c3fc 100644 --- a/applicationset/utils/utils_test.go +++ b/applicationset/utils/utils_test.go @@ -1243,6 +1243,43 @@ func TestNormalizeBitbucketBasePath(t *testing.T) { } } +func TestSlugify(t *testing.T) { + for _, c := range []struct { + branch string + smartTruncate bool + length int + expectedBasePath string + }{ + { + branch: "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature", + smartTruncate: false, + length: 50, + expectedBasePath: "feat-a-really-long-pull-request-name-to-test-argo", + }, + { + branch: "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature", + smartTruncate: true, + length: 53, + expectedBasePath: "feat-a-really-long-pull-request-name-to-test-argo", + }, + { + branch: "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature", + smartTruncate: true, + length: 50, + expectedBasePath: "feat", + }, + { + branch: "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature", + smartTruncate: false, + length: 50, + expectedBasePath: "feat-areallylongpullrequestnametotestargoslugifica", + }, + } { + result := SlugifyName(c.length, c.smartTruncate, c.branch) + assert.Equal(t, c.expectedBasePath, result, c.branch) + } +} + func TestGetTLSConfig(t *testing.T) { // certParsed, err := tls.X509KeyPair(test.Cert, test.PrivateKey) // require.NoError(t, err) diff --git a/applicationset/webhook/testdata/github-pull-request-labeled-event.json b/applicationset/webhook/testdata/github-pull-request-labeled-event.json new file mode 100644 index 00000000000000..f912a2fdb4a978 --- /dev/null +++ b/applicationset/webhook/testdata/github-pull-request-labeled-event.json @@ -0,0 +1,473 @@ +{ + "action": "labeled", + "number": 2, + "label": { + "id": 6129306173, + "node_id": "LA_kwDOIqudU88AAAABbVXKPQ", + "url": "https://api.github.com/repos/SG60/backstage/labels/deploy-preview", + "name": "deploy-preview", + "color": "bfd4f2", + "default": false, + "description": "" + }, + "pull_request": { + "url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2", + "id": 279147437, + "node_id": "MDExOlB1bGxSZXF1ZXN0Mjc5MTQ3NDM3", + "html_url": "https://github.com/Codertocat/Hello-World/pull/2", + "diff_url": "https://github.com/Codertocat/Hello-World/pull/2.diff", + "patch_url": "https://github.com/Codertocat/Hello-World/pull/2.patch", + "issue_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2", + "number": 2, + "state": "open", + "locked": false, + "title": "Update the README with new information.", + "user": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "body": "This is a pretty simple change that we need to pull into master.", + "created_at": "2019-05-15T15:20:33Z", + "updated_at": "2019-05-15T15:20:33Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": null, + "assignee": null, + "assignees": [], + "requested_reviewers": [], + "requested_teams": [], + "labels": [ + { + "id": 6129306173, + "node_id": "LA_kwDOIqudU88AAAABbVXKPQ", + "url": "https://api.github.com/repos/Codertocat/Hello-World/labels/deploy-preview", + "name": "deploy-preview", + "color": "bfd4f2", + "default": false, + "description": "" + } + ], + "milestone": null, + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits", + "review_comments_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments", + "review_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "head": { + "label": "Codertocat:changes", + "ref": "changes", + "sha": "ec26c3e57ca3a959ca5aad62de7213c562f8c821", + "user": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/Codertocat/Hello-World", + "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", + "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", + "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", + "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:19:27Z", + "pushed_at": "2019-05-15T15:20:32Z", + "git_url": "git://github.com/Codertocat/Hello-World.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/Codertocat/Hello-World.git", + "svn_url": "https://github.com/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "delete_branch_on_merge": false + } + }, + "base": { + "label": "Codertocat:master", + "ref": "master", + "sha": "f95f852bd8fca8fcc58a9a2d6c842781e32a215e", + "user": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/Codertocat/Hello-World", + "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", + "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", + "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", + "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:19:27Z", + "pushed_at": "2019-05-15T15:20:32Z", + "git_url": "git://github.com/Codertocat/Hello-World.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/Codertocat/Hello-World.git", + "svn_url": "https://github.com/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master", + "allow_squash_merge": true, + "allow_merge_commit": true, + "allow_rebase_merge": true, + "delete_branch_on_merge": false + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2" + }, + "html": { + "href": "https://github.com/Codertocat/Hello-World/pull/2" + }, + "issue": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2" + }, + "comments": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/issues/2/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/pulls/2/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/Codertocat/Hello-World/statuses/ec26c3e57ca3a959ca5aad62de7213c562f8c821" + } + }, + "author_association": "OWNER", + "draft": false, + "merged": false, + "mergeable": null, + "rebaseable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 0, + "review_comments": 0, + "maintainer_can_modify": false, + "commits": 1, + "additions": 1, + "deletions": 1, + "changed_files": 1 + }, + "repository": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/Codertocat/Hello-World", + "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", + "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", + "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", + "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", + "created_at": "2019-05-15T15:19:25Z", + "updated_at": "2019-05-15T15:19:27Z", + "pushed_at": "2019-05-15T15:20:32Z", + "git_url": "git://github.com/Codertocat/Hello-World.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/Codertocat/Hello-World.git", + "svn_url": "https://github.com/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 0, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 0, + "open_issues": 2, + "watchers": 0, + "default_branch": "master" + }, + "sender": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + } +} diff --git a/applicationset/webhook/webhook.go b/applicationset/webhook/webhook.go index ce099df35ea35c..d55e63e064f5a3 100644 --- a/applicationset/webhook/webhook.go +++ b/applicationset/webhook/webhook.go @@ -412,10 +412,12 @@ func shouldRefreshPRGenerator(gen *v1alpha1.PullRequestGenerator, info *prGenera } if gen.Github != nil && info.Github != nil { - if gen.Github.Owner != info.Github.Owner { + // repository owner and name are case-insensitive + // See https://docs.github.com/en/rest/pulls/pulls?apiVersion=2022-11-28#list-pull-requests + if !strings.EqualFold(gen.Github.Owner, info.Github.Owner) { return false } - if gen.Github.Repo != info.Github.Repo { + if !strings.EqualFold(gen.Github.Repo, info.Github.Repo) { return false } api := gen.Github.API diff --git a/applicationset/webhook/webhook_test.go b/applicationset/webhook/webhook_test.go index 349d275948aee0..d22b1a07ca6f26 100644 --- a/applicationset/webhook/webhook_test.go +++ b/applicationset/webhook/webhook_test.go @@ -111,7 +111,7 @@ func TestWebhookHandler(t *testing.T) { expectedRefresh: false, }, { - desc: "WebHook from a GitHub repository via pull_reqeuest opened event", + desc: "WebHook from a GitHub repository via pull_request opened event", headerKey: "X-GitHub-Event", headerValue: "pull_request", payloadFile: "github-pull-request-opened-event.json", @@ -120,7 +120,7 @@ func TestWebhookHandler(t *testing.T) { expectedRefresh: true, }, { - desc: "WebHook from a GitHub repository via pull_reqeuest assigned event", + desc: "WebHook from a GitHub repository via pull_request assigned event", headerKey: "X-GitHub-Event", headerValue: "pull_request", payloadFile: "github-pull-request-assigned-event.json", @@ -128,6 +128,15 @@ func TestWebhookHandler(t *testing.T) { expectedStatusCode: http.StatusOK, expectedRefresh: false, }, + { + desc: "WebHook from a GitHub repository via pull_request labeled event", + headerKey: "X-GitHub-Event", + headerValue: "pull_request", + payloadFile: "github-pull-request-labeled-event.json", + effectedAppSets: []string{"pull-request-github", "matrix-pull-request-github", "matrix-scm-pull-request-github", "merge-pull-request-github", "plugin", "matrix-pull-request-github-plugin"}, + expectedStatusCode: http.StatusOK, + expectedRefresh: true, + }, { desc: "WebHook from a GitLab repository via open merge request event", headerKey: "X-Gitlab-Event", @@ -180,7 +189,7 @@ func TestWebhookHandler(t *testing.T) { fakeAppWithGitGenerator("git-github", namespace, "https://github.com/org/repo"), fakeAppWithGitGenerator("git-gitlab", namespace, "https://gitlab/group/name"), fakeAppWithGitGenerator("git-azure-devops", namespace, "https://dev.azure.com/fabrikam-fiber-inc/DefaultCollection/_git/Fabrikam-Fiber-Git"), - fakeAppWithGithubPullRequestGenerator("pull-request-github", namespace, "Codertocat", "Hello-World"), + fakeAppWithGithubPullRequestGenerator("pull-request-github", namespace, "CodErTOcat", "Hello-World"), fakeAppWithGitlabPullRequestGenerator("pull-request-gitlab", namespace, "100500"), fakeAppWithAzureDevOpsPullRequestGenerator("pull-request-azure-devops", namespace, "DefaultCollection", "Fabrikam"), fakeAppWithPluginGenerator("plugin", namespace), @@ -189,7 +198,7 @@ func TestWebhookHandler(t *testing.T) { fakeAppWithMatrixAndScmWithGitGenerator("matrix-scm-git-github", namespace, "org"), fakeAppWithMatrixAndScmWithPullRequestGenerator("matrix-scm-pull-request-github", namespace, "Codertocat"), fakeAppWithMatrixAndNestedGitGenerator("matrix-nested-git-github", namespace, "https://github.com/org/repo"), - fakeAppWithMatrixAndPullRequestGeneratorWithPluginGenerator("matrix-pull-request-github-plugin", namespace, "Codertocat", "Hello-World", "plugin-cm"), + fakeAppWithMatrixAndPullRequestGeneratorWithPluginGenerator("matrix-pull-request-github-plugin", namespace, "coDErtoCat", "HeLLO-WorLD", "plugin-cm"), fakeAppWithMergeAndGitGenerator("merge-git-github", namespace, "https://github.com/org/repo"), fakeAppWithMergeAndPullRequestGenerator("merge-pull-request-github", namespace, "Codertocat", "Hello-World"), fakeAppWithMergeAndNestedGitGenerator("merge-nested-git-github", namespace, "https://github.com/org/repo"), diff --git a/assets/badge.svg b/assets/badge.svg index cc216ccdd15084..f1dab6b6cb711d 100644 --- a/assets/badge.svg +++ b/assets/badge.svg @@ -5,6 +5,7 @@ + @@ -14,6 +15,7 @@ + diff --git a/assets/swagger.json b/assets/swagger.json index 49861cdec7f564..14621997f0bdef 100644 --- a/assets/swagger.json +++ b/assets/swagger.json @@ -5664,6 +5664,10 @@ "type": "string", "title": "ClusterName contains AWS cluster name" }, + "profile": { + "description": "Profile contains optional role ARN. If set then AWS IAM Authenticator uses the profile to perform cluster operations instead of the default AWS credential provider chain.", + "type": "string" + }, "roleARN": { "description": "RoleARN contains optional role ARN. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain.", "type": "string" @@ -6143,6 +6147,9 @@ }, "template": { "$ref": "#/definitions/v1alpha1ApplicationSetTemplate" + }, + "templatePatch": { + "type": "string" } } }, @@ -6417,6 +6424,10 @@ "type": "string" } }, + "labelWithoutSelector": { + "type": "boolean", + "title": "LabelWithoutSelector specifies whether to apply common labels to resource selectors or not" + }, "namePrefix": { "type": "string", "title": "NamePrefix is a prefix appended to resources for Kustomize apps" @@ -8524,6 +8535,9 @@ "format": "int64", "title": "ID is an auto incrementing identifier of the RevisionHistory" }, + "initiatedBy": { + "$ref": "#/definitions/v1alpha1OperationInitiator" + }, "revision": { "type": "string", "title": "Revision holds the revision the sync was performed against" diff --git a/cmd/argocd-application-controller/commands/argocd_application_controller.go b/cmd/argocd-application-controller/commands/argocd_application_controller.go index 4f7e587e36564c..3c7fe8bbac1074 100644 --- a/cmd/argocd-application-controller/commands/argocd_application_controller.go +++ b/cmd/argocd-application-controller/commands/argocd_application_controller.go @@ -6,7 +6,6 @@ import ( "math" "time" - "github.com/argoproj/argo-cd/v2/pkg/ratelimiter" "github.com/argoproj/pkg/stats" "github.com/redis/go-redis/v9" log "github.com/sirupsen/logrus" @@ -20,19 +19,17 @@ import ( "github.com/argoproj/argo-cd/v2/controller/sharding" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" + "github.com/argoproj/argo-cd/v2/pkg/ratelimiter" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" cacheutil "github.com/argoproj/argo-cd/v2/util/cache" appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate" "github.com/argoproj/argo-cd/v2/util/cli" - "github.com/argoproj/argo-cd/v2/util/db" "github.com/argoproj/argo-cd/v2/util/env" "github.com/argoproj/argo-cd/v2/util/errors" kubeutil "github.com/argoproj/argo-cd/v2/util/kube" "github.com/argoproj/argo-cd/v2/util/settings" "github.com/argoproj/argo-cd/v2/util/tls" "github.com/argoproj/argo-cd/v2/util/trace" - kubeerrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( @@ -50,6 +47,7 @@ func NewCommand() *cobra.Command { clientConfig clientcmd.ClientConfig appResyncPeriod int64 appHardResyncPeriod int64 + appResyncJitter int64 repoErrorGracePeriod int64 repoServerAddress string repoServerTimeoutSeconds int @@ -66,11 +64,14 @@ func NewCommand() *cobra.Command { repoServerPlaintext bool repoServerStrictTLS bool otlpAddress string + otlpInsecure bool + otlpHeaders map[string]string otlpAttrs []string applicationNamespaces []string persistResourceHealth bool shardingAlgorithm string enableDynamicClusterDistribution bool + serverSideDiff bool ) var command = cobra.Command{ Use: cliName, @@ -143,7 +144,8 @@ func NewCommand() *cobra.Command { appController.InvalidateProjectsCache() })) kubectl := kubeutil.NewKubectl() - clusterFilter := getClusterFilter(kubeClient, settingsMgr, shardingAlgorithm, enableDynamicClusterDistribution) + clusterSharding, err := sharding.GetClusterSharding(kubeClient, settingsMgr, shardingAlgorithm, enableDynamicClusterDistribution) + errors.CheckError(err) appController, err = controller.NewApplicationController( namespace, settingsMgr, @@ -154,6 +156,7 @@ func NewCommand() *cobra.Command { kubectl, resyncDuration, hardResyncDuration, + time.Duration(appResyncJitter)*time.Second, time.Duration(selfHealTimeoutSeconds)*time.Second, time.Duration(repoErrorGracePeriod)*time.Second, metricsPort, @@ -161,9 +164,11 @@ func NewCommand() *cobra.Command { metricsAplicationLabels, kubectlParallelismLimit, persistResourceHealth, - clusterFilter, + clusterSharding, applicationNamespaces, &workqueueRateLimit, + serverSideDiff, + enableDynamicClusterDistribution, ) errors.CheckError(err) cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer()) @@ -173,7 +178,7 @@ func NewCommand() *cobra.Command { stats.RegisterHeapDumper("memprofile") if otlpAddress != "" { - closeTracer, err := trace.InitTracer(ctx, "argocd-controller", otlpAddress, otlpAttrs) + closeTracer, err := trace.InitTracer(ctx, "argocd-controller", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs) if err != nil { log.Fatalf("failed to initialize tracing: %v", err) } @@ -190,6 +195,7 @@ func NewCommand() *cobra.Command { clientConfig = cli.AddKubectlFlagsToCmd(&command) command.Flags().Int64Var(&appResyncPeriod, "app-resync", int64(env.ParseDurationFromEnv("ARGOCD_RECONCILIATION_TIMEOUT", defaultAppResyncPeriod*time.Second, 0, math.MaxInt64).Seconds()), "Time period in seconds for application resync.") command.Flags().Int64Var(&appHardResyncPeriod, "app-hard-resync", int64(env.ParseDurationFromEnv("ARGOCD_HARD_RECONCILIATION_TIMEOUT", defaultAppHardResyncPeriod*time.Second, 0, math.MaxInt64).Seconds()), "Time period in seconds for application hard resync.") + command.Flags().Int64Var(&appResyncJitter, "app-resync-jitter", int64(env.ParseDurationFromEnv("ARGOCD_RECONCILIATION_JITTER", 0*time.Second, 0, math.MaxInt64).Seconds()), "Maximum time period in seconds to add as a delay jitter for application resync.") command.Flags().Int64Var(&repoErrorGracePeriod, "repo-error-grace-period-seconds", int64(env.ParseDurationFromEnv("ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS", defaultAppResyncPeriod*time.Second, 0, math.MaxInt64).Seconds()), "Grace period in seconds for ignoring consecutive errors while communicating with repo server.") command.Flags().StringVar(&repoServerAddress, "repo-server", env.StringFromEnv("ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER", common.DefaultRepoServerAddr), "Repo server address.") command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_TIMEOUT_SECONDS", 60, 0, math.MaxInt64), "Repo server RPC call timeout seconds.") @@ -206,13 +212,15 @@ func NewCommand() *cobra.Command { command.Flags().BoolVar(&repoServerStrictTLS, "repo-server-strict-tls", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_STRICT_TLS", false), "Whether to use strict validation of the TLS cert presented by the repo server") command.Flags().StringSliceVar(&metricsAplicationLabels, "metrics-application-labels", []string{}, "List of Application labels that will be added to the argocd_application_labels metric") command.Flags().StringVar(&otlpAddress, "otlp-address", env.StringFromEnv("ARGOCD_APPLICATION_CONTROLLER_OTLP_ADDRESS", ""), "OpenTelemetry collector address to send traces to") + command.Flags().BoolVar(&otlpInsecure, "otlp-insecure", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE", true), "OpenTelemetry collector insecure mode") + command.Flags().StringToStringVar(&otlpHeaders, "otlp-headers", env.ParseStringToStringFromEnv("ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS", map[string]string{}, ","), "List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2)") command.Flags().StringSliceVar(&otlpAttrs, "otlp-attrs", env.StringsFromEnv("ARGOCD_APPLICATION_CONTROLLER_OTLP_ATTRS", []string{}, ","), "List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)") command.Flags().StringSliceVar(&applicationNamespaces, "application-namespaces", env.StringsFromEnv("ARGOCD_APPLICATION_NAMESPACES", []string{}, ","), "List of additional namespaces that applications are allowed to be reconciled from") command.Flags().BoolVar(&persistResourceHealth, "persist-resource-health", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_PERSIST_RESOURCE_HEALTH", true), "Enables storing the managed resources health in the Application CRD") command.Flags().StringVar(&shardingAlgorithm, "sharding-method", env.StringFromEnv(common.EnvControllerShardingAlgorithm, common.DefaultShardingAlgorithm), "Enables choice of sharding method. Supported sharding methods are : [legacy, round-robin] ") // global queue rate limit config command.Flags().Int64Var(&workqueueRateLimit.BucketSize, "wq-bucket-size", env.ParseInt64FromEnv("WORKQUEUE_BUCKET_SIZE", 500, 1, math.MaxInt64), "Set Workqueue Rate Limiter Bucket Size, default 500") - command.Flags().Int64Var(&workqueueRateLimit.BucketQPS, "wq-bucket-qps", env.ParseInt64FromEnv("WORKQUEUE_BUCKET_QPS", 50, 1, math.MaxInt64), "Set Workqueue Rate Limiter Bucket QPS, default 50") + command.Flags().Float64Var(&workqueueRateLimit.BucketQPS, "wq-bucket-qps", env.ParseFloat64FromEnv("WORKQUEUE_BUCKET_QPS", math.MaxFloat64, 1, math.MaxFloat64), "Set Workqueue Rate Limiter Bucket QPS, default set to MaxFloat64 which disables the bucket limiter") // individual item rate limit config // when WORKQUEUE_FAILURE_COOLDOWN is 0 per item rate limiting is disabled(default) command.Flags().DurationVar(&workqueueRateLimit.FailureCoolDown, "wq-cooldown-ns", time.Duration(env.ParseInt64FromEnv("WORKQUEUE_FAILURE_COOLDOWN_NS", 0, 0, (24*time.Hour).Nanoseconds())), "Set Workqueue Per Item Rate Limiter Cooldown duration in ns, default 0(per item rate limiter disabled)") @@ -220,63 +228,11 @@ func NewCommand() *cobra.Command { command.Flags().DurationVar(&workqueueRateLimit.MaxDelay, "wq-maxdelay-ns", time.Duration(env.ParseInt64FromEnv("WORKQUEUE_MAX_DELAY_NS", time.Second.Nanoseconds(), 1*time.Millisecond.Nanoseconds(), (24*time.Hour).Nanoseconds())), "Set Workqueue Per Item Rate Limiter Max Delay duration in nanoseconds, default 1000000000 (1s)") command.Flags().Float64Var(&workqueueRateLimit.BackoffFactor, "wq-backoff-factor", env.ParseFloat64FromEnv("WORKQUEUE_BACKOFF_FACTOR", 1.5, 0, math.MaxFloat64), "Set Workqueue Per Item Rate Limiter Backoff Factor, default is 1.5") command.Flags().BoolVar(&enableDynamicClusterDistribution, "dynamic-cluster-distribution-enabled", env.ParseBoolFromEnv(common.EnvEnableDynamicClusterDistribution, false), "Enables dynamic cluster distribution.") - cacheSource = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) { - redisClient = client + command.Flags().BoolVar(&serverSideDiff, "server-side-diff-enabled", env.ParseBoolFromEnv(common.EnvServerSideDiff, false), "Feature flag to enable ServerSide diff. Default (\"false\")") + cacheSource = appstatecache.AddCacheFlagsToCmd(&command, cacheutil.Options{ + OnClientCreated: func(client *redis.Client) { + redisClient = client + }, }) return &command } - -func getClusterFilter(kubeClient *kubernetes.Clientset, settingsMgr *settings.SettingsManager, shardingAlgorithm string, enableDynamicClusterDistribution bool) sharding.ClusterFilterFunction { - - var replicas int - shard := env.ParseNumFromEnv(common.EnvControllerShard, -1, -math.MaxInt32, math.MaxInt32) - - applicationControllerName := env.StringFromEnv(common.EnvAppControllerName, common.DefaultApplicationControllerName) - appControllerDeployment, err := kubeClient.AppsV1().Deployments(settingsMgr.GetNamespace()).Get(context.Background(), applicationControllerName, metav1.GetOptions{}) - - // if the application controller deployment was not found, the Get() call returns an empty Deployment object. So, set the variable to nil explicitly - if err != nil && kubeerrors.IsNotFound(err) { - appControllerDeployment = nil - } - - if enableDynamicClusterDistribution && appControllerDeployment != nil && appControllerDeployment.Spec.Replicas != nil { - replicas = int(*appControllerDeployment.Spec.Replicas) - } else { - replicas = env.ParseNumFromEnv(common.EnvControllerReplicas, 0, 0, math.MaxInt32) - } - - var clusterFilter func(cluster *v1alpha1.Cluster) bool - if replicas > 1 { - // check for shard mapping using configmap if application-controller is a deployment - // else use existing logic to infer shard from pod name if application-controller is a statefulset - if enableDynamicClusterDistribution && appControllerDeployment != nil { - - var err error - // retry 3 times if we find a conflict while updating shard mapping configMap. - // If we still see conflicts after the retries, wait for next iteration of heartbeat process. - for i := 0; i <= common.AppControllerHeartbeatUpdateRetryCount; i++ { - shard, err = sharding.GetOrUpdateShardFromConfigMap(kubeClient, settingsMgr, replicas, shard) - if !kubeerrors.IsConflict(err) { - err = fmt.Errorf("unable to get shard due to error updating the sharding config map: %s", err) - break - } - log.Warnf("conflict when getting shard from shard mapping configMap. Retrying (%d/3)", i) - } - errors.CheckError(err) - } else { - if shard < 0 { - var err error - shard, err = sharding.InferShard() - errors.CheckError(err) - } - } - log.Infof("Processing clusters from shard %d", shard) - db := db.NewDB(settingsMgr.GetNamespace(), settingsMgr, kubeClient) - log.Infof("Using filter function: %s", shardingAlgorithm) - distributionFunction := sharding.GetDistributionFunction(db, shardingAlgorithm) - clusterFilter = sharding.GetClusterFilter(db, distributionFunction, shard) - } else { - log.Info("Processing all cluster shards") - } - return clusterFilter -} diff --git a/cmd/argocd-cmp-server/commands/argocd_cmp_server.go b/cmd/argocd-cmp-server/commands/argocd_cmp_server.go index 62f45b24aedb5d..526a199cb54905 100644 --- a/cmd/argocd-cmp-server/commands/argocd_cmp_server.go +++ b/cmd/argocd-cmp-server/commands/argocd_cmp_server.go @@ -26,6 +26,8 @@ func NewCommand() *cobra.Command { var ( configFilePath string otlpAddress string + otlpInsecure bool + otlpHeaders map[string]string otlpAttrs []string ) var command = cobra.Command{ @@ -56,7 +58,7 @@ func NewCommand() *cobra.Command { if otlpAddress != "" { var closer func() var err error - closer, err = traceutil.InitTracer(ctx, "argocd-cmp-server", otlpAddress, otlpAttrs) + closer, err = traceutil.InitTracer(ctx, "argocd-cmp-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs) if err != nil { log.Fatalf("failed to initialize tracing: %v", err) } @@ -83,6 +85,8 @@ func NewCommand() *cobra.Command { command.Flags().StringVar(&cmdutil.LogLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error") command.Flags().StringVar(&configFilePath, "config-dir-path", common.DefaultPluginConfigFilePath, "Config management plugin configuration file location, Default is '/home/argocd/cmp-server/config/'") command.Flags().StringVar(&otlpAddress, "otlp-address", env.StringFromEnv("ARGOCD_CMP_SERVER_OTLP_ADDRESS", ""), "OpenTelemetry collector address to send traces to") + command.Flags().BoolVar(&otlpInsecure, "otlp-insecure", env.ParseBoolFromEnv("ARGOCD_CMP_SERVER_OTLP_INSECURE", true), "OpenTelemetry collector insecure mode") + command.Flags().StringToStringVar(&otlpHeaders, "otlp-headers", env.ParseStringToStringFromEnv("ARGOCD_CMP_SERVER_OTLP_HEADERS", map[string]string{}, ","), "List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2)") command.Flags().StringSliceVar(&otlpAttrs, "otlp-attrs", env.StringsFromEnv("ARGOCD_CMP_SERVER_OTLP_ATTRS", []string{}, ","), "List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)") return &command } diff --git a/cmd/argocd-k8s-auth/commands/aws.go b/cmd/argocd-k8s-auth/commands/aws.go index 79a118d2653a36..9b750ac5f92f8c 100644 --- a/cmd/argocd-k8s-auth/commands/aws.go +++ b/cmd/argocd-k8s-auth/commands/aws.go @@ -37,13 +37,14 @@ func newAWSCommand() *cobra.Command { var ( clusterName string roleARN string + profile string ) var command = &cobra.Command{ Use: "aws", Run: func(c *cobra.Command, args []string) { ctx := c.Context() - presignedURLString, err := getSignedRequestWithRetry(ctx, time.Minute, 5*time.Second, clusterName, roleARN, getSignedRequest) + presignedURLString, err := getSignedRequestWithRetry(ctx, time.Minute, 5*time.Second, clusterName, roleARN, profile, getSignedRequest) errors.CheckError(err) token := v1Prefix + base64.RawURLEncoding.EncodeToString([]byte(presignedURLString)) // Set token expiration to 1 minute before the presigned URL expires for some cushion @@ -53,16 +54,17 @@ func newAWSCommand() *cobra.Command { } command.Flags().StringVar(&clusterName, "cluster-name", "", "AWS Cluster name") command.Flags().StringVar(&roleARN, "role-arn", "", "AWS Role ARN") + command.Flags().StringVar(&profile, "profile", "", "AWS Profile") return command } -type getSignedRequestFunc func(clusterName, roleARN string) (string, error) +type getSignedRequestFunc func(clusterName, roleARN string, profile string) (string, error) -func getSignedRequestWithRetry(ctx context.Context, timeout, interval time.Duration, clusterName, roleARN string, fn getSignedRequestFunc) (string, error) { +func getSignedRequestWithRetry(ctx context.Context, timeout, interval time.Duration, clusterName, roleARN string, profile string, fn getSignedRequestFunc) (string, error) { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() for { - signed, err := fn(clusterName, roleARN) + signed, err := fn(clusterName, roleARN, profile) if err == nil { return signed, nil } @@ -74,8 +76,10 @@ func getSignedRequestWithRetry(ctx context.Context, timeout, interval time.Durat } } -func getSignedRequest(clusterName, roleARN string) (string, error) { - sess, err := session.NewSession() +func getSignedRequest(clusterName, roleARN string, profile string) (string, error) { + sess, err := session.NewSessionWithOptions(session.Options{ + Profile: profile, + }) if err != nil { return "", fmt.Errorf("error creating new AWS session: %s", err) } diff --git a/cmd/argocd-k8s-auth/commands/aws_test.go b/cmd/argocd-k8s-auth/commands/aws_test.go index c22449eba42be3..578aae71a2c290 100644 --- a/cmd/argocd-k8s-auth/commands/aws_test.go +++ b/cmd/argocd-k8s-auth/commands/aws_test.go @@ -22,7 +22,7 @@ func TestGetSignedRequestWithRetry(t *testing.T) { } // when - signed, err := getSignedRequestWithRetry(ctx, time.Second, time.Millisecond, "cluster-name", "", mock.getSignedRequestMock) + signed, err := getSignedRequestWithRetry(ctx, time.Second, time.Millisecond, "cluster-name", "", "", mock.getSignedRequestMock) // then assert.NoError(t, err) @@ -41,7 +41,7 @@ func TestGetSignedRequestWithRetry(t *testing.T) { } // when - signed, err := getSignedRequestWithRetry(ctx, time.Second, time.Millisecond, "cluster-name", "", mock.getSignedRequestMock) + signed, err := getSignedRequestWithRetry(ctx, time.Second, time.Millisecond, "cluster-name", "", "", mock.getSignedRequestMock) // then assert.NoError(t, err) @@ -57,7 +57,7 @@ func TestGetSignedRequestWithRetry(t *testing.T) { } // when - signed, err := getSignedRequestWithRetry(ctx, time.Second, time.Millisecond, "cluster-name", "", mock.getSignedRequestMock) + signed, err := getSignedRequestWithRetry(ctx, time.Second, time.Millisecond, "cluster-name", "", "", mock.getSignedRequestMock) // then assert.Error(t, err) @@ -70,7 +70,7 @@ type signedRequestMock struct { returnFunc func(m *signedRequestMock) (string, error) } -func (m *signedRequestMock) getSignedRequestMock(clusterName, roleARN string) (string, error) { +func (m *signedRequestMock) getSignedRequestMock(clusterName, roleARN string, profile string) (string, error) { m.getSignedRequestCalls++ return m.returnFunc(m) } diff --git a/cmd/argocd-notification/commands/controller.go b/cmd/argocd-notification/commands/controller.go index abd9a2e8475f0b..cb30fd5277d4bc 100644 --- a/cmd/argocd-notification/commands/controller.go +++ b/cmd/argocd-notification/commands/controller.go @@ -43,19 +43,20 @@ func addK8SFlagsToCmd(cmd *cobra.Command) clientcmd.ClientConfig { func NewCommand() *cobra.Command { var ( - clientConfig clientcmd.ClientConfig - processorsCount int - namespace string - appLabelSelector string - logLevel string - logFormat string - metricsPort int - argocdRepoServer string - argocdRepoServerPlaintext bool - argocdRepoServerStrictTLS bool - configMapName string - secretName string - applicationNamespaces []string + clientConfig clientcmd.ClientConfig + processorsCount int + namespace string + appLabelSelector string + logLevel string + logFormat string + metricsPort int + argocdRepoServer string + argocdRepoServerPlaintext bool + argocdRepoServerStrictTLS bool + configMapName string + secretName string + applicationNamespaces []string + selfServiceNotificationEnabled bool ) var command = cobra.Command{ Use: "controller", @@ -139,7 +140,7 @@ func NewCommand() *cobra.Command { log.Infof("serving metrics on port %d", metricsPort) log.Infof("loading configuration %d", metricsPort) - ctrl := notificationscontroller.NewController(k8sClient, dynamicClient, argocdService, namespace, applicationNamespaces, appLabelSelector, registry, secretName, configMapName) + ctrl := notificationscontroller.NewController(k8sClient, dynamicClient, argocdService, namespace, applicationNamespaces, appLabelSelector, registry, secretName, configMapName, selfServiceNotificationEnabled) err = ctrl.Init(ctx) if err != nil { return fmt.Errorf("failed to initialize controller: %w", err) @@ -163,5 +164,6 @@ func NewCommand() *cobra.Command { command.Flags().StringVar(&configMapName, "config-map-name", "argocd-notifications-cm", "Set notifications ConfigMap name") command.Flags().StringVar(&secretName, "secret-name", "argocd-notifications-secret", "Set notifications Secret name") command.Flags().StringSliceVar(&applicationNamespaces, "application-namespaces", env.StringsFromEnv("ARGOCD_APPLICATION_NAMESPACES", []string{}, ","), "List of additional namespaces that this controller should send notifications for") + command.Flags().BoolVar(&selfServiceNotificationEnabled, "self-service-notification-enabled", env.ParseBoolFromEnv("ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED", false), "Allows the Argo CD notification controller to pull notification config from the namespace that the resource is in. This is useful for self-service notification.") return &command } diff --git a/cmd/argocd-repo-server/commands/argocd_repo_server.go b/cmd/argocd-repo-server/commands/argocd_repo_server.go index 69358d2a91efdc..84b50e7cd5ab92 100644 --- a/cmd/argocd-repo-server/commands/argocd_repo_server.go +++ b/cmd/argocd-repo-server/commands/argocd_repo_server.go @@ -54,6 +54,8 @@ func NewCommand() *cobra.Command { metricsPort int metricsHost string otlpAddress string + otlpInsecure bool + otlpHeaders map[string]string otlpAttrs []string cacheSrc func() (*reposervercache.Cache, error) tlsConfigCustomizer tls.ConfigCustomizer @@ -129,7 +131,7 @@ func NewCommand() *cobra.Command { if otlpAddress != "" { var closer func() var err error - closer, err = traceutil.InitTracer(ctx, "argocd-repo-server", otlpAddress, otlpAttrs) + closer, err = traceutil.InitTracer(ctx, "argocd-repo-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs) if err != nil { log.Fatalf("failed to initialize tracing: %v", err) } @@ -196,6 +198,8 @@ func NewCommand() *cobra.Command { command.Flags().StringVar(&metricsHost, "metrics-address", env.StringFromEnv("ARGOCD_REPO_SERVER_METRICS_LISTEN_ADDRESS", common.DefaultAddressRepoServerMetrics), "Listen on given address for metrics") command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortRepoServerMetrics, "Start metrics server on given port") command.Flags().StringVar(&otlpAddress, "otlp-address", env.StringFromEnv("ARGOCD_REPO_SERVER_OTLP_ADDRESS", ""), "OpenTelemetry collector address to send traces to") + command.Flags().BoolVar(&otlpInsecure, "otlp-insecure", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_OTLP_INSECURE", true), "OpenTelemetry collector insecure mode") + command.Flags().StringToStringVar(&otlpHeaders, "otlp-headers", env.ParseStringToStringFromEnv("ARGOCD_REPO_OTLP_HEADERS", map[string]string{}, ","), "List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2)") command.Flags().StringSliceVar(&otlpAttrs, "otlp-attrs", env.StringsFromEnv("ARGOCD_REPO_SERVER_OTLP_ATTRS", []string{}, ","), "List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)") command.Flags().BoolVar(&disableTLS, "disable-tls", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_DISABLE_TLS", false), "Disable TLS on the gRPC endpoint") command.Flags().StringVar(&maxCombinedDirectoryManifestsSize, "max-combined-directory-manifests-size", env.StringFromEnv("ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE", "10M"), "Max combined size of manifest files in a directory-type Application") @@ -206,8 +210,10 @@ func NewCommand() *cobra.Command { command.Flags().StringVar(&helmManifestMaxExtractedSize, "helm-manifest-max-extracted-size", env.StringFromEnv("ARGOCD_REPO_SERVER_HELM_MANIFEST_MAX_EXTRACTED_SIZE", "1G"), "Maximum size of helm manifest archives when extracted") command.Flags().BoolVar(&disableManifestMaxExtractedSize, "disable-helm-manifest-max-extracted-size", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_DISABLE_HELM_MANIFEST_MAX_EXTRACTED_SIZE", false), "Disable maximum size of helm manifest archives when extracted") tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command) - cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, func(client *redis.Client) { - redisClient = client + cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, cacheutil.Options{ + OnClientCreated: func(client *redis.Client) { + redisClient = client + }, }) return &command } diff --git a/cmd/argocd-server/commands/argocd_server.go b/cmd/argocd-server/commands/argocd_server.go index 8709c7de095237..27a2db34189b49 100644 --- a/cmd/argocd-server/commands/argocd_server.go +++ b/cmd/argocd-server/commands/argocd_server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "strings" "time" "github.com/argoproj/pkg/stats" @@ -18,8 +19,10 @@ import ( "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" + reposervercache "github.com/argoproj/argo-cd/v2/reposerver/cache" "github.com/argoproj/argo-cd/v2/server" servercache "github.com/argoproj/argo-cd/v2/server/cache" + cacheutil "github.com/argoproj/argo-cd/v2/util/cache" "github.com/argoproj/argo-cd/v2/util/cli" "github.com/argoproj/argo-cd/v2/util/dex" "github.com/argoproj/argo-cd/v2/util/env" @@ -50,6 +53,8 @@ func NewCommand() *cobra.Command { metricsHost string metricsPort int otlpAddress string + otlpInsecure bool + otlpHeaders map[string]string otlpAttrs []string glogLevel int clientConfig clientcmd.ClientConfig @@ -59,9 +64,11 @@ func NewCommand() *cobra.Command { repoServerAddress string dexServerAddress string disableAuth bool + contentTypes string enableGZip bool tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error) cacheSrc func() (*servercache.Cache, error) + repoServerCacheSrc func() (*reposervercache.Cache, error) frameOptions string contentSecurityPolicy string repoServerPlaintext bool @@ -103,6 +110,8 @@ func NewCommand() *cobra.Command { errors.CheckError(err) cache, err := cacheSrc() errors.CheckError(err) + repoServerCache, err := repoServerCacheSrc() + errors.CheckError(err) kubeclientset := kubernetes.NewForConfigOrDie(config) @@ -163,6 +172,11 @@ func NewCommand() *cobra.Command { baseHRef = rootPath } + var contentTypesList []string + if contentTypes != "" { + contentTypesList = strings.Split(contentTypes, ";") + } + argoCDOpts := server.ArgoCDServerOpts{ Insecure: insecure, ListenPort: listenPort, @@ -178,9 +192,11 @@ func NewCommand() *cobra.Command { DexServerAddr: dexServerAddress, DexTLSConfig: dexTlsConfig, DisableAuth: disableAuth, + ContentTypes: contentTypesList, EnableGZip: enableGZip, TLSConfigCustomizer: tlsConfigCustomizer, Cache: cache, + RepoServerCache: repoServerCache, XFrameOptions: frameOptions, ContentSecurityPolicy: contentSecurityPolicy, RedisClient: redisClient, @@ -200,7 +216,7 @@ func NewCommand() *cobra.Command { var closer func() ctx, cancel := context.WithCancel(ctx) if otlpAddress != "" { - closer, err = traceutil.InitTracer(ctx, "argocd-server", otlpAddress, otlpAttrs) + closer, err = traceutil.InitTracer(ctx, "argocd-server", otlpAddress, otlpInsecure, otlpHeaders, otlpAttrs) if err != nil { log.Fatalf("failed to initialize tracing: %v", err) } @@ -232,6 +248,7 @@ func NewCommand() *cobra.Command { command.Flags().StringVar(&repoServerAddress, "repo-server", env.StringFromEnv("ARGOCD_SERVER_REPO_SERVER", common.DefaultRepoServerAddr), "Repo server address") command.Flags().StringVar(&dexServerAddress, "dex-server", env.StringFromEnv("ARGOCD_SERVER_DEX_SERVER", common.DefaultDexServerAddr), "Dex server address") command.Flags().BoolVar(&disableAuth, "disable-auth", env.ParseBoolFromEnv("ARGOCD_SERVER_DISABLE_AUTH", false), "Disable client authentication") + command.Flags().StringVar(&contentTypes, "api-content-types", env.StringFromEnv("ARGOCD_API_CONTENT_TYPES", "application/json", env.StringFromEnvOpts{AllowEmpty: true}), "Semicolon separated list of allowed content types for non GET api requests. Any content type is allowed if empty.") command.Flags().BoolVar(&enableGZip, "enable-gzip", env.ParseBoolFromEnv("ARGOCD_SERVER_ENABLE_GZIP", true), "Enable GZIP compression") command.AddCommand(cli.NewVersionCmd(cliName)) command.Flags().StringVar(&listenHost, "address", env.StringFromEnv("ARGOCD_SERVER_LISTEN_ADDRESS", common.DefaultAddressAPIServer), "Listen on given address") @@ -239,6 +256,8 @@ func NewCommand() *cobra.Command { command.Flags().StringVar(&metricsHost, env.StringFromEnv("ARGOCD_SERVER_METRICS_LISTEN_ADDRESS", "metrics-address"), common.DefaultAddressAPIServerMetrics, "Listen for metrics on given address") command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDAPIServerMetrics, "Start metrics on given port") command.Flags().StringVar(&otlpAddress, "otlp-address", env.StringFromEnv("ARGOCD_SERVER_OTLP_ADDRESS", ""), "OpenTelemetry collector address to send traces to") + command.Flags().BoolVar(&otlpInsecure, "otlp-insecure", env.ParseBoolFromEnv("ARGOCD_SERVER_OTLP_INSECURE", true), "OpenTelemetry collector insecure mode") + command.Flags().StringToStringVar(&otlpHeaders, "otlp-headers", env.ParseStringToStringFromEnv("ARGOCD_SERVER_OTLP_HEADERS", map[string]string{}, ","), "List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2)") command.Flags().StringSliceVar(&otlpAttrs, "otlp-attrs", env.StringsFromEnv("ARGOCD_SERVER_OTLP_ATTRS", []string{}, ","), "List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value)") command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", env.ParseNumFromEnv("ARGOCD_SERVER_REPO_SERVER_TIMEOUT_SECONDS", 60, 0, math.MaxInt64), "Repo server RPC call timeout seconds.") command.Flags().StringVar(&frameOptions, "x-frame-options", env.StringFromEnv("ARGOCD_SERVER_X_FRAME_OPTIONS", "sameorigin"), "Set X-Frame-Options header in HTTP responses to `value`. To disable, set to \"\".") @@ -250,8 +269,11 @@ func NewCommand() *cobra.Command { command.Flags().StringSliceVar(&applicationNamespaces, "application-namespaces", env.StringsFromEnv("ARGOCD_APPLICATION_NAMESPACES", []string{}, ","), "List of additional namespaces where application resources can be managed in") command.Flags().BoolVar(&enableProxyExtension, "enable-proxy-extension", env.ParseBoolFromEnv("ARGOCD_SERVER_ENABLE_PROXY_EXTENSION", false), "Enable Proxy Extension feature") tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command) - cacheSrc = servercache.AddCacheFlagsToCmd(command, func(client *redis.Client) { - redisClient = client + cacheSrc = servercache.AddCacheFlagsToCmd(command, cacheutil.Options{ + OnClientCreated: func(client *redis.Client) { + redisClient = client + }, }) + repoServerCacheSrc = reposervercache.AddCacheFlagsToCmd(command, cacheutil.Options{FlagPrefix: "repo-server-"}) return command } diff --git a/cmd/argocd/commands/admin/admin.go b/cmd/argocd/commands/admin/admin.go index 0615166175259f..49c81e4da4bfee 100644 --- a/cmd/argocd/commands/admin/admin.go +++ b/cmd/argocd/commands/admin/admin.go @@ -138,7 +138,7 @@ $ argocd admin initial-password reset command.AddCommand(NewRepoCommand()) command.AddCommand(NewImportCommand()) command.AddCommand(NewExportCommand()) - command.AddCommand(NewDashboardCommand()) + command.AddCommand(NewDashboardCommand(clientOpts)) command.AddCommand(NewNotificationsCommand()) command.AddCommand(NewInitialPasswordCommand()) diff --git a/cmd/argocd/commands/admin/app.go b/cmd/argocd/commands/admin/app.go index 10e4effe8797aa..096c92f9feb01b 100644 --- a/cmd/argocd/commands/admin/app.go +++ b/cmd/argocd/commands/admin/app.go @@ -243,6 +243,7 @@ func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command repoServerAddress string outputFormat string refresh bool + serverSideDiff bool ) var command = &cobra.Command{ @@ -280,7 +281,7 @@ func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command appClientset := appclientset.NewForConfigOrDie(cfg) kubeClientset := kubernetes.NewForConfigOrDie(cfg) - result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache) + result, err = reconcileApplications(ctx, kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache, serverSideDiff) errors.CheckError(err) } else { appClientset := appclientset.NewForConfigOrDie(cfg) @@ -295,6 +296,7 @@ func NewReconcileCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command command.Flags().StringVar(&selector, "l", "", "Label selector") command.Flags().StringVar(&outputFormat, "o", "yaml", "Output format (yaml|json)") command.Flags().BoolVar(&refresh, "refresh", false, "If set to true then recalculates apps reconciliation") + command.Flags().BoolVar(&serverSideDiff, "server-side-diff", false, "If set to \"true\" will use server-side diff while comparing resources. Default (\"false\")") return command } @@ -344,6 +346,7 @@ func reconcileApplications( repoServerClient reposerverclient.Clientset, selector string, createLiveStateCache func(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache, + serverSideDiff bool, ) ([]appReconcileResult, error) { settingsMgr := settings.NewSettingsManager(ctx, kubeClientset, namespace) argoDB := db.NewDB(namespace, settingsMgr, kubeClientset) @@ -384,7 +387,7 @@ func reconcileApplications( ) appStateManager := controller.NewAppStateManager( - argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server, cache, time.Second, argo.NewResourceTracking(), false, 0) + argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server, cache, time.Second, argo.NewResourceTracking(), false, 0, serverSideDiff) appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(ctx, v1.ListOptions{LabelSelector: selector}) if err != nil { diff --git a/cmd/argocd/commands/admin/app_test.go b/cmd/argocd/commands/admin/app_test.go index 0cad2485e66964..a0284fe8ffa090 100644 --- a/cmd/argocd/commands/admin/app_test.go +++ b/cmd/argocd/commands/admin/app_test.go @@ -113,6 +113,7 @@ func TestGetReconcileResults_Refresh(t *testing.T) { func(argoDB db.ArgoDB, appInformer cache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) statecache.LiveStateCache { return &liveStateCache }, + false, ) if !assert.NoError(t, err) { diff --git a/cmd/argocd/commands/admin/cluster.go b/cmd/argocd/commands/admin/cluster.go index a72aaebc201a0b..2e833a68927f49 100644 --- a/cmd/argocd/commands/admin/cluster.go +++ b/cmd/argocd/commands/admin/cluster.go @@ -25,7 +25,7 @@ import ( "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/controller/sharding" argocdclient "github.com/argoproj/argo-cd/v2/pkg/apiclient" - argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" "github.com/argoproj/argo-cd/v2/util/argo" cacheutil "github.com/argoproj/argo-cd/v2/util/cache" @@ -44,6 +44,15 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc var command = &cobra.Command{ Use: "cluster", Short: "Manage clusters configuration", + Example: ` +#Generate declarative config for a cluster +argocd admin cluster generate-spec my-cluster -o yaml + +#Generate a kubeconfig for a cluster named "my-cluster" and display it in the console +argocd admin cluster kubeconfig my-cluster + +#Print information namespaces which Argo CD manages in each cluster +argocd admin cluster namespaces my-cluster `, Run: func(c *cobra.Command, args []string) { c.HelpFunc()(c, args) }, @@ -62,14 +71,14 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc } type ClusterWithInfo struct { - argoappv1.Cluster + v1alpha1.Cluster // Shard holds controller shard number that handles the cluster Shard int // Namespaces holds list of namespaces managed by Argo CD in the cluster Namespaces []string } -func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClient *versioned.Clientset, replicas int, namespace string, portForwardRedis bool, cacheSrc func() (*appstatecache.Cache, error), shard int, redisName string, redisHaProxyName string) ([]ClusterWithInfo, error) { +func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClient *versioned.Clientset, replicas int, shardingAlgorithm string, namespace string, portForwardRedis bool, cacheSrc func() (*appstatecache.Cache, error), shard int, redisName string, redisHaProxyName string, redisCompressionStr string) ([]ClusterWithInfo, error) { settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace) argoDB := db.NewDB(namespace, settingsMgr, kubeClient) @@ -77,6 +86,14 @@ func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClie if err != nil { return nil, err } + appItems, err := appClient.ArgoprojV1alpha1().Applications(namespace).List(ctx, v1.ListOptions{}) + if err != nil { + return nil, err + } + clusterShardingCache := sharding.NewClusterSharding(argoDB, shard, replicas, shardingAlgorithm) + clusterShardingCache.Init(clustersList, appItems) + clusterShards := clusterShardingCache.GetDistribution() + var cache *appstatecache.Cache if portForwardRedis { overrides := clientcmd.ConfigOverrides{} @@ -88,7 +105,11 @@ func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClie return nil, err } client := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("localhost:%d", port)}) - cache = appstatecache.NewCache(cacheutil.NewCache(cacheutil.NewRedisCache(client, time.Hour, cacheutil.RedisCompressionNone)), time.Hour) + compressionType, err := cacheutil.CompressionTypeFromString(redisCompressionStr) + if err != nil { + return nil, err + } + cache = appstatecache.NewCache(cacheutil.NewCache(cacheutil.NewRedisCache(client, time.Hour, compressionType)), time.Hour) } else { cache, err = cacheSrc() if err != nil { @@ -96,10 +117,6 @@ func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClie } } - appItems, err := appClient.ArgoprojV1alpha1().Applications(namespace).List(ctx, v1.ListOptions{}) - if err != nil { - return nil, err - } apps := appItems.Items for i, app := range apps { err := argo.ValidateDestination(ctx, &app.Spec.Destination, argoDB) @@ -109,6 +126,7 @@ func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClie apps[i] = app } clusters := make([]ClusterWithInfo, len(clustersList.Items)) + batchSize := 10 batchesCount := int(math.Ceil(float64(len(clusters)) / float64(batchSize))) for batchNum := 0; batchNum < batchesCount; batchNum++ { @@ -122,12 +140,10 @@ func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClie clusterShard := 0 cluster := batch[i] if replicas > 0 { - distributionFunction := sharding.GetDistributionFunction(argoDB, common.DefaultShardingAlgorithm) - distributionFunction(&cluster) + clusterShard = clusterShards[cluster.Server] cluster.Shard = pointer.Int64(int64(clusterShard)) log.Infof("Cluster with uid: %s will be processed by shard %d", cluster.ID, clusterShard) } - if shard != -1 && clusterShard != shard { return nil } @@ -161,15 +177,17 @@ func getControllerReplicas(ctx context.Context, kubeClient *kubernetes.Clientset func NewClusterShardsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( - shard int - replicas int - clientConfig clientcmd.ClientConfig - cacheSrc func() (*appstatecache.Cache, error) - portForwardRedis bool + shard int + replicas int + shardingAlgorithm string + clientConfig clientcmd.ClientConfig + cacheSrc func() (*appstatecache.Cache, error) + portForwardRedis bool + redisCompressionStr string ) var command = cobra.Command{ Use: "shards", - Short: "Print information about each controller shard and portion of Kubernetes resources it is responsible for.", + Short: "Print information about each controller shard and the estimated portion of Kubernetes resources it is responsible for.", Run: func(cmd *cobra.Command, args []string) { ctx := cmd.Context() @@ -189,8 +207,7 @@ func NewClusterShardsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm if replicas == 0 { return } - - clusters, err := loadClusters(ctx, kubeClient, appClient, replicas, namespace, portForwardRedis, cacheSrc, shard, clientOpts.RedisName, clientOpts.RedisHaProxyName) + clusters, err := loadClusters(ctx, kubeClient, appClient, replicas, shardingAlgorithm, namespace, portForwardRedis, cacheSrc, shard, clientOpts.RedisName, clientOpts.RedisHaProxyName, redisCompressionStr) errors.CheckError(err) if len(clusters) == 0 { return @@ -202,8 +219,16 @@ func NewClusterShardsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm clientConfig = cli.AddKubectlFlagsToCmd(&command) command.Flags().IntVar(&shard, "shard", -1, "Cluster shard filter") command.Flags().IntVar(&replicas, "replicas", 0, "Application controller replicas count. Inferred from number of running controller pods if not specified") + command.Flags().StringVar(&shardingAlgorithm, "sharding-method", common.DefaultShardingAlgorithm, "Sharding method. Defaults: legacy. Supported sharding methods are : [legacy, round-robin] ") command.Flags().BoolVar(&portForwardRedis, "port-forward-redis", true, "Automatically port-forward ha proxy redis from current namespace?") + cacheSrc = appstatecache.AddCacheFlagsToCmd(&command) + + // parse all added flags so far to get the redis-compression flag that was added by AddCacheFlagsToCmd() above + // we can ignore unchecked error here as the command will be parsed again and checked when command.Execute() is run later + // nolint:errcheck + command.ParseFlags(os.Args[1:]) + redisCompressionStr, _ = command.Flags().GetString(cacheutil.CLIFlagRedisCompress) return &command } @@ -439,15 +464,26 @@ func NewClusterDisableNamespacedMode() *cobra.Command { func NewClusterStatsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( - shard int - replicas int - clientConfig clientcmd.ClientConfig - cacheSrc func() (*appstatecache.Cache, error) - portForwardRedis bool + shard int + replicas int + shardingAlgorithm string + clientConfig clientcmd.ClientConfig + cacheSrc func() (*appstatecache.Cache, error) + portForwardRedis bool + redisCompressionStr string ) var command = cobra.Command{ Use: "stats", Short: "Prints information cluster statistics and inferred shard number", + Example: ` +#Display stats and shards for clusters +argocd admin cluster stats + +#Display Cluster Statistics for a Specific Shard +argocd admin cluster stats --shard=1 + +#In a multi-cluster environment to print stats for a specific cluster say(target-cluster) +argocd admin cluster stats target-cluster`, Run: func(cmd *cobra.Command, args []string) { ctx := cmd.Context() @@ -464,7 +500,7 @@ func NewClusterStatsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma replicas, err = getControllerReplicas(ctx, kubeClient, namespace, clientOpts.AppControllerName) errors.CheckError(err) } - clusters, err := loadClusters(ctx, kubeClient, appClient, replicas, namespace, portForwardRedis, cacheSrc, shard, clientOpts.RedisName, clientOpts.RedisHaProxyName) + clusters, err := loadClusters(ctx, kubeClient, appClient, replicas, shardingAlgorithm, namespace, portForwardRedis, cacheSrc, shard, clientOpts.RedisName, clientOpts.RedisHaProxyName, redisCompressionStr) errors.CheckError(err) w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) @@ -478,8 +514,15 @@ func NewClusterStatsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma clientConfig = cli.AddKubectlFlagsToCmd(&command) command.Flags().IntVar(&shard, "shard", -1, "Cluster shard filter") command.Flags().IntVar(&replicas, "replicas", 0, "Application controller replicas count. Inferred from number of running controller pods if not specified") + command.Flags().StringVar(&shardingAlgorithm, "sharding-method", common.DefaultShardingAlgorithm, "Sharding method. Defaults: legacy. Supported sharding methods are : [legacy, round-robin] ") command.Flags().BoolVar(&portForwardRedis, "port-forward-redis", true, "Automatically port-forward ha proxy redis from current namespace?") cacheSrc = appstatecache.AddCacheFlagsToCmd(&command) + + // parse all added flags so far to get the redis-compression flag that was added by AddCacheFlagsToCmd() above + // we can ignore unchecked error here as the command will be parsed again and checked when command.Execute() is run later + // nolint:errcheck + command.ParseFlags(os.Args[1:]) + redisCompressionStr, _ = command.Flags().GetString(cacheutil.CLIFlagRedisCompress) return &command } @@ -492,6 +535,18 @@ func NewClusterConfig() *cobra.Command { Use: "kubeconfig CLUSTER_URL OUTPUT_PATH", Short: "Generates kubeconfig for the specified cluster", DisableAutoGenTag: true, + Example: ` +#Generate a kubeconfig for a cluster named "my-cluster" on console +argocd admin cluster kubeconfig my-cluster + +#Listing available kubeconfigs for clusters managed by argocd +argocd admin cluster kubeconfig + +#Removing a specific kubeconfig file +argocd admin cluster kubeconfig my-cluster --delete + +#Generate a Kubeconfig for a Cluster with TLS Verification Disabled +argocd admin cluster kubeconfig https://cluster-api-url:6443 /path/to/output/kubeconfig.yaml --insecure-skip-tls-verify`, Run: func(c *cobra.Command, args []string) { ctx := c.Context() @@ -562,15 +617,16 @@ func NewGenClusterConfigCommand(pathOpts *clientcmd.PathOptions) *cobra.Command errors.CheckError(err) kubeClientset := fake.NewSimpleClientset() - var awsAuthConf *argoappv1.AWSAuthConfig - var execProviderConf *argoappv1.ExecProviderConfig + var awsAuthConf *v1alpha1.AWSAuthConfig + var execProviderConf *v1alpha1.ExecProviderConfig if clusterOpts.AwsClusterName != "" { - awsAuthConf = &argoappv1.AWSAuthConfig{ + awsAuthConf = &v1alpha1.AWSAuthConfig{ ClusterName: clusterOpts.AwsClusterName, RoleARN: clusterOpts.AwsRoleArn, + Profile: clusterOpts.AwsProfile, } } else if clusterOpts.ExecProviderCommand != "" { - execProviderConf = &argoappv1.ExecProviderConfig{ + execProviderConf = &v1alpha1.ExecProviderConfig{ Command: clusterOpts.ExecProviderCommand, Args: clusterOpts.ExecProviderArgs, Env: clusterOpts.ExecProviderEnv, @@ -594,7 +650,7 @@ func NewGenClusterConfigCommand(pathOpts *clientcmd.PathOptions) *cobra.Command clst := cmdutil.NewCluster(contextName, clusterOpts.Namespaces, clusterOpts.ClusterResources, conf, bearerToken, awsAuthConf, execProviderConf, labelsMap, annotationsMap) if clusterOpts.InClusterEndpoint() { - clst.Server = argoappv1.KubernetesInternalAPIServerAddr + clst.Server = v1alpha1.KubernetesInternalAPIServerAddr } if clusterOpts.ClusterEndpoint == string(cmdutil.KubePublicEndpoint) { // Ignore `kube-public` cluster endpoints, since this command is intended to run without invoking any network connections. diff --git a/cmd/argocd/commands/admin/dashboard.go b/cmd/argocd/commands/admin/dashboard.go index 33d2866e1f1c09..21b621d264022f 100644 --- a/cmd/argocd/commands/admin/dashboard.go +++ b/cmd/argocd/commands/admin/dashboard.go @@ -3,7 +3,9 @@ package admin import ( "fmt" + "github.com/argoproj/argo-cd/v2/util/cli" "github.com/spf13/cobra" + "k8s.io/client-go/tools/clientcmd" "github.com/argoproj/argo-cd/v2/cmd/argocd/commands/headless" "github.com/argoproj/argo-cd/v2/cmd/argocd/commands/initialize" @@ -14,11 +16,12 @@ import ( "github.com/argoproj/argo-cd/v2/util/errors" ) -func NewDashboardCommand() *cobra.Command { +func NewDashboardCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( port int address string compressionStr string + clientConfig clientcmd.ClientConfig ) cmd := &cobra.Command{ Use: "dashboard", @@ -28,7 +31,8 @@ func NewDashboardCommand() *cobra.Command { compression, err := cache.CompressionTypeFromString(compressionStr) errors.CheckError(err) - errors.CheckError(headless.MaybeStartLocalServer(ctx, &argocdclient.ClientOptions{Core: true}, initialize.RetrieveContextIfChanged(cmd.Flag("context")), &port, &address, compression)) + clientOpts.Core = true + errors.CheckError(headless.MaybeStartLocalServer(ctx, clientOpts, initialize.RetrieveContextIfChanged(cmd.Flag("context")), &port, &address, compression, clientConfig)) println(fmt.Sprintf("Argo CD UI is available at http://%s:%d", address, port)) <-ctx.Done() }, @@ -42,7 +46,7 @@ $ argocd admin dashboard --port 8080 --address 127.0.0.1 $ argocd admin dashboard --redis-compress gzip `, } - initialize.InitCommand(cmd) + clientConfig = cli.AddKubectlFlagsToSet(cmd.Flags()) cmd.Flags().IntVar(&port, "port", common.DefaultPortAPIServer, "Listen on given port") cmd.Flags().StringVar(&address, "address", common.DefaultAddressAdminDashboard, "Listen on given address") cmd.Flags().StringVar(&compressionStr, "redis-compress", env.StringFromEnv("REDIS_COMPRESSION", string(cache.RedisCompressionGZip)), "Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none)") diff --git a/cmd/argocd/commands/admin/notifications.go b/cmd/argocd/commands/admin/notifications.go index a1234cc53b7fe8..3cbac0a53b5c24 100644 --- a/cmd/argocd/commands/admin/notifications.go +++ b/cmd/argocd/commands/admin/notifications.go @@ -36,7 +36,7 @@ func NewNotificationsCommand() *cobra.Command { "notifications", "argocd admin notifications", applications, - settings.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm"), func(clientConfig clientcmd.ClientConfig) { + settings.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm", false), func(clientConfig clientcmd.ClientConfig) { k8sCfg, err := clientConfig.ClientConfig() if err != nil { log.Fatalf("Failed to parse k8s config: %v", err) diff --git a/cmd/argocd/commands/admin/settings.go b/cmd/argocd/commands/admin/settings.go index 281d9875691c4e..0274b4a422f098 100644 --- a/cmd/argocd/commands/admin/settings.go +++ b/cmd/argocd/commands/admin/settings.go @@ -373,11 +373,7 @@ func executeResourceOverrideCommand(ctx context.Context, cmdCtx commandContext, if gvk.Group != "" { key = fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind) } - override, hasOverride := overrides[key] - if !hasOverride { - _, _ = fmt.Printf("No overrides configured for '%s/%s'\n", gvk.Group, gvk.Kind) - return - } + override := overrides[key] callback(res, override, overrides) } @@ -519,16 +515,16 @@ argocd admin settings resource-overrides health ./deploy.yaml --argocd-cm-path . executeResourceOverrideCommand(ctx, cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) { gvk := res.GroupVersionKind() - if override.HealthLua == "" { - _, _ = fmt.Printf("Health script is not configured for '%s/%s'\n", gvk.Group, gvk.Kind) - return - } - resHealth, err := healthutil.GetResourceHealth(&res, lua.ResourceHealthOverrides(overrides)) - errors.CheckError(err) - _, _ = fmt.Printf("STATUS: %s\n", resHealth.Status) - _, _ = fmt.Printf("MESSAGE: %s\n", resHealth.Message) + if err != nil { + errors.CheckError(err) + } else if resHealth == nil { + fmt.Printf("Health script is not configured for '%s/%s'\n", gvk.Group, gvk.Kind) + } else { + _, _ = fmt.Printf("STATUS: %s\n", resHealth.Status) + _, _ = fmt.Printf("MESSAGE: %s\n", resHealth.Message) + } }) }, } diff --git a/cmd/argocd/commands/admin/settings_test.go b/cmd/argocd/commands/admin/settings_test.go index adb18c80ee84eb..ff817017f4be5a 100644 --- a/cmd/argocd/commands/admin/settings_test.go +++ b/cmd/argocd/commands/admin/settings_test.go @@ -226,6 +226,18 @@ spec: replicas: 0` ) +const ( + testCustomResourceYAML = `apiVersion: v1 +apiVersion: example.com/v1alpha1 +kind: ExampleResource +metadata: + name: example-resource + labels: + app: example +spec: + replicas: 0` +) + const ( testCronJobYAML = `apiVersion: batch/v1 kind: CronJob @@ -285,7 +297,7 @@ func TestResourceOverrideIgnoreDifferences(t *testing.T) { assert.NoError(t, err) }) assert.NoError(t, err) - assert.Contains(t, out, "No overrides configured") + assert.Contains(t, out, "Ignore differences are not configured for 'apps/Deployment'\n") }) t.Run("DataIgnored", func(t *testing.T) { @@ -305,7 +317,7 @@ func TestResourceOverrideIgnoreDifferences(t *testing.T) { } func TestResourceOverrideHealth(t *testing.T) { - f, closer, err := tempFile(testDeploymentYAML) + f, closer, err := tempFile(testCustomResourceYAML) if !assert.NoError(t, err) { return } @@ -313,19 +325,34 @@ func TestResourceOverrideHealth(t *testing.T) { t.Run("NoHealthAssessment", func(t *testing.T) { cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{ - "resource.customizations": `apps/Deployment: {}`})) + "resource.customizations": `example.com/ExampleResource: {}`})) out, err := captureStdout(func() { cmd.SetArgs([]string{"health", f}) err := cmd.Execute() assert.NoError(t, err) }) assert.NoError(t, err) - assert.Contains(t, out, "Health script is not configured") + assert.Contains(t, out, "Health script is not configured for 'example.com/ExampleResource'\n") }) t.Run("HealthAssessmentConfigured", func(t *testing.T) { cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{ - "resource.customizations": `apps/Deployment: + "resource.customizations": `example.com/ExampleResource: + health.lua: | + return { status = "Progressing" } +`})) + out, err := captureStdout(func() { + cmd.SetArgs([]string{"health", f}) + err := cmd.Execute() + assert.NoError(t, err) + }) + assert.NoError(t, err) + assert.Contains(t, out, "Progressing") + }) + + t.Run("HealthAssessmentConfiguredWildcard", func(t *testing.T) { + cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{ + "resource.customizations": `example.com/*: health.lua: | return { status = "Progressing" } `})) @@ -412,7 +439,7 @@ resume false action.lua: | job1 = {} job1.apiVersion = "batch/v1" - job1.kind = "Job" + job1.kind = "Job" job1.metadata = {} job1.metadata.name = "hello-1" job1.metadata.namespace = "obj.metadata.namespace" diff --git a/cmd/argocd/commands/app.go b/cmd/argocd/commands/app.go index 0a54c517ca6960..11762c026b25d4 100644 --- a/cmd/argocd/commands/app.go +++ b/cmd/argocd/commands/app.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + k8swatch "k8s.io/apimachinery/pkg/watch" "k8s.io/utils/pointer" "sigs.k8s.io/yaml" @@ -92,6 +93,8 @@ func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman command.AddCommand(NewApplicationResourceActionsCommand(clientOpts)) command.AddCommand(NewApplicationListResourcesCommand(clientOpts)) command.AddCommand(NewApplicationLogsCommand(clientOpts)) + command.AddCommand(NewApplicationAddSourceCommand(clientOpts)) + command.AddCommand(NewApplicationRemoveSourceCommand(clientOpts)) return command } @@ -101,6 +104,7 @@ type watchOpts struct { operation bool suspended bool degraded bool + delete bool } // NewApplicationCreateCommand returns a new instance of an `argocd app create` command @@ -301,7 +305,7 @@ func printHeader(acdClient argocdclient.Client, app *argoappv1.Application, ctx fmt.Println() printOperationResult(app.Status.OperationState) } - if showParams { + if !app.Spec.HasMultipleSources() && showParams { printParams(app) } } @@ -314,6 +318,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com output string showParams bool showOperation bool + appNamespace string ) var command = &cobra.Command{ Use: "get APPNAME", @@ -357,7 +362,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com conn, appIf := acdClient.NewApplicationClientOrDie() defer argoio.Close(conn) - appName, appNs := argo.ParseFromQualifiedName(args[0], "") + appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace) app, err := appIf.Get(ctx, &application.ApplicationQuery{ Name: &appName, @@ -410,6 +415,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com command.Flags().BoolVar(&showParams, "show-params", false, "Show application parameters and overrides") command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving") command.Flags().BoolVar(&hardRefresh, "hard-refresh", false, "Refresh application data as well as target manifests cache") + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only get application from namespace") return command } @@ -545,16 +551,19 @@ func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co } func printAppSummaryTable(app *argoappv1.Application, appURL string, windows *argoappv1.SyncWindows) { - source := app.Spec.GetSource() fmt.Printf(printOpFmtStr, "Name:", app.QualifiedName()) fmt.Printf(printOpFmtStr, "Project:", app.Spec.GetProject()) fmt.Printf(printOpFmtStr, "Server:", getServer(app)) fmt.Printf(printOpFmtStr, "Namespace:", app.Spec.Destination.Namespace) fmt.Printf(printOpFmtStr, "URL:", appURL) - fmt.Printf(printOpFmtStr, "Repo:", source.RepoURL) - fmt.Printf(printOpFmtStr, "Target:", source.TargetRevision) - fmt.Printf(printOpFmtStr, "Path:", source.Path) - printAppSourceDetails(&source) + if !app.Spec.HasMultipleSources() { + fmt.Println("Source:") + } else { + fmt.Println("Sources:") + } + for _, source := range app.Spec.GetSources() { + printAppSourceDetails(&source) + } var wds []string var status string var allow, deny, inactiveAllows bool @@ -624,11 +633,19 @@ func printAppSummaryTable(app *argoappv1.Application, appURL string, windows *ar } func printAppSourceDetails(appSrc *argoappv1.ApplicationSource) { + fmt.Printf(printOpFmtStr, "- Repo:", appSrc.RepoURL) + fmt.Printf(printOpFmtStr, " Target:", appSrc.TargetRevision) + if appSrc.Path != "" { + fmt.Printf(printOpFmtStr, " Path:", appSrc.Path) + } + if appSrc.Ref != "" { + fmt.Printf(printOpFmtStr, " Ref:", appSrc.Ref) + } if appSrc.Helm != nil && len(appSrc.Helm.ValueFiles) > 0 { - fmt.Printf(printOpFmtStr, "Helm Values:", strings.Join(appSrc.Helm.ValueFiles, ",")) + fmt.Printf(printOpFmtStr, " Helm Values:", strings.Join(appSrc.Helm.ValueFiles, ",")) } if appSrc.Kustomize != nil && appSrc.Kustomize.NamePrefix != "" { - fmt.Printf(printOpFmtStr, "Name Prefix:", appSrc.Kustomize.NamePrefix) + fmt.Printf(printOpFmtStr, " Name Prefix:", appSrc.Kustomize.NamePrefix) } } @@ -1057,6 +1074,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co localRepoRoot string serverSideGenerate bool localIncludes []string + appNamespace string ) shortDesc := "Perform a diff against the target and live state." var command = &cobra.Command{ @@ -1073,7 +1091,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co clientset := headless.NewClientOrDie(clientOpts, c) conn, appIf := clientset.NewApplicationClientOrDie() defer argoio.Close(conn) - appName, appNs := argo.ParseFromQualifiedName(args[0], "") + appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace) app, err := appIf.Get(ctx, &application.ApplicationQuery{ Name: &appName, Refresh: getRefreshType(refresh, hardRefresh), @@ -1116,6 +1134,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co defer argoio.Close(conn) cluster, err := clusterIf.Get(ctx, &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server}) errors.CheckError(err) + diffOption.local = local diffOption.localRepoRoot = localRepoRoot diffOption.cluster = cluster @@ -1136,6 +1155,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root") command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing") command.Flags().StringArrayVar(&localIncludes, "local-include", []string{"*.yaml", "*.yml", "*.json"}, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path.") + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only render the difference in namespace") return command } @@ -1276,6 +1296,8 @@ func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra. noPrompt bool propagationPolicy string selector string + wait bool + appNamespace string ) var command = &cobra.Command{ Use: "delete APPNAME", @@ -1299,7 +1321,8 @@ func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra. c.HelpFunc()(c, args) os.Exit(1) } - conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie() + acdClient := headless.NewClientOrDie(clientOpts, c) + conn, appIf := acdClient.NewApplicationClientOrDie() defer argoio.Close(conn) var isTerminal bool = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) var isConfirmAll bool = false @@ -1317,7 +1340,7 @@ func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra. } for _, appFullName := range appNames { - appName, appNs := argo.ParseFromQualifiedName(appFullName, "") + appName, appNs := argo.ParseFromQualifiedName(appFullName, appNamespace) appDeleteReq := application.ApplicationDeleteRequest{ Name: &appName, AppNamespace: &appNs, @@ -1346,6 +1369,9 @@ func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra. if lowercaseAnswer == "y" { _, err := appIf.Delete(ctx, &appDeleteReq) errors.CheckError(err) + if wait { + checkForDeleteEvent(ctx, acdClient, appFullName) + } fmt.Printf("application '%s' deleted\n", appFullName) } else { fmt.Println("The command to delete '" + appFullName + "' was cancelled.") @@ -1353,6 +1379,10 @@ func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra. } else { _, err := appIf.Delete(ctx, &appDeleteReq) errors.CheckError(err) + + if wait { + checkForDeleteEvent(ctx, acdClient, appFullName) + } } } }, @@ -1361,9 +1391,20 @@ func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra. command.Flags().StringVarP(&propagationPolicy, "propagation-policy", "p", "foreground", "Specify propagation policy for deletion of application's resources. One of: foreground|background") command.Flags().BoolVarP(&noPrompt, "yes", "y", false, "Turn off prompting to confirm cascaded deletion of application resources") command.Flags().StringVarP(&selector, "selector", "l", "", "Delete all apps with matching label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.") + command.Flags().BoolVar(&wait, "wait", false, "Wait until deletion of the application(s) completes") + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace where the application will be deleted from") return command } +func checkForDeleteEvent(ctx context.Context, acdClient argocdclient.Client, appFullName string) { + appEventCh := acdClient.WatchApplicationWithRetry(ctx, appFullName, "") + for appEvent := range appEventCh { + if appEvent.Type == k8swatch.Deleted { + return + } + } +} + // Print simple list of application names func printApplicationNames(apps []argoappv1.Application) { for _, app := range apps { @@ -1575,11 +1616,12 @@ func getWatchOpts(watch watchOpts) watchOpts { // NewApplicationWaitCommand returns a new instance of an `argocd app wait` command func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( - watch watchOpts - timeout uint - selector string - resources []string - output string + watch watchOpts + timeout uint + selector string + resources []string + output string + appNamespace string ) var command = &cobra.Command{ Use: "wait [APPNAME.. | -l selector]", @@ -1624,10 +1666,14 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co list, err := appIf.List(ctx, &application.ApplicationQuery{Selector: pointer.String(selector)}) errors.CheckError(err) for _, i := range list.Items { - appNames = append(appNames, i.Name) + appNames = append(appNames, i.QualifiedName()) } } for _, appName := range appNames { + // Construct QualifiedName + if appNamespace != "" && !strings.Contains(appName, "/") { + appName = appNamespace + "/" + appName + } _, _, err := waitOnApplicationStatus(ctx, acdClient, appName, timeout, watch, selectedResources, output) errors.CheckError(err) } @@ -1637,10 +1683,12 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co command.Flags().BoolVar(&watch.health, "health", false, "Wait for health") command.Flags().BoolVar(&watch.suspended, "suspended", false, "Wait for suspended") command.Flags().BoolVar(&watch.degraded, "degraded", false, "Wait for degraded") + command.Flags().BoolVar(&watch.delete, "delete", false, "Wait for delete") command.Flags().StringVarP(&selector, "selector", "l", "", "Wait for apps by label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints.") command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%[1]sKIND%[1]sNAME or %[2]sGROUP%[1]sKIND%[1]sNAME. Fields may be blank and '*' can be used. This option may be specified repeatedly", resourceFieldDelimiter, resourceExcludeIndicator)) command.Flags().BoolVar(&watch.operation, "operation", false, "Wait for pending operations") command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds") + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only wait for an application in namespace") command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed") return command } @@ -1698,6 +1746,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co diffChangesConfirm bool projects []string output string + appNamespace string ) var command = &cobra.Command{ Use: "sync [APPNAME... | -l selector | --project project-name]", @@ -1742,7 +1791,10 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co appNames := args if selector != "" || len(projects) > 0 { - list, err := appIf.List(ctx, &application.ApplicationQuery{Selector: pointer.String(selector), Projects: projects}) + list, err := appIf.List(ctx, &application.ApplicationQuery{ + Selector: pointer.String(selector), + AppNamespace: &appNamespace, + Projects: projects}) errors.CheckError(err) // unlike list, we'd want to fail if nothing was found @@ -1763,6 +1815,10 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co } for _, appQualifiedName := range appNames { + // Construct QualifiedName + if appNamespace != "" && !strings.Contains(appQualifiedName, "/") { + appQualifiedName = appNamespace + "/" + appQualifiedName + } appName, appNs := argo.ParseFromQualifiedName(appQualifiedName, "") if len(selectedLabels) > 0 { @@ -1980,6 +2036,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co command.Flags().BoolVar(&diffChanges, "preview-changes", false, "Preview difference against the target and live state before syncing app and wait for user confirmation") command.Flags().StringArrayVar(&projects, "project", []string{}, "Sync apps that belong to the specified projects. This option may be specified repeatedly.") command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed") + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only sync an application in namespace") return command } @@ -1995,7 +2052,7 @@ func getAppNamesBySelector(ctx context.Context, appIf application.ApplicationSer return []string{}, fmt.Errorf("no apps match selector %v", selector) } for _, i := range list.Items { - appNames = append(appNames, i.Name) + appNames = append(appNames, i.QualifiedName()) } } return appNames, nil @@ -2131,6 +2188,9 @@ func groupResourceStates(app *argoappv1.Application, selectedResources []*argoap // check if resource health, sync and operation statuses matches watch options func checkResourceStatus(watch watchOpts, healthStatus string, syncStatus string, operationStatus *argoappv1.Operation) bool { + if watch.delete { + return false + } healthCheckPassed := true if watch.suspended && watch.health && watch.degraded { @@ -2283,6 +2343,12 @@ func waitOnApplicationStatus(ctx context.Context, acdClient argocdclient.Client, finalOperationState = app.Status.OperationState operationInProgress := false + + if watch.delete && appEvent.Type == k8swatch.Deleted { + fmt.Printf("Application '%s' deleted\n", app.QualifiedName()) + return nil, nil, nil + } + // consider the operation is in progress if app.Operation != nil { // if it just got requested @@ -2407,7 +2473,8 @@ func printApplicationHistoryTable(revHistory []argoappv1.RevisionHistory) { // NewApplicationHistoryCommand returns a new instance of an `argocd app history` command func NewApplicationHistoryCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( - output string + output string + appNamespace string ) var command = &cobra.Command{ Use: "history APPNAME", @@ -2421,7 +2488,7 @@ func NewApplicationHistoryCommand(clientOpts *argocdclient.ClientOptions) *cobra } conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie() defer argoio.Close(conn) - appName, appNs := argo.ParseFromQualifiedName(args[0], "") + appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace) app, err := appIf.Get(ctx, &application.ApplicationQuery{ Name: &appName, AppNamespace: &appNs, @@ -2435,6 +2502,7 @@ func NewApplicationHistoryCommand(clientOpts *argocdclient.ClientOptions) *cobra } }, } + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only show application deployment history in namespace") command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|id") return command } @@ -2459,9 +2527,10 @@ func findRevisionHistory(application *argoappv1.Application, historyId int64) (* // NewApplicationRollbackCommand returns a new instance of an `argocd app rollback` command func NewApplicationRollbackCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { var ( - prune bool - timeout uint - output string + prune bool + timeout uint + output string + appNamespace string ) var command = &cobra.Command{ Use: "rollback APPNAME [ID]", @@ -2472,7 +2541,7 @@ func NewApplicationRollbackCommand(clientOpts *argocdclient.ClientOptions) *cobr c.HelpFunc()(c, args) os.Exit(1) } - appName, appNs := argo.ParseFromQualifiedName(args[0], "") + appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace) var err error depID := -1 if len(args) > 1 { @@ -2508,6 +2577,7 @@ func NewApplicationRollbackCommand(clientOpts *argocdclient.ClientOptions) *cobr command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources") command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds") command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|tree|tree=detailed") + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Rollback application in namespace") return command } @@ -2520,7 +2590,11 @@ func printOperationResult(opState *argoappv1.OperationState) { } if opState.SyncResult != nil { fmt.Printf(printOpFmtStr, "Operation:", "Sync") - fmt.Printf(printOpFmtStr, "Sync Revision:", opState.SyncResult.Revision) + if opState.SyncResult.Sources != nil && opState.SyncResult.Revisions != nil { + fmt.Printf(printOpFmtStr, "Sync Revision:", strings.Join(opState.SyncResult.Revisions, ", ")) + } else { + fmt.Printf(printOpFmtStr, "Sync Revision:", opState.SyncResult.Revision) + } } fmt.Printf(printOpFmtStr, "Phase:", opState.Phase) fmt.Printf(printOpFmtStr, "Start:", opState.StartedAt) @@ -2653,6 +2727,7 @@ func NewApplicationTerminateOpCommand(clientOpts *argocdclient.ClientOptions) *c } func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { + var appNamespace string var command = &cobra.Command{ Use: "edit APPNAME", Short: "Edit application", @@ -2663,7 +2738,7 @@ func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co c.HelpFunc()(c, args) os.Exit(1) } - appName, appNs := argo.ParseFromQualifiedName(args[0], "") + appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace) conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie() defer argoio.Close(conn) app, err := appIf.Get(ctx, &application.ApplicationQuery{ @@ -2703,12 +2778,16 @@ func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co }) }, } + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only edit application in namespace") return command } func NewApplicationPatchCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { - var patch string - var patchType string + var ( + patch string + patchType string + appNamespace string + ) command := cobra.Command{ Use: "patch APPNAME", @@ -2725,7 +2804,7 @@ func NewApplicationPatchCommand(clientOpts *argocdclient.ClientOptions) *cobra.C c.HelpFunc()(c, args) os.Exit(1) } - appName, appNs := argo.ParseFromQualifiedName(args[0], "") + appName, appNs := argo.ParseFromQualifiedName(args[0], appNamespace) conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie() defer argoio.Close(conn) @@ -2743,8 +2822,129 @@ func NewApplicationPatchCommand(clientOpts *argocdclient.ClientOptions) *cobra.C fmt.Println(string(yamlBytes)) }, } - + command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only patch application in namespace") command.Flags().StringVar(&patch, "patch", "", "Patch body") command.Flags().StringVar(&patchType, "type", "json", "The type of patch being provided; one of [json merge]") return &command } + +// NewApplicationAddSourceCommand returns a new instance of an `argocd app add-source` command +func NewApplicationAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { + var ( + appOpts cmdutil.AppOptions + ) + var command = &cobra.Command{ + Use: "add-source APPNAME", + Short: "Adds a source to the list of sources in the application", + Example: ` # Append a source to the list of sources in the application + argocd app add-source guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook`, + Run: func(c *cobra.Command, args []string) { + ctx := c.Context() + if len(args) != 1 { + c.HelpFunc()(c, args) + os.Exit(1) + } + + argocdClient := headless.NewClientOrDie(clientOpts, c) + conn, appIf := argocdClient.NewApplicationClientOrDie() + defer argoio.Close(conn) + + appName, appNs := argo.ParseFromQualifiedName(args[0], "") + + app, err := appIf.Get(ctx, &application.ApplicationQuery{ + Name: &appName, + Refresh: getRefreshType(false, false), + AppNamespace: &appNs, + }) + + errors.CheckError(err) + + if c.Flags() == nil { + errors.CheckError(fmt.Errorf("ApplicationSource needs atleast repoUrl, path or chart or ref field. No source to add.")) + } + + if len(app.Spec.Sources) > 0 { + appSource, _ := cmdutil.ConstructSource(&argoappv1.ApplicationSource{}, appOpts, c.Flags()) + + app.Spec.Sources = append(app.Spec.Sources, *appSource) + + _, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{ + Name: &app.Name, + Spec: &app.Spec, + Validate: &appOpts.Validate, + AppNamespace: &appNs, + }) + errors.CheckError(err) + + fmt.Printf("Application '%s' updated successfully\n", app.ObjectMeta.Name) + } else { + errors.CheckError(fmt.Errorf("Cannot add source: application %s does not have spec.sources defined", appName)) + } + }, + } + cmdutil.AddAppFlags(command, &appOpts) + return command +} + +// NewApplicationRemoveSourceCommand returns a new instance of an `argocd app remove-source` command +func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { + var ( + source_index int + ) + command := &cobra.Command{ + Use: "remove-source APPNAME", + Short: "Remove a source from multiple sources application. Index starts with 0.", + Example: ` # Remove the source at index 1 from application's sources + argocd app remove-source myapplication --source-index 1`, + Run: func(c *cobra.Command, args []string) { + ctx := c.Context() + + if len(args) != 1 { + c.HelpFunc()(c, args) + os.Exit(1) + } + + if source_index < 0 { + errors.CheckError(fmt.Errorf("Index value of source cannot be less than 0")) + } + + argocdClient := headless.NewClientOrDie(clientOpts, c) + conn, appIf := argocdClient.NewApplicationClientOrDie() + defer argoio.Close(conn) + + appName, appNs := argo.ParseFromQualifiedName(args[0], "") + + app, err := appIf.Get(ctx, &application.ApplicationQuery{ + Name: &appName, + Refresh: getRefreshType(false, false), + AppNamespace: &appNs, + }) + errors.CheckError(err) + + if !app.Spec.HasMultipleSources() { + errors.CheckError(fmt.Errorf("Application does not have multiple sources configured")) + } + + if len(app.Spec.GetSources()) == 1 { + errors.CheckError(fmt.Errorf("Cannot remove the only source remaining in the app")) + } + + if len(app.Spec.GetSources()) < source_index { + errors.CheckError(fmt.Errorf("Application does not have source at %d\n", source_index)) + } + + app.Spec.Sources = append(app.Spec.Sources[:source_index], app.Spec.Sources[source_index+1:]...) + + _, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{ + Name: &app.Name, + Spec: &app.Spec, + AppNamespace: &appNs, + }) + errors.CheckError(err) + + fmt.Printf("Application '%s' updated successfully\n", app.ObjectMeta.Name) + }, + } + command.Flags().IntVar(&source_index, "source-index", -1, "Index of the source from the list of sources of the app. Index starts from 0.") + return command +} diff --git a/cmd/argocd/commands/app_test.go b/cmd/argocd/commands/app_test.go index 68983560999c86..5217604d269870 100644 --- a/cmd/argocd/commands/app_test.go +++ b/cmd/argocd/commands/app_test.go @@ -1,23 +1,43 @@ package commands import ( + "context" "fmt" + "io" + "net/http" "os" "testing" "time" argocdclient "github.com/argoproj/argo-cd/v2/pkg/apiclient" + accountpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/account" + applicationpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/application" + applicationsetpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/applicationset" + certificatepkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/certificate" + clusterpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster" + gpgkeypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/gpgkey" + notificationpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/notification" + projectpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/project" + repocredspkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repocreds" + repositorypkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/repository" + sessionpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/session" + settingspkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/settings" + versionpkg "github.com/argoproj/argo-cd/v2/pkg/apiclient/version" "github.com/argoproj/argo-cd/v2/pkg/apis/application" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/argoproj/gitops-engine/pkg/health" "github.com/argoproj/gitops-engine/pkg/utils/kube" + "github.com/coreos/go-oidc/v3/oidc" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" + "golang.org/x/oauth2" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" + k8swatch "k8s.io/apimachinery/pkg/watch" ) func Test_getInfos(t *testing.T) { @@ -639,11 +659,110 @@ Project: default Server: local Namespace: argocd URL: url -Repo: test -Target: master -Path: /test -Helm Values: path1,path2 -Name Prefix: prefix +Source: +- Repo: test + Target: master + Path: /test + Helm Values: path1,path2 + Name Prefix: prefix +SyncWindow: Sync Denied +Assigned Windows: allow:0 0 * * *:24h,deny:0 0 * * *:24h,allow:0 0 * * *:24h +Sync Policy: Automated (Prune) +Sync Status: OutOfSync from master +Health Status: Progressing (health-message) +` + assert.Equalf(t, expectation, output, "Incorrect print app summary output %q, should be %q", output, expectation) +} + +func TestPrintAppSummaryTable_MultipleSources(t *testing.T) { + output, _ := captureOutput(func() error { + app := &v1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "argocd", + }, + Spec: v1alpha1.ApplicationSpec{ + SyncPolicy: &v1alpha1.SyncPolicy{ + Automated: &v1alpha1.SyncPolicyAutomated{ + Prune: true, + }, + }, + Project: "default", + Destination: v1alpha1.ApplicationDestination{Server: "local", Namespace: "argocd"}, + Sources: v1alpha1.ApplicationSources{ + { + RepoURL: "test", + TargetRevision: "master", + Path: "/test", + Helm: &v1alpha1.ApplicationSourceHelm{ + ValueFiles: []string{"path1", "path2"}, + }, + Kustomize: &v1alpha1.ApplicationSourceKustomize{NamePrefix: "prefix"}, + }, { + RepoURL: "test2", + TargetRevision: "master2", + Path: "/test2", + }, + }, + }, + Status: v1alpha1.ApplicationStatus{ + Sync: v1alpha1.SyncStatus{ + Status: v1alpha1.SyncStatusCodeOutOfSync, + }, + Health: v1alpha1.HealthStatus{ + Status: health.HealthStatusProgressing, + Message: "health-message", + }, + }, + } + + windows := &v1alpha1.SyncWindows{ + { + Kind: "allow", + Schedule: "0 0 * * *", + Duration: "24h", + Applications: []string{ + "*-prod", + }, + ManualSync: true, + }, + { + Kind: "deny", + Schedule: "0 0 * * *", + Duration: "24h", + Namespaces: []string{ + "default", + }, + }, + { + Kind: "allow", + Schedule: "0 0 * * *", + Duration: "24h", + Clusters: []string{ + "in-cluster", + "cluster1", + }, + }, + } + + printAppSummaryTable(app, "url", windows) + return nil + }) + + expectation := `Name: argocd/test +Project: default +Server: local +Namespace: argocd +URL: url +Sources: +- Repo: test + Target: master + Path: /test + Helm Values: path1,path2 + Name Prefix: prefix +- Repo: test2 + Target: master2 + Path: /test2 SyncWindow: Sync Denied Assigned Windows: allow:0 0 * * *:24h,deny:0 0 * * *:24h,allow:0 0 * * *:24h Sync Policy: Automated (Prune) @@ -806,6 +925,14 @@ func TestTargetObjects_invalid(t *testing.T) { assert.Error(t, err) } +func TestCheckForDeleteEvent(t *testing.T) { + + ctx := context.Background() + fakeClient := new(fakeAcdClient) + + checkForDeleteEvent(ctx, fakeClient, "testApp") +} + func TestPrintApplicationNames(t *testing.T) { output, _ := captureOutput(func() error { app := &v1alpha1.Application{ @@ -1599,3 +1726,104 @@ func testApp(name, project string, labels map[string]string, annotations map[str }, } } + +type fakeAcdClient struct{} + +func (c *fakeAcdClient) ClientOptions() argocdclient.ClientOptions { + return argocdclient.ClientOptions{} +} +func (c *fakeAcdClient) HTTPClient() (*http.Client, error) { return nil, nil } +func (c *fakeAcdClient) OIDCConfig(context.Context, *settingspkg.Settings) (*oauth2.Config, *oidc.Provider, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewRepoClient() (io.Closer, repositorypkg.RepositoryServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewRepoClientOrDie() (io.Closer, repositorypkg.RepositoryServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewRepoCredsClient() (io.Closer, repocredspkg.RepoCredsServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewRepoCredsClientOrDie() (io.Closer, repocredspkg.RepoCredsServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewCertClient() (io.Closer, certificatepkg.CertificateServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewCertClientOrDie() (io.Closer, certificatepkg.CertificateServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewClusterClient() (io.Closer, clusterpkg.ClusterServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewClusterClientOrDie() (io.Closer, clusterpkg.ClusterServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewGPGKeyClient() (io.Closer, gpgkeypkg.GPGKeyServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewGPGKeyClientOrDie() (io.Closer, gpgkeypkg.GPGKeyServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewApplicationClient() (io.Closer, applicationpkg.ApplicationServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewApplicationSetClient() (io.Closer, applicationsetpkg.ApplicationSetServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewApplicationClientOrDie() (io.Closer, applicationpkg.ApplicationServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewApplicationSetClientOrDie() (io.Closer, applicationsetpkg.ApplicationSetServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewNotificationClient() (io.Closer, notificationpkg.NotificationServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewNotificationClientOrDie() (io.Closer, notificationpkg.NotificationServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewSessionClient() (io.Closer, sessionpkg.SessionServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewSessionClientOrDie() (io.Closer, sessionpkg.SessionServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewSettingsClient() (io.Closer, settingspkg.SettingsServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewSettingsClientOrDie() (io.Closer, settingspkg.SettingsServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewVersionClient() (io.Closer, versionpkg.VersionServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewVersionClientOrDie() (io.Closer, versionpkg.VersionServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewProjectClient() (io.Closer, projectpkg.ProjectServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewProjectClientOrDie() (io.Closer, projectpkg.ProjectServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) NewAccountClient() (io.Closer, accountpkg.AccountServiceClient, error) { + return nil, nil, nil +} +func (c *fakeAcdClient) NewAccountClientOrDie() (io.Closer, accountpkg.AccountServiceClient) { + return nil, nil +} +func (c *fakeAcdClient) WatchApplicationWithRetry(ctx context.Context, appName string, revision string) chan *v1alpha1.ApplicationWatchEvent { + appEventsCh := make(chan *v1alpha1.ApplicationWatchEvent) + + go func() { + modifiedEvent := new(v1alpha1.ApplicationWatchEvent) + modifiedEvent.Type = k8swatch.Modified + appEventsCh <- modifiedEvent + deletedEvent := new(v1alpha1.ApplicationWatchEvent) + deletedEvent.Type = k8swatch.Deleted + appEventsCh <- deletedEvent + }() + return appEventsCh +} diff --git a/cmd/argocd/commands/applicationset.go b/cmd/argocd/commands/applicationset.go index b38f8837598fba..f5ed6a15b6208a 100644 --- a/cmd/argocd/commands/applicationset.go +++ b/cmd/argocd/commands/applicationset.go @@ -350,9 +350,11 @@ func printAppSetSummaryTable(appSet *arogappsetv1.ApplicationSet) { fmt.Printf(printOpFmtStr, "Project:", appSet.Spec.Template.Spec.GetProject()) fmt.Printf(printOpFmtStr, "Server:", getServerForAppSet(appSet)) fmt.Printf(printOpFmtStr, "Namespace:", appSet.Spec.Template.Spec.Destination.Namespace) - fmt.Printf(printOpFmtStr, "Repo:", source.RepoURL) - fmt.Printf(printOpFmtStr, "Target:", source.TargetRevision) - fmt.Printf(printOpFmtStr, "Path:", source.Path) + if !appSet.Spec.Template.Spec.HasMultipleSources() { + fmt.Println("Source:") + } else { + fmt.Println("Sources:") + } printAppSourceDetails(&source) var ( diff --git a/cmd/argocd/commands/applicationset_test.go b/cmd/argocd/commands/applicationset_test.go index 18e5f85feebbc7..7740c95a4e63b0 100644 --- a/cmd/argocd/commands/applicationset_test.go +++ b/cmd/argocd/commands/applicationset_test.go @@ -180,9 +180,9 @@ func TestPrintAppSetSummaryTable(t *testing.T) { Project: default Server: Namespace: -Repo: -Target: -Path: +Source: +- Repo: + Target: SyncPolicy: `, }, @@ -193,9 +193,9 @@ SyncPolicy: Project: default Server: Namespace: -Repo: -Target: -Path: +Source: +- Repo: + Target: SyncPolicy: Automated `, }, @@ -206,9 +206,9 @@ SyncPolicy: Automated Project: default Server: Namespace: -Repo: -Target: -Path: +Source: +- Repo: + Target: SyncPolicy: Automated `, }, diff --git a/cmd/argocd/commands/cluster.go b/cmd/argocd/commands/cluster.go index 3df4be6632d858..f203b82ae9ac0d 100644 --- a/cmd/argocd/commands/cluster.go +++ b/cmd/argocd/commands/cluster.go @@ -111,6 +111,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie awsAuthConf = &argoappv1.AWSAuthConfig{ ClusterName: clusterOpts.AwsClusterName, RoleARN: clusterOpts.AwsRoleArn, + Profile: clusterOpts.AwsProfile, } } else if clusterOpts.ExecProviderCommand != "" { execProviderConf = &argoappv1.ExecProviderConfig{ diff --git a/cmd/argocd/commands/headless/headless.go b/cmd/argocd/commands/headless/headless.go index 070d9c9c83bcb7..d48019a2216b96 100644 --- a/cmd/argocd/commands/headless/headless.go +++ b/cmd/argocd/commands/headless/headless.go @@ -78,6 +78,12 @@ func (c *forwardCacheClient) Set(item *cache.Item) error { }) } +func (c *forwardCacheClient) Rename(oldKey string, newKey string, expiration time.Duration) error { + return c.doLazy(func(client cache.CacheClient) error { + return client.Rename(oldKey, newKey, expiration) + }) +} + func (c *forwardCacheClient) Get(key string, obj interface{}) error { return c.doLazy(func(client cache.CacheClient) error { return client.Get(key, obj) @@ -153,9 +159,11 @@ func testAPI(ctx context.Context, clientOpts *apiclient.ClientOptions) error { // // If the clientOpts enables core mode, but the local config does not have core mode enabled, this function will // not start the local server. -func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions, ctxStr string, port *int, address *string, compression cache.RedisCompressionType) error { - flags := pflag.NewFlagSet("tmp", pflag.ContinueOnError) - clientConfig := cli.AddKubectlFlagsToSet(flags) +func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOptions, ctxStr string, port *int, address *string, compression cache.RedisCompressionType, clientConfig clientcmd.ClientConfig) error { + if clientConfig == nil { + flags := pflag.NewFlagSet("tmp", pflag.ContinueOnError) + clientConfig = cli.AddKubectlFlagsToSet(flags) + } startInProcessAPI := clientOpts.Core if !startInProcessAPI { // Core mode is enabled on client options. Check the local config to see if we should start the API server. @@ -244,6 +252,7 @@ func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOpti if !cache2.WaitForCacheSync(ctx.Done(), srv.Initialized) { log.Fatal("Timed out waiting for project cache to sync") } + tries := 5 for i := 0; i < tries; i++ { err = testAPI(ctx, clientOpts) @@ -265,7 +274,7 @@ func NewClientOrDie(opts *apiclient.ClientOptions, c *cobra.Command) apiclient.C ctxStr := initialize.RetrieveContextIfChanged(c.Flag("context")) // If we're in core mode, start the API server on the fly and configure the client `opts` to use it. // If we're not in core mode, this function call will do nothing. - err := MaybeStartLocalServer(ctx, opts, ctxStr, nil, nil, cache.RedisCompressionNone) + err := MaybeStartLocalServer(ctx, opts, ctxStr, nil, nil, cache.RedisCompressionNone, nil) if err != nil { log.Fatal(err) } diff --git a/cmd/argocd/commands/login.go b/cmd/argocd/commands/login.go index 3e2ad4e7d1b73d..abb2b004291c22 100644 --- a/cmd/argocd/commands/login.go +++ b/cmd/argocd/commands/login.go @@ -106,6 +106,7 @@ argocd login cd.argoproj.io --core`, PortForwardNamespace: globalClientOpts.PortForwardNamespace, Headers: globalClientOpts.Headers, KubeOverrides: globalClientOpts.KubeOverrides, + ServerName: globalClientOpts.ServerName, } if ctxName == "" { diff --git a/cmd/argocd/commands/project.go b/cmd/argocd/commands/project.go index 32fb9e779e8ed7..be7517b843375a 100644 --- a/cmd/argocd/commands/project.go +++ b/cmd/argocd/commands/project.go @@ -78,6 +78,8 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { command.AddCommand(NewProjectWindowsCommand(clientOpts)) command.AddCommand(NewProjectAddOrphanedIgnoreCommand(clientOpts)) command.AddCommand(NewProjectRemoveOrphanedIgnoreCommand(clientOpts)) + command.AddCommand(NewProjectAddSourceNamespace(clientOpts)) + command.AddCommand(NewProjectRemoveSourceNamespace(clientOpts)) return command } @@ -509,6 +511,88 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C return command } +// NewProjectAddSourceNamespace returns a new instance of an `argocd proj add-source-namespace` command +func NewProjectAddSourceNamespace(clientOpts *argocdclient.ClientOptions) *cobra.Command { + var command = &cobra.Command{ + Use: "add-source-namespace PROJECT NAMESPACE", + Short: "Add source namespace to the AppProject", + Example: templates.Examples(` + # Add Kubernetes namespace as source namespace to the AppProject where application resources are allowed to be created in. + argocd proj add-source-namespace PROJECT NAMESPACE + `), + Run: func(c *cobra.Command, args []string) { + ctx := c.Context() + + if len(args) != 2 { + c.HelpFunc()(c, args) + os.Exit(1) + } + projName := args[0] + srcNamespace := args[1] + conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie() + defer argoio.Close(conn) + + proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName}) + errors.CheckError(err) + + for _, item := range proj.Spec.SourceNamespaces { + if item == "*" || item == srcNamespace { + fmt.Printf("Source namespace '*' already allowed in project\n") + return + } + } + proj.Spec.SourceNamespaces = append(proj.Spec.SourceNamespaces, srcNamespace) + _, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj}) + errors.CheckError(err) + }, + } + return command +} + +// NewProjectRemoveSourceNamespace returns a new instance of an `argocd proj remove-source-namespace` command +func NewProjectRemoveSourceNamespace(clientOpts *argocdclient.ClientOptions) *cobra.Command { + var command = &cobra.Command{ + Use: "remove-source-namespace PROJECT NAMESPACE", + Short: "Removes the source namespace from the AppProject", + Example: templates.Examples(` + # Remove source NAMESPACE in PROJECT + argocd proj remove-source-namespace PROJECT NAMESPACE + `), + Run: func(c *cobra.Command, args []string) { + ctx := c.Context() + + if len(args) != 2 { + c.HelpFunc()(c, args) + os.Exit(1) + } + projName := args[0] + srcNamespace := args[1] + conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie() + defer argoio.Close(conn) + + proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName}) + errors.CheckError(err) + + index := -1 + for i, item := range proj.Spec.SourceNamespaces { + if item == srcNamespace && item != "*" { + index = i + break + } + } + if index == -1 { + fmt.Printf("Source namespace '%s' does not exist in project or cannot be removed\n", srcNamespace) + } else { + proj.Spec.SourceNamespaces = append(proj.Spec.SourceNamespaces[:index], proj.Spec.SourceNamespaces[index+1:]...) + _, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj}) + errors.CheckError(err) + } + }, + } + + return command +} + func modifyResourcesList(list *[]metav1.GroupKind, add bool, listDesc string, group string, kind string) bool { if add { for _, item := range *list { diff --git a/cmd/argocd/commands/projectwindows.go b/cmd/argocd/commands/projectwindows.go index a46f9ece64c360..93843130ebb135 100644 --- a/cmd/argocd/commands/projectwindows.go +++ b/cmd/argocd/commands/projectwindows.go @@ -22,6 +22,18 @@ func NewProjectWindowsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com roleCommand := &cobra.Command{ Use: "windows", Short: "Manage a project's sync windows", + Example: ` +#Add a sync window to a project +argocd proj windows add my-project \ +--schedule "0 0 * * 1-5" \ +--duration 3600 \ +--prune + +#Delete a sync window from a project +argocd proj windows delete + +#List project sync windows +argocd proj windows list `, Run: func(c *cobra.Command, args []string) { c.HelpFunc()(c, args) os.Exit(1) @@ -42,6 +54,12 @@ func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOp Use: "disable-manual-sync PROJECT ID", Short: "Disable manual sync for a sync window", Long: "Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"", + Example: ` +#Disable manual sync for a sync window for the Project +argocd proj windows disable-manual-sync PROJECT ID + +#Disbaling manual sync for a windows set on the default project with Id 0 +argocd proj windows disable-manual-sync default 0`, Run: func(c *cobra.Command, args []string) { ctx := c.Context() @@ -79,6 +97,15 @@ func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOpt Use: "enable-manual-sync PROJECT ID", Short: "Enable manual sync for a sync window", Long: "Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"", + Example: ` +#Enabling manual sync for a general case +argocd proj windows enable-manual-sync PROJECT ID + +#Enabling manual sync for a windows set on the default project with Id 2 +argocd proj windows enable-manual-sync default 2 + +#Enabling manual sync with a custom message +argocd proj windows enable-manual-sync my-app-project --message "Manual sync initiated by admin`, Run: func(c *cobra.Command, args []string) { ctx := c.Context() @@ -125,14 +152,15 @@ func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) * var command = &cobra.Command{ Use: "add PROJECT", Short: "Add a sync window to a project", - Example: `# Add a 1 hour allow sync window + Example: ` +#Add a 1 hour allow sync window argocd proj windows add PROJECT \ --kind allow \ --schedule "0 22 * * *" \ --duration 1h \ --applications "*" -# Add a deny sync window with the ability to manually sync. +#Add a deny sync window with the ability to manually sync. argocd proj windows add PROJECT \ --kind deny \ --schedule "30 10 * * *" \ @@ -180,6 +208,12 @@ func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cob var command = &cobra.Command{ Use: "delete PROJECT ID", Short: "Delete a sync window from a project. Requires ID which can be found by running \"argocd proj windows list PROJECT\"", + Example: ` +#Delete a sync window from a project (default) with ID 0 +argocd proj windows delete default 0 + +#Delete a sync window from a project (new-project) with ID 1 +argocd proj windows delete new-project 1`, Run: func(c *cobra.Command, args []string) { ctx := c.Context() @@ -274,12 +308,15 @@ func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra var command = &cobra.Command{ Use: "list PROJECT", Short: "List project sync windows", - Example: `# List project windows + Example: ` +#List project windows argocd proj windows list PROJECT - -# List project windows in yaml format + +#List project windows in yaml format argocd proj windows list PROJECT -o yaml -`, + +#List project windows info for a project name (test-project) +argocd proj windows list test-project`, Run: func(c *cobra.Command, args []string) { ctx := c.Context() diff --git a/cmd/argocd/commands/repo.go b/cmd/argocd/commands/repo.go index 2bf9714a06f11e..1a5b4388fbeba9 100644 --- a/cmd/argocd/commands/repo.go +++ b/cmd/argocd/commands/repo.go @@ -64,6 +64,12 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { # Add a Git repository via SSH on a non-default port - need to use ssh:// style URLs here argocd repo add ssh://git@git.example.com:2222/repos/repo --ssh-private-key-path ~/id_rsa + # Add a Git repository via SSH using socks5 proxy with no proxy credentials + argocd repo add ssh://git@github.com/argoproj/argocd-example-apps --ssh-private-key-path ~/id_rsa --proxy socks5://your.proxy.server.ip:1080 + + # Add a Git repository via SSH using socks5 proxy with proxy credentials + argocd repo add ssh://git@github.com/argoproj/argocd-example-apps --ssh-private-key-path ~/id_rsa --proxy socks5://username:password@your.proxy.server.ip:1080 + # Add a private Git repository via HTTPS using username/password and TLS client certificates: argocd repo add https://git.example.com/repos/repo --username git --password secret --tls-client-cert-path ~/mycert.crt --tls-client-cert-key-path ~/mycert.key diff --git a/cmd/util/app.go b/cmd/util/app.go index e08ee80305c480..307b4badd94eb9 100644 --- a/cmd/util/app.go +++ b/cmd/util/app.go @@ -68,6 +68,7 @@ type AppOptions struct { kustomizeVersion string kustomizeCommonLabels []string kustomizeCommonAnnotations []string + kustomizeLabelWithoutSelector bool kustomizeForceCommonLabels bool kustomizeForceCommonAnnotations bool kustomizeNamespace string @@ -79,6 +80,7 @@ type AppOptions struct { retryBackoffDuration time.Duration retryBackoffMaxDuration time.Duration retryBackoffFactor int64 + ref string } func AddAppFlags(command *cobra.Command, opts *AppOptions) { @@ -124,6 +126,7 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) { command.Flags().BoolVar(&opts.Validate, "validate", true, "Validation of repo and cluster") command.Flags().StringArrayVar(&opts.kustomizeCommonLabels, "kustomize-common-label", []string{}, "Set common labels in Kustomize") command.Flags().StringArrayVar(&opts.kustomizeCommonAnnotations, "kustomize-common-annotation", []string{}, "Set common labels in Kustomize") + command.Flags().BoolVar(&opts.kustomizeLabelWithoutSelector, "kustomize-label-without-selector", false, "Do not apply common label to selectors or templates") command.Flags().BoolVar(&opts.kustomizeForceCommonLabels, "kustomize-force-common-label", false, "Force common labels in Kustomize") command.Flags().BoolVar(&opts.kustomizeForceCommonAnnotations, "kustomize-force-common-annotation", false, "Force common annotations in Kustomize") command.Flags().StringVar(&opts.kustomizeNamespace, "kustomize-namespace", "", "Kustomize namespace") @@ -133,6 +136,7 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) { command.Flags().DurationVar(&opts.retryBackoffDuration, "sync-retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)") command.Flags().DurationVar(&opts.retryBackoffMaxDuration, "sync-retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)") command.Flags().Int64Var(&opts.retryBackoffFactor, "sync-retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed sync retry") + command.Flags().StringVar(&opts.ref, "ref", "", "Ref is reference to another source within sources field") } func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, appOpts *AppOptions) int { @@ -140,74 +144,18 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap if flags == nil { return visited } + source := spec.GetSourcePtr() + if source == nil { + source = &argoappv1.ApplicationSource{} + } + source, visited = ConstructSource(source, *appOpts, flags) flags.Visit(func(f *pflag.Flag) { visited++ - source := spec.GetSourcePtr() - if source == nil { - source = &argoappv1.ApplicationSource{} - } + switch f.Name { - case "repo": - source.RepoURL = appOpts.repoURL - case "path": - source.Path = appOpts.appPath - case "helm-chart": - source.Chart = appOpts.chart - case "revision": - source.TargetRevision = appOpts.revision case "revision-history-limit": i := int64(appOpts.revisionHistoryLimit) spec.RevisionHistoryLimit = &i - case "values": - setHelmOpt(source, helmOpts{valueFiles: appOpts.valuesFiles}) - case "ignore-missing-value-files": - setHelmOpt(source, helmOpts{ignoreMissingValueFiles: appOpts.ignoreMissingValueFiles}) - case "values-literal-file": - var data []byte - - // read uri - parsedURL, err := url.ParseRequestURI(appOpts.values) - if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") { - data, err = os.ReadFile(appOpts.values) - } else { - data, err = config.ReadRemoteFile(appOpts.values) - } - errors.CheckError(err) - setHelmOpt(source, helmOpts{values: string(data)}) - case "release-name": - setHelmOpt(source, helmOpts{releaseName: appOpts.releaseName}) - case "helm-version": - setHelmOpt(source, helmOpts{version: appOpts.helmVersion}) - case "helm-pass-credentials": - setHelmOpt(source, helmOpts{passCredentials: appOpts.helmPassCredentials}) - case "helm-set": - setHelmOpt(source, helmOpts{helmSets: appOpts.helmSets}) - case "helm-set-string": - setHelmOpt(source, helmOpts{helmSetStrings: appOpts.helmSetStrings}) - case "helm-set-file": - setHelmOpt(source, helmOpts{helmSetFiles: appOpts.helmSetFiles}) - case "helm-skip-crds": - setHelmOpt(source, helmOpts{skipCrds: appOpts.helmSkipCrds}) - case "directory-recurse": - if source.Directory != nil { - source.Directory.Recurse = appOpts.directoryRecurse - } else { - source.Directory = &argoappv1.ApplicationSourceDirectory{Recurse: appOpts.directoryRecurse} - } - case "directory-exclude": - if source.Directory != nil { - source.Directory.Exclude = appOpts.directoryExclude - } else { - source.Directory = &argoappv1.ApplicationSourceDirectory{Exclude: appOpts.directoryExclude} - } - case "directory-include": - if source.Directory != nil { - source.Directory.Include = appOpts.directoryInclude - } else { - source.Directory = &argoappv1.ApplicationSourceDirectory{Include: appOpts.directoryInclude} - } - case "config-management-plugin": - source.Plugin = &argoappv1.ApplicationSourcePlugin{Name: appOpts.configManagementPlugin} case "dest-name": spec.Destination.Name = appOpts.destName case "dest-server": @@ -216,42 +164,6 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap spec.Destination.Namespace = appOpts.destNamespace case "project": spec.Project = appOpts.project - case "nameprefix": - setKustomizeOpt(source, kustomizeOpts{namePrefix: appOpts.namePrefix}) - case "namesuffix": - setKustomizeOpt(source, kustomizeOpts{nameSuffix: appOpts.nameSuffix}) - case "kustomize-image": - setKustomizeOpt(source, kustomizeOpts{images: appOpts.kustomizeImages}) - case "kustomize-replica": - setKustomizeOpt(source, kustomizeOpts{replicas: appOpts.kustomizeReplicas}) - case "kustomize-version": - setKustomizeOpt(source, kustomizeOpts{version: appOpts.kustomizeVersion}) - case "kustomize-namespace": - setKustomizeOpt(source, kustomizeOpts{namespace: appOpts.kustomizeNamespace}) - case "kustomize-common-label": - parsedLabels, err := label.Parse(appOpts.kustomizeCommonLabels) - errors.CheckError(err) - setKustomizeOpt(source, kustomizeOpts{commonLabels: parsedLabels}) - case "kustomize-common-annotation": - parsedAnnotations, err := label.Parse(appOpts.kustomizeCommonAnnotations) - errors.CheckError(err) - setKustomizeOpt(source, kustomizeOpts{commonAnnotations: parsedAnnotations}) - case "kustomize-force-common-label": - setKustomizeOpt(source, kustomizeOpts{forceCommonLabels: appOpts.kustomizeForceCommonLabels}) - case "kustomize-force-common-annotation": - setKustomizeOpt(source, kustomizeOpts{forceCommonAnnotations: appOpts.kustomizeForceCommonAnnotations}) - case "jsonnet-tla-str": - setJsonnetOpt(source, appOpts.jsonnetTlaStr, false) - case "jsonnet-tla-code": - setJsonnetOpt(source, appOpts.jsonnetTlaCode, true) - case "jsonnet-ext-var-str": - setJsonnetOptExtVar(source, appOpts.jsonnetExtVarStr, false) - case "jsonnet-ext-var-code": - setJsonnetOptExtVar(source, appOpts.jsonnetExtVarCode, true) - case "jsonnet-libs": - setJsonnetOptLibs(source, appOpts.jsonnetLibs) - case "plugin-env": - setPluginOptEnvs(source, appOpts.pluginEnvs) case "sync-policy": switch appOpts.syncPolicy { case "none": @@ -340,6 +252,7 @@ type kustomizeOpts struct { version string commonLabels map[string]string commonAnnotations map[string]string + labelWithoutSelector bool forceCommonLabels bool forceCommonAnnotations bool namespace string @@ -367,6 +280,9 @@ func setKustomizeOpt(src *argoappv1.ApplicationSource, opts kustomizeOpts) { if opts.commonAnnotations != nil { src.Kustomize.CommonAnnotations = opts.commonAnnotations } + if opts.labelWithoutSelector { + src.Kustomize.LabelWithoutSelector = opts.labelWithoutSelector + } if opts.forceCommonLabels { src.Kustomize.ForceCommonLabels = opts.forceCommonLabels } @@ -640,6 +556,7 @@ func constructAppsFromFileUrl(fileURL, appName string, labels, annotations, args if app.Name == "" { return nil, fmt.Errorf("app.Name is empty. --name argument can be used to provide app.Name") } + SetAppSpecOptions(flags, &app.Spec, &appOpts) SetParameterOverrides(app, appOpts.Parameters) mergeLabels(app, labels) @@ -654,9 +571,117 @@ func ConstructApps(fileURL, appName string, labels, annotations, args []string, } else if fileURL != "" { return constructAppsFromFileUrl(fileURL, appName, labels, annotations, args, appOpts, flags) } + return constructAppsBaseOnName(appName, labels, annotations, args, appOpts, flags) } +func ConstructSource(source *argoappv1.ApplicationSource, appOpts AppOptions, flags *pflag.FlagSet) (*argoappv1.ApplicationSource, int) { + visited := 0 + flags.Visit(func(f *pflag.Flag) { + visited++ + switch f.Name { + case "repo": + source.RepoURL = appOpts.repoURL + case "path": + source.Path = appOpts.appPath + case "helm-chart": + source.Chart = appOpts.chart + case "revision": + source.TargetRevision = appOpts.revision + case "values": + setHelmOpt(source, helmOpts{valueFiles: appOpts.valuesFiles}) + case "ignore-missing-value-files": + setHelmOpt(source, helmOpts{ignoreMissingValueFiles: appOpts.ignoreMissingValueFiles}) + case "values-literal-file": + var data []byte + // read uri + parsedURL, err := url.ParseRequestURI(appOpts.values) + if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") { + data, err = os.ReadFile(appOpts.values) + } else { + data, err = config.ReadRemoteFile(appOpts.values) + } + errors.CheckError(err) + setHelmOpt(source, helmOpts{values: string(data)}) + case "release-name": + setHelmOpt(source, helmOpts{releaseName: appOpts.releaseName}) + case "helm-version": + setHelmOpt(source, helmOpts{version: appOpts.helmVersion}) + case "helm-pass-credentials": + setHelmOpt(source, helmOpts{passCredentials: appOpts.helmPassCredentials}) + case "helm-set": + setHelmOpt(source, helmOpts{helmSets: appOpts.helmSets}) + case "helm-set-string": + setHelmOpt(source, helmOpts{helmSetStrings: appOpts.helmSetStrings}) + case "helm-set-file": + setHelmOpt(source, helmOpts{helmSetFiles: appOpts.helmSetFiles}) + case "helm-skip-crds": + setHelmOpt(source, helmOpts{skipCrds: appOpts.helmSkipCrds}) + case "directory-recurse": + if source.Directory != nil { + source.Directory.Recurse = appOpts.directoryRecurse + } else { + source.Directory = &argoappv1.ApplicationSourceDirectory{Recurse: appOpts.directoryRecurse} + } + case "directory-exclude": + if source.Directory != nil { + source.Directory.Exclude = appOpts.directoryExclude + } else { + source.Directory = &argoappv1.ApplicationSourceDirectory{Exclude: appOpts.directoryExclude} + } + case "directory-include": + if source.Directory != nil { + source.Directory.Include = appOpts.directoryInclude + } else { + source.Directory = &argoappv1.ApplicationSourceDirectory{Include: appOpts.directoryInclude} + } + case "config-management-plugin": + source.Plugin = &argoappv1.ApplicationSourcePlugin{Name: appOpts.configManagementPlugin} + case "nameprefix": + setKustomizeOpt(source, kustomizeOpts{namePrefix: appOpts.namePrefix}) + case "namesuffix": + setKustomizeOpt(source, kustomizeOpts{nameSuffix: appOpts.nameSuffix}) + case "kustomize-image": + setKustomizeOpt(source, kustomizeOpts{images: appOpts.kustomizeImages}) + case "kustomize-replica": + setKustomizeOpt(source, kustomizeOpts{replicas: appOpts.kustomizeReplicas}) + case "kustomize-version": + setKustomizeOpt(source, kustomizeOpts{version: appOpts.kustomizeVersion}) + case "kustomize-namespace": + setKustomizeOpt(source, kustomizeOpts{namespace: appOpts.kustomizeNamespace}) + case "kustomize-common-label": + parsedLabels, err := label.Parse(appOpts.kustomizeCommonLabels) + errors.CheckError(err) + setKustomizeOpt(source, kustomizeOpts{commonLabels: parsedLabels}) + case "kustomize-common-annotation": + parsedAnnotations, err := label.Parse(appOpts.kustomizeCommonAnnotations) + errors.CheckError(err) + setKustomizeOpt(source, kustomizeOpts{commonAnnotations: parsedAnnotations}) + case "kustomize-label-without-selector": + setKustomizeOpt(source, kustomizeOpts{labelWithoutSelector: appOpts.kustomizeLabelWithoutSelector}) + case "kustomize-force-common-label": + setKustomizeOpt(source, kustomizeOpts{forceCommonLabels: appOpts.kustomizeForceCommonLabels}) + case "kustomize-force-common-annotation": + setKustomizeOpt(source, kustomizeOpts{forceCommonAnnotations: appOpts.kustomizeForceCommonAnnotations}) + case "jsonnet-tla-str": + setJsonnetOpt(source, appOpts.jsonnetTlaStr, false) + case "jsonnet-tla-code": + setJsonnetOpt(source, appOpts.jsonnetTlaCode, true) + case "jsonnet-ext-var-str": + setJsonnetOptExtVar(source, appOpts.jsonnetExtVarStr, false) + case "jsonnet-ext-var-code": + setJsonnetOptExtVar(source, appOpts.jsonnetExtVarCode, true) + case "jsonnet-libs": + setJsonnetOptLibs(source, appOpts.jsonnetLibs) + case "plugin-env": + setPluginOptEnvs(source, appOpts.pluginEnvs) + case "ref": + source.Ref = appOpts.ref + } + }) + return source, visited +} + func mergeLabels(app *argoappv1.Application, labels []string) { mapLabels, err := label.Parse(labels) errors.CheckError(err) diff --git a/cmd/util/app_test.go b/cmd/util/app_test.go index 2f49a3cc4c8c48..b5fce9c1e663ed 100644 --- a/cmd/util/app_test.go +++ b/cmd/util/app_test.go @@ -123,6 +123,11 @@ func Test_setKustomizeOpt(t *testing.T) { setKustomizeOpt(&src, kustomizeOpts{commonAnnotations: map[string]string{"foo1": "bar1", "foo2": "bar2"}}) assert.Equal(t, &v1alpha1.ApplicationSourceKustomize{CommonAnnotations: map[string]string{"foo1": "bar1", "foo2": "bar2"}}, src.Kustomize) }) + t.Run("Label Without Selector", func(t *testing.T) { + src := v1alpha1.ApplicationSource{} + setKustomizeOpt(&src, kustomizeOpts{commonLabels: map[string]string{"foo1": "bar1", "foo2": "bar2"}, labelWithoutSelector: true}) + assert.Equal(t, &v1alpha1.ApplicationSourceKustomize{CommonLabels: map[string]string{"foo1": "bar1", "foo2": "bar2"}, LabelWithoutSelector: true}, src.Kustomize) + }) } func Test_setJsonnetOpt(t *testing.T) { diff --git a/cmd/util/cluster.go b/cmd/util/cluster.go index 95c071c882b12e..dffb52e775a971 100644 --- a/cmd/util/cluster.go +++ b/cmd/util/cluster.go @@ -144,6 +144,7 @@ type ClusterOptions struct { Upsert bool ServiceAccount string AwsRoleArn string + AwsProfile string AwsClusterName string SystemNamespace string Namespaces []string @@ -169,6 +170,7 @@ func AddClusterFlags(command *cobra.Command, opts *ClusterOptions) { command.Flags().BoolVar(&opts.InCluster, "in-cluster", false, "Indicates Argo CD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)") command.Flags().StringVar(&opts.AwsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws cli eks token command will be used to access cluster") command.Flags().StringVar(&opts.AwsRoleArn, "aws-role-arn", "", "Optional AWS role arn. If set then AWS IAM Authenticator assumes a role to perform cluster operations instead of the default AWS credential provider chain.") + command.Flags().StringVar(&opts.AwsProfile, "aws-profile", "", "Optional AWS profile. If set then AWS IAM Authenticator uses this profile to perform cluster operations instead of the default AWS credential provider chain.") command.Flags().StringArrayVar(&opts.Namespaces, "namespace", nil, "List of namespaces which are allowed to manage") command.Flags().BoolVar(&opts.ClusterResources, "cluster-resources", false, "Indicates if cluster level resources should be managed. The setting is used only if list of managed namespaces is not empty.") command.Flags().StringVar(&opts.Name, "name", "", "Overwrite the cluster name") diff --git a/common/common.go b/common/common.go index 5faedc2e344c46..1d04d0e47eb650 100644 --- a/common/common.go +++ b/common/common.go @@ -115,9 +115,9 @@ const ( LegacyShardingAlgorithm = "legacy" // RoundRobinShardingAlgorithm is a flag value that can be opted for Sharding Algorithm it uses an equal distribution accross all shards RoundRobinShardingAlgorithm = "round-robin" - DefaultShardingAlgorithm = LegacyShardingAlgorithm // AppControllerHeartbeatUpdateRetryCount is the retry count for updating the Shard Mapping to the Shard Mapping ConfigMap used by Application Controller AppControllerHeartbeatUpdateRetryCount = 3 + DefaultShardingAlgorithm = LegacyShardingAlgorithm ) // Dex related constants @@ -149,10 +149,14 @@ const ( LabelKeyAppInstance = "app.kubernetes.io/instance" // LabelKeyAppName is the label key to use to uniquely identify the name of the Kubernetes application LabelKeyAppName = "app.kubernetes.io/name" + // LabelKeyAutoLabelClusterInfo if set to true will automatically add extra labels from the cluster info (currently it only adds a k8s version label) + LabelKeyAutoLabelClusterInfo = "argocd.argoproj.io/auto-label-cluster-info" // LabelKeyLegacyApplicationName is the legacy label (v0.10 and below) and is superseded by 'app.kubernetes.io/instance' LabelKeyLegacyApplicationName = "applications.argoproj.io/app-name" // LabelKeySecretType contains the type of argocd secret (currently: 'cluster', 'repository', 'repo-config' or 'repo-creds') LabelKeySecretType = "argocd.argoproj.io/secret-type" + // LabelKeyClusterKubernetesVersion contains the kubernetes version of the cluster secret if it has been enabled + LabelKeyClusterKubernetesVersion = "argocd.argoproj.io/kubernetes-version" // LabelValueSecretTypeCluster indicates a secret type of cluster LabelValueSecretTypeCluster = "cluster" // LabelValueSecretTypeRepository indicates a secret type of repository @@ -260,6 +264,9 @@ const ( EnvRedisHaProxyName = "ARGOCD_REDIS_HAPROXY_NAME" // EnvGRPCKeepAliveMin defines the GRPCKeepAliveEnforcementMinimum, used in the grpc.KeepaliveEnforcementPolicy. Expects a "Duration" format (e.g. 10s). EnvGRPCKeepAliveMin = "ARGOCD_GRPC_KEEP_ALIVE_MIN" + // EnvServerSideDiff defines the env var used to enable ServerSide Diff feature. + // If defined, value must be "true" or "false". + EnvServerSideDiff = "ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF" ) // Config Management Plugin related constants diff --git a/controller/appcontroller.go b/controller/appcontroller.go index 964bcc1aa0dec9..9d89b6e6b37d65 100644 --- a/controller/appcontroller.go +++ b/controller/appcontroller.go @@ -6,6 +6,7 @@ import ( goerrors "errors" "fmt" "math" + "math/rand" "net/http" "reflect" "runtime/debug" @@ -15,7 +16,6 @@ import ( "sync" "time" - "github.com/argoproj/argo-cd/v2/pkg/ratelimiter" clustercache "github.com/argoproj/gitops-engine/pkg/cache" "github.com/argoproj/gitops-engine/pkg/diff" "github.com/argoproj/gitops-engine/pkg/health" @@ -48,7 +48,6 @@ import ( "github.com/argoproj/argo-cd/v2/controller/sharding" "github.com/argoproj/argo-cd/v2/pkg/apis/application" appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" - argov1alpha "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions/application/v1alpha1" applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1" @@ -59,6 +58,7 @@ import ( kubeerrors "k8s.io/apimachinery/pkg/api/errors" + "github.com/argoproj/argo-cd/v2/pkg/ratelimiter" appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate" "github.com/argoproj/argo-cd/v2/util/db" "github.com/argoproj/argo-cd/v2/util/errors" @@ -113,11 +113,11 @@ type ApplicationController struct { appInformer cache.SharedIndexInformer appLister applisters.ApplicationLister projInformer cache.SharedIndexInformer - deploymentInformer informerv1.DeploymentInformer appStateManager AppStateManager stateCache statecache.LiveStateCache statusRefreshTimeout time.Duration statusHardRefreshTimeout time.Duration + statusRefreshJitter time.Duration selfHealTimeout time.Duration repoClientset apiclient.Clientset db db.ArgoDB @@ -126,9 +126,13 @@ type ApplicationController struct { refreshRequestedAppsMutex *sync.Mutex metricsServer *metrics.MetricsServer kubectlSemaphore *semaphore.Weighted - clusterFilter func(cluster *appv1.Cluster) bool + clusterSharding sharding.ClusterShardingCache projByNameCache sync.Map applicationNamespaces []string + + // dynamicClusterDistributionEnabled if disabled deploymentInformer is never initialized + dynamicClusterDistributionEnabled bool + deploymentInformer informerv1.DeploymentInformer } // NewApplicationController creates new instance of ApplicationController. @@ -142,6 +146,7 @@ func NewApplicationController( kubectl kube.Kubectl, appResyncPeriod time.Duration, appHardResyncPeriod time.Duration, + appResyncJitter time.Duration, selfHealTimeout time.Duration, repoErrorGracePeriod time.Duration, metricsPort int, @@ -149,38 +154,42 @@ func NewApplicationController( metricsApplicationLabels []string, kubectlParallelismLimit int64, persistResourceHealth bool, - clusterFilter func(cluster *appv1.Cluster) bool, + clusterSharding sharding.ClusterShardingCache, applicationNamespaces []string, rateLimiterConfig *ratelimiter.AppControllerRateLimiterConfig, + serverSideDiff bool, + dynamicClusterDistributionEnabled bool, ) (*ApplicationController, error) { - log.Infof("appResyncPeriod=%v, appHardResyncPeriod=%v", appResyncPeriod, appHardResyncPeriod) + log.Infof("appResyncPeriod=%v, appHardResyncPeriod=%v, appResyncJitter=%v", appResyncPeriod, appHardResyncPeriod, appResyncJitter) db := db.NewDB(namespace, settingsMgr, kubeClientset) if rateLimiterConfig == nil { rateLimiterConfig = ratelimiter.GetDefaultAppRateLimiterConfig() log.Info("Using default workqueue rate limiter config") } ctrl := ApplicationController{ - cache: argoCache, - namespace: namespace, - kubeClientset: kubeClientset, - kubectl: kubectl, - applicationClientset: applicationClientset, - repoClientset: repoClientset, - appRefreshQueue: workqueue.NewNamedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), "app_reconciliation_queue"), - appOperationQueue: workqueue.NewNamedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), "app_operation_processing_queue"), - projectRefreshQueue: workqueue.NewNamedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), "project_reconciliation_queue"), - appComparisonTypeRefreshQueue: workqueue.NewRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig)), - db: db, - statusRefreshTimeout: appResyncPeriod, - statusHardRefreshTimeout: appHardResyncPeriod, - refreshRequestedApps: make(map[string]CompareWith), - refreshRequestedAppsMutex: &sync.Mutex{}, - auditLogger: argo.NewAuditLogger(namespace, kubeClientset, common.ApplicationController), - settingsMgr: settingsMgr, - selfHealTimeout: selfHealTimeout, - clusterFilter: clusterFilter, - projByNameCache: sync.Map{}, - applicationNamespaces: applicationNamespaces, + cache: argoCache, + namespace: namespace, + kubeClientset: kubeClientset, + kubectl: kubectl, + applicationClientset: applicationClientset, + repoClientset: repoClientset, + appRefreshQueue: workqueue.NewNamedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), "app_reconciliation_queue"), + appOperationQueue: workqueue.NewNamedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), "app_operation_processing_queue"), + projectRefreshQueue: workqueue.NewNamedRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig), "project_reconciliation_queue"), + appComparisonTypeRefreshQueue: workqueue.NewRateLimitingQueue(ratelimiter.NewCustomAppControllerRateLimiter(rateLimiterConfig)), + db: db, + statusRefreshTimeout: appResyncPeriod, + statusHardRefreshTimeout: appHardResyncPeriod, + statusRefreshJitter: appResyncJitter, + refreshRequestedApps: make(map[string]CompareWith), + refreshRequestedAppsMutex: &sync.Mutex{}, + auditLogger: argo.NewAuditLogger(namespace, kubeClientset, common.ApplicationController), + settingsMgr: settingsMgr, + selfHealTimeout: selfHealTimeout, + clusterSharding: clusterSharding, + projByNameCache: sync.Map{}, + applicationNamespaces: applicationNamespaces, + dynamicClusterDistributionEnabled: dynamicClusterDistributionEnabled, } if kubectlParallelismLimit > 0 { ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit) @@ -223,25 +232,33 @@ func NewApplicationController( } factory := informers.NewSharedInformerFactoryWithOptions(ctrl.kubeClientset, defaultDeploymentInformerResyncDuration, informers.WithNamespace(settingsMgr.GetNamespace())) - deploymentInformer := factory.Apps().V1().Deployments() + + var deploymentInformer informerv1.DeploymentInformer + + // only initialize deployment informer if dynamic distribution is enabled + if dynamicClusterDistributionEnabled { + deploymentInformer = factory.Apps().V1().Deployments() + } readinessHealthCheck := func(r *http.Request) error { - applicationControllerName := env.StringFromEnv(common.EnvAppControllerName, common.DefaultApplicationControllerName) - appControllerDeployment, err := deploymentInformer.Lister().Deployments(settingsMgr.GetNamespace()).Get(applicationControllerName) - if err != nil { - if kubeerrors.IsNotFound(err) { - appControllerDeployment = nil - } else { - return fmt.Errorf("error retrieving Application Controller Deployment: %s", err) - } - } - if appControllerDeployment != nil { - if appControllerDeployment.Spec.Replicas != nil && int(*appControllerDeployment.Spec.Replicas) <= 0 { - return fmt.Errorf("application controller deployment replicas is not set or is less than 0, replicas: %d", appControllerDeployment.Spec.Replicas) + if dynamicClusterDistributionEnabled { + applicationControllerName := env.StringFromEnv(common.EnvAppControllerName, common.DefaultApplicationControllerName) + appControllerDeployment, err := deploymentInformer.Lister().Deployments(settingsMgr.GetNamespace()).Get(applicationControllerName) + if err != nil { + if kubeerrors.IsNotFound(err) { + appControllerDeployment = nil + } else { + return fmt.Errorf("error retrieving Application Controller Deployment: %s", err) + } } - shard := env.ParseNumFromEnv(common.EnvControllerShard, -1, -math.MaxInt32, math.MaxInt32) - if _, err := sharding.GetOrUpdateShardFromConfigMap(kubeClientset.(*kubernetes.Clientset), settingsMgr, int(*appControllerDeployment.Spec.Replicas), shard); err != nil { - return fmt.Errorf("error while updating the heartbeat for to the Shard Mapping ConfigMap: %s", err) + if appControllerDeployment != nil { + if appControllerDeployment.Spec.Replicas != nil && int(*appControllerDeployment.Spec.Replicas) <= 0 { + return fmt.Errorf("application controller deployment replicas is not set or is less than 0, replicas: %d", appControllerDeployment.Spec.Replicas) + } + shard := env.ParseNumFromEnv(common.EnvControllerShard, -1, -math.MaxInt32, math.MaxInt32) + if _, err := sharding.GetOrUpdateShardFromConfigMap(kubeClientset.(*kubernetes.Clientset), settingsMgr, int(*appControllerDeployment.Spec.Replicas), shard); err != nil { + return fmt.Errorf("error while updating the heartbeat for to the Shard Mapping ConfigMap: %s", err) + } } } return nil @@ -259,8 +276,8 @@ func NewApplicationController( return nil, err } } - stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated, clusterFilter, argo.NewResourceTracking()) - appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer, argoCache, ctrl.statusRefreshTimeout, argo.NewResourceTracking(), persistResourceHealth, repoErrorGracePeriod) + stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated, clusterSharding, argo.NewResourceTracking()) + appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer, argoCache, ctrl.statusRefreshTimeout, argo.NewResourceTracking(), persistResourceHealth, repoErrorGracePeriod, serverSideDiff) ctrl.appInformer = appInformer ctrl.appLister = appLister ctrl.projInformer = projInformer @@ -493,13 +510,13 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed if err != nil { return nil, fmt.Errorf("failed to unmarshal live state of managed resources: %w", err) } - var target = &unstructured.Unstructured{} - err = json.Unmarshal([]byte(managedResource.TargetState), &target) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal target state of managed resources: %w", err) - } if live == nil { + var target = &unstructured.Unstructured{} + err = json.Unmarshal([]byte(managedResource.TargetState), &target) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal target state of managed resources: %w", err) + } nodes = append(nodes, appv1.ResourceNode{ ResourceRef: appv1.ResourceRef{ Version: target.GroupVersionKind().Version, @@ -769,7 +786,24 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int go ctrl.appInformer.Run(ctx.Done()) go ctrl.projInformer.Run(ctx.Done()) - go ctrl.deploymentInformer.Informer().Run(ctx.Done()) + + if ctrl.dynamicClusterDistributionEnabled { + // only start deployment informer if dynamic distribution is enabled + go ctrl.deploymentInformer.Informer().Run(ctx.Done()) + } + + clusters, err := ctrl.db.ListClusters(ctx) + if err != nil { + log.Warnf("Cannot init sharding. Error while querying clusters list from database: %v", err) + } else { + appItems, err := ctrl.getAppList(metav1.ListOptions{}) + + if err != nil { + log.Warnf("Cannot init sharding. Error while querying application list from database: %v", err) + } else { + ctrl.clusterSharding.Init(clusters, appItems) + } + } errors.CheckError(ctrl.stateCache.Init()) @@ -884,11 +918,10 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b if app.Operation != nil { ctrl.processRequestedAppOperation(app) - } else if app.DeletionTimestamp != nil && app.CascadedDeletion() { - _, err = ctrl.finalizeApplicationDeletion(app, func(project string) ([]*appv1.Cluster, error) { + } else if app.DeletionTimestamp != nil { + if err = ctrl.finalizeApplicationDeletion(app, func(project string) ([]*appv1.Cluster, error) { return ctrl.db.GetProjectClusters(context.Background(), project) - }) - if err != nil { + }); err != nil { ctrl.setAppCondition(app, appv1.ApplicationCondition{ Type: appv1.ApplicationConditionDeletionError, Message: err.Error(), @@ -1023,57 +1056,63 @@ func (ctrl *ApplicationController) getPermittedAppLiveObjects(app *appv1.Applica return objsMap, nil } -func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application, projectClusters func(project string) ([]*appv1.Cluster, error)) ([]*unstructured.Unstructured, error) { +func (ctrl *ApplicationController) isValidDestination(app *appv1.Application) (bool, *appv1.Cluster) { + // Validate the cluster using the Application destination's `name` field, if applicable, + // and set the Server field, if needed. + if err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db); err != nil { + log.Warnf("Unable to validate destination of the Application being deleted: %v", err) + return false, nil + } + + cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server) + if err != nil { + log.Warnf("Unable to locate cluster URL for Application being deleted: %v", err) + return false, nil + } + return true, cluster +} + +func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application, projectClusters func(project string) ([]*appv1.Cluster, error)) error { logCtx := log.WithField("application", app.QualifiedName()) - logCtx.Infof("Deleting resources") // Get refreshed application info, since informer app copy might be stale app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(context.Background(), app.Name, metav1.GetOptions{}) if err != nil { if !apierr.IsNotFound(err) { logCtx.Errorf("Unable to get refreshed application info prior deleting resources: %v", err) } - return nil, nil + return nil } proj, err := ctrl.getAppProj(app) if err != nil { - return nil, err - } - - // validDestination is true if the Application destination points to a cluster that is managed by Argo CD - // (and thus either a cluster secret exists for it, or it's local); validDestination is false otherwise. - validDestination := true - - // Validate the cluster using the Application destination's `name` field, if applicable, - // and set the Server field, if needed. - if err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db); err != nil { - log.Warnf("Unable to validate destination of the Application being deleted: %v", err) - validDestination = false + return err } - objs := make([]*unstructured.Unstructured, 0) - var cluster *appv1.Cluster - - // Attempt to validate the destination via its URL - if validDestination { - if cluster, err = ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server); err != nil { - log.Warnf("Unable to locate cluster URL for Application being deleted: %v", err) - validDestination = false + isValid, cluster := ctrl.isValidDestination(app) + if !isValid { + app.UnSetCascadedDeletion() + app.UnSetPostDeleteFinalizer() + if err := ctrl.updateFinalizers(app); err != nil { + return err } + logCtx.Infof("Resource entries removed from undefined cluster") + return nil } + config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig()) - if validDestination { + if app.CascadedDeletion() { + logCtx.Infof("Deleting resources") // ApplicationDestination points to a valid cluster, so we may clean up the live objects - + objs := make([]*unstructured.Unstructured, 0) objsMap, err := ctrl.getPermittedAppLiveObjects(app, proj, projectClusters) if err != nil { - return nil, err + return err } for k := range objsMap { // Wait for objects pending deletion to complete before proceeding with next sync wave if objsMap[k].GetDeletionTimestamp() != nil { logCtx.Infof("%d objects remaining for deletion", len(objsMap)) - return objs, nil + return nil } if ctrl.shouldBeDeleted(app, objsMap[k]) { @@ -1081,8 +1120,6 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic } } - config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig()) - filteredObjs := FilterObjectsForDeletion(objs) propagationPolicy := metav1.DeletePropagationForeground @@ -1096,12 +1133,12 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic return ctrl.kubectl.DeleteResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), metav1.DeleteOptions{PropagationPolicy: &propagationPolicy}) }) if err != nil { - return objs, err + return err } objsMap, err = ctrl.getPermittedAppLiveObjects(app, proj, projectClusters) if err != nil { - return nil, err + return err } for k, obj := range objsMap { @@ -1111,38 +1148,67 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic } if len(objsMap) > 0 { logCtx.Infof("%d objects remaining for deletion", len(objsMap)) - return objs, nil + return nil } + logCtx.Infof("Successfully deleted %d resources", len(objs)) + app.UnSetCascadedDeletion() + return ctrl.updateFinalizers(app) } - if err := ctrl.cache.SetAppManagedResources(app.Name, nil); err != nil { - return objs, err - } + if app.HasPostDeleteFinalizer() { + objsMap, err := ctrl.getPermittedAppLiveObjects(app, proj, projectClusters) + if err != nil { + return err + } - if err := ctrl.cache.SetAppResourcesTree(app.Name, nil); err != nil { - return objs, err + done, err := ctrl.executePostDeleteHooks(app, proj, objsMap, config, logCtx) + if err != nil { + return err + } + if !done { + return nil + } + app.UnSetPostDeleteFinalizer() + return ctrl.updateFinalizers(app) } - if err := ctrl.removeCascadeFinalizer(app); err != nil { - return objs, err + if app.HasPostDeleteFinalizer("cleanup") { + objsMap, err := ctrl.getPermittedAppLiveObjects(app, proj, projectClusters) + if err != nil { + return err + } + + done, err := ctrl.cleanupPostDeleteHooks(objsMap, config, logCtx) + if err != nil { + return err + } + if !done { + return nil + } + app.UnSetPostDeleteFinalizer("cleanup") + return ctrl.updateFinalizers(app) } - if validDestination { - logCtx.Infof("Successfully deleted %d resources", len(objs)) - } else { - logCtx.Infof("Resource entries removed from undefined cluster") + if !app.CascadedDeletion() && !app.HasPostDeleteFinalizer() { + if err := ctrl.cache.SetAppManagedResources(app.Name, nil); err != nil { + return err + } + + if err := ctrl.cache.SetAppResourcesTree(app.Name, nil); err != nil { + return err + } + ctrl.projectRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, app.Spec.GetProject())) } - ctrl.projectRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, app.Spec.GetProject())) - return objs, nil + return nil } -func (ctrl *ApplicationController) removeCascadeFinalizer(app *appv1.Application) error { +func (ctrl *ApplicationController) updateFinalizers(app *appv1.Application) error { _, err := ctrl.getAppProj(app) if err != nil { return fmt.Errorf("error getting project: %w", err) } - app.UnSetCascadedDeletion() + var patch []byte patch, _ = json.Marshal(map[string]interface{}{ "metadata": map[string]interface{}{ @@ -1566,6 +1632,20 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo app.Status.SourceTypes = compareResult.appSourceTypes app.Status.ControllerNamespace = ctrl.namespace patchMs = ctrl.persistAppStatus(origApp, &app.Status) + if (compareResult.hasPostDeleteHooks != app.HasPostDeleteFinalizer() || compareResult.hasPostDeleteHooks != app.HasPostDeleteFinalizer("cleanup")) && + app.GetDeletionTimestamp() == nil { + if compareResult.hasPostDeleteHooks { + app.SetPostDeleteFinalizer() + app.SetPostDeleteFinalizer("cleanup") + } else { + app.UnSetPostDeleteFinalizer() + app.UnSetPostDeleteFinalizer("cleanup") + } + + if err := ctrl.updateFinalizers(app); err != nil { + logCtx.Errorf("Failed to update finalizers: %v", err) + } + } return } @@ -1589,6 +1669,7 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application, var reason string compareWith := CompareWithLatest refreshType := appv1.RefreshTypeNormal + softExpired := app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Add(statusRefreshTimeout).Before(time.Now().UTC()) hardExpired := (app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Add(statusHardRefreshTimeout).Before(time.Now().UTC())) && statusHardRefreshTimeout.Seconds() != 0 @@ -1929,15 +2010,11 @@ func (ctrl *ApplicationController) canProcessApp(obj interface{}) bool { } } - if ctrl.clusterFilter != nil { - cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server) - if err != nil { - return ctrl.clusterFilter(nil) - } - return ctrl.clusterFilter(cluster) + cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server) + if err != nil { + return ctrl.clusterSharding.IsManagedCluster(nil) } - - return true + return ctrl.clusterSharding.IsManagedCluster(cluster) } func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister) { @@ -2035,6 +2112,10 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar ctrl.appRefreshQueue.AddRateLimited(key) ctrl.appOperationQueue.AddRateLimited(key) } + newApp, newOK := obj.(*appv1.Application) + if err == nil && newOK { + ctrl.clusterSharding.AddApp(newApp) + } }, UpdateFunc: func(old, new interface{}) { if !ctrl.canProcessApp(new) { @@ -2045,15 +2126,27 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar if err != nil { return } + var compareWith *CompareWith + var delay *time.Duration + oldApp, oldOK := old.(*appv1.Application) newApp, newOK := new.(*appv1.Application) - if oldOK && newOK && automatedSyncEnabled(oldApp, newApp) { - log.WithField("application", newApp.QualifiedName()).Info("Enabled automated sync") - compareWith = CompareWithLatest.Pointer() + if oldOK && newOK { + if automatedSyncEnabled(oldApp, newApp) { + log.WithField("application", newApp.QualifiedName()).Info("Enabled automated sync") + compareWith = CompareWithLatest.Pointer() + } + if ctrl.statusRefreshJitter != 0 && oldApp.ResourceVersion == newApp.ResourceVersion { + // Handler is refreshing the apps, add a random jitter to spread the load and avoid spikes + jitter := time.Duration(float64(ctrl.statusRefreshJitter) * rand.Float64()) + delay = &jitter + } } - ctrl.requestAppRefresh(newApp.QualifiedName(), compareWith, nil) + + ctrl.requestAppRefresh(newApp.QualifiedName(), compareWith, delay) ctrl.appOperationQueue.AddRateLimited(key) + ctrl.clusterSharding.UpdateApp(newApp) }, DeleteFunc: func(obj interface{}) { if !ctrl.canProcessApp(obj) { @@ -2066,6 +2159,10 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar // for deletes, we immediately add to the refresh queue ctrl.appRefreshQueue.Add(key) } + delApp, delOK := obj.(*appv1.Application) + if err == nil && delOK { + ctrl.clusterSharding.DeleteApp(delApp) + } }, }, ) @@ -2089,7 +2186,7 @@ func (ctrl *ApplicationController) projectErrorToCondition(err error, app *appv1 } func (ctrl *ApplicationController) RegisterClusterSecretUpdater(ctx context.Context) { - updater := NewClusterInfoUpdater(ctrl.stateCache, ctrl.db, ctrl.appLister.Applications(""), ctrl.cache, ctrl.clusterFilter, ctrl.getAppProj, ctrl.namespace) + updater := NewClusterInfoUpdater(ctrl.stateCache, ctrl.db, ctrl.appLister.Applications(""), ctrl.cache, ctrl.clusterSharding.IsManagedCluster, ctrl.getAppProj, ctrl.namespace) go updater.Run(ctx) } @@ -2141,4 +2238,26 @@ func (ctrl *ApplicationController) toAppQualifiedName(appName, appNamespace stri return fmt.Sprintf("%s/%s", appNamespace, appName) } -type ClusterFilterFunction func(c *argov1alpha.Cluster, distributionFunction sharding.DistributionFunction) bool +func (ctrl *ApplicationController) getAppList(options metav1.ListOptions) (*appv1.ApplicationList, error) { + watchNamespace := ctrl.namespace + // If we have at least one additional namespace configured, we need to + // watch on them all. + if len(ctrl.applicationNamespaces) > 0 { + watchNamespace = "" + } + + appList, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(watchNamespace).List(context.TODO(), options) + if err != nil { + return nil, err + } + newItems := []appv1.Application{} + for _, app := range appList.Items { + if ctrl.isAppNamespaceAllowed(&app) { + newItems = append(newItems, app) + } + } + appList.Items = newItems + return appList, nil +} + +type ClusterFilterFunction func(c *appv1.Cluster, distributionFunction sharding.DistributionFunction) bool diff --git a/controller/appcontroller_test.go b/controller/appcontroller_test.go index 9c2933feeb6df4..33a29bc5ca3f89 100644 --- a/controller/appcontroller_test.go +++ b/controller/appcontroller_test.go @@ -7,18 +7,22 @@ import ( "testing" "time" + "github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest" "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/client-go/rest" clustercache "github.com/argoproj/gitops-engine/pkg/cache" "github.com/argoproj/argo-cd/v2/common" statecache "github.com/argoproj/argo-cd/v2/controller/cache" + "github.com/argoproj/argo-cd/v2/controller/sharding" + dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks" "github.com/argoproj/gitops-engine/pkg/cache/mocks" synccommon "github.com/argoproj/gitops-engine/pkg/sync/common" "github.com/argoproj/gitops-engine/pkg/utils/kube" - "github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" corev1 "k8s.io/api/core/v1" @@ -59,6 +63,23 @@ type fakeData struct { applicationNamespaces []string } +type MockKubectl struct { + kube.Kubectl + + DeletedResources []kube.ResourceKey + CreatedResources []*unstructured.Unstructured +} + +func (m *MockKubectl) CreateResource(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string, obj *unstructured.Unstructured, createOptions metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error) { + m.CreatedResources = append(m.CreatedResources, obj) + return m.Kubectl.CreateResource(ctx, config, gvk, name, namespace, obj, createOptions, subresources...) +} + +func (m *MockKubectl) DeleteResource(ctx context.Context, config *rest.Config, gvk schema.GroupVersionKind, name string, namespace string, deleteOptions metav1.DeleteOptions) error { + m.DeletedResources = append(m.DeletedResources, kube.NewResourceKey(gvk.Group, gvk.Kind, namespace, name)) + return m.Kubectl.DeleteResource(ctx, config, gvk, name, namespace, deleteOptions) +} + func newFakeController(data *fakeData, repoErr error) *ApplicationController { var clust corev1.Secret err := yaml.Unmarshal([]byte(fakeCluster), &clust) @@ -109,7 +130,7 @@ func newFakeController(data *fakeData, repoErr error) *ApplicationController { } kubeClient := fake.NewSimpleClientset(&clust, &cm, &secret) settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace) - kubectl := &kubetest.MockKubectlCmd{} + kubectl := &MockKubectl{Kubectl: &kubetest.MockKubectlCmd{}} ctrl, err := NewApplicationController( test.FakeArgoCDNamespace, settingsMgr, @@ -123,6 +144,7 @@ func newFakeController(data *fakeData, repoErr error) *ApplicationController { kubectl, time.Minute, time.Hour, + time.Second, time.Minute, time.Second*10, common.DefaultPortArgoCDMetrics, @@ -133,7 +155,14 @@ func newFakeController(data *fakeData, repoErr error) *ApplicationController { nil, data.applicationNamespaces, nil, + + false, + false, ) + db := &dbmocks.ArgoDB{} + db.On("GetApplicationControllerReplicas").Return(1) + // Setting a default sharding algorithm for the tests where we cannot set it. + ctrl.clusterSharding = sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm) if err != nil { panic(err) } @@ -337,6 +366,38 @@ metadata: data: ` +var fakePostDeleteHook = ` +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "post-delete-hook", + "namespace": "default", + "labels": { + "app.kubernetes.io/instance": "my-app" + }, + "annotations": { + "argocd.argoproj.io/hook": "PostDelete", + "argocd.argoproj.io/hook-delete-policy": "HookSucceeded" + } + }, + "spec": { + "containers": [ + { + "name": "post-delete-hook", + "image": "busybox", + "restartPolicy": "Never", + "command": [ + "/bin/sh", + "-c", + "sleep 5 && echo hello from the post-delete-hook pod" + ] + } + ] + } +} +` + func newFakeApp() *v1alpha1.Application { return createFakeApp(fakeApp) } @@ -371,6 +432,15 @@ func newFakeCM() map[string]interface{} { return cm } +func newFakePostDeleteHook() map[string]interface{} { + var cm map[string]interface{} + err := yaml.Unmarshal([]byte(fakePostDeleteHook), &cm) + if err != nil { + panic(err) + } + return cm +} + func TestAutoSync(t *testing.T) { app := newFakeApp() ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}}, nil) @@ -619,12 +689,12 @@ func TestFinalizeAppDeletion(t *testing.T) { // Ensure app can be deleted cascading t.Run("CascadingDelete", func(t *testing.T) { app := newFakeApp() + app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName) app.Spec.Destination.Namespace = test.FakeArgoCDNamespace appObj := kube.MustToUnstructured(&app) ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ kube.GetResourceKey(appObj): appObj, }}, nil) - patched := false fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) defaultReactor := fakeAppCs.ReactionChain[0] @@ -636,7 +706,7 @@ func TestFinalizeAppDeletion(t *testing.T) { patched = true return true, &v1alpha1.Application{}, nil }) - _, err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { + err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { return []*v1alpha1.Cluster{}, nil }) assert.NoError(t, err) @@ -662,6 +732,7 @@ func TestFinalizeAppDeletion(t *testing.T) { }, } app := newFakeApp() + app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName) app.Spec.Destination.Namespace = test.FakeArgoCDNamespace app.Spec.Project = "restricted" appObj := kube.MustToUnstructured(&app) @@ -686,7 +757,7 @@ func TestFinalizeAppDeletion(t *testing.T) { patched = true return true, &v1alpha1.Application{}, nil }) - objs, err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { + err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { return []*v1alpha1.Cluster{}, nil }) assert.NoError(t, err) @@ -697,14 +768,16 @@ func TestFinalizeAppDeletion(t *testing.T) { } // Managed objects must be empty assert.Empty(t, objsMap) + // Loop through all deleted objects, ensure that test-cm is none of them - for _, o := range objs { - assert.NotEqual(t, "test-cm", o.GetName()) + for _, o := range ctrl.kubectl.(*MockKubectl).DeletedResources { + assert.NotEqual(t, "test-cm", o.Name) } }) t.Run("DeleteWithDestinationClusterName", func(t *testing.T) { app := newFakeAppWithDestName() + app.SetCascadedDeletion(v1alpha1.ResourcesFinalizerName) appObj := kube.MustToUnstructured(&app) ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ kube.GetResourceKey(appObj): appObj, @@ -720,7 +793,7 @@ func TestFinalizeAppDeletion(t *testing.T) { patched = true return true, &v1alpha1.Application{}, nil }) - _, err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { + err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { return []*v1alpha1.Cluster{}, nil }) assert.NoError(t, err) @@ -745,7 +818,7 @@ func TestFinalizeAppDeletion(t *testing.T) { fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { return defaultReactor.React(action) }) - _, err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { + err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { return []*v1alpha1.Cluster{}, nil }) assert.NoError(t, err) @@ -766,6 +839,109 @@ func TestFinalizeAppDeletion(t *testing.T) { }) + t.Run("PostDelete_HookIsCreated", func(t *testing.T) { + app := newFakeApp() + app.SetPostDeleteFinalizer() + app.Spec.Destination.Namespace = test.FakeArgoCDNamespace + ctrl := newFakeController(&fakeData{ + manifestResponses: []*apiclient.ManifestResponse{{ + Manifests: []string{fakePostDeleteHook}, + }}, + apps: []runtime.Object{app, &defaultProj}, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{}}, nil) + + patched := false + fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) + defaultReactor := fakeAppCs.ReactionChain[0] + fakeAppCs.ReactionChain = nil + fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + return defaultReactor.React(action) + }) + fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + patched = true + return true, &v1alpha1.Application{}, nil + }) + err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { + return []*v1alpha1.Cluster{}, nil + }) + assert.NoError(t, err) + // finalizer is not deleted + assert.False(t, patched) + // post-delete hook is created + require.Len(t, ctrl.kubectl.(*MockKubectl).CreatedResources, 1) + require.Equal(t, "post-delete-hook", ctrl.kubectl.(*MockKubectl).CreatedResources[0].GetName()) + }) + + t.Run("PostDelete_HookIsExecuted", func(t *testing.T) { + app := newFakeApp() + app.SetPostDeleteFinalizer() + app.Spec.Destination.Namespace = test.FakeArgoCDNamespace + liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()} + require.NoError(t, unstructured.SetNestedField(liveHook.Object, "Succeeded", "status", "phase")) + ctrl := newFakeController(&fakeData{ + manifestResponses: []*apiclient.ManifestResponse{{ + Manifests: []string{fakePostDeleteHook}, + }}, + apps: []runtime.Object{app, &defaultProj}, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ + kube.GetResourceKey(liveHook): liveHook, + }}, nil) + + patched := false + fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) + defaultReactor := fakeAppCs.ReactionChain[0] + fakeAppCs.ReactionChain = nil + fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + return defaultReactor.React(action) + }) + fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + patched = true + return true, &v1alpha1.Application{}, nil + }) + err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { + return []*v1alpha1.Cluster{}, nil + }) + assert.NoError(t, err) + // finalizer is removed + assert.True(t, patched) + }) + + t.Run("PostDelete_HookIsDeleted", func(t *testing.T) { + app := newFakeApp() + app.SetPostDeleteFinalizer("cleanup") + app.Spec.Destination.Namespace = test.FakeArgoCDNamespace + liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()} + require.NoError(t, unstructured.SetNestedField(liveHook.Object, "Succeeded", "status", "phase")) + ctrl := newFakeController(&fakeData{ + manifestResponses: []*apiclient.ManifestResponse{{ + Manifests: []string{fakePostDeleteHook}, + }}, + apps: []runtime.Object{app, &defaultProj}, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ + kube.GetResourceKey(liveHook): liveHook, + }}, nil) + + patched := false + fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset) + defaultReactor := fakeAppCs.ReactionChain[0] + fakeAppCs.ReactionChain = nil + fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + return defaultReactor.React(action) + }) + fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) { + patched = true + return true, &v1alpha1.Application{}, nil + }) + err := ctrl.finalizeApplicationDeletion(app, func(project string) ([]*v1alpha1.Cluster, error) { + return []*v1alpha1.Cluster{}, nil + }) + assert.NoError(t, err) + // post-delete hook is deleted + require.Len(t, ctrl.kubectl.(*MockKubectl).DeletedResources, 1) + require.Equal(t, "post-delete-hook", ctrl.kubectl.(*MockKubectl).DeletedResources[0].Name) + // finalizer is not removed + assert.False(t, patched) + }) } // TestNormalizeApplication verifies we normalize an application during reconciliation @@ -1641,13 +1817,11 @@ func Test_canProcessApp(t *testing.T) { }) t.Run("with cluster filter, good namespace", func(t *testing.T) { app.Namespace = "good" - ctrl.clusterFilter = func(_ *v1alpha1.Cluster) bool { return true } canProcess := ctrl.canProcessApp(app) assert.True(t, canProcess) }) t.Run("with cluster filter, bad namespace", func(t *testing.T) { app.Namespace = "bad" - ctrl.clusterFilter = func(_ *v1alpha1.Cluster) bool { return true } canProcess := ctrl.canProcessApp(app) assert.False(t, canProcess) }) diff --git a/controller/cache/cache.go b/controller/cache/cache.go index 9eac1617140897..d1ae8989cd8e67 100644 --- a/controller/cache/cache.go +++ b/controller/cache/cache.go @@ -29,6 +29,7 @@ import ( "k8s.io/client-go/tools/cache" "github.com/argoproj/argo-cd/v2/controller/metrics" + "github.com/argoproj/argo-cd/v2/controller/sharding" "github.com/argoproj/argo-cd/v2/pkg/apis/application" appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/util/argo" @@ -168,7 +169,7 @@ func NewLiveStateCache( kubectl kube.Kubectl, metricsServer *metrics.MetricsServer, onObjectUpdated ObjectUpdatedHandler, - clusterFilter func(cluster *appv1.Cluster) bool, + clusterSharding sharding.ClusterShardingCache, resourceTracking argo.ResourceTracking) LiveStateCache { return &liveStateCache{ @@ -179,7 +180,7 @@ func NewLiveStateCache( kubectl: kubectl, settingsMgr: settingsMgr, metricsServer: metricsServer, - clusterFilter: clusterFilter, + clusterSharding: clusterSharding, resourceTracking: resourceTracking, } } @@ -202,7 +203,7 @@ type liveStateCache struct { kubectl kube.Kubectl settingsMgr *settings.SettingsManager metricsServer *metrics.MetricsServer - clusterFilter func(cluster *appv1.Cluster) bool + clusterSharding sharding.ClusterShardingCache resourceTracking argo.ResourceTracking clusters map[string]clustercache.ClusterCache @@ -722,22 +723,24 @@ func (c *liveStateCache) Run(ctx context.Context) error { } func (c *liveStateCache) canHandleCluster(cluster *appv1.Cluster) bool { - if c.clusterFilter == nil { - return true - } - return c.clusterFilter(cluster) + return c.clusterSharding.IsManagedCluster(cluster) } func (c *liveStateCache) handleAddEvent(cluster *appv1.Cluster) { + c.clusterSharding.Add(cluster) if !c.canHandleCluster(cluster) { log.Infof("Ignoring cluster %s", cluster.Server) return } - c.lock.Lock() _, ok := c.clusters[cluster.Server] c.lock.Unlock() if !ok { + log.Debugf("Checking if cache %v / cluster %v has appInformer %v", c, cluster, c.appInformer) + if c.appInformer == nil { + log.Warn("Cannot get a cluster appInformer. Cache may not be started this time") + return + } if c.isClusterHasApps(c.appInformer.GetStore().List(), cluster) { go func() { // warm up cache for cluster with apps @@ -748,6 +751,7 @@ func (c *liveStateCache) handleAddEvent(cluster *appv1.Cluster) { } func (c *liveStateCache) handleModEvent(oldCluster *appv1.Cluster, newCluster *appv1.Cluster) { + c.clusterSharding.Update(oldCluster, newCluster) c.lock.Lock() cluster, ok := c.clusters[newCluster.Server] c.lock.Unlock() @@ -790,6 +794,7 @@ func (c *liveStateCache) handleModEvent(oldCluster *appv1.Cluster, newCluster *a func (c *liveStateCache) handleDeleteEvent(clusterServer string) { c.lock.RLock() + c.clusterSharding.Delete(clusterServer) cluster, ok := c.clusters[clusterServer] c.lock.RUnlock() if ok { diff --git a/controller/cache/cache_test.go b/controller/cache/cache_test.go index c94038a89b8812..53a03ca81995ef 100644 --- a/controller/cache/cache_test.go +++ b/controller/cache/cache_test.go @@ -21,7 +21,11 @@ import ( "github.com/stretchr/testify/mock" "k8s.io/client-go/kubernetes/fake" + "github.com/argoproj/argo-cd/v2/common" + "github.com/argoproj/argo-cd/v2/controller/metrics" + "github.com/argoproj/argo-cd/v2/controller/sharding" appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks" argosettings "github.com/argoproj/argo-cd/v2/util/settings" ) @@ -35,11 +39,13 @@ func TestHandleModEvent_HasChanges(t *testing.T) { clusterCache := &mocks.ClusterCache{} clusterCache.On("Invalidate", mock.Anything, mock.Anything).Return(nil).Once() clusterCache.On("EnsureSynced").Return(nil).Once() - + db := &dbmocks.ArgoDB{} + db.On("GetApplicationControllerReplicas").Return(1) clustersCache := liveStateCache{ clusters: map[string]cache.ClusterCache{ "https://mycluster": clusterCache, }, + clusterSharding: sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm), } clustersCache.handleModEvent(&appv1.Cluster{ @@ -56,14 +62,22 @@ func TestHandleModEvent_ClusterExcluded(t *testing.T) { clusterCache := &mocks.ClusterCache{} clusterCache.On("Invalidate", mock.Anything, mock.Anything).Return(nil).Once() clusterCache.On("EnsureSynced").Return(nil).Once() - + db := &dbmocks.ArgoDB{} + db.On("GetApplicationControllerReplicas").Return(1) clustersCache := liveStateCache{ - clusters: map[string]cache.ClusterCache{ - "https://mycluster": clusterCache, - }, - clusterFilter: func(cluster *appv1.Cluster) bool { - return false + db: nil, + appInformer: nil, + onObjectUpdated: func(managedByApp map[string]bool, ref v1.ObjectReference) { }, + kubectl: nil, + settingsMgr: &argosettings.SettingsManager{}, + metricsServer: &metrics.MetricsServer{}, + // returns a shard that never process any cluster + clusterSharding: sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm), + resourceTracking: nil, + clusters: map[string]cache.ClusterCache{"https://mycluster": clusterCache}, + cacheSettings: cacheSettings{}, + lock: sync.RWMutex{}, } clustersCache.handleModEvent(&appv1.Cluster{ @@ -75,18 +89,20 @@ func TestHandleModEvent_ClusterExcluded(t *testing.T) { Namespaces: []string{"default"}, }) - assert.Len(t, clustersCache.clusters, 0) + assert.Len(t, clustersCache.clusters, 1) } func TestHandleModEvent_NoChanges(t *testing.T) { clusterCache := &mocks.ClusterCache{} clusterCache.On("Invalidate", mock.Anything).Panic("should not invalidate") clusterCache.On("EnsureSynced").Return(nil).Panic("should not re-sync") - + db := &dbmocks.ArgoDB{} + db.On("GetApplicationControllerReplicas").Return(1) clustersCache := liveStateCache{ clusters: map[string]cache.ClusterCache{ "https://mycluster": clusterCache, }, + clusterSharding: sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm), } clustersCache.handleModEvent(&appv1.Cluster{ @@ -99,11 +115,11 @@ func TestHandleModEvent_NoChanges(t *testing.T) { } func TestHandleAddEvent_ClusterExcluded(t *testing.T) { + db := &dbmocks.ArgoDB{} + db.On("GetApplicationControllerReplicas").Return(1) clustersCache := liveStateCache{ - clusters: map[string]cache.ClusterCache{}, - clusterFilter: func(cluster *appv1.Cluster) bool { - return false - }, + clusters: map[string]cache.ClusterCache{}, + clusterSharding: sharding.NewClusterSharding(db, 0, 2, common.DefaultShardingAlgorithm), } clustersCache.handleAddEvent(&appv1.Cluster{ Server: "https://mycluster", @@ -118,6 +134,8 @@ func TestHandleDeleteEvent_CacheDeadlock(t *testing.T) { Server: "https://mycluster", Config: appv1.ClusterConfig{Username: "bar"}, } + db := &dbmocks.ArgoDB{} + db.On("GetApplicationControllerReplicas").Return(1) fakeClient := fake.NewSimpleClientset() settingsMgr := argosettings.NewSettingsManager(context.TODO(), fakeClient, "argocd") liveStateCacheLock := sync.RWMutex{} @@ -126,10 +144,8 @@ func TestHandleDeleteEvent_CacheDeadlock(t *testing.T) { clusters: map[string]cache.ClusterCache{ testCluster.Server: gitopsEngineClusterCache, }, - clusterFilter: func(cluster *appv1.Cluster) bool { - return true - }, - settingsMgr: settingsMgr, + clusterSharding: sharding.NewClusterSharding(db, 0, 1, common.DefaultShardingAlgorithm), + settingsMgr: settingsMgr, // Set the lock here so we can reference it later // nolint We need to overwrite here to have access to the lock lock: liveStateCacheLock, diff --git a/controller/cache/info_test.go b/controller/cache/info_test.go index ad5202532589b0..7b480400092843 100644 --- a/controller/cache/info_test.go +++ b/controller/cache/info_test.go @@ -52,7 +52,7 @@ var ( resourceVersion: "123" uid: "4" annotations: - link.argocd.argoproj.io/external-link: http://my-grafana.com/pre-generated-link + link.argocd.argoproj.io/external-link: http://my-grafana.example.com/pre-generated-link spec: selector: app: guestbook @@ -74,7 +74,7 @@ var ( serviceName: not-found-service servicePort: 443 rules: - - host: helm-guestbook.com + - host: helm-guestbook.example.com http: paths: - backend: @@ -86,7 +86,7 @@ var ( servicePort: https path: / tls: - - host: helm-guestbook.com + - host: helm-guestbook.example.com secretName: my-tls-secret status: loadBalancer: @@ -101,13 +101,13 @@ var ( namespace: default uid: "4" annotations: - link.argocd.argoproj.io/external-link: http://my-grafana.com/ingress-link + link.argocd.argoproj.io/external-link: http://my-grafana.example.com/ingress-link spec: backend: serviceName: not-found-service servicePort: 443 rules: - - host: helm-guestbook.com + - host: helm-guestbook.example.com http: paths: - backend: @@ -119,7 +119,7 @@ var ( servicePort: https path: / tls: - - host: helm-guestbook.com + - host: helm-guestbook.example.com secretName: my-tls-secret status: loadBalancer: @@ -138,7 +138,7 @@ var ( serviceName: not-found-service servicePort: 443 rules: - - host: helm-guestbook.com + - host: helm-guestbook.example.com http: paths: - backend: @@ -150,7 +150,7 @@ var ( servicePort: https path: /* tls: - - host: helm-guestbook.com + - host: helm-guestbook.example.com secretName: my-tls-secret status: loadBalancer: @@ -169,7 +169,7 @@ var ( serviceName: not-found-service servicePort: 443 rules: - - host: helm-guestbook.com + - host: helm-guestbook.example.com http: paths: - backend: @@ -199,7 +199,7 @@ var ( port: number: 443 rules: - - host: helm-guestbook.com + - host: helm-guestbook.example.com http: paths: - backend: @@ -215,7 +215,7 @@ var ( name: https path: / tls: - - host: helm-guestbook.com + - host: helm-guestbook.example.com secretName: my-tls-secret status: loadBalancer: @@ -327,7 +327,7 @@ func TestGetLinkAnnotatedServiceInfo(t *testing.T) { assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{ TargetLabels: map[string]string{"app": "guestbook"}, Ingress: []v1.LoadBalancerIngress{{Hostname: "localhost"}}, - ExternalURLs: []string{"http://my-grafana.com/pre-generated-link"}, + ExternalURLs: []string{"http://my-grafana.example.com/pre-generated-link"}, }, info.NetworkingInfo) } @@ -381,7 +381,7 @@ func TestGetIngressInfo(t *testing.T) { Kind: kube.ServiceKind, Name: "helm-guestbook", }}, - ExternalURLs: []string{"https://helm-guestbook.com/"}, + ExternalURLs: []string{"https://helm-guestbook.example.com/"}, }, info.NetworkingInfo) } } @@ -406,7 +406,7 @@ func TestGetLinkAnnotatedIngressInfo(t *testing.T) { Kind: kube.ServiceKind, Name: "helm-guestbook", }}, - ExternalURLs: []string{"http://my-grafana.com/ingress-link", "https://helm-guestbook.com/"}, + ExternalURLs: []string{"http://my-grafana.example.com/ingress-link", "https://helm-guestbook.example.com/"}, }, info.NetworkingInfo) } @@ -430,7 +430,7 @@ func TestGetIngressInfoWildCardPath(t *testing.T) { Kind: kube.ServiceKind, Name: "helm-guestbook", }}, - ExternalURLs: []string{"https://helm-guestbook.com/"}, + ExternalURLs: []string{"https://helm-guestbook.example.com/"}, }, info.NetworkingInfo) } @@ -454,7 +454,7 @@ func TestGetIngressInfoWithoutTls(t *testing.T) { Kind: kube.ServiceKind, Name: "helm-guestbook", }}, - ExternalURLs: []string{"http://helm-guestbook.com/"}, + ExternalURLs: []string{"http://helm-guestbook.example.com/"}, }, info.NetworkingInfo) } @@ -563,7 +563,7 @@ func TestExternalUrlWithMultipleSubPaths(t *testing.T) { namespace: default spec: rules: - - host: helm-guestbook.com + - host: helm-guestbook.example.com http: paths: - backend: @@ -587,7 +587,7 @@ func TestExternalUrlWithMultipleSubPaths(t *testing.T) { info := &ResourceInfo{} populateNodeInfo(ingress, info, []string{}) - expectedExternalUrls := []string{"https://helm-guestbook.com/my/sub/path/", "https://helm-guestbook.com/my/sub/path/2", "https://helm-guestbook.com"} + expectedExternalUrls := []string{"https://helm-guestbook.example.com/my/sub/path/", "https://helm-guestbook.example.com/my/sub/path/2", "https://helm-guestbook.example.com"} actualURLs := info.NetworkingInfo.ExternalURLs sort.Strings(expectedExternalUrls) sort.Strings(actualURLs) diff --git a/controller/clusterinfoupdater.go b/controller/clusterinfoupdater.go index a2f488534aeb0f..d87cdad6be85d0 100644 --- a/controller/clusterinfoupdater.go +++ b/controller/clusterinfoupdater.go @@ -3,6 +3,7 @@ package controller import ( "context" "fmt" + "github.com/argoproj/argo-cd/v2/common" "time" "github.com/argoproj/argo-cd/v2/util/env" @@ -101,8 +102,11 @@ func (c *clusterInfoUpdater) updateClusters() { } _ = kube.RunAllAsync(len(clustersFiltered), func(i int) error { cluster := clustersFiltered[i] - if err := c.updateClusterInfo(ctx, cluster, infoByServer[cluster.Server]); err != nil { - log.Warnf("Failed to save clusters info: %v", err) + clusterInfo := infoByServer[cluster.Server] + if err := c.updateClusterInfo(ctx, cluster, clusterInfo); err != nil { + log.Warnf("Failed to save cluster info: %v", err) + } else if err := updateClusterLabels(ctx, clusterInfo, cluster, c.db.UpdateCluster); err != nil { + log.Warnf("Failed to update cluster labels: %v", err) } return nil }) @@ -114,6 +118,12 @@ func (c *clusterInfoUpdater) updateClusterInfo(ctx context.Context, cluster appv if err != nil { return fmt.Errorf("error while fetching the apps list: %w", err) } + + updated := c.getUpdatedClusterInfo(ctx, apps, cluster, info, metav1.Now()) + return c.cache.SetClusterInfo(cluster.Server, &updated) +} + +func (c *clusterInfoUpdater) getUpdatedClusterInfo(ctx context.Context, apps []*appv1.Application, cluster appv1.Cluster, info *cache.ClusterInfo, now metav1.Time) appv1.ClusterInfo { var appCount int64 for _, a := range apps { if c.projGetter != nil { @@ -129,7 +139,6 @@ func (c *clusterInfoUpdater) updateClusterInfo(ctx context.Context, cluster appv appCount += 1 } } - now := metav1.Now() clusterInfo := appv1.ClusterInfo{ ConnectionState: appv1.ConnectionState{ModifiedAt: &now}, ApplicationsCount: appCount, @@ -156,5 +165,15 @@ func (c *clusterInfoUpdater) updateClusterInfo(ctx context.Context, cluster appv } } - return c.cache.SetClusterInfo(cluster.Server, &clusterInfo) + return clusterInfo +} + +func updateClusterLabels(ctx context.Context, clusterInfo *cache.ClusterInfo, cluster appv1.Cluster, updateCluster func(context.Context, *appv1.Cluster) (*appv1.Cluster, error)) error { + if clusterInfo != nil && cluster.Labels[common.LabelKeyAutoLabelClusterInfo] == "true" && cluster.Labels[common.LabelKeyClusterKubernetesVersion] != clusterInfo.K8SVersion { + cluster.Labels[common.LabelKeyClusterKubernetesVersion] = clusterInfo.K8SVersion + _, err := updateCluster(ctx, &cluster) + return err + } + + return nil } diff --git a/controller/clusterinfoupdater_test.go b/controller/clusterinfoupdater_test.go index bac0bb56cbe088..d11d4412bf30ce 100644 --- a/controller/clusterinfoupdater_test.go +++ b/controller/clusterinfoupdater_test.go @@ -2,6 +2,7 @@ package controller import ( "context" + "errors" "fmt" "testing" "time" @@ -98,3 +99,92 @@ func TestClusterSecretUpdater(t *testing.T) { assert.Equal(t, test.ExpectedStatus, clusterInfo.ConnectionState.Status) } } + +func TestUpdateClusterLabels(t *testing.T) { + shouldNotBeInvoked := func(ctx context.Context, cluster *v1alpha1.Cluster) (*v1alpha1.Cluster, error) { + shouldNotHappen := errors.New("if an error happens here, something's wrong") + assert.NoError(t, shouldNotHappen) + return nil, shouldNotHappen + } + tests := []struct { + name string + clusterInfo *clustercache.ClusterInfo + cluster v1alpha1.Cluster + updateCluster func(context.Context, *v1alpha1.Cluster) (*v1alpha1.Cluster, error) + wantErr assert.ErrorAssertionFunc + }{ + { + "enableClusterInfoLabels = false", + &clustercache.ClusterInfo{ + Server: "kubernetes.svc.local", + K8SVersion: "1.28", + }, + v1alpha1.Cluster{ + Server: "kubernetes.svc.local", + Labels: nil, + }, + shouldNotBeInvoked, + assert.NoError, + }, + { + "clusterInfo = nil", + nil, + v1alpha1.Cluster{ + Server: "kubernetes.svc.local", + Labels: map[string]string{"argocd.argoproj.io/auto-label-cluster-info": "true"}, + }, + shouldNotBeInvoked, + assert.NoError, + }, + { + "clusterInfo.k8sversion == cluster k8s label", + &clustercache.ClusterInfo{ + Server: "kubernetes.svc.local", + K8SVersion: "1.28", + }, + v1alpha1.Cluster{ + Server: "kubernetes.svc.local", + Labels: map[string]string{"argocd.argoproj.io/kubernetes-version": "1.28", "argocd.argoproj.io/auto-label-cluster-info": "true"}, + }, + shouldNotBeInvoked, + assert.NoError, + }, + { + "clusterInfo.k8sversion != cluster k8s label, no error", + &clustercache.ClusterInfo{ + Server: "kubernetes.svc.local", + K8SVersion: "1.28", + }, + v1alpha1.Cluster{ + Server: "kubernetes.svc.local", + Labels: map[string]string{"argocd.argoproj.io/kubernetes-version": "1.27", "argocd.argoproj.io/auto-label-cluster-info": "true"}, + }, + func(ctx context.Context, cluster *v1alpha1.Cluster) (*v1alpha1.Cluster, error) { + assert.Equal(t, cluster.Labels["argocd.argoproj.io/kubernetes-version"], "1.28") + return nil, nil + }, + assert.NoError, + }, + { + "clusterInfo.k8sversion != cluster k8s label, some error", + &clustercache.ClusterInfo{ + Server: "kubernetes.svc.local", + K8SVersion: "1.28", + }, + v1alpha1.Cluster{ + Server: "kubernetes.svc.local", + Labels: map[string]string{"argocd.argoproj.io/kubernetes-version": "1.27", "argocd.argoproj.io/auto-label-cluster-info": "true"}, + }, + func(ctx context.Context, cluster *v1alpha1.Cluster) (*v1alpha1.Cluster, error) { + assert.Equal(t, cluster.Labels["argocd.argoproj.io/kubernetes-version"], "1.28") + return nil, errors.New("some error happened while saving") + }, + assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.wantErr(t, updateClusterLabels(context.Background(), tt.clusterInfo, tt.cluster, tt.updateCluster), fmt.Sprintf("updateClusterLabels(%v, %v, %v)", context.Background(), tt.clusterInfo, tt.cluster)) + }) + } +} diff --git a/controller/hook.go b/controller/hook.go new file mode 100644 index 00000000000000..0c019ac6a1e086 --- /dev/null +++ b/controller/hook.go @@ -0,0 +1,158 @@ +package controller + +import ( + "context" + + "github.com/argoproj/gitops-engine/pkg/health" + "github.com/argoproj/gitops-engine/pkg/sync/common" + "github.com/argoproj/gitops-engine/pkg/sync/hook" + "github.com/argoproj/gitops-engine/pkg/utils/kube" + log "github.com/sirupsen/logrus" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/rest" + + "github.com/argoproj/argo-cd/v2/util/lua" + + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" +) + +var ( + postDeleteHook = "PostDelete" + postDeleteHooks = map[string]string{ + "argocd.argoproj.io/hook": postDeleteHook, + "helm.sh/hook": "post-delete", + } +) + +func isHook(obj *unstructured.Unstructured) bool { + return hook.IsHook(obj) || isPostDeleteHook(obj) +} + +func isPostDeleteHook(obj *unstructured.Unstructured) bool { + if obj == nil || obj.GetAnnotations() == nil { + return false + } + for k, v := range postDeleteHooks { + if val, ok := obj.GetAnnotations()[k]; ok && val == v { + return true + } + } + return false +} + +func (ctrl *ApplicationController) executePostDeleteHooks(app *v1alpha1.Application, proj *v1alpha1.AppProject, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) { + appLabelKey, err := ctrl.settingsMgr.GetAppInstanceLabelKey() + if err != nil { + return false, err + } + var revisions []string + for _, src := range app.Spec.GetSources() { + revisions = append(revisions, src.TargetRevision) + } + + targets, _, err := ctrl.appStateManager.GetRepoObjs(app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj) + if err != nil { + return false, err + } + runningHooks := map[kube.ResourceKey]*unstructured.Unstructured{} + for key, obj := range liveObjs { + if isPostDeleteHook(obj) { + runningHooks[key] = obj + } + } + + expectedHook := map[kube.ResourceKey]*unstructured.Unstructured{} + for _, obj := range targets { + if obj.GetNamespace() == "" { + obj.SetNamespace(app.Spec.Destination.Namespace) + } + if !isPostDeleteHook(obj) { + continue + } + if runningHook := runningHooks[kube.GetResourceKey(obj)]; runningHook == nil { + expectedHook[kube.GetResourceKey(obj)] = obj + } + } + createdCnt := 0 + for _, obj := range expectedHook { + _, err = ctrl.kubectl.CreateResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), obj, v1.CreateOptions{}) + if err != nil { + return false, err + } + createdCnt++ + } + if createdCnt > 0 { + logCtx.Infof("Created %d post-delete hooks", createdCnt) + return false, nil + } + resourceOverrides, err := ctrl.settingsMgr.GetResourceOverrides() + if err != nil { + return false, err + } + healthOverrides := lua.ResourceHealthOverrides(resourceOverrides) + + progressingHooksCnt := 0 + for _, obj := range runningHooks { + hookHealth, err := health.GetResourceHealth(obj, healthOverrides) + if err != nil { + return false, err + } + if hookHealth.Status == health.HealthStatusProgressing { + progressingHooksCnt++ + } + } + if progressingHooksCnt > 0 { + logCtx.Infof("Waiting for %d post-delete hooks to complete", progressingHooksCnt) + return false, nil + } + + return true, nil +} + +func (ctrl *ApplicationController) cleanupPostDeleteHooks(liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) { + resourceOverrides, err := ctrl.settingsMgr.GetResourceOverrides() + if err != nil { + return false, err + } + healthOverrides := lua.ResourceHealthOverrides(resourceOverrides) + + pendingDeletionCount := 0 + aggregatedHealth := health.HealthStatusHealthy + var hooks []*unstructured.Unstructured + for _, obj := range liveObjs { + if !isPostDeleteHook(obj) { + continue + } + hookHealth, err := health.GetResourceHealth(obj, healthOverrides) + if err != nil { + return false, err + } + if health.IsWorse(aggregatedHealth, hookHealth.Status) { + aggregatedHealth = hookHealth.Status + } + hooks = append(hooks, obj) + } + + for _, obj := range hooks { + for _, policy := range hook.DeletePolicies(obj) { + if policy == common.HookDeletePolicyHookFailed && aggregatedHealth == health.HealthStatusDegraded || policy == common.HookDeletePolicyHookSucceeded && aggregatedHealth == health.HealthStatusHealthy { + pendingDeletionCount++ + if obj.GetDeletionTimestamp() != nil { + continue + } + logCtx.Infof("Deleting post-delete hook %s/%s", obj.GetNamespace(), obj.GetName()) + err = ctrl.kubectl.DeleteResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), v1.DeleteOptions{}) + if err != nil { + return false, err + } + } + } + + } + if pendingDeletionCount > 0 { + logCtx.Infof("Waiting for %d post-delete hooks to be deleted", pendingDeletionCount) + return false, nil + } + return true, nil +} diff --git a/controller/metrics/metrics.go b/controller/metrics/metrics.go index e4ef09552c09d2..94405b51eac756 100644 --- a/controller/metrics/metrics.go +++ b/controller/metrics/metrics.go @@ -23,6 +23,8 @@ import ( "github.com/argoproj/argo-cd/v2/util/git" "github.com/argoproj/argo-cd/v2/util/healthz" "github.com/argoproj/argo-cd/v2/util/profile" + + ctrl_metrics "sigs.k8s.io/controller-runtime/pkg/metrics" ) type MetricsServer struct { @@ -160,12 +162,12 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, appFil mux := http.NewServeMux() registry := NewAppRegistry(appLister, appFilter, appLabels) - registry.MustRegister(depth, adds, latency, workDuration, unfinished, longestRunningProcessor, retries) + mux.Handle(MetricsPath, promhttp.HandlerFor(prometheus.Gatherers{ // contains app controller specific metrics registry, - // contains process, golang and controller workqueues metrics - prometheus.DefaultGatherer, + // contains workqueue metrics, process and golang metrics + ctrl_metrics.Registry, }, promhttp.HandlerOpts{})) profile.RegisterProfiler(mux) healthz.ServeHealthCheck(mux, healthCheck) diff --git a/controller/metrics/metrics_test.go b/controller/metrics/metrics_test.go index 61a99a46492a20..23628c38347a5a 100644 --- a/controller/metrics/metrics_test.go +++ b/controller/metrics/metrics_test.go @@ -2,6 +2,7 @@ package metrics import ( "context" + "fmt" "log" "net/http" "net/http/httptest" @@ -15,12 +16,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" "sigs.k8s.io/yaml" argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake" appinformer "github.com/argoproj/argo-cd/v2/pkg/client/informers/externalversions" applister "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1" + + "sigs.k8s.io/controller-runtime/pkg/controller" ) const fakeApp = ` @@ -140,6 +144,12 @@ var appFilter = func(obj interface{}) bool { return true } +func init() { + // Create a fake controller so we initialize the internal controller metrics. + // https://github.com/kubernetes-sigs/controller-runtime/blob/4000e996a202917ad7d40f02ed8a2079a9ce25e9/pkg/internal/controller/metrics/metrics.go + _, _ = controller.New("test-controller", nil, controller.Options{}) +} + func newFakeApp(fakeAppYAML string) *argoappv1.Application { var app argoappv1.Application err := yaml.Unmarshal([]byte(fakeAppYAML), &app) @@ -360,7 +370,7 @@ func assertMetricsPrinted(t *testing.T, expectedLines, body string) { if line == "" { continue } - assert.Contains(t, body, line, "expected metrics mismatch") + assert.Contains(t, body, line, fmt.Sprintf("expected metrics mismatch for line: %s", line)) } } @@ -443,3 +453,70 @@ argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespa err = metricsServ.SetExpiration(time.Second) assert.Error(t, err) } + +func TestWorkqueueMetrics(t *testing.T) { + cancel, appLister := newFakeLister() + defer cancel() + metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}) + assert.NoError(t, err) + + expectedMetrics := ` +# TYPE workqueue_adds_total counter +workqueue_adds_total{name="test"} + +# TYPE workqueue_depth gauge +workqueue_depth{name="test"} + +# TYPE workqueue_longest_running_processor_seconds gauge +workqueue_longest_running_processor_seconds{name="test"} + +# TYPE workqueue_queue_duration_seconds histogram + +# TYPE workqueue_unfinished_work_seconds gauge +workqueue_unfinished_work_seconds{name="test"} + +# TYPE workqueue_work_duration_seconds histogram +` + workqueue.NewNamed("test") + + req, err := http.NewRequest(http.MethodGet, "/metrics", nil) + assert.NoError(t, err) + rr := httptest.NewRecorder() + metricsServ.Handler.ServeHTTP(rr, req) + assert.Equal(t, rr.Code, http.StatusOK) + body := rr.Body.String() + log.Println(body) + assertMetricsPrinted(t, expectedMetrics, body) +} + +func TestGoMetrics(t *testing.T) { + cancel, appLister := newFakeLister() + defer cancel() + metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck, []string{}) + assert.NoError(t, err) + + expectedMetrics := ` +# TYPE go_gc_duration_seconds summary +go_gc_duration_seconds_sum +go_gc_duration_seconds_count +# TYPE go_goroutines gauge +go_goroutines +# TYPE go_info gauge +go_info +# TYPE go_memstats_alloc_bytes gauge +go_memstats_alloc_bytes +# TYPE go_memstats_sys_bytes gauge +go_memstats_sys_bytes +# TYPE go_threads gauge +go_threads +` + + req, err := http.NewRequest(http.MethodGet, "/metrics", nil) + assert.NoError(t, err) + rr := httptest.NewRecorder() + metricsServ.Handler.ServeHTTP(rr, req) + assert.Equal(t, rr.Code, http.StatusOK) + body := rr.Body.String() + log.Println(body) + assertMetricsPrinted(t, expectedMetrics, body) +} diff --git a/controller/metrics/workqueue.go b/controller/metrics/workqueue.go deleted file mode 100644 index 2ef10685ee47d5..00000000000000 --- a/controller/metrics/workqueue.go +++ /dev/null @@ -1,101 +0,0 @@ -package metrics - -import ( - "github.com/prometheus/client_golang/prometheus" - "k8s.io/client-go/util/workqueue" -) - -const ( - WorkQueueSubsystem = "workqueue" - DepthKey = "depth" - AddsKey = "adds_total" - QueueLatencyKey = "queue_duration_seconds" - WorkDurationKey = "work_duration_seconds" - UnfinishedWorkKey = "unfinished_work_seconds" - LongestRunningProcessorKey = "longest_running_processor_seconds" - RetriesKey = "retries_total" -) - -var ( - depth = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Subsystem: WorkQueueSubsystem, - Name: DepthKey, - Help: "Current depth of workqueue", - }, []string{"name"}) - - adds = prometheus.NewCounterVec(prometheus.CounterOpts{ - Subsystem: WorkQueueSubsystem, - Name: AddsKey, - Help: "Total number of adds handled by workqueue", - }, []string{"name"}) - - latency = prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Subsystem: WorkQueueSubsystem, - Name: QueueLatencyKey, - Help: "How long in seconds an item stays in workqueue before being requested", - Buckets: []float64{1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 5, 10, 15, 30, 60, 120, 180}, - }, []string{"name"}) - - workDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ - Subsystem: WorkQueueSubsystem, - Name: WorkDurationKey, - Help: "How long in seconds processing an item from workqueue takes.", - Buckets: []float64{1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 5, 10, 15, 30, 60, 120, 180}, - }, []string{"name"}) - - unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Subsystem: WorkQueueSubsystem, - Name: UnfinishedWorkKey, - Help: "How many seconds of work has been done that " + - "is in progress and hasn't been observed by work_duration. Large " + - "values indicate stuck threads. One can deduce the number of stuck " + - "threads by observing the rate at which this increases.", - }, []string{"name"}) - - longestRunningProcessor = prometheus.NewGaugeVec(prometheus.GaugeOpts{ - Subsystem: WorkQueueSubsystem, - Name: LongestRunningProcessorKey, - Help: "How many seconds has the longest running " + - "processor for workqueue been running.", - }, []string{"name"}) - - retries = prometheus.NewCounterVec(prometheus.CounterOpts{ - Subsystem: WorkQueueSubsystem, - Name: RetriesKey, - Help: "Total number of retries handled by workqueue", - }, []string{"name"}) -) - -func init() { - workqueue.SetProvider(workqueueMetricsProvider{}) -} - -type workqueueMetricsProvider struct{} - -func (workqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric { - return depth.WithLabelValues(name) -} - -func (workqueueMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric { - return adds.WithLabelValues(name) -} - -func (workqueueMetricsProvider) NewLatencyMetric(name string) workqueue.HistogramMetric { - return latency.WithLabelValues(name) -} - -func (workqueueMetricsProvider) NewWorkDurationMetric(name string) workqueue.HistogramMetric { - return workDuration.WithLabelValues(name) -} - -func (workqueueMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric { - return unfinished.WithLabelValues(name) -} - -func (workqueueMetricsProvider) NewLongestRunningProcessorSecondsMetric(name string) workqueue.SettableGaugeMetric { - return longestRunningProcessor.WithLabelValues(name) -} - -func (workqueueMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric { - return retries.WithLabelValues(name) -} diff --git a/controller/sharding/cache.go b/controller/sharding/cache.go new file mode 100644 index 00000000000000..2f3ffcbcb95c66 --- /dev/null +++ b/controller/sharding/cache.go @@ -0,0 +1,264 @@ +package sharding + +import ( + "sync" + + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v2/util/db" + log "github.com/sirupsen/logrus" +) + +type ClusterShardingCache interface { + Init(clusters *v1alpha1.ClusterList, apps *v1alpha1.ApplicationList) + Add(c *v1alpha1.Cluster) + Delete(clusterServer string) + Update(oldCluster *v1alpha1.Cluster, newCluster *v1alpha1.Cluster) + AddApp(a *v1alpha1.Application) + DeleteApp(a *v1alpha1.Application) + UpdateApp(a *v1alpha1.Application) + IsManagedCluster(c *v1alpha1.Cluster) bool + GetDistribution() map[string]int + GetAppDistribution() map[string]int +} + +type ClusterSharding struct { + Shard int + Replicas int + Shards map[string]int + Clusters map[string]*v1alpha1.Cluster + Apps map[string]*v1alpha1.Application + lock sync.RWMutex + getClusterShard DistributionFunction +} + +func NewClusterSharding(_ db.ArgoDB, shard, replicas int, shardingAlgorithm string) ClusterShardingCache { + log.Debugf("Processing clusters from shard %d: Using filter function: %s", shard, shardingAlgorithm) + clusterSharding := &ClusterSharding{ + Shard: shard, + Replicas: replicas, + Shards: make(map[string]int), + Clusters: make(map[string]*v1alpha1.Cluster), + Apps: make(map[string]*v1alpha1.Application), + } + distributionFunction := NoShardingDistributionFunction() + if replicas > 1 { + log.Debugf("Processing clusters from shard %d: Using filter function: %s", shard, shardingAlgorithm) + distributionFunction = GetDistributionFunction(clusterSharding.getClusterAccessor(), clusterSharding.getAppAccessor(), shardingAlgorithm, replicas) + } else { + log.Info("Processing all cluster shards") + } + clusterSharding.getClusterShard = distributionFunction + return clusterSharding +} + +// IsManagedCluster returns wheter or not the cluster should be processed by a given shard. +func (s *ClusterSharding) IsManagedCluster(c *v1alpha1.Cluster) bool { + s.lock.RLock() + defer s.lock.RUnlock() + if c == nil { // nil cluster (in-cluster) is always managed by current clusterShard + return true + } + clusterShard := 0 + if shard, ok := s.Shards[c.Server]; ok { + clusterShard = shard + } else { + log.Warnf("The cluster %s has no assigned shard.", c.Server) + } + log.Debugf("Checking if cluster %s with clusterShard %d should be processed by shard %d", c.Server, clusterShard, s.Shard) + return clusterShard == s.Shard +} + +func (sharding *ClusterSharding) Init(clusters *v1alpha1.ClusterList, apps *v1alpha1.ApplicationList) { + sharding.lock.Lock() + defer sharding.lock.Unlock() + newClusters := make(map[string]*v1alpha1.Cluster, len(clusters.Items)) + for _, c := range clusters.Items { + cluster := c + newClusters[c.Server] = &cluster + } + sharding.Clusters = newClusters + + newApps := make(map[string]*v1alpha1.Application, len(apps.Items)) + for i := range apps.Items { + app := apps.Items[i] + newApps[app.Name] = &app + } + sharding.Apps = newApps + sharding.updateDistribution() +} + +func (sharding *ClusterSharding) Add(c *v1alpha1.Cluster) { + sharding.lock.Lock() + defer sharding.lock.Unlock() + + old, ok := sharding.Clusters[c.Server] + sharding.Clusters[c.Server] = c + if !ok || hasShardingUpdates(old, c) { + sharding.updateDistribution() + } else { + log.Debugf("Skipping sharding distribution update. Cluster already added") + } +} + +func (sharding *ClusterSharding) Delete(clusterServer string) { + sharding.lock.Lock() + defer sharding.lock.Unlock() + if _, ok := sharding.Clusters[clusterServer]; ok { + delete(sharding.Clusters, clusterServer) + delete(sharding.Shards, clusterServer) + sharding.updateDistribution() + } +} + +func (sharding *ClusterSharding) Update(oldCluster *v1alpha1.Cluster, newCluster *v1alpha1.Cluster) { + sharding.lock.Lock() + defer sharding.lock.Unlock() + + if _, ok := sharding.Clusters[oldCluster.Server]; ok && oldCluster.Server != newCluster.Server { + delete(sharding.Clusters, oldCluster.Server) + delete(sharding.Shards, oldCluster.Server) + } + sharding.Clusters[newCluster.Server] = newCluster + if hasShardingUpdates(oldCluster, newCluster) { + sharding.updateDistribution() + } else { + log.Debugf("Skipping sharding distribution update. No relevant changes") + } +} + +func (sharding *ClusterSharding) GetDistribution() map[string]int { + sharding.lock.RLock() + defer sharding.lock.RUnlock() + shards := sharding.Shards + + distribution := make(map[string]int, len(shards)) + for k, v := range shards { + distribution[k] = v + } + return distribution +} + +func (sharding *ClusterSharding) updateDistribution() { + for k, c := range sharding.Clusters { + shard := 0 + if c.Shard != nil { + requestedShard := int(*c.Shard) + if requestedShard < sharding.Replicas { + shard = requestedShard + } else { + log.Warnf("Specified cluster shard (%d) for cluster: %s is greater than the number of available shard (%d). Using shard 0.", requestedShard, c.Server, sharding.Replicas) + } + } else { + shard = sharding.getClusterShard(c) + } + + existingShard, ok := sharding.Shards[k] + if ok && existingShard != shard { + log.Infof("Cluster %s has changed shard from %d to %d", k, existingShard, shard) + } else if !ok { + log.Infof("Cluster %s has been assigned to shard %d", k, shard) + } else { + log.Debugf("Cluster %s has not changed shard", k) + } + sharding.Shards[k] = shard + } +} + +// hasShardingUpdates returns true if the sharding distribution has explicitly changed +func hasShardingUpdates(old, new *v1alpha1.Cluster) bool { + if old == nil || new == nil { + return false + } + + // returns true if the cluster id has changed because some sharding algorithms depend on it. + if old.ID != new.ID { + return true + } + + if old.Server != new.Server { + return true + } + + // return false if the shard field has not been modified + if old.Shard == nil && new.Shard == nil { + return false + } + return old.Shard == nil || new.Shard == nil || int64(*old.Shard) != int64(*new.Shard) +} + +// A read lock should be acquired before calling getClusterAccessor. +func (d *ClusterSharding) getClusterAccessor() clusterAccessor { + return func() []*v1alpha1.Cluster { + // no need to lock, as this is only called from the updateDistribution function + clusters := make([]*v1alpha1.Cluster, 0, len(d.Clusters)) + for _, c := range d.Clusters { + clusters = append(clusters, c) + } + return clusters + } +} + +// A read lock should be acquired before calling getAppAccessor. +func (d *ClusterSharding) getAppAccessor() appAccessor { + return func() []*v1alpha1.Application { + apps := make([]*v1alpha1.Application, 0, len(d.Apps)) + for _, a := range d.Apps { + apps = append(apps, a) + } + return apps + } +} + +func (sharding *ClusterSharding) AddApp(a *v1alpha1.Application) { + sharding.lock.Lock() + defer sharding.lock.Unlock() + + _, ok := sharding.Apps[a.Name] + sharding.Apps[a.Name] = a + if !ok { + sharding.updateDistribution() + } else { + log.Debugf("Skipping sharding distribution update. App already added") + } +} + +func (sharding *ClusterSharding) DeleteApp(a *v1alpha1.Application) { + sharding.lock.Lock() + defer sharding.lock.Unlock() + if _, ok := sharding.Apps[a.Name]; ok { + delete(sharding.Apps, a.Name) + sharding.updateDistribution() + } +} + +func (sharding *ClusterSharding) UpdateApp(a *v1alpha1.Application) { + sharding.lock.Lock() + defer sharding.lock.Unlock() + + _, ok := sharding.Apps[a.Name] + sharding.Apps[a.Name] = a + if !ok { + sharding.updateDistribution() + } else { + log.Debugf("Skipping sharding distribution update. No relevant changes") + } +} + +// GetAppDistribution should be not be called from a DestributionFunction because +// it could cause a deadlock when updateDistribution is called. +func (sharding *ClusterSharding) GetAppDistribution() map[string]int { + sharding.lock.RLock() + clusters := sharding.Clusters + apps := sharding.Apps + sharding.lock.RUnlock() + + appDistribution := make(map[string]int, len(clusters)) + + for _, a := range apps { + if _, ok := appDistribution[a.Spec.Destination.Server]; !ok { + appDistribution[a.Spec.Destination.Server] = 0 + } + appDistribution[a.Spec.Destination.Server]++ + } + return appDistribution +} diff --git a/controller/sharding/cache_test.go b/controller/sharding/cache_test.go new file mode 100644 index 00000000000000..f7798c31e36082 --- /dev/null +++ b/controller/sharding/cache_test.go @@ -0,0 +1,511 @@ +package sharding + +import ( + "testing" + + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks" + "github.com/stretchr/testify/assert" +) + +func setupTestSharding(shard int, replicas int) *ClusterSharding { + shardingAlgorithm := "legacy" // we are using the legacy algorithm as it is deterministic based on the cluster id which is easier to test + db := &dbmocks.ArgoDB{} + return NewClusterSharding(db, shard, replicas, shardingAlgorithm).(*ClusterSharding) +} + +func TestNewClusterSharding(t *testing.T) { + shard := 1 + replicas := 2 + sharding := setupTestSharding(shard, replicas) + + assert.NotNil(t, sharding) + assert.Equal(t, shard, sharding.Shard) + assert.Equal(t, replicas, sharding.Replicas) + assert.NotNil(t, sharding.Shards) + assert.NotNil(t, sharding.Clusters) +} + +func TestClusterSharding_Add(t *testing.T) { + shard := 1 + replicas := 2 + sharding := setupTestSharding(shard, replicas) + + clusterA := &v1alpha1.Cluster{ + ID: "2", + Server: "https://127.0.0.1:6443", + } + + sharding.Add(clusterA) + + clusterB := v1alpha1.Cluster{ + ID: "1", + Server: "https://kubernetes.default.svc", + } + + sharding.Add(&clusterB) + + distribution := sharding.GetDistribution() + + assert.Contains(t, sharding.Clusters, clusterA.Server) + assert.Contains(t, sharding.Clusters, clusterB.Server) + + clusterDistribution, ok := distribution[clusterA.Server] + assert.True(t, ok) + assert.Equal(t, 1, clusterDistribution) + + myClusterDistribution, ok := distribution[clusterB.Server] + assert.True(t, ok) + assert.Equal(t, 0, myClusterDistribution) + + assert.Equal(t, 2, len(distribution)) +} + +func TestClusterSharding_AddRoundRobin_Redistributes(t *testing.T) { + shard := 1 + replicas := 2 + + db := &dbmocks.ArgoDB{} + + sharding := NewClusterSharding(db, shard, replicas, "round-robin").(*ClusterSharding) + + clusterA := &v1alpha1.Cluster{ + ID: "1", + Server: "https://127.0.0.1:6443", + } + sharding.Add(clusterA) + + clusterB := v1alpha1.Cluster{ + ID: "3", + Server: "https://kubernetes.default.svc", + } + sharding.Add(&clusterB) + + distributionBefore := sharding.GetDistribution() + + assert.Contains(t, sharding.Clusters, clusterA.Server) + assert.Contains(t, sharding.Clusters, clusterB.Server) + + clusterDistributionA, ok := distributionBefore[clusterA.Server] + assert.True(t, ok) + assert.Equal(t, 0, clusterDistributionA) + + clusterDistributionB, ok := distributionBefore[clusterB.Server] + assert.True(t, ok) + assert.Equal(t, 1, clusterDistributionB) + + assert.Equal(t, 2, len(distributionBefore)) + + clusterC := v1alpha1.Cluster{ + ID: "2", + Server: "https://1.1.1.1", + } + sharding.Add(&clusterC) + + distributionAfter := sharding.GetDistribution() + + assert.Contains(t, sharding.Clusters, clusterA.Server) + assert.Contains(t, sharding.Clusters, clusterB.Server) + assert.Contains(t, sharding.Clusters, clusterC.Server) + + clusterDistributionA, ok = distributionAfter[clusterA.Server] + assert.True(t, ok) + assert.Equal(t, 0, clusterDistributionA) + + clusterDistributionC, ok := distributionAfter[clusterC.Server] + assert.True(t, ok) + assert.Equal(t, 1, clusterDistributionC) // will be assigned to shard 1 because the .ID is smaller then the "B" cluster + + clusterDistributionB, ok = distributionAfter[clusterB.Server] + assert.True(t, ok) + assert.Equal(t, 0, clusterDistributionB) // will be reassigned to shard 0 because the .ID is bigger then the "C" cluster +} + +func TestClusterSharding_Delete(t *testing.T) { + shard := 1 + replicas := 2 + sharding := setupTestSharding(shard, replicas) + + sharding.Init( + &v1alpha1.ClusterList{ + Items: []v1alpha1.Cluster{ + { + ID: "2", + Server: "https://127.0.0.1:6443", + }, + { + ID: "1", + Server: "https://kubernetes.default.svc", + }, + }, + }, + &v1alpha1.ApplicationList{ + Items: []v1alpha1.Application{ + createApp("app2", "https://127.0.0.1:6443"), + createApp("app1", "https://kubernetes.default.svc"), + }, + }, + ) + + sharding.Delete("https://kubernetes.default.svc") + distribution := sharding.GetDistribution() + assert.Equal(t, 1, len(distribution)) +} + +func TestClusterSharding_Update(t *testing.T) { + shard := 1 + replicas := 2 + sharding := setupTestSharding(shard, replicas) + + sharding.Init( + &v1alpha1.ClusterList{ + Items: []v1alpha1.Cluster{ + { + ID: "2", + Server: "https://127.0.0.1:6443", + }, + { + ID: "1", + Server: "https://kubernetes.default.svc", + }, + }, + }, + &v1alpha1.ApplicationList{ + Items: []v1alpha1.Application{ + createApp("app2", "https://127.0.0.1:6443"), + createApp("app1", "https://kubernetes.default.svc"), + }, + }, + ) + + distributionBefore := sharding.GetDistribution() + assert.Equal(t, 2, len(distributionBefore)) + + distributionA, ok := distributionBefore["https://kubernetes.default.svc"] + assert.True(t, ok) + assert.Equal(t, 0, distributionA) + + sharding.Update(&v1alpha1.Cluster{ + ID: "1", + Server: "https://kubernetes.default.svc", + }, &v1alpha1.Cluster{ + ID: "4", + Server: "https://kubernetes.default.svc", + }) + + distributionAfter := sharding.GetDistribution() + assert.Equal(t, 2, len(distributionAfter)) + + distributionA, ok = distributionAfter["https://kubernetes.default.svc"] + assert.True(t, ok) + assert.Equal(t, 1, distributionA) +} + +func TestClusterSharding_UpdateServerName(t *testing.T) { + shard := 1 + replicas := 2 + sharding := setupTestSharding(shard, replicas) + + sharding.Init( + &v1alpha1.ClusterList{ + Items: []v1alpha1.Cluster{ + { + ID: "2", + Server: "https://127.0.0.1:6443", + }, + { + ID: "1", + Server: "https://kubernetes.default.svc", + }, + }, + }, + &v1alpha1.ApplicationList{ + Items: []v1alpha1.Application{ + createApp("app2", "https://127.0.0.1:6443"), + createApp("app1", "https://kubernetes.default.svc"), + }, + }, + ) + + distributionBefore := sharding.GetDistribution() + assert.Equal(t, 2, len(distributionBefore)) + + distributionA, ok := distributionBefore["https://kubernetes.default.svc"] + assert.True(t, ok) + assert.Equal(t, 0, distributionA) + + sharding.Update(&v1alpha1.Cluster{ + ID: "1", + Server: "https://kubernetes.default.svc", + }, &v1alpha1.Cluster{ + ID: "1", + Server: "https://server2", + }) + + distributionAfter := sharding.GetDistribution() + assert.Equal(t, 2, len(distributionAfter)) + + _, ok = distributionAfter["https://kubernetes.default.svc"] + assert.False(t, ok) // the old server name should not be present anymore + + _, ok = distributionAfter["https://server2"] + assert.True(t, ok) // the new server name should be present +} + +func TestClusterSharding_IsManagedCluster(t *testing.T) { + replicas := 2 + sharding0 := setupTestSharding(0, replicas) + + sharding0.Init( + &v1alpha1.ClusterList{ + Items: []v1alpha1.Cluster{ + { + ID: "1", + Server: "https://kubernetes.default.svc", + }, + { + ID: "2", + Server: "https://127.0.0.1:6443", + }, + }, + }, + &v1alpha1.ApplicationList{ + Items: []v1alpha1.Application{ + createApp("app2", "https://127.0.0.1:6443"), + createApp("app1", "https://kubernetes.default.svc"), + }, + }, + ) + + assert.True(t, sharding0.IsManagedCluster(&v1alpha1.Cluster{ + ID: "1", + Server: "https://kubernetes.default.svc", + })) + + assert.False(t, sharding0.IsManagedCluster(&v1alpha1.Cluster{ + ID: "2", + Server: "https://127.0.0.1:6443", + })) + + sharding1 := setupTestSharding(1, replicas) + + sharding1.Init( + &v1alpha1.ClusterList{ + Items: []v1alpha1.Cluster{ + { + ID: "2", + Server: "https://127.0.0.1:6443", + }, + { + ID: "1", + Server: "https://kubernetes.default.svc", + }, + }, + }, + &v1alpha1.ApplicationList{ + Items: []v1alpha1.Application{ + createApp("app2", "https://127.0.0.1:6443"), + createApp("app1", "https://kubernetes.default.svc"), + }, + }, + ) + + assert.False(t, sharding1.IsManagedCluster(&v1alpha1.Cluster{ + ID: "1", + Server: "https://kubernetes.default.svc", + })) + + assert.True(t, sharding1.IsManagedCluster(&v1alpha1.Cluster{ + ID: "2", + Server: "https://127.0.0.1:6443", + })) + +} + +func TestClusterSharding_ClusterShardOfResourceShouldNotBeChanged(t *testing.T) { + shard := 1 + replicas := 2 + sharding := setupTestSharding(shard, replicas) + + Int64Ptr := func(i int64) *int64 { + return &i + } + + clusterWithNil := &v1alpha1.Cluster{ + ID: "2", + Server: "https://127.0.0.1:6443", + Shard: nil, + } + + clusterWithValue := &v1alpha1.Cluster{ + ID: "1", + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(1), + } + + clusterWithToBigValue := &v1alpha1.Cluster{ + ID: "3", + Server: "https://1.1.1.1", + Shard: Int64Ptr(999), // shard value is explicitly bigger than the number of replicas + } + + sharding.Init( + &v1alpha1.ClusterList{ + Items: []v1alpha1.Cluster{ + *clusterWithNil, + *clusterWithValue, + *clusterWithToBigValue, + }, + }, + &v1alpha1.ApplicationList{ + Items: []v1alpha1.Application{ + createApp("app2", "https://127.0.0.1:6443"), + createApp("app1", "https://kubernetes.default.svc"), + }, + }, + ) + distribution := sharding.GetDistribution() + assert.Equal(t, 3, len(distribution)) + + assert.Nil(t, sharding.Clusters[clusterWithNil.Server].Shard) + + assert.NotNil(t, sharding.Clusters[clusterWithValue.Server].Shard) + assert.Equal(t, int64(1), *sharding.Clusters[clusterWithValue.Server].Shard) + assert.Equal(t, 1, distribution[clusterWithValue.Server]) + + assert.NotNil(t, sharding.Clusters[clusterWithToBigValue.Server].Shard) + assert.Equal(t, int64(999), *sharding.Clusters[clusterWithToBigValue.Server].Shard) + assert.Equal(t, 0, distribution[clusterWithToBigValue.Server]) // will be assigned to shard 0 because the value is bigger than the number of replicas +} + +func TestHasShardingUpdates(t *testing.T) { + Int64Ptr := func(i int64) *int64 { + return &i + } + + testCases := []struct { + name string + old *v1alpha1.Cluster + new *v1alpha1.Cluster + expected bool + }{ + { + name: "No updates", + old: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(1), + }, + new: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(1), + }, + expected: false, + }, + { + name: "Updates", + old: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(1), + }, + new: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(2), + }, + expected: true, + }, + { + name: "Old is nil", + old: nil, + new: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(2), + }, + expected: false, + }, + { + name: "New is nil", + old: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(2), + }, + new: nil, + expected: false, + }, + { + name: "Both are nil", + old: nil, + new: nil, + expected: false, + }, + { + name: "Both shards are nil", + old: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: nil, + }, + new: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: nil, + }, + expected: false, + }, + { + name: "Old shard is nil", + old: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: nil, + }, + new: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(2), + }, + expected: true, + }, + { + name: "New shard is nil", + old: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(2), + }, + new: &v1alpha1.Cluster{ + Server: "https://kubernetes.default.svc", + Shard: nil, + }, + expected: true, + }, + { + name: "Cluster ID has changed", + old: &v1alpha1.Cluster{ + ID: "1", + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(2), + }, + new: &v1alpha1.Cluster{ + ID: "2", + Server: "https://kubernetes.default.svc", + Shard: Int64Ptr(2), + }, + expected: true, + }, + { + name: "Server has changed", + old: &v1alpha1.Cluster{ + ID: "1", + Server: "https://server1", + Shard: Int64Ptr(2), + }, + new: &v1alpha1.Cluster{ + ID: "1", + Server: "https://server2", + Shard: Int64Ptr(2), + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, hasShardingUpdates(tc.old, tc.new)) + }) + } +} diff --git a/controller/sharding/sharding.go b/controller/sharding/sharding.go index 526896531dbca2..e4af7010931c6f 100644 --- a/controller/sharding/sharding.go +++ b/controller/sharding/sharding.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "hash/fnv" + "math" "os" "sort" "strconv" @@ -20,6 +21,7 @@ import ( "github.com/argoproj/argo-cd/v2/util/db" "github.com/argoproj/argo-cd/v2/util/env" + "github.com/argoproj/argo-cd/v2/util/errors" "github.com/argoproj/argo-cd/v2/util/settings" log "github.com/sirupsen/logrus" kubeerrors "k8s.io/apimachinery/pkg/api/errors" @@ -40,6 +42,8 @@ const ShardControllerMappingKey = "shardControllerMapping" type DistributionFunction func(c *v1alpha1.Cluster) int type ClusterFilterFunction func(c *v1alpha1.Cluster) bool +type clusterAccessor func() []*v1alpha1.Cluster +type appAccessor func() []*v1alpha1.Application // shardApplicationControllerMapping stores the mapping of Shard Number to Application Controller in ConfigMap. // It also stores the heartbeat of last synced time of the application controller. @@ -53,8 +57,7 @@ type shardApplicationControllerMapping struct { // and returns wheter or not the cluster should be processed by a given shard. It calls the distributionFunction // to determine which shard will process the cluster, and if the given shard is equal to the calculated shard // the function will return true. -func GetClusterFilter(db db.ArgoDB, distributionFunction DistributionFunction, shard int) ClusterFilterFunction { - replicas := db.GetApplicationControllerReplicas() +func GetClusterFilter(db db.ArgoDB, distributionFunction DistributionFunction, replicas, shard int) ClusterFilterFunction { return func(c *v1alpha1.Cluster) bool { clusterShard := 0 if c != nil && c.Shard != nil { @@ -73,14 +76,14 @@ func GetClusterFilter(db db.ArgoDB, distributionFunction DistributionFunction, s // GetDistributionFunction returns which DistributionFunction should be used based on the passed algorithm and // the current datas. -func GetDistributionFunction(db db.ArgoDB, shardingAlgorithm string) DistributionFunction { - log.Infof("Using filter function: %s", shardingAlgorithm) - distributionFunction := LegacyDistributionFunction(db) +func GetDistributionFunction(clusters clusterAccessor, apps appAccessor, shardingAlgorithm string, replicasCount int) DistributionFunction { + log.Debugf("Using filter function: %s", shardingAlgorithm) + distributionFunction := LegacyDistributionFunction(replicasCount) switch shardingAlgorithm { case common.RoundRobinShardingAlgorithm: - distributionFunction = RoundRobinDistributionFunction(db) + distributionFunction = RoundRobinDistributionFunction(clusters, replicasCount) case common.LegacyShardingAlgorithm: - distributionFunction = LegacyDistributionFunction(db) + distributionFunction = LegacyDistributionFunction(replicasCount) default: log.Warnf("distribution type %s is not supported, defaulting to %s", shardingAlgorithm, common.DefaultShardingAlgorithm) } @@ -92,15 +95,21 @@ func GetDistributionFunction(db db.ArgoDB, shardingAlgorithm string) Distributio // is lightweight and can be distributed easily, however, it does not ensure an homogenous distribution as // some shards may get assigned more clusters than others. It is the legacy function distribution that is // kept for compatibility reasons -func LegacyDistributionFunction(db db.ArgoDB) DistributionFunction { - replicas := db.GetApplicationControllerReplicas() +func LegacyDistributionFunction(replicas int) DistributionFunction { return func(c *v1alpha1.Cluster) int { if replicas == 0 { + log.Debugf("Replicas count is : %d, returning -1", replicas) return -1 } if c == nil { + log.Debug("In-cluster: returning 0") return 0 } + // if Shard is manually set and the assigned value is lower than the number of replicas, + // then its value is returned otherwise it is the default calculated value + if c.Shard != nil && int(*c.Shard) < replicas { + return int(*c.Shard) + } id := c.ID log.Debugf("Calculating cluster shard for cluster id: %s", id) if id == "" { @@ -121,14 +130,19 @@ func LegacyDistributionFunction(db db.ArgoDB) DistributionFunction { // This function ensures an homogenous distribution: each shards got assigned the same number of // clusters +/-1 , but with the drawback of a reshuffling of clusters accross shards in case of some changes // in the cluster list -func RoundRobinDistributionFunction(db db.ArgoDB) DistributionFunction { - replicas := db.GetApplicationControllerReplicas() + +func RoundRobinDistributionFunction(clusters clusterAccessor, replicas int) DistributionFunction { return func(c *v1alpha1.Cluster) int { if replicas > 0 { if c == nil { // in-cluster does not necessarly have a secret assigned. So we are receiving a nil cluster here. return 0 + } + // if Shard is manually set and the assigned value is lower than the number of replicas, + // then its value is returned otherwise it is the default calculated value + if c.Shard != nil && int(*c.Shard) < replicas { + return int(*c.Shard) } else { - clusterIndexdByClusterIdMap := createClusterIndexByClusterIdMap(db) + clusterIndexdByClusterIdMap := createClusterIndexByClusterIdMap(clusters) clusterIndex, ok := clusterIndexdByClusterIdMap[c.ID] if !ok { log.Warnf("Cluster with id=%s not found in cluster map.", c.ID) @@ -144,6 +158,12 @@ func RoundRobinDistributionFunction(db db.ArgoDB) DistributionFunction { } } +// NoShardingDistributionFunction returns a DistributionFunction that will process all cluster by shard 0 +// the function is created for API compatibility purposes and is not supposed to be activated. +func NoShardingDistributionFunction() DistributionFunction { + return func(c *v1alpha1.Cluster) int { return 0 } +} + // InferShard extracts the shard index based on its hostname. func InferShard() (int, error) { hostname, err := osHostnameFunction() @@ -152,33 +172,29 @@ func InferShard() (int, error) { } parts := strings.Split(hostname, "-") if len(parts) == 0 { - return 0, fmt.Errorf("hostname should ends with shard number separated by '-' but got: %s", hostname) + log.Warnf("hostname should end with shard number separated by '-' but got: %s", hostname) + return 0, nil } shard, err := strconv.Atoi(parts[len(parts)-1]) if err != nil { - return 0, fmt.Errorf("hostname should ends with shard number separated by '-' but got: %s", hostname) + log.Warnf("hostname should end with shard number separated by '-' but got: %s", hostname) + return 0, nil } return int(shard), nil } -func getSortedClustersList(db db.ArgoDB) []v1alpha1.Cluster { - ctx := context.Background() - clustersList, dbErr := db.ListClusters(ctx) - if dbErr != nil { - log.Warnf("Error while querying clusters list from database: %v", dbErr) - return []v1alpha1.Cluster{} - } - clusters := clustersList.Items +func getSortedClustersList(getCluster clusterAccessor) []*v1alpha1.Cluster { + clusters := getCluster() sort.Slice(clusters, func(i, j int) bool { return clusters[i].ID < clusters[j].ID }) return clusters } -func createClusterIndexByClusterIdMap(db db.ArgoDB) map[string]int { - clusters := getSortedClustersList(db) +func createClusterIndexByClusterIdMap(getCluster clusterAccessor) map[string]int { + clusters := getSortedClustersList(getCluster) log.Debugf("ClustersList has %d items", len(clusters)) - clusterById := make(map[string]v1alpha1.Cluster) + clusterById := make(map[string]*v1alpha1.Cluster) clusterIndexedByClusterId := make(map[string]int) for i, cluster := range clusters { log.Debugf("Adding cluster with id=%s and name=%s to cluster's map", cluster.ID, cluster.Name) @@ -193,8 +209,7 @@ func createClusterIndexByClusterIdMap(db db.ArgoDB) map[string]int { // The function takes the shard number from the environment variable (default value -1, if not set) and passes it to this function. // If the shard value passed to this function is -1, that is, the shard was not set as an environment variable, // we default the shard number to 0 for computing the default config map. -func GetOrUpdateShardFromConfigMap(kubeClient *kubernetes.Clientset, settingsMgr *settings.SettingsManager, replicas, shard int) (int, error) { - +func GetOrUpdateShardFromConfigMap(kubeClient kubernetes.Interface, settingsMgr *settings.SettingsManager, replicas, shard int) (int, error) { hostname, err := osHostnameFunction() if err != nil { return -1, err @@ -351,3 +366,59 @@ func getDefaultShardMappingData(replicas int) []shardApplicationControllerMappin } return shardMappingData } + +func GetClusterSharding(kubeClient kubernetes.Interface, settingsMgr *settings.SettingsManager, shardingAlgorithm string, enableDynamicClusterDistribution bool) (ClusterShardingCache, error) { + var replicasCount int + if enableDynamicClusterDistribution { + applicationControllerName := env.StringFromEnv(common.EnvAppControllerName, common.DefaultApplicationControllerName) + appControllerDeployment, err := kubeClient.AppsV1().Deployments(settingsMgr.GetNamespace()).Get(context.Background(), applicationControllerName, metav1.GetOptions{}) + + // if app controller deployment is not found when dynamic cluster distribution is enabled error out + if err != nil { + return nil, fmt.Errorf("(dynamic cluster distribution) failed to get app controller deployment: %v", err) + } + + if appControllerDeployment != nil && appControllerDeployment.Spec.Replicas != nil { + replicasCount = int(*appControllerDeployment.Spec.Replicas) + } else { + return nil, fmt.Errorf("(dynamic cluster distribution) failed to get app controller deployment replica count") + } + + } else { + replicasCount = env.ParseNumFromEnv(common.EnvControllerReplicas, 0, 0, math.MaxInt32) + } + shardNumber := env.ParseNumFromEnv(common.EnvControllerShard, -1, -math.MaxInt32, math.MaxInt32) + if replicasCount > 1 { + // check for shard mapping using configmap if application-controller is a deployment + // else use existing logic to infer shard from pod name if application-controller is a statefulset + if enableDynamicClusterDistribution { + var err error + // retry 3 times if we find a conflict while updating shard mapping configMap. + // If we still see conflicts after the retries, wait for next iteration of heartbeat process. + for i := 0; i <= common.AppControllerHeartbeatUpdateRetryCount; i++ { + shardNumber, err = GetOrUpdateShardFromConfigMap(kubeClient, settingsMgr, replicasCount, shardNumber) + if err != nil && !kubeerrors.IsConflict(err) { + err = fmt.Errorf("unable to get shard due to error updating the sharding config map: %s", err) + break + } + log.Warnf("conflict when getting shard from shard mapping configMap. Retrying (%d/3)", i) + } + errors.CheckError(err) + } else { + if shardNumber < 0 { + var err error + shardNumber, err = InferShard() + errors.CheckError(err) + } + if shardNumber > replicasCount { + log.Warnf("Calculated shard number %d is greated than the number of replicas count. Defaulting to 0", shardNumber) + shardNumber = 0 + } + } + } else { + log.Info("Processing all cluster shards") + shardNumber = 0 + } + db := db.NewDB(settingsMgr.GetNamespace(), settingsMgr, kubeClient) + return NewClusterSharding(db, shardNumber, replicasCount, shardingAlgorithm), nil +} diff --git a/controller/sharding/sharding_test.go b/controller/sharding/sharding_test.go index a8a25e11c49787..1c338aac5f2713 100644 --- a/controller/sharding/sharding_test.go +++ b/controller/sharding/sharding_test.go @@ -1,36 +1,45 @@ package sharding import ( + "context" "encoding/json" "errors" "fmt" "os" + "strconv" "testing" "time" "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks" + "github.com/argoproj/argo-cd/v2/util/settings" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + kubefake "k8s.io/client-go/kubernetes/fake" + "sigs.k8s.io/yaml" ) func TestGetShardByID_NotEmptyID(t *testing.T) { db := &dbmocks.ArgoDB{} - db.On("GetApplicationControllerReplicas").Return(1) - assert.Equal(t, 0, LegacyDistributionFunction(db)(&v1alpha1.Cluster{ID: "1"})) - assert.Equal(t, 0, LegacyDistributionFunction(db)(&v1alpha1.Cluster{ID: "2"})) - assert.Equal(t, 0, LegacyDistributionFunction(db)(&v1alpha1.Cluster{ID: "3"})) - assert.Equal(t, 0, LegacyDistributionFunction(db)(&v1alpha1.Cluster{ID: "4"})) + replicasCount := 1 + db.On("GetApplicationControllerReplicas").Return(replicasCount) + assert.Equal(t, 0, LegacyDistributionFunction(replicasCount)(&v1alpha1.Cluster{ID: "1"})) + assert.Equal(t, 0, LegacyDistributionFunction(replicasCount)(&v1alpha1.Cluster{ID: "2"})) + assert.Equal(t, 0, LegacyDistributionFunction(replicasCount)(&v1alpha1.Cluster{ID: "3"})) + assert.Equal(t, 0, LegacyDistributionFunction(replicasCount)(&v1alpha1.Cluster{ID: "4"})) } func TestGetShardByID_EmptyID(t *testing.T) { db := &dbmocks.ArgoDB{} - db.On("GetApplicationControllerReplicas").Return(1) + replicasCount := 1 + db.On("GetApplicationControllerReplicas").Return(replicasCount) distributionFunction := LegacyDistributionFunction - shard := distributionFunction(db)(&v1alpha1.Cluster{}) + shard := distributionFunction(replicasCount)(&v1alpha1.Cluster{}) assert.Equal(t, 0, shard) } @@ -38,7 +47,7 @@ func TestGetShardByID_NoReplicas(t *testing.T) { db := &dbmocks.ArgoDB{} db.On("GetApplicationControllerReplicas").Return(0) distributionFunction := LegacyDistributionFunction - shard := distributionFunction(db)(&v1alpha1.Cluster{}) + shard := distributionFunction(0)(&v1alpha1.Cluster{}) assert.Equal(t, -1, shard) } @@ -46,16 +55,16 @@ func TestGetShardByID_NoReplicasUsingHashDistributionFunction(t *testing.T) { db := &dbmocks.ArgoDB{} db.On("GetApplicationControllerReplicas").Return(0) distributionFunction := LegacyDistributionFunction - shard := distributionFunction(db)(&v1alpha1.Cluster{}) + shard := distributionFunction(0)(&v1alpha1.Cluster{}) assert.Equal(t, -1, shard) } func TestGetShardByID_NoReplicasUsingHashDistributionFunctionWithClusters(t *testing.T) { - db, cluster1, cluster2, cluster3, cluster4, cluster5 := createTestClusters() + clusters, db, cluster1, cluster2, cluster3, cluster4, cluster5 := createTestClusters() // Test with replicas set to 0 db.On("GetApplicationControllerReplicas").Return(0) t.Setenv(common.EnvControllerShardingAlgorithm, common.RoundRobinShardingAlgorithm) - distributionFunction := RoundRobinDistributionFunction(db) + distributionFunction := RoundRobinDistributionFunction(clusters, 0) assert.Equal(t, -1, distributionFunction(nil)) assert.Equal(t, -1, distributionFunction(&cluster1)) assert.Equal(t, -1, distributionFunction(&cluster2)) @@ -65,137 +74,115 @@ func TestGetShardByID_NoReplicasUsingHashDistributionFunctionWithClusters(t *tes } func TestGetClusterFilterDefault(t *testing.T) { - shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) + //shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) + clusterAccessor, _, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() os.Unsetenv(common.EnvControllerShardingAlgorithm) - db := &dbmocks.ArgoDB{} - db.On("GetApplicationControllerReplicas").Return(2) - filter := GetClusterFilter(db, GetDistributionFunction(db, common.DefaultShardingAlgorithm), shardIndex) - assert.False(t, filter(&v1alpha1.Cluster{ID: "1"})) - assert.True(t, filter(&v1alpha1.Cluster{ID: "2"})) - assert.False(t, filter(&v1alpha1.Cluster{ID: "3"})) - assert.True(t, filter(&v1alpha1.Cluster{ID: "4"})) + replicasCount := 2 + distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) + assert.Equal(t, 0, distributionFunction(nil)) + assert.Equal(t, 0, distributionFunction(&cluster1)) + assert.Equal(t, 1, distributionFunction(&cluster2)) + assert.Equal(t, 0, distributionFunction(&cluster3)) + assert.Equal(t, 1, distributionFunction(&cluster4)) } func TestGetClusterFilterLegacy(t *testing.T) { - shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) - db := &dbmocks.ArgoDB{} - db.On("GetApplicationControllerReplicas").Return(2) + //shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) + clusterAccessor, db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() + replicasCount := 2 + db.On("GetApplicationControllerReplicas").Return(replicasCount) t.Setenv(common.EnvControllerShardingAlgorithm, common.LegacyShardingAlgorithm) - filter := GetClusterFilter(db, GetDistributionFunction(db, common.LegacyShardingAlgorithm), shardIndex) - assert.False(t, filter(&v1alpha1.Cluster{ID: "1"})) - assert.True(t, filter(&v1alpha1.Cluster{ID: "2"})) - assert.False(t, filter(&v1alpha1.Cluster{ID: "3"})) - assert.True(t, filter(&v1alpha1.Cluster{ID: "4"})) + distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) + assert.Equal(t, 0, distributionFunction(nil)) + assert.Equal(t, 0, distributionFunction(&cluster1)) + assert.Equal(t, 1, distributionFunction(&cluster2)) + assert.Equal(t, 0, distributionFunction(&cluster3)) + assert.Equal(t, 1, distributionFunction(&cluster4)) } func TestGetClusterFilterUnknown(t *testing.T) { - shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) - db := &dbmocks.ArgoDB{} - db.On("GetApplicationControllerReplicas").Return(2) + clusterAccessor, db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() + appAccessor, _, _, _, _, _ := createTestApps() + // Test with replicas set to 0 + t.Setenv(common.EnvControllerReplicas, "2") + os.Unsetenv(common.EnvControllerShardingAlgorithm) t.Setenv(common.EnvControllerShardingAlgorithm, "unknown") - filter := GetClusterFilter(db, GetDistributionFunction(db, "unknown"), shardIndex) - assert.False(t, filter(&v1alpha1.Cluster{ID: "1"})) - assert.True(t, filter(&v1alpha1.Cluster{ID: "2"})) - assert.False(t, filter(&v1alpha1.Cluster{ID: "3"})) - assert.True(t, filter(&v1alpha1.Cluster{ID: "4"})) + replicasCount := 2 + db.On("GetApplicationControllerReplicas").Return(replicasCount) + distributionFunction := GetDistributionFunction(clusterAccessor, appAccessor, "unknown", replicasCount) + assert.Equal(t, 0, distributionFunction(nil)) + assert.Equal(t, 0, distributionFunction(&cluster1)) + assert.Equal(t, 1, distributionFunction(&cluster2)) + assert.Equal(t, 0, distributionFunction(&cluster3)) + assert.Equal(t, 1, distributionFunction(&cluster4)) } func TestLegacyGetClusterFilterWithFixedShard(t *testing.T) { - shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) - db := &dbmocks.ArgoDB{} - db.On("GetApplicationControllerReplicas").Return(2) - filter := GetClusterFilter(db, GetDistributionFunction(db, common.DefaultShardingAlgorithm), shardIndex) - assert.False(t, filter(nil)) - assert.False(t, filter(&v1alpha1.Cluster{ID: "1"})) - assert.True(t, filter(&v1alpha1.Cluster{ID: "2"})) - assert.False(t, filter(&v1alpha1.Cluster{ID: "3"})) - assert.True(t, filter(&v1alpha1.Cluster{ID: "4"})) + //shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) + t.Setenv(common.EnvControllerReplicas, "5") + clusterAccessor, db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() + appAccessor, _, _, _, _, _ := createTestApps() + replicasCount := 5 + db.On("GetApplicationControllerReplicas").Return(replicasCount) + filter := GetDistributionFunction(clusterAccessor, appAccessor, common.DefaultShardingAlgorithm, replicasCount) + assert.Equal(t, 0, filter(nil)) + assert.Equal(t, 4, filter(&cluster1)) + assert.Equal(t, 1, filter(&cluster2)) + assert.Equal(t, 2, filter(&cluster3)) + assert.Equal(t, 2, filter(&cluster4)) var fixedShard int64 = 4 - filter = GetClusterFilter(db, GetDistributionFunction(db, common.DefaultShardingAlgorithm), int(fixedShard)) - assert.False(t, filter(&v1alpha1.Cluster{ID: "4", Shard: &fixedShard})) + cluster5 := &v1alpha1.Cluster{ID: "5", Shard: &fixedShard} + clusterAccessor = getClusterAccessor([]v1alpha1.Cluster{cluster1, cluster2, cluster2, cluster4, *cluster5}) + filter = GetDistributionFunction(clusterAccessor, appAccessor, common.DefaultShardingAlgorithm, replicasCount) + assert.Equal(t, int(fixedShard), filter(cluster5)) fixedShard = 1 - filter = GetClusterFilter(db, GetDistributionFunction(db, common.DefaultShardingAlgorithm), int(fixedShard)) - assert.True(t, filter(&v1alpha1.Cluster{Name: "cluster4", ID: "4", Shard: &fixedShard})) + cluster5.Shard = &fixedShard + clusterAccessor = getClusterAccessor([]v1alpha1.Cluster{cluster1, cluster2, cluster2, cluster4, *cluster5}) + filter = GetDistributionFunction(clusterAccessor, appAccessor, common.DefaultShardingAlgorithm, replicasCount) + assert.Equal(t, int(fixedShard), filter(&v1alpha1.Cluster{ID: "4", Shard: &fixedShard})) } func TestRoundRobinGetClusterFilterWithFixedShard(t *testing.T) { - shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) - db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() - db.On("GetApplicationControllerReplicas").Return(2) - filter := GetClusterFilter(db, GetDistributionFunction(db, common.RoundRobinShardingAlgorithm), shardIndex) - assert.False(t, filter(nil)) - assert.False(t, filter(&cluster1)) - assert.True(t, filter(&cluster2)) - assert.False(t, filter(&cluster3)) - assert.True(t, filter(&cluster4)) - - // a cluster with a fixed shard should be processed by the specified exact - // same shard unless the specified shard index is greater than the number of replicas. - var fixedShard int64 = 4 - filter = GetClusterFilter(db, GetDistributionFunction(db, common.RoundRobinShardingAlgorithm), int(fixedShard)) - assert.False(t, filter(&v1alpha1.Cluster{Name: "cluster4", ID: "4", Shard: &fixedShard})) - - fixedShard = 1 - filter = GetClusterFilter(db, GetDistributionFunction(db, common.RoundRobinShardingAlgorithm), int(fixedShard)) - assert.True(t, filter(&v1alpha1.Cluster{Name: "cluster4", ID: "4", Shard: &fixedShard})) -} - -func TestGetClusterFilterLegacyHash(t *testing.T) { - shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) - t.Setenv(common.EnvControllerShardingAlgorithm, "hash") - db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() - db.On("GetApplicationControllerReplicas").Return(2) - filter := GetClusterFilter(db, GetDistributionFunction(db, common.LegacyShardingAlgorithm), shardIndex) - assert.False(t, filter(&cluster1)) - assert.True(t, filter(&cluster2)) - assert.False(t, filter(&cluster3)) - assert.True(t, filter(&cluster4)) + //shardIndex := 1 // ensuring that a shard with index 1 will process all the clusters with an "even" id (2,4,6,...) + t.Setenv(common.EnvControllerReplicas, "4") + clusterAccessor, db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() + appAccessor, _, _, _, _, _ := createTestApps() + replicasCount := 4 + db.On("GetApplicationControllerReplicas").Return(replicasCount) + + filter := GetDistributionFunction(clusterAccessor, appAccessor, common.RoundRobinShardingAlgorithm, replicasCount) + assert.Equal(t, filter(nil), 0) + assert.Equal(t, filter(&cluster1), 0) + assert.Equal(t, filter(&cluster2), 1) + assert.Equal(t, filter(&cluster3), 2) + assert.Equal(t, filter(&cluster4), 3) // a cluster with a fixed shard should be processed by the specified exact // same shard unless the specified shard index is greater than the number of replicas. - var fixedShard int64 = 4 - filter = GetClusterFilter(db, GetDistributionFunction(db, common.LegacyShardingAlgorithm), int(fixedShard)) - assert.False(t, filter(&v1alpha1.Cluster{Name: "cluster4", ID: "4", Shard: &fixedShard})) + var fixedShard int64 = 1 + cluster5 := v1alpha1.Cluster{Name: "cluster5", ID: "5", Shard: &fixedShard} + clusters := []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5} + clusterAccessor = getClusterAccessor(clusters) + filter = GetDistributionFunction(clusterAccessor, appAccessor, common.RoundRobinShardingAlgorithm, replicasCount) + assert.Equal(t, int(fixedShard), filter(&cluster5)) fixedShard = 1 - filter = GetClusterFilter(db, GetDistributionFunction(db, common.LegacyShardingAlgorithm), int(fixedShard)) - assert.True(t, filter(&v1alpha1.Cluster{Name: "cluster4", ID: "4", Shard: &fixedShard})) -} - -func TestGetClusterFilterWithEnvControllerShardingAlgorithms(t *testing.T) { - db, cluster1, cluster2, cluster3, cluster4, _ := createTestClusters() - shardIndex := 1 - db.On("GetApplicationControllerReplicas").Return(2) - - t.Run("legacy", func(t *testing.T) { - t.Setenv(common.EnvControllerShardingAlgorithm, common.LegacyShardingAlgorithm) - shardShouldProcessCluster := GetClusterFilter(db, GetDistributionFunction(db, common.LegacyShardingAlgorithm), shardIndex) - assert.False(t, shardShouldProcessCluster(&cluster1)) - assert.True(t, shardShouldProcessCluster(&cluster2)) - assert.False(t, shardShouldProcessCluster(&cluster3)) - assert.True(t, shardShouldProcessCluster(&cluster4)) - assert.False(t, shardShouldProcessCluster(nil)) - }) - - t.Run("roundrobin", func(t *testing.T) { - t.Setenv(common.EnvControllerShardingAlgorithm, common.RoundRobinShardingAlgorithm) - shardShouldProcessCluster := GetClusterFilter(db, GetDistributionFunction(db, common.LegacyShardingAlgorithm), shardIndex) - assert.False(t, shardShouldProcessCluster(&cluster1)) - assert.True(t, shardShouldProcessCluster(&cluster2)) - assert.False(t, shardShouldProcessCluster(&cluster3)) - assert.True(t, shardShouldProcessCluster(&cluster4)) - assert.False(t, shardShouldProcessCluster(nil)) - }) + cluster5 = v1alpha1.Cluster{Name: "cluster5", ID: "5", Shard: &fixedShard} + clusters = []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5} + clusterAccessor = getClusterAccessor(clusters) + filter = GetDistributionFunction(clusterAccessor, appAccessor, common.RoundRobinShardingAlgorithm, replicasCount) + assert.Equal(t, int(fixedShard), filter(&v1alpha1.Cluster{Name: "cluster4", ID: "4", Shard: &fixedShard})) } func TestGetShardByIndexModuloReplicasCountDistributionFunction2(t *testing.T) { - db, cluster1, cluster2, cluster3, cluster4, cluster5 := createTestClusters() + clusters, db, cluster1, cluster2, cluster3, cluster4, cluster5 := createTestClusters() t.Run("replicas set to 1", func(t *testing.T) { - db.On("GetApplicationControllerReplicas").Return(1).Once() - distributionFunction := RoundRobinDistributionFunction(db) + replicasCount := 1 + db.On("GetApplicationControllerReplicas").Return(replicasCount).Once() + distributionFunction := RoundRobinDistributionFunction(clusters, replicasCount) assert.Equal(t, 0, distributionFunction(nil)) assert.Equal(t, 0, distributionFunction(&cluster1)) assert.Equal(t, 0, distributionFunction(&cluster2)) @@ -205,8 +192,9 @@ func TestGetShardByIndexModuloReplicasCountDistributionFunction2(t *testing.T) { }) t.Run("replicas set to 2", func(t *testing.T) { - db.On("GetApplicationControllerReplicas").Return(2).Once() - distributionFunction := RoundRobinDistributionFunction(db) + replicasCount := 2 + db.On("GetApplicationControllerReplicas").Return(replicasCount).Once() + distributionFunction := RoundRobinDistributionFunction(clusters, replicasCount) assert.Equal(t, 0, distributionFunction(nil)) assert.Equal(t, 0, distributionFunction(&cluster1)) assert.Equal(t, 1, distributionFunction(&cluster2)) @@ -216,8 +204,9 @@ func TestGetShardByIndexModuloReplicasCountDistributionFunction2(t *testing.T) { }) t.Run("replicas set to 3", func(t *testing.T) { - db.On("GetApplicationControllerReplicas").Return(3).Once() - distributionFunction := RoundRobinDistributionFunction(db) + replicasCount := 3 + db.On("GetApplicationControllerReplicas").Return(replicasCount).Once() + distributionFunction := RoundRobinDistributionFunction(clusters, replicasCount) assert.Equal(t, 0, distributionFunction(nil)) assert.Equal(t, 0, distributionFunction(&cluster1)) assert.Equal(t, 1, distributionFunction(&cluster2)) @@ -233,17 +222,19 @@ func TestGetShardByIndexModuloReplicasCountDistributionFunctionWhenClusterNumber // Initial tests where showing that under 1024 clusters, execution time was around 400ms // and for 4096 clusters, execution time was under 9s // The other implementation was giving almost linear time of 400ms up to 10'000 clusters - db := dbmocks.ArgoDB{} - clusterList := &v1alpha1.ClusterList{Items: []v1alpha1.Cluster{}} + clusterPointers := []*v1alpha1.Cluster{} for i := 0; i < 2048; i++ { cluster := createCluster(fmt.Sprintf("cluster-%d", i), fmt.Sprintf("%d", i)) - clusterList.Items = append(clusterList.Items, cluster) + clusterPointers = append(clusterPointers, &cluster) } - db.On("ListClusters", mock.Anything).Return(clusterList, nil) - db.On("GetApplicationControllerReplicas").Return(2) - distributionFunction := RoundRobinDistributionFunction(&db) - for i, c := range clusterList.Items { - assert.Equal(t, i%2, distributionFunction(&c)) + replicasCount := 2 + t.Setenv(common.EnvControllerReplicas, strconv.Itoa(replicasCount)) + _, db, _, _, _, _, _ := createTestClusters() + clusterAccessor := func() []*v1alpha1.Cluster { return clusterPointers } + db.On("GetApplicationControllerReplicas").Return(replicasCount) + distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) + for i, c := range clusterPointers { + assert.Equal(t, i%2, distributionFunction(c)) } } @@ -256,12 +247,15 @@ func TestGetShardByIndexModuloReplicasCountDistributionFunctionWhenClusterIsAdde cluster5 := createCluster("cluster5", "5") cluster6 := createCluster("cluster6", "6") + clusters := []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5} + clusterAccessor := getClusterAccessor(clusters) + clusterList := &v1alpha1.ClusterList{Items: []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5}} db.On("ListClusters", mock.Anything).Return(clusterList, nil) - // Test with replicas set to 2 - db.On("GetApplicationControllerReplicas").Return(2) - distributionFunction := RoundRobinDistributionFunction(&db) + replicasCount := 2 + db.On("GetApplicationControllerReplicas").Return(replicasCount) + distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) assert.Equal(t, 0, distributionFunction(nil)) assert.Equal(t, 0, distributionFunction(&cluster1)) assert.Equal(t, 1, distributionFunction(&cluster2)) @@ -272,17 +266,20 @@ func TestGetShardByIndexModuloReplicasCountDistributionFunctionWhenClusterIsAdde // Now, the database knows cluster6. Shard should be assigned a proper shard clusterList.Items = append(clusterList.Items, cluster6) + distributionFunction = RoundRobinDistributionFunction(getClusterAccessor(clusterList.Items), replicasCount) assert.Equal(t, 1, distributionFunction(&cluster6)) // Now, we remove the last added cluster, it should be unassigned as well clusterList.Items = clusterList.Items[:len(clusterList.Items)-1] + distributionFunction = RoundRobinDistributionFunction(getClusterAccessor(clusterList.Items), replicasCount) assert.Equal(t, -1, distributionFunction(&cluster6)) } func TestGetShardByIndexModuloReplicasCountDistributionFunction(t *testing.T) { - db, cluster1, cluster2, _, _, _ := createTestClusters() - db.On("GetApplicationControllerReplicas").Return(2) - distributionFunction := RoundRobinDistributionFunction(db) + clusters, db, cluster1, cluster2, _, _, _ := createTestClusters() + replicasCount := 2 + db.On("GetApplicationControllerReplicas").Return(replicasCount) + distributionFunction := RoundRobinDistributionFunction(clusters, replicasCount) // Test that the function returns the correct shard for cluster1 and cluster2 expectedShardForCluster1 := 0 @@ -315,14 +312,14 @@ func TestInferShard(t *testing.T) { osHostnameFunction = func() (string, error) { return "exampleshard", nil } _, err = InferShard() - assert.NotNil(t, err) + assert.Nil(t, err) osHostnameFunction = func() (string, error) { return "example-shard", nil } _, err = InferShard() - assert.NotNil(t, err) + assert.Nil(t, err) } -func createTestClusters() (*dbmocks.ArgoDB, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster) { +func createTestClusters() (clusterAccessor, *dbmocks.ArgoDB, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster, v1alpha1.Cluster) { db := dbmocks.ArgoDB{} cluster1 := createCluster("cluster1", "1") cluster2 := createCluster("cluster2", "2") @@ -330,10 +327,27 @@ func createTestClusters() (*dbmocks.ArgoDB, v1alpha1.Cluster, v1alpha1.Cluster, cluster4 := createCluster("cluster4", "4") cluster5 := createCluster("cluster5", "5") + clusters := []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5} + db.On("ListClusters", mock.Anything).Return(&v1alpha1.ClusterList{Items: []v1alpha1.Cluster{ cluster1, cluster2, cluster3, cluster4, cluster5, }}, nil) - return &db, cluster1, cluster2, cluster3, cluster4, cluster5 + return getClusterAccessor(clusters), &db, cluster1, cluster2, cluster3, cluster4, cluster5 +} + +func getClusterAccessor(clusters []v1alpha1.Cluster) clusterAccessor { + // Convert the array to a slice of pointers + clusterPointers := getClusterPointers(clusters) + clusterAccessor := func() []*v1alpha1.Cluster { return clusterPointers } + return clusterAccessor +} + +func getClusterPointers(clusters []v1alpha1.Cluster) []*v1alpha1.Cluster { + var clusterPointers []*v1alpha1.Cluster + for i := range clusters { + clusterPointers = append(clusterPointers, &clusters[i]) + } + return clusterPointers } func createCluster(name string, id string) v1alpha1.Cluster { @@ -676,3 +690,265 @@ func Test_getOrUpdateShardNumberForController(t *testing.T) { }) } } + +func TestGetClusterSharding(t *testing.T) { + IntPtr := func(i int32) *int32 { + return &i + } + + deployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: common.DefaultApplicationControllerName, + Namespace: "argocd", + }, + Spec: appsv1.DeploymentSpec{ + Replicas: IntPtr(1), + }, + } + + deploymentMultiReplicas := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "argocd-application-controller-multi-replicas", + Namespace: "argocd", + }, + Spec: appsv1.DeploymentSpec{ + Replicas: IntPtr(3), + }, + } + + objects := append([]runtime.Object{}, deployment, deploymentMultiReplicas) + kubeclientset := kubefake.NewSimpleClientset(objects...) + + settingsMgr := settings.NewSettingsManager(context.TODO(), kubeclientset, "argocd", settings.WithRepoOrClusterChangedHandler(func() { + })) + + testCases := []struct { + name string + useDynamicSharding bool + envsSetter func(t *testing.T) + cleanup func() + expectedShard int + expectedReplicas int + expectedErr error + }{ + { + name: "Default sharding with statefulset", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvControllerReplicas, "1") + }, + cleanup: func() {}, + useDynamicSharding: false, + expectedShard: 0, + expectedReplicas: 1, + expectedErr: nil, + }, + { + name: "Default sharding with deployment", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvAppControllerName, common.DefaultApplicationControllerName) + }, + cleanup: func() {}, + useDynamicSharding: true, + expectedShard: 0, + expectedReplicas: 1, + expectedErr: nil, + }, + { + name: "Default sharding with deployment and multiple replicas", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvAppControllerName, "argocd-application-controller-multi-replicas") + }, + cleanup: func() {}, + useDynamicSharding: true, + expectedShard: 0, + expectedReplicas: 3, + expectedErr: nil, + }, + { + name: "Statefulset multiple replicas", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvControllerReplicas, "3") + osHostnameFunction = func() (string, error) { return "example-shard-3", nil } + }, + cleanup: func() { + osHostnameFunction = os.Hostname + }, + useDynamicSharding: false, + expectedShard: 3, + expectedReplicas: 3, + expectedErr: nil, + }, + { + name: "Explicit shard with statefulset and 1 replica", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvControllerReplicas, "1") + t.Setenv(common.EnvControllerShard, "3") + }, + cleanup: func() {}, + useDynamicSharding: false, + expectedShard: 0, + expectedReplicas: 1, + expectedErr: nil, + }, + { + name: "Explicit shard with statefulset and 2 replica - and to high shard", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvControllerReplicas, "2") + t.Setenv(common.EnvControllerShard, "3") + }, + cleanup: func() {}, + useDynamicSharding: false, + expectedShard: 0, + expectedReplicas: 2, + expectedErr: nil, + }, + { + name: "Explicit shard with statefulset and 2 replica", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvControllerReplicas, "2") + t.Setenv(common.EnvControllerShard, "1") + }, + cleanup: func() {}, + useDynamicSharding: false, + expectedShard: 1, + expectedReplicas: 2, + expectedErr: nil, + }, + { + name: "Explicit shard with deployment", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvControllerShard, "3") + }, + cleanup: func() {}, + useDynamicSharding: true, + expectedShard: 0, + expectedReplicas: 1, + expectedErr: nil, + }, + { + name: "Explicit shard with deployment and multiple replicas will read from configmap", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvAppControllerName, "argocd-application-controller-multi-replicas") + t.Setenv(common.EnvControllerShard, "3") + }, + cleanup: func() {}, + useDynamicSharding: true, + expectedShard: 0, + expectedReplicas: 3, + expectedErr: nil, + }, + { + name: "Dynamic sharding but missing deployment", + envsSetter: func(t *testing.T) { + t.Setenv(common.EnvAppControllerName, "missing-deployment") + }, + cleanup: func() {}, + useDynamicSharding: true, + expectedShard: 0, + expectedReplicas: 1, + expectedErr: fmt.Errorf("(dynamic cluster distribution) failed to get app controller deployment: deployments.apps \"missing-deployment\" not found"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tc.envsSetter(t) + defer tc.cleanup() + shardingCache, err := GetClusterSharding(kubeclientset, settingsMgr, "round-robin", tc.useDynamicSharding) + + if shardingCache != nil { + clusterSharding := shardingCache.(*ClusterSharding) + assert.Equal(t, tc.expectedShard, clusterSharding.Shard) + assert.Equal(t, tc.expectedReplicas, clusterSharding.Replicas) + } + + if tc.expectedErr != nil { + if err != nil { + assert.Equal(t, tc.expectedErr.Error(), err.Error()) + } else { + t.Errorf("Expected error %v but got nil", tc.expectedErr) + } + } else { + assert.Nil(t, err) + } + }) + } +} + +func TestAppAwareCache(t *testing.T) { + _, db, cluster1, cluster2, cluster3, cluster4, cluster5 := createTestClusters() + _, app1, app2, app3, app4, app5 := createTestApps() + + clusterSharding := NewClusterSharding(db, 0, 1, "legacy") + + clusterList := &v1alpha1.ClusterList{Items: []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5}} + appList := &v1alpha1.ApplicationList{Items: []v1alpha1.Application{app1, app2, app3, app4, app5}} + clusterSharding.Init(clusterList, appList) + + appDistribution := clusterSharding.GetAppDistribution() + + assert.Equal(t, 2, appDistribution["cluster1"]) + assert.Equal(t, 2, appDistribution["cluster2"]) + assert.Equal(t, 1, appDistribution["cluster3"]) + + app6 := createApp("app6", "cluster4") + clusterSharding.AddApp(&app6) + + app1Update := createApp("app1", "cluster2") + clusterSharding.UpdateApp(&app1Update) + + clusterSharding.DeleteApp(&app3) + + appDistribution = clusterSharding.GetAppDistribution() + + assert.Equal(t, 1, appDistribution["cluster1"]) + assert.Equal(t, 2, appDistribution["cluster2"]) + assert.Equal(t, 1, appDistribution["cluster3"]) + assert.Equal(t, 1, appDistribution["cluster4"]) +} + +func createTestApps() (appAccessor, v1alpha1.Application, v1alpha1.Application, v1alpha1.Application, v1alpha1.Application, v1alpha1.Application) { + app1 := createApp("app1", "cluster1") + app2 := createApp("app2", "cluster1") + app3 := createApp("app3", "cluster2") + app4 := createApp("app4", "cluster2") + app5 := createApp("app5", "cluster3") + + apps := []v1alpha1.Application{app1, app2, app3, app4, app5} + + return getAppAccessor(apps), app1, app2, app3, app4, app5 +} + +func getAppAccessor(apps []v1alpha1.Application) appAccessor { + // Convert the array to a slice of pointers + appPointers := getAppPointers(apps) + appAccessor := func() []*v1alpha1.Application { return appPointers } + return appAccessor +} + +func getAppPointers(apps []v1alpha1.Application) []*v1alpha1.Application { + var appPointers []*v1alpha1.Application + for i := range apps { + appPointers = append(appPointers, &apps[i]) + } + return appPointers +} + +func createApp(name string, server string) v1alpha1.Application { + var testApp = ` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ` + name + ` +spec: + destination: + server: ` + server + ` +` + + var app v1alpha1.Application + err := yaml.Unmarshal([]byte(testApp), &app) + if err != nil { + panic(err) + } + return app +} diff --git a/controller/sharding/shuffle_test.go b/controller/sharding/shuffle_test.go index 9e089e31bad0fc..1cca783a2afe9f 100644 --- a/controller/sharding/shuffle_test.go +++ b/controller/sharding/shuffle_test.go @@ -3,6 +3,7 @@ package sharding import ( "fmt" "math" + "strconv" "testing" "github.com/argoproj/argo-cd/v2/common" @@ -22,9 +23,11 @@ func TestLargeShuffle(t *testing.T) { clusterList.Items = append(clusterList.Items, cluster) } db.On("ListClusters", mock.Anything).Return(clusterList, nil) + clusterAccessor := getClusterAccessor(clusterList.Items) // Test with replicas set to 256 - t.Setenv(common.EnvControllerReplicas, "256") - distributionFunction := RoundRobinDistributionFunction(&db) + replicasCount := 256 + t.Setenv(common.EnvControllerReplicas, strconv.Itoa(replicasCount)) + distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) for i, c := range clusterList.Items { assert.Equal(t, i%2567, distributionFunction(&c)) } @@ -44,10 +47,11 @@ func TestShuffle(t *testing.T) { clusterList := &v1alpha1.ClusterList{Items: []v1alpha1.Cluster{cluster1, cluster2, cluster3, cluster4, cluster5, cluster6}} db.On("ListClusters", mock.Anything).Return(clusterList, nil) - + clusterAccessor := getClusterAccessor(clusterList.Items) // Test with replicas set to 3 t.Setenv(common.EnvControllerReplicas, "3") - distributionFunction := RoundRobinDistributionFunction(&db) + replicasCount := 3 + distributionFunction := RoundRobinDistributionFunction(clusterAccessor, replicasCount) assert.Equal(t, 0, distributionFunction(nil)) assert.Equal(t, 0, distributionFunction(&cluster1)) assert.Equal(t, 1, distributionFunction(&cluster2)) diff --git a/controller/state.go b/controller/state.go index 15da3d2e624ed9..704411558669b8 100644 --- a/controller/state.go +++ b/controller/state.go @@ -71,6 +71,7 @@ type managedResource struct { type AppStateManager interface { CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool) (*comparisonResult, error) SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState) + GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error) } // comparisonResult holds the state of an application after the reconciliation @@ -85,8 +86,9 @@ type comparisonResult struct { // appSourceTypes stores the SourceType for each application source under sources field appSourceTypes []v1alpha1.ApplicationSourceType // timings maps phases of comparison to the duration it took to complete (for statistical purposes) - timings map[string]time.Duration - diffResultList *diff.DiffResultList + timings map[string]time.Duration + diffResultList *diff.DiffResultList + hasPostDeleteHooks bool } func (res *comparisonResult) GetSyncStatus() *v1alpha1.SyncStatus { @@ -114,9 +116,14 @@ type appStateManager struct { persistResourceHealth bool repoErrorCache goSync.Map repoErrorGracePeriod time.Duration + serverSideDiff bool } -func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error) { +// GetRepoObjs will generate the manifests for the given application delegating the +// task to the repo-server. It returns the list of generated manifests as unstructured +// objects. It also returns the full response from all calls to the repo server as the +// second argument. +func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, error) { ts := stats.NewTimingStats() helmRepos, err := m.db.ListHelmRepositories(context.Background()) if err != nil { @@ -232,7 +239,7 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, sources []v1alp logCtx = logCtx.WithField(k, v.Milliseconds()) } logCtx = logCtx.WithField("time_ms", time.Since(ts.StartTime).Milliseconds()) - logCtx.Info("getRepoObjs stats") + logCtx.Info("GetRepoObjs stats") return targetObjs, manifestInfos, nil } @@ -411,7 +418,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 } } - targetObjs, manifestInfos, err = m.getRepoObjs(app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project) + targetObjs, manifestInfos, err = m.GetRepoObjs(app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project) if err != nil { targetObjs = make([]*unstructured.Unstructured, 0) msg := fmt.Sprintf("Failed to load target state: %s", err.Error()) @@ -565,6 +572,12 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 } } } + hasPostDeleteHooks := false + for _, obj := range targetObjs { + if isPostDeleteHook(obj) { + hasPostDeleteHooks = true + } + } reconciliation := sync.Reconcile(targetObjs, liveObjByKey, app.Spec.Destination.Namespace, infoProvider) ts.AddCheckpoint("live_ms") @@ -580,21 +593,29 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 manifestRevisions = append(manifestRevisions, manifestInfo.Revision) } - // restore comparison using cached diff result if previous comparison was performed for the same revision - revisionChanged := len(manifestInfos) != len(sources) || !reflect.DeepEqual(app.Status.Sync.Revisions, manifestRevisions) - specChanged := !reflect.DeepEqual(app.Status.Sync.ComparedTo, v1alpha1.ComparedTo{Source: app.Spec.GetSource(), Destination: app.Spec.Destination, Sources: sources, IgnoreDifferences: app.Spec.IgnoreDifferences}) + serverSideDiff := m.serverSideDiff || + resourceutil.HasAnnotationOption(app, common.AnnotationCompareOptions, "ServerSideDiff=true") + + // This allows turning SSD off for a given app if it is enabled at the + // controller level + if resourceutil.HasAnnotationOption(app, common.AnnotationCompareOptions, "ServerSideDiff=false") { + serverSideDiff = false + } - _, refreshRequested := app.IsRefreshRequested() - noCache = noCache || refreshRequested || app.Status.Expired(m.statusRefreshTimeout) || specChanged || revisionChanged + useDiffCache := useDiffCache(noCache, manifestInfos, sources, app, manifestRevisions, m.statusRefreshTimeout, serverSideDiff, logCtx) diffConfigBuilder := argodiff.NewDiffConfigBuilder(). WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles). WithTracking(appLabelKey, string(trackingMethod)) - if noCache { - diffConfigBuilder.WithNoCache() + if useDiffCache { + diffConfigBuilder.WithCache(m.cache, app.InstanceName(m.namespace)) } else { - diffConfigBuilder.WithCache(m.cache, app.GetName()) + diffConfigBuilder.WithNoCache() + } + + if resourceutil.HasAnnotationOption(app, common.AnnotationCompareOptions, "IncludeMutationWebhook=true") { + diffConfigBuilder.WithIgnoreMutationWebhook(false) } gvkParser, err := m.getGVKParser(app.Spec.Destination.Server) @@ -604,6 +625,18 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 diffConfigBuilder.WithGVKParser(gvkParser) diffConfigBuilder.WithManager(common.ArgoCDSSAManager) + diffConfigBuilder.WithServerSideDiff(serverSideDiff) + + if serverSideDiff { + resourceOps, cleanup, err := m.getResourceOperations(app.Spec.Destination.Server) + if err != nil { + log.Errorf("CompareAppState error getting resource operations: %s", err) + conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionUnknownError, Message: err.Error(), LastTransitionTime: &now}) + } + defer cleanup() + diffConfigBuilder.WithServerSideDryRunner(diff.NewK8sServerSideDryRunner(resourceOps)) + } + // enable structured merge diff if application syncs with server-side apply if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.SyncOptions.HasOption("ServerSideApply=true") { diffConfigBuilder.WithStructuredMergeDiff(true) @@ -644,7 +677,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 Kind: gvk.Kind, Version: gvk.Version, Group: gvk.Group, - Hook: hookutil.IsHook(obj), + Hook: isHook(obj), RequiresPruning: targetObj == nil && liveObj != nil && isSelfReferencedObj, } if targetObj != nil { @@ -777,6 +810,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 reconciliationResult: reconciliation, diffConfig: diffConfig, diffResultList: diffResults, + hasPostDeleteHooks: hasPostDeleteHooks, } if hasMultipleSources { @@ -801,7 +835,61 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1 return &compRes, nil } -func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, revisions []string, sources []v1alpha1.ApplicationSource, hasMultipleSources bool, startedAt metav1.Time) error { +// useDiffCache will determine if the diff should be calculated based +// on the existing live state cache or not. +func useDiffCache(noCache bool, manifestInfos []*apiclient.ManifestResponse, sources []v1alpha1.ApplicationSource, app *v1alpha1.Application, manifestRevisions []string, statusRefreshTimeout time.Duration, serverSideDiff bool, log *log.Entry) bool { + + if noCache { + log.WithField("useDiffCache", "false").Debug("noCache is true") + return false + } + refreshType, refreshRequested := app.IsRefreshRequested() + if refreshRequested { + log.WithField("useDiffCache", "false").Debugf("refresh type %s requested", string(refreshType)) + return false + } + // serverSideDiff should still use cache even if status is expired. + // This is an attempt to avoid hitting k8s API server too frequently during + // app refresh with serverSideDiff is enabled. If there are negative side + // effects identified with this approach, the serverSideDiff should be removed + // from this condition. + if app.Status.Expired(statusRefreshTimeout) && !serverSideDiff { + log.WithField("useDiffCache", "false").Debug("app.status.expired") + return false + } + + if len(manifestInfos) != len(sources) { + log.WithField("useDiffCache", "false").Debug("manifestInfos len != sources len") + return false + } + + revisionChanged := !reflect.DeepEqual(app.Status.GetRevisions(), manifestRevisions) + if revisionChanged { + log.WithField("useDiffCache", "false").Debug("revisionChanged") + return false + } + + currentSpec := app.BuildComparedToStatus() + specChanged := !reflect.DeepEqual(app.Status.Sync.ComparedTo, currentSpec) + if specChanged { + log.WithField("useDiffCache", "false").Debug("specChanged") + return false + } + + log.WithField("useDiffCache", "true").Debug("using diff cache") + return true +} + +func (m *appStateManager) persistRevisionHistory( + app *v1alpha1.Application, + revision string, + source v1alpha1.ApplicationSource, + revisions []string, + sources []v1alpha1.ApplicationSource, + hasMultipleSources bool, + startedAt metav1.Time, + initiatedBy v1alpha1.OperationInitiator, +) error { var nextID int64 if len(app.Status.History) > 0 { nextID = app.Status.History.LastRevisionHistory().ID + 1 @@ -814,6 +902,7 @@ func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revi ID: nextID, Sources: sources, Revisions: revisions, + InitiatedBy: initiatedBy, }) } else { app.Status.History = append(app.Status.History, v1alpha1.RevisionHistory{ @@ -822,6 +911,7 @@ func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revi DeployStartedAt: &startedAt, ID: nextID, Source: source, + InitiatedBy: initiatedBy, }) } @@ -855,6 +945,7 @@ func NewAppStateManager( resourceTracking argo.ResourceTracking, persistResourceHealth bool, repoErrorGracePeriod time.Duration, + serverSideDiff bool, ) AppStateManager { return &appStateManager{ liveStateCache: liveStateCache, @@ -871,6 +962,7 @@ func NewAppStateManager( resourceTracking: resourceTracking, persistResourceHealth: persistResourceHealth, repoErrorGracePeriod: repoErrorGracePeriod, + serverSideDiff: serverSideDiff, } } diff --git a/controller/state_test.go b/controller/state_test.go index 9d2df1f2391858..d21cda62137dec 100644 --- a/controller/state_test.go +++ b/controller/state_test.go @@ -11,6 +11,9 @@ import ( synccommon "github.com/argoproj/gitops-engine/pkg/sync/common" "github.com/argoproj/gitops-engine/pkg/utils/kube" . "github.com/argoproj/gitops-engine/pkg/utils/testing" + "github.com/imdario/mergo" + "github.com/sirupsen/logrus" + logrustest "github.com/sirupsen/logrus/hooks/test" "github.com/stretchr/testify/assert" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -20,6 +23,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "github.com/argoproj/argo-cd/v2/common" + "github.com/argoproj/argo-cd/v2/controller/testdata" + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" "github.com/argoproj/argo-cd/v2/test" @@ -117,6 +122,124 @@ func TestCompareAppStateNamespaceMetadataDiffers(t *testing.T) { assert.Len(t, app.Status.Conditions, 0) } +// TestCompareAppStateNamespaceMetadataDiffers tests comparison when managed namespace metadata differs to live and manifest ns +func TestCompareAppStateNamespaceMetadataDiffersToManifest(t *testing.T) { + ns := NewNamespace() + ns.SetName(test.FakeDestNamespace) + ns.SetNamespace(test.FakeDestNamespace) + ns.SetAnnotations(map[string]string{"bar": "bat"}) + + app := newFakeApp() + app.Spec.SyncPolicy.ManagedNamespaceMetadata = &argoappv1.ManagedNamespaceMetadata{ + Labels: map[string]string{ + "foo": "bar", + }, + Annotations: map[string]string{ + "foo": "bar", + }, + } + app.Status.OperationState = &argoappv1.OperationState{ + SyncResult: &argoappv1.SyncOperationResult{}, + } + + liveNs := ns.DeepCopy() + liveNs.SetAnnotations(nil) + + data := fakeData{ + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{toJSON(t, liveNs)}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ + kube.GetResourceKey(ns): ns, + }, + } + ctrl := newFakeController(&data, nil) + sources := make([]argoappv1.ApplicationSource, 0) + sources = append(sources, app.Spec.GetSource()) + revisions := make([]string, 0) + revisions = append(revisions, "") + compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false) + assert.Nil(t, err) + assert.NotNil(t, compRes) + assert.NotNil(t, compRes.syncStatus) + assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status) + assert.Len(t, compRes.resources, 1) + assert.Len(t, compRes.managedResources, 1) + assert.NotNil(t, compRes.diffResultList) + assert.Len(t, compRes.diffResultList.Diffs, 1) + + result := NewNamespace() + assert.NoError(t, json.Unmarshal(compRes.diffResultList.Diffs[0].PredictedLive, result)) + + labels := result.GetLabels() + delete(labels, "kubernetes.io/metadata.name") + + assert.Equal(t, map[string]string{}, labels) + // Manifests override definitions in managedNamespaceMetadata + assert.Equal(t, map[string]string{"bar": "bat"}, result.GetAnnotations()) + assert.Len(t, app.Status.Conditions, 0) +} + +// TestCompareAppStateNamespaceMetadata tests comparison when managed namespace metadata differs to live +func TestCompareAppStateNamespaceMetadata(t *testing.T) { + ns := NewNamespace() + ns.SetName(test.FakeDestNamespace) + ns.SetNamespace(test.FakeDestNamespace) + ns.SetAnnotations(map[string]string{"bar": "bat"}) + + app := newFakeApp() + app.Spec.SyncPolicy.ManagedNamespaceMetadata = &argoappv1.ManagedNamespaceMetadata{ + Labels: map[string]string{ + "foo": "bar", + }, + Annotations: map[string]string{ + "foo": "bar", + }, + } + app.Status.OperationState = &argoappv1.OperationState{ + SyncResult: &argoappv1.SyncOperationResult{}, + } + + data := fakeData{ + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{ + kube.GetResourceKey(ns): ns, + }, + } + ctrl := newFakeController(&data, nil) + sources := make([]argoappv1.ApplicationSource, 0) + sources = append(sources, app.Spec.GetSource()) + revisions := make([]string, 0) + revisions = append(revisions, "") + compRes, err := ctrl.appStateManager.CompareAppState(app, &defaultProj, revisions, sources, false, false, nil, false) + assert.Nil(t, err) + assert.NotNil(t, compRes) + assert.NotNil(t, compRes.syncStatus) + assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status) + assert.Len(t, compRes.resources, 1) + assert.Len(t, compRes.managedResources, 1) + assert.NotNil(t, compRes.diffResultList) + assert.Len(t, compRes.diffResultList.Diffs, 1) + + result := NewNamespace() + assert.NoError(t, json.Unmarshal(compRes.diffResultList.Diffs[0].PredictedLive, result)) + + labels := result.GetLabels() + delete(labels, "kubernetes.io/metadata.name") + + assert.Equal(t, map[string]string{"foo": "bar"}, labels) + assert.Equal(t, map[string]string{"argocd.argoproj.io/sync-options": "ServerSideApply=true", "bar": "bat", "foo": "bar"}, result.GetAnnotations()) + assert.Len(t, app.Status.Conditions, 0) +} + // TestCompareAppStateNamespaceMetadataIsTheSame tests comparison when managed namespace metadata is the same func TestCompareAppStateNamespaceMetadataIsTheSame(t *testing.T) { app := newFakeApp() @@ -717,7 +840,7 @@ func Test_appStateManager_persistRevisionHistory(t *testing.T) { app.Spec.RevisionHistoryLimit = &i } addHistory := func() { - err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{}, []string{}, []argoappv1.ApplicationSource{}, false, metav1.Time{}) + err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{}, []string{}, []argoappv1.ApplicationSource{}, false, metav1.Time{}, v1alpha1.OperationInitiator{}) assert.NoError(t, err) } addHistory() @@ -753,7 +876,7 @@ func Test_appStateManager_persistRevisionHistory(t *testing.T) { assert.Len(t, app.Status.History, 9) metav1NowTime := metav1.NewTime(time.Now()) - err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{}, []string{}, []argoappv1.ApplicationSource{}, false, metav1NowTime) + err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{}, []string{}, []argoappv1.ApplicationSource{}, false, metav1NowTime, v1alpha1.OperationInitiator{}) assert.NoError(t, err) assert.Equal(t, app.Status.History.LastRevisionHistory().DeployStartedAt, &metav1NowTime) } @@ -1275,3 +1398,283 @@ func TestIsLiveResourceManaged(t *testing.T) { assert.True(t, manager.isSelfReferencedObj(managedWrongAPIGroup, config, appName, common.AnnotationKeyAppInstance, argo.TrackingMethodAnnotation)) }) } + +func TestUseDiffCache(t *testing.T) { + type fixture struct { + testName string + noCache bool + manifestInfos []*apiclient.ManifestResponse + sources []argoappv1.ApplicationSource + app *argoappv1.Application + manifestRevisions []string + statusRefreshTimeout time.Duration + expectedUseCache bool + serverSideDiff bool + } + + manifestInfos := func(revision string) []*apiclient.ManifestResponse { + return []*apiclient.ManifestResponse{ + { + Manifests: []string{ + "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-svc\",\"namespace\":\"httpbin\"},\"spec\":{\"ports\":[{\"name\":\"http-port\",\"port\":7777,\"targetPort\":80},{\"name\":\"test\",\"port\":333}],\"selector\":{\"app\":\"httpbin\"}}}", + "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"httpbin\"},\"name\":\"httpbin-deployment\",\"namespace\":\"httpbin\"},\"spec\":{\"replicas\":2,\"selector\":{\"matchLabels\":{\"app\":\"httpbin\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"httpbin\"}},\"spec\":{\"containers\":[{\"image\":\"kennethreitz/httpbin\",\"imagePullPolicy\":\"Always\",\"name\":\"httpbin\",\"ports\":[{\"containerPort\":80}]}]}}}}", + }, + Namespace: "", + Server: "", + Revision: revision, + SourceType: "Kustomize", + VerifyResult: "", + }, + } + } + sources := func() []argoappv1.ApplicationSource { + return []argoappv1.ApplicationSource{ + { + RepoURL: "https://some-repo.com", + Path: "argocd/httpbin", + TargetRevision: "HEAD", + }, + } + } + + app := func(namespace string, revision string, refresh bool, a *argoappv1.Application) *argoappv1.Application { + app := &argoappv1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "httpbin", + Namespace: namespace, + }, + Spec: argoappv1.ApplicationSpec{ + Source: &argoappv1.ApplicationSource{ + RepoURL: "https://some-repo.com", + Path: "argocd/httpbin", + TargetRevision: "HEAD", + }, + Destination: argoappv1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "httpbin", + }, + Project: "default", + SyncPolicy: &argoappv1.SyncPolicy{ + SyncOptions: []string{ + "CreateNamespace=true", + "ServerSideApply=true", + }, + }, + }, + Status: argoappv1.ApplicationStatus{ + Resources: []argoappv1.ResourceStatus{}, + Sync: argoappv1.SyncStatus{ + Status: argoappv1.SyncStatusCodeSynced, + ComparedTo: argoappv1.ComparedTo{ + Source: argoappv1.ApplicationSource{ + RepoURL: "https://some-repo.com", + Path: "argocd/httpbin", + TargetRevision: "HEAD", + }, + Destination: argoappv1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "httpbin", + }, + }, + Revision: revision, + Revisions: []string{}, + }, + ReconciledAt: &metav1.Time{ + Time: time.Now().Add(-time.Hour), + }, + }, + } + if refresh { + annotations := make(map[string]string) + annotations[argoappv1.AnnotationKeyRefresh] = string(argoappv1.RefreshTypeNormal) + app.SetAnnotations(annotations) + } + if a != nil { + err := mergo.Merge(app, a, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue) + if err != nil { + t.Fatalf("error merging app: %s", err) + } + } + return app + } + + cases := []fixture{ + { + testName: "will use diff cache", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "rev1", false, nil), + manifestRevisions: []string{"rev1"}, + statusRefreshTimeout: time.Hour * 24, + expectedUseCache: true, + serverSideDiff: false, + }, + { + testName: "will use diff cache with sync policy", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: test.YamlToApplication(testdata.DiffCacheYaml), + manifestRevisions: []string{"rev1"}, + statusRefreshTimeout: time.Hour * 24, + expectedUseCache: true, + serverSideDiff: true, + }, + { + testName: "will use diff cache for multisource", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "", false, &argoappv1.Application{ + Spec: argoappv1.ApplicationSpec{ + Source: nil, + Sources: argoappv1.ApplicationSources{ + { + RepoURL: "multisource repo1", + }, + { + RepoURL: "multisource repo2", + }, + }, + }, + Status: argoappv1.ApplicationStatus{ + Resources: []argoappv1.ResourceStatus{}, + Sync: argoappv1.SyncStatus{ + Status: argoappv1.SyncStatusCodeSynced, + ComparedTo: argoappv1.ComparedTo{ + Source: argoappv1.ApplicationSource{}, + Sources: argoappv1.ApplicationSources{ + { + RepoURL: "multisource repo1", + }, + { + RepoURL: "multisource repo2", + }, + }, + }, + Revisions: []string{"rev1", "rev2"}, + }, + ReconciledAt: &metav1.Time{ + Time: time.Now().Add(-time.Hour), + }, + }, + }), + manifestRevisions: []string{"rev1", "rev2"}, + statusRefreshTimeout: time.Hour * 24, + expectedUseCache: true, + serverSideDiff: false, + }, + { + testName: "will return false if nocache is true", + noCache: true, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "rev1", false, nil), + manifestRevisions: []string{"rev1"}, + statusRefreshTimeout: time.Hour * 24, + expectedUseCache: false, + serverSideDiff: false, + }, + { + testName: "will return false if requested refresh", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "rev1", true, nil), + manifestRevisions: []string{"rev1"}, + statusRefreshTimeout: time.Hour * 24, + expectedUseCache: false, + serverSideDiff: false, + }, + { + testName: "will return false if status expired", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "rev1", false, nil), + manifestRevisions: []string{"rev1"}, + statusRefreshTimeout: time.Minute, + expectedUseCache: false, + serverSideDiff: false, + }, + { + testName: "will return true if status expired and server-side diff", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "rev1", false, nil), + manifestRevisions: []string{"rev1"}, + statusRefreshTimeout: time.Minute, + expectedUseCache: true, + serverSideDiff: true, + }, + { + testName: "will return false if there is a new revision", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "rev1", false, nil), + manifestRevisions: []string{"rev2"}, + statusRefreshTimeout: time.Hour * 24, + expectedUseCache: false, + serverSideDiff: false, + }, + { + testName: "will return false if app spec repo changed", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "rev1", false, &argoappv1.Application{ + Spec: argoappv1.ApplicationSpec{ + Source: &argoappv1.ApplicationSource{ + RepoURL: "new-repo", + }, + }, + }), + manifestRevisions: []string{"rev1"}, + statusRefreshTimeout: time.Hour * 24, + expectedUseCache: false, + serverSideDiff: false, + }, + { + testName: "will return false if app spec IgnoreDifferences changed", + noCache: false, + manifestInfos: manifestInfos("rev1"), + sources: sources(), + app: app("httpbin", "rev1", false, &argoappv1.Application{ + Spec: argoappv1.ApplicationSpec{ + IgnoreDifferences: []argoappv1.ResourceIgnoreDifferences{ + { + Group: "app/v1", + Kind: "application", + Name: "httpbin", + Namespace: "httpbin", + JQPathExpressions: []string{"."}, + }, + }, + }, + }), + manifestRevisions: []string{"rev1"}, + statusRefreshTimeout: time.Hour * 24, + expectedUseCache: false, + serverSideDiff: false, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.testName, func(t *testing.T) { + // Given + t.Parallel() + logger, _ := logrustest.NewNullLogger() + log := logrus.NewEntry(logger) + + // When + useDiffCache := useDiffCache(tc.noCache, tc.manifestInfos, tc.sources, tc.app, tc.manifestRevisions, tc.statusRefreshTimeout, tc.serverSideDiff, log) + + // Then + assert.Equal(t, useDiffCache, tc.expectedUseCache) + }) + } +} diff --git a/controller/sync.go b/controller/sync.go index 2b925b7782b9eb..34c12bdb5da3ce 100644 --- a/controller/sync.go +++ b/controller/sync.go @@ -57,6 +57,27 @@ func (m *appStateManager) getGVKParser(server string) (*managedfields.GvkParser, return cluster.GetGVKParser(), nil } +// getResourceOperations will return the kubectl implementation of the ResourceOperations +// interface that provides functionality to manage kubernetes resources. Returns a +// cleanup function that must be called to remove the generated kube config for this +// server. +func (m *appStateManager) getResourceOperations(server string) (kube.ResourceOperations, func(), error) { + clusterCache, err := m.liveStateCache.GetClusterCache(server) + if err != nil { + return nil, nil, fmt.Errorf("error getting cluster cache: %w", err) + } + + cluster, err := m.db.GetCluster(context.Background(), server) + if err != nil { + return nil, nil, fmt.Errorf("error getting cluster: %w", err) + } + ops, cleanup, err := m.kubectl.ManageResources(cluster.RawRestConfig(), clusterCache.GetOpenAPISchema()) + if err != nil { + return nil, nil, fmt.Errorf("error creating kubectl ResourceOperations: %w", err) + } + return ops, cleanup, nil +} + func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState) { // Sync requests might be requested with ambiguous revisions (e.g. master, HEAD, v1.2.3). // This can change meaning when resuming operations (e.g a hook sync). After calculating a @@ -82,7 +103,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha if syncOp.SyncOptions.HasOption("FailOnSharedResource=true") && hasSharedResource { state.Phase = common.OperationFailed - state.Message = fmt.Sprintf("Shared resouce found: %s", sharedResourceMessage) + state.Message = fmt.Sprintf("Shared resource found: %s", sharedResourceMessage) return } @@ -283,6 +304,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha sync.WithInitialState(state.Phase, state.Message, initialResourcesRes, state.StartedAt), sync.WithResourcesFilter(func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool { return (len(syncOp.Resources) == 0 || + isPostDeleteHook(target) || argo.ContainsSyncResource(key.Name, key.Namespace, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources)) && m.isSelfReferencedObj(live, target, app.GetName(), appLabelKey, trackingMethod) }), @@ -369,7 +391,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha logEntry.WithField("duration", time.Since(start)).Info("sync/terminate complete") if !syncOp.DryRun && len(syncOp.Resources) == 0 && state.Phase.Successful() { - err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source, compareResult.syncStatus.Revisions, compareResult.syncStatus.ComparedTo.Sources, app.Spec.HasMultipleSources(), state.StartedAt) + err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source, compareResult.syncStatus.Revisions, compareResult.syncStatus.ComparedTo.Sources, app.Spec.HasMultipleSources(), state.StartedAt, state.Operation.InitiatedBy) if err != nil { state.Phase = common.OperationError state.Message = fmt.Sprintf("failed to record sync to history: %v", err) diff --git a/controller/testdata/data.go b/controller/testdata/data.go index a53c6a8a88b356..028a7caaeac6b7 100644 --- a/controller/testdata/data.go +++ b/controller/testdata/data.go @@ -11,4 +11,7 @@ var ( //go:embed target-deployment-new-entries.yaml TargetDeploymentNewEntries string + + //go:embed diff-cache.yaml + DiffCacheYaml string ) diff --git a/controller/testdata/diff-cache.yaml b/controller/testdata/diff-cache.yaml new file mode 100644 index 00000000000000..41a1e8a4bbeb1e --- /dev/null +++ b/controller/testdata/diff-cache.yaml @@ -0,0 +1,498 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + argocd-image-updater.argoproj.io/allow-tags: any + argocd-image-updater.argoproj.io/ignore-tags: "" + argocd-image-updater.argoproj.io/image-list-disabled-hack: "" + argocd-image-updater.argoproj.io/update-strategy: semver + argocd-image-updater.argoproj.io/write-back-method: git + argocd-image-updater.argoproj.io/write-back-target: kustomization + argocd-notif-onDeployed.slack-disabled: "" + argocd-notif-onHealthDegraded.slack-disabled: "" + argocd-notif-onSyncFailed.slack-disabled: "" + argocd-notif-onSyncRunning.slack-disabled: "" + argocd-notif-onSyncStatusUnknown.slack-disabled: "" + argocd-notif-onSyncSucceeded.slack-disabled: "" + argocd.argoproj.io/compare-options: ServerSideDiff=true + argocd.argoproj.io/manifest-generate-paths: .;/chart + creationTimestamp: "2024-03-04T21:30:33Z" + finalizers: + - resources-finalizer.argocd.argoproj.io + generation: 263 + labels: + cloud_provider: gcp + cluster_name: gke-alpha-01-europe-west1 + foo: bar + preview: "true" + project: sre + service_class: alpha + stack: gke-v2 + name: velero-test + namespace: argo-cd + ownerReferences: + - apiVersion: argoproj.io/v1alpha1 + blockOwnerDeletion: true + controller: true + kind: ApplicationSet + name: velero + uid: 86cdfba4-8697-47b3-8489-71fab7f4a805 + resourceVersion: "722811357" + uid: 94978696-4fd4-40b3-a1de-38d9df9e9316 +spec: + destination: + name: gke-alpha-01-europe-west1 + namespace: test-lla + project: sre + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + syncPolicy: + retry: + backoff: + duration: 5s + factor: 2 + maxDuration: 3m + limit: 10 + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true + - RespectIgnoreDifferences=false + - ServerSideApply=true + - Validate=true +status: + controllerNamespace: argo-cd + health: + status: Healthy + history: + - deployStartedAt: "2024-03-04T22:00:05Z" + deployedAt: "2024-03-04T22:00:06Z" + id: 14 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-04T22:08:29Z" + deployedAt: "2024-03-04T22:08:30Z" + id: 15 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-04T22:09:16Z" + deployedAt: "2024-03-04T22:09:16Z" + id: 16 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-04T22:11:41Z" + deployedAt: "2024-03-04T22:11:41Z" + id: 17 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-04T22:50:55Z" + deployedAt: "2024-03-04T22:50:55Z" + id: 18 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-04T22:52:56Z" + deployedAt: "2024-03-04T22:52:56Z" + id: 19 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-04T22:56:15Z" + deployedAt: "2024-03-04T22:56:15Z" + id: 20 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-05T07:31:56Z" + deployedAt: "2024-03-05T07:31:57Z" + id: 21 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-05T07:32:44Z" + deployedAt: "2024-03-05T07:32:44Z" + id: 22 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + - deployStartedAt: "2024-03-05T07:33:03Z" + deployedAt: "2024-03-05T07:33:04Z" + id: 23 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + operationState: + finishedAt: "2024-03-05T07:33:04Z" + message: successfully synced (all tasks run) + operation: + initiatedBy: + username: laurent.lavaud@mirakl.com + retry: + backoff: + duration: 5s + factor: 2 + maxDuration: 3m + limit: 10 + sync: + revision: ea8759964626a583667a2bfd08f334ec2070040a + syncOptions: + - ServerSideApply=true + syncStrategy: + hook: {} + phase: Succeeded + startedAt: "2024-03-05T07:33:03Z" + syncResult: + resources: + - group: "" + hookPhase: Running + kind: Service + message: service/test-lla serverside-applied + name: test-lla + namespace: test-lla + status: Synced + syncPhase: Sync + version: v1 + - group: apps + hookPhase: Running + kind: Deployment + message: deployment.apps/test-lla serverside-applied + name: test-lla + namespace: test-lla + status: Synced + syncPhase: Sync + version: v1 + revision: ea8759964626a583667a2bfd08f334ec2070040a + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + reconciledAt: "2024-03-05T07:33:04Z" + resources: + - health: + status: Healthy + kind: Service + name: test-lla + namespace: test-lla + status: Synced + version: v1 + - group: apps + health: + status: Healthy + kind: Deployment + name: test-lla + namespace: test-lla + status: Synced + version: v1 + sourceType: Plugin + summary: + images: + - nginx:latest + sync: + comparedTo: + destination: + name: gke-alpha-01-europe-west1 + namespace: test-lla + source: + path: instances/test + plugin: + env: + - name: RELEASE_NAME + value: test-lla + - name: CHART_REPOSITORY + value: oci://europe-west1-docker.pkg.dev/platform-89be/charts + - name: CHART_NAME + value: velero + - name: PREVIEW + value: "false" + - name: HELM_VALUES + value: | + global: + app: + cluster_name: gke-alpha-01-europe-west1 + service_class: alpha + cloud_provider: gcp + cluster_stack: gke-v2 + - name: HELM_ARGS + value: "" + name: cmp-helm-v2 + repoURL: https://github.com/mirakl/manifests-velero.git + targetRevision: test-lla + revision: rev1 + status: Synced diff --git a/docs/assets/api-management.png b/docs/assets/api-management.png deleted file mode 100644 index ae066f0a6a87d3..00000000000000 Binary files a/docs/assets/api-management.png and /dev/null differ diff --git a/docs/assets/groups-claim.png b/docs/assets/groups-claim.png deleted file mode 100644 index d27e03b661f825..00000000000000 Binary files a/docs/assets/groups-claim.png and /dev/null differ diff --git a/docs/assets/groups-scope.png b/docs/assets/groups-scope.png deleted file mode 100644 index 45557b51ead7f6..00000000000000 Binary files a/docs/assets/groups-scope.png and /dev/null differ diff --git a/docs/assets/okta-app.png b/docs/assets/okta-app.png new file mode 100644 index 00000000000000..bfc4570826b0a2 Binary files /dev/null and b/docs/assets/okta-app.png differ diff --git a/docs/assets/okta-auth-policy.png b/docs/assets/okta-auth-policy.png new file mode 100644 index 00000000000000..dbf99a88ed6e3c Binary files /dev/null and b/docs/assets/okta-auth-policy.png differ diff --git a/docs/assets/okta-auth-rule.png b/docs/assets/okta-auth-rule.png new file mode 100644 index 00000000000000..4e85b062f357be Binary files /dev/null and b/docs/assets/okta-auth-rule.png differ diff --git a/docs/assets/okta-create-oidc-app.png b/docs/assets/okta-create-oidc-app.png new file mode 100644 index 00000000000000..cf0b75b0e4a21f Binary files /dev/null and b/docs/assets/okta-create-oidc-app.png differ diff --git a/docs/assets/okta-groups-claim.png b/docs/assets/okta-groups-claim.png new file mode 100644 index 00000000000000..4edb93d42ea91c Binary files /dev/null and b/docs/assets/okta-groups-claim.png differ diff --git a/docs/assets/okta-groups-scope.png b/docs/assets/okta-groups-scope.png new file mode 100644 index 00000000000000..6cd1783c726536 Binary files /dev/null and b/docs/assets/okta-groups-scope.png differ diff --git a/docs/cli_installation.md b/docs/cli_installation.md index 42938bcd751ba3..5a314d4ce6be2f 100644 --- a/docs/cli_installation.md +++ b/docs/cli_installation.md @@ -37,6 +37,17 @@ sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd rm argocd-linux-amd64 ``` +#### Download latest stable version + +You can download the latest stable release by executing below steps: + +```bash +VERSION=$(curl -L -s https://raw.githubusercontent.com/argoproj/argo-cd/stable/VERSION) +curl -sSL -o argocd-linux-amd64 https://github.com/argoproj/argo-cd/releases/download/v$VERSION/argocd-linux-amd64 +sudo install -m 555 argocd-linux-amd64 /usr/local/bin/argocd +rm argocd-linux-amd64 +``` + You should now be able to run `argocd` commands. diff --git a/docs/developer-guide/architecture/components.md b/docs/developer-guide/architecture/components.md index eb2904b531ccb2..e073751da48673 100644 --- a/docs/developer-guide/architecture/components.md +++ b/docs/developer-guide/architecture/components.md @@ -71,7 +71,7 @@ and the CLI functionalities. ### Application Controller The Application Controller is responsible for reconciling the -Application resource in Kubernetes syncronizing the desired +Application resource in Kubernetes synchronizing the desired application state (provided in Git) with the live state (in Kubernetes). The Application Controller is also responsible for reconciling the Project resource. diff --git a/docs/developer-guide/contributors-quickstart.md b/docs/developer-guide/contributors-quickstart.md index 0e98fab7ec940e..68cda35b6d08e9 100644 --- a/docs/developer-guide/contributors-quickstart.md +++ b/docs/developer-guide/contributors-quickstart.md @@ -9,7 +9,9 @@ and the [toolchain guide](toolchain-guide.md). ### Install Go -Install version 1.18 or newer (Verify version by running `go version`) + + +Install Go with a version equal to or greater than the version listed in `go.mod` (verify go version with `go version`). ### Clone the Argo CD repo @@ -23,16 +25,29 @@ git clone https://github.com/argoproj/argo-cd.git -### Install or Upgrade `kind` (Optional - Should work with any local cluster) +### Install or Upgrade a Tool for Running Local Clusters (e.g. kind or minikube) + +#### Installation guide for kind: +#### Installation guide for minikube: + + + ### Start Your Local Cluster +For example, if you are using kind: ```shell kind create cluster ``` +Or, if you are using minikube: + +```shell +minikube start +``` + ### Install Argo CD ```shell diff --git a/docs/developer-guide/debugging-remote-environment.md b/docs/developer-guide/debugging-remote-environment.md index 7f8102a75c5022..5548d3444af8c9 100644 --- a/docs/developer-guide/debugging-remote-environment.md +++ b/docs/developer-guide/debugging-remote-environment.md @@ -45,7 +45,7 @@ And uninstall telepresence from your cluster: telepresence helm uninstall ``` -See [this quickstart](https://www.telepresence.io/docs/latest/howtos/intercepts/) for more information on how to intercept services using Telepresence. +See [this quickstart](https://www.telepresence.io/docs/latest/quick-start/) for more information on how to intercept services using Telepresence. ### Connect (telepresence v1) Use the following command instead: diff --git a/docs/developer-guide/extensions/proxy-extensions.md b/docs/developer-guide/extensions/proxy-extensions.md index 9982a5cdee59a8..c53946cade95f6 100644 --- a/docs/developer-guide/extensions/proxy-extensions.md +++ b/docs/developer-guide/extensions/proxy-extensions.md @@ -15,7 +15,7 @@ requests before forwarding to the backend service. As proxy extension is in [Alpha][1] phase, the feature is disabled by default. To enable it, it is necessary to configure the feature flag -in Argo CD command parameters. The easiest way to to properly enable +in Argo CD command parameters. The easiest way to properly enable this feature flag is by adding the `server.enable.proxy.extension` key in the existing `argocd-cmd-params-cm`. For example: diff --git a/docs/developer-guide/extensions/ui-extensions.md b/docs/developer-guide/extensions/ui-extensions.md index ec1631038ad53f..8d3d9dc4a38825 100644 --- a/docs/developer-guide/extensions/ui-extensions.md +++ b/docs/developer-guide/extensions/ui-extensions.md @@ -95,3 +95,66 @@ Below is an example of a simple system level extension: Since the Argo CD Application is a Kubernetes resource, application tabs can be the same as any other resource tab. Make sure to use 'argoproj.io'/'Application' as group/kind and an extension will be used to render the application-level tab. + +## Application Status Panel Extensions + +The status panel is the bar at the top of the application view where the sync status is displayed. Argo CD allows you to add new items to the status panel of an application. The extension should be registered using the `extensionsAPI.registerStatusPanelExtension` method: + +```typescript +registerStatusPanelExtension(component: StatusPanelExtensionComponent, title: string, id: string, flyout?: ExtensionComponent) +``` + +Below is an example of a simple extension: + +```typescript +((window) => { + const component = () => { + return React.createElement( + "div", + { style: { padding: "10px" } }, + "Hello World" + ); + }; + window.extensionsAPI.registerStatusPanelExtension( + component, + "My Extension", + "my_extension" + ); +})(window); +``` + +### Flyout widget + +It is also possible to add an optional flyout widget to your extension. It can be opened by calling `openFlyout()` from your extension's component. Your flyout component will then be rendered in a sliding panel, similar to the panel that opens when clicking on `History and rollback`. + +Below is an example of an extension using the flyout widget: + +```typescript +((window) => { + const component = (props: { + openFlyout: () => any + }) => { + return React.createElement( + "div", + { + style: { padding: "10px" }, + onClick: () => props.openFlyout() + }, + "Hello World" + ); + }; + const flyout = () => { + return React.createElement( + "div", + { style: { padding: "10px" } }, + "This is a flyout" + ); + }; + window.extensionsAPI.registerStatusPanelExtension( + component, + "My Extension", + "my_extension", + flyout + ); +})(window); +``` diff --git a/docs/developer-guide/release-process-and-cadence.md b/docs/developer-guide/release-process-and-cadence.md index 337d5bafc35282..737c6eba6a8d93 100644 --- a/docs/developer-guide/release-process-and-cadence.md +++ b/docs/developer-guide/release-process-and-cadence.md @@ -6,14 +6,15 @@ These are the upcoming releases dates: -| Release | Release Planning Meeting | Release Candidate 1 | General Availability | Release Champion | Checklist | -|---------|--------------------------|-----------------------|----------------------|-------------------------------------------------------|---------------------------------------------------------------| -| v2.6 | Monday, Dec. 12, 2022 | Monday, Dec. 19, 2022 | Monday, Feb. 6, 2023 | [William Tam](https://github.com/wtam2018) | [checklist](https://github.com/argoproj/argo-cd/issues/11563) | -| v2.7 | Monday, Mar. 6, 2023 | Monday, Mar. 20, 2023 | Monday, May. 1, 2023 | [Pavel Kostohrys](https://github.com/pasha-codefresh) | [checklist](https://github.com/argoproj/argo-cd/issues/12762) | -| v2.8 | Monday, Jun. 20, 2023 | Monday, Jun. 26, 2023 | Monday, Aug. 7, 2023 | [Keith Chong](https://github.com/keithchong) | [checklist](https://github.com/argoproj/argo-cd/issues/13742) | -| v2.9 | Monday, Sep. 4, 2023 | Monday, Sep. 18, 2023 | Monday, Nov. 6, 2023 | [Leonardo Almeida](https://github.com/leoluz) | [checklist](https://github.com/argoproj/argo-cd/issues/14078) | -| v2.10 | Monday, Dec. 4, 2023 | Monday, Dec. 18, 2023 | Monday, Feb. 5, 2024 | - +| Release | Release Candidate 1 | General Availability | Release Champion | Release Approver |Checklist | +|---------|-----------------------|----------------------|-------------------------------------------------------|-------------------------------------------------------|---------------------------------------------------------------| +| v2.6 | Monday, Dec. 19, 2022 | Monday, Feb. 6, 2023 | [William Tam](https://github.com/wtam2018) | [William Tam](https://github.com/wtam2018) | [checklist](https://github.com/argoproj/argo-cd/issues/11563) | +| v2.7 | Monday, Mar. 20, 2023 | Monday, May 1, 2023 | [Pavel Kostohrys](https://github.com/pasha-codefresh) | [Pavel Kostohrys](https://github.com/pasha-codefresh) | [checklist](https://github.com/argoproj/argo-cd/issues/12762) | +| v2.8 | Monday, Jun. 26, 2023 | Monday, Aug. 7, 2023 | [Keith Chong](https://github.com/keithchong) | [Keith Chong](https://github.com/keithchong) | [checklist](https://github.com/argoproj/argo-cd/issues/13742) | +| v2.9 | Monday, Sep. 18, 2023 | Monday, Nov. 6, 2023 | [Leonardo Almeida](https://github.com/leoluz) | [Leonardo Almeida](https://github.com/leoluz) | [checklist](https://github.com/argoproj/argo-cd/issues/14078) | +| v2.10 | Monday, Dec. 18, 2023 | Monday, Feb. 5, 2024 | [Katie Lamkin](https://github.com/kmlamkin9) | | [checklist](https://github.com/argoproj/argo-cd/issues/16339) | +| v2.11 | Monday, Mar. 18, 2024 | Monday, May 6, 2024 | +| v2.12 | Monday, Jun. 17, 2024 | Monday, Aug. 5, 2024 | Actual release dates might differ from the plan by a few days. @@ -22,8 +23,8 @@ Actual release dates might differ from the plan by a few days. #### Minor Releases (e.g. 2.x.0) A minor Argo CD release occurs four times a year, once every three months. Each General Availability (GA) release is -preceded by several Release Candidates (RCs). The first RC is released three weeks before the scheduled GA date. This -effectively means that there is a three-week feature freeze. +preceded by several Release Candidates (RCs). The first RC is released seven weeks before the scheduled GA date. This +effectively means that there is a seven-week feature freeze. These are the approximate release dates: @@ -40,17 +41,6 @@ Argo CD patch releases occur on an as-needed basis. Only the three most recent m releases. Versions older than the three most recent minor versions are considered EOL and will not receive bug fixes or security updates. -#### Minor Release Planning Meeting - -Roughly two weeks before the RC date, there will be a meeting to discuss which features are planned for the RC. This meeting is -for contributors to advocate for certain features. Features which have at least one approver (besides the contributor) -who can assure they will review/merge by the RC date will be included in the release milestone. All other features will -be dropped from the milestone (and potentially shifted to the next one). - -Since not everyone will be able to attend the meeting, there will be a meeting doc. Contributors can add their feature -to a table, and Approvers can add their name to the table. Features with a corresponding approver will remain in the -release milestone. - #### Release Champion To help manage all the steps involved in a release, we will have a Release Champion. The Release Champion will be diff --git a/docs/developer-guide/site.md b/docs/developer-guide/site.md index af32753a323e26..efd6aece9aedb1 100644 --- a/docs/developer-guide/site.md +++ b/docs/developer-guide/site.md @@ -7,20 +7,14 @@ The website is built using `mkdocs` and `mkdocs-material`. To test: ```bash +make build-docs make serve-docs ``` - Once running, you can view your locally built documentation at [http://0.0.0.0:8000/](http://0.0.0.0:8000/). -## Deploying - -```bash -make publish-docs -``` - ## Analytics !!! tip Don't forget to disable your ad-blocker when testing. -We collect [Google Analytics](https://analytics.google.com/analytics/web/#/report-home/a105170809w198079555p192782995). \ No newline at end of file +We collect [Google Analytics](https://analytics.google.com/analytics/web/#/report-home/a105170809w198079555p192782995). diff --git a/docs/developer-guide/toolchain-guide.md b/docs/developer-guide/toolchain-guide.md index 42ca7fac874048..335180438dac60 100644 --- a/docs/developer-guide/toolchain-guide.md +++ b/docs/developer-guide/toolchain-guide.md @@ -304,7 +304,7 @@ For installing the tools required to build and test Argo CD on your local system You can change the target location by setting the `BIN` environment before running the installer scripts. For example, you can install the binaries into `~/go/bin` (which should then be the first component in your `PATH` environment, i.e. `export PATH=~/go/bin:$PATH`): ```shell -make BIN=~/go/bin install-tools-local +BIN=~/go/bin make install-tools-local ``` Additionally, you have to install at least the following tools via your OS's package manager (this list might not be always up-to-date): diff --git a/docs/faq.md b/docs/faq.md index 19273acc04d23e..83bdf8d7d38b5c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -36,6 +36,15 @@ which might cause health check to return `Progressing` state instead of `Healthy As workaround Argo CD allows providing [health check](operator-manual/health.md) customization which overrides default behavior. +If you are using Traefik for your Ingress, you can update the Traefik config to publish the loadBalancer IP using [publishedservice](https://doc.traefik.io/traefik/providers/kubernetes-ingress/#publishedservice), which will resolve this issue. + +```yaml +providers: + kubernetesIngress: + publishedService: + enabled: true +``` + ## I forgot the admin password, how do I reset it? For Argo CD v1.8 and earlier, the initial password is set to the name of the server pod, as @@ -88,7 +97,7 @@ data: ## After deploying my Helm application with Argo CD I cannot see it with `helm ls` and other Helm commands -When deploying a Helm application Argo CD is using Helm +When deploying a Helm application Argo CD is using Helm only as a template mechanism. It runs `helm template` and then deploys the resulting manifests on the cluster instead of doing `helm install`. This means that you cannot use any Helm command to view/verify the application. It is fully managed by Argo CD. @@ -131,15 +140,15 @@ Argo CD automatically sets the `app.kubernetes.io/instance` label and uses it to If the tool does this too, this causes confusion. You can change this label by setting the `application.instanceLabelKey` value in the `argocd-cm`. We recommend that you use `argocd.argoproj.io/instance`. -!!! note +!!! note When you make this change your applications will become out of sync and will need re-syncing. See [#1482](https://github.com/argoproj/argo-cd/issues/1482). ## How often does Argo CD check for changes to my Git or Helm repository ? -The default polling interval is 3 minutes (180 seconds). -You can change the setting by updating the `timeout.reconciliation` value in the [argocd-cm](https://github.com/argoproj/argo-cd/blob/2d6ce088acd4fb29271ffb6f6023dbb27594d59b/docs/operator-manual/argocd-cm.yaml#L279-L282) config map. If there are any Git changes, Argo CD will only update applications with the [auto-sync setting](user-guide/auto_sync.md) enabled. If you set it to `0` then Argo CD will stop polling Git repositories automatically and you can only use alternative methods such as [webhooks](operator-manual/webhook.md) and/or manual syncs for deploying applications. +The default polling interval is 3 minutes (180 seconds) with a configurable jitter. +You can change the setting by updating the `timeout.reconciliation` value and the `timeout.reconciliation.jitter` in the [argocd-cm](https://github.com/argoproj/argo-cd/blob/2d6ce088acd4fb29271ffb6f6023dbb27594d59b/docs/operator-manual/argocd-cm.yaml#L279-L282) config map. If there are any Git changes, Argo CD will only update applications with the [auto-sync setting](user-guide/auto_sync.md) enabled. If you set it to `0` then Argo CD will stop polling Git repositories automatically and you can only use alternative methods such as [webhooks](operator-manual/webhook.md) and/or manual syncs for deploying applications. ## Why Are My Resource Limits `Out Of Sync`? @@ -241,7 +250,7 @@ There are two parts to the message: > map[name:**KEY_BC** value:150] map[name:**KEY_BC** value:500] map[name:**KEY_BD** value:250] map[name:**KEY_BD** value:500] map[name:KEY_BI value:something] - You'll want to identify the keys that are duplicated -- you can focus on the first part, as each duplicated key will appear, once for each of its value with its value in the first list. The second list is really just + You'll want to identify the keys that are duplicated -- you can focus on the first part, as each duplicated key will appear, once for each of its value with its value in the first list. The second list is really just `]` @@ -250,7 +259,7 @@ There are two parts to the message: This includes all of the keys. It's included for debugging purposes -- you don't need to pay much attention to it. It will give you a hint about the precise location in the list for the duplicated keys: > map[name:KEY_AA] map[name:KEY_AB] map[name:KEY_AC] map[name:KEY_AD] map[name:KEY_AE] map[name:KEY_AF] map[name:KEY_AG] map[name:KEY_AH] map[name:KEY_AI] map[name:KEY_AJ] map[name:KEY_AK] map[name:KEY_AL] map[name:KEY_AM] map[name:KEY_AN] map[name:KEY_AO] map[name:KEY_AP] map[name:KEY_AQ] map[name:KEY_AR] map[name:KEY_AS] map[name:KEY_AT] map[name:KEY_AU] map[name:KEY_AV] map[name:KEY_AW] map[name:KEY_AX] map[name:KEY_AY] map[name:KEY_AZ] map[name:KEY_BA] map[name:KEY_BB] map[name:**KEY_BC**] map[name:**KEY_BD**] map[name:KEY_BE] map[name:KEY_BF] map[name:KEY_BG] map[name:KEY_BH] map[name:KEY_BI] map[name:**KEY_BC**] map[name:**KEY_BD**] - + `]` In this case, the duplicated keys have been **emphasized** to help you identify the problematic keys. Many editors have the ability to highlight all instances of a string, using such an editor can help with such problems. diff --git a/docs/getting_started.md b/docs/getting_started.md index d81bd08897ad8f..1000206eaf9723 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -22,12 +22,8 @@ This will create a new namespace, `argocd`, where Argo CD services and applicati The installation manifests include `ClusterRoleBinding` resources that reference `argocd` namespace. If you are installing Argo CD into a different namespace then make sure to update the namespace reference. -If you are not interested in UI, SSO, multi-cluster features then you can install [core](operator-manual/installation.md#core) Argo CD components only: - -```bash -kubectl create namespace argocd -kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/core-install.yaml -``` +!!! tip + If you are not interested in UI, SSO, and multi-cluster features, then you can install only the [core](operator-manual/core/#installing) Argo CD components. This default installation will have a self-signed certificate and cannot be accessed without a bit of extra work. Do one of: diff --git a/docs/operator-manual/app-any-namespace.md b/docs/operator-manual/app-any-namespace.md index 21743b7bc003d0..21bfa5c4f5a0b1 100644 --- a/docs/operator-manual/app-any-namespace.md +++ b/docs/operator-manual/app-any-namespace.md @@ -15,7 +15,10 @@ Some manual steps will need to be performed by the Argo CD administrator in orde !!! note This feature is considered beta as of now. Some of the implementation details may change over the course of time until it is promoted to a stable status. We will be happy if early adopters use this feature and provide us with bug reports and feedback. - + + +One additional advantage of adopting applications in any namespace is to allow end-users to configure notifications for their Argo CD application in the namespace where Argo CD application is running in. See notifications [namespace based configuration](notifications/index.md#namespace-based-configuration) page for more information. + ## Prerequisites ### Cluster-scoped Argo CD installation diff --git a/docs/operator-manual/application.yaml b/docs/operator-manual/application.yaml index 75a0d3b0df8ae1..864a293ce68903 100644 --- a/docs/operator-manual/application.yaml +++ b/docs/operator-manual/application.yaml @@ -119,7 +119,7 @@ spec: extVars: - name: foo value: bar - # You can use "code to determine if the value is either string (false, the default) or Jsonnet code (if code is true). + # You can use "code" to determine if the value is either string (false, the default) or Jsonnet code (if code is true). - code: true name: baz value: "true" @@ -189,6 +189,7 @@ spec: - PrunePropagationPolicy=foreground # Supported policies are background, foreground and orphan. - PruneLast=true # Allow the ability for resource pruning to happen as a final, implicit wave of a sync operation - RespectIgnoreDifferences=true # When syncing changes, respect fields ignored by the ignoreDifferences configuration + - ApplyOutOfSyncOnly=true # Only sync out-of-sync resources, rather than applying every object in the application managedNamespaceMetadata: # Sets the metadata for the application namespace. Only valid if CreateNamespace=true (see above), otherwise it's a no-op. labels: # The labels to set on the application namespace any: label diff --git a/docs/operator-manual/applicationset.yaml b/docs/operator-manual/applicationset.yaml index 65935802c674ae..d05b08f1101a07 100644 --- a/docs/operator-manual/applicationset.yaml +++ b/docs/operator-manual/applicationset.yaml @@ -33,6 +33,6 @@ spec: - jsonPointers: - /spec/source/targetRevision - name: some-app - jqExpressions: + jqPathExpressions: - .spec.source.helm.values diff --git a/docs/operator-manual/applicationset/Appset-Any-Namespace.md b/docs/operator-manual/applicationset/Appset-Any-Namespace.md index bf3f8ffecfaf18..4e28bc3a8172d4 100644 --- a/docs/operator-manual/applicationset/Appset-Any-Namespace.md +++ b/docs/operator-manual/applicationset/Appset-Any-Namespace.md @@ -72,7 +72,7 @@ data: The allow-list only applies to SCM providers for which the user may configure a custom `api`. Where an SCM or PR generator does not accept a custom API URL, the provider is implicitly allowed. -If you do not intend to allow users to use the SCM or PR generators, you can disable them entirely by setting the environment variable `ARGOCD_APPLICATIONSET_CONTROLLER_ALLOW_SCM_PROVIDERS` to argocd-cmd-params-cm `applicationsetcontroller.allow.scm.providers` to `false`. +If you do not intend to allow users to use the SCM or PR generators, you can disable them entirely by setting the environment variable `ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_SCM_PROVIDERS` to argocd-cmd-params-cm `applicationsetcontroller.enable.scm.providers` to `false`. ### Overview diff --git a/docs/operator-manual/applicationset/Controlling-Resource-Modification.md b/docs/operator-manual/applicationset/Controlling-Resource-Modification.md index 44b6797b24d116..d72cee60ad4013 100644 --- a/docs/operator-manual/applicationset/Controlling-Resource-Modification.md +++ b/docs/operator-manual/applicationset/Controlling-Resource-Modification.md @@ -32,15 +32,13 @@ spec: ``` -- Policy `create-only`: Prevents ApplicationSet controller from modifying or deleting Applications. -- Policy `create-update`: Prevents ApplicationSet controller from deleting Applications. Update is allowed. +- Policy `create-only`: Prevents ApplicationSet controller from modifying or deleting Applications. Prevents Application controller from deleting Applications according to [ownerReferences](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/). +- Policy `create-update`: Prevents ApplicationSet controller from deleting Applications. Update is allowed. Prevents Application controller from deleting Applications according to [ownerReferences](https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/). - Policy `create-delete`: Prevents ApplicationSet controller from modifying Applications. Delete is allowed. - Policy `sync`: Update and Delete are allowed. If the controller parameter `--policy` is set, it takes precedence on the field `applicationsSync`. It is possible to allow per ApplicationSet sync policy by setting variable `ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_POLICY_OVERRIDE` to argocd-cmd-params-cm `applicationsetcontroller.enable.policy.override` or directly with controller parameter `--enable-policy-override` (default to `false`). -This does not prevent deletion of Applications if the ApplicationSet is deleted - ### Controller parameter To allow the ApplicationSet controller to *create* `Application` resources, but prevent any further modification, such as deletion, or modification of Application fields, add this parameter in the ApplicationSet controller: diff --git a/docs/operator-manual/applicationset/Generators-Cluster.md b/docs/operator-manual/applicationset/Generators-Cluster.md index ca1a49aad295b1..aa18983fe3d548 100644 --- a/docs/operator-manual/applicationset/Generators-Cluster.md +++ b/docs/operator-manual/applicationset/Generators-Cluster.md @@ -136,6 +136,29 @@ However, if you do wish to target both local and non-local clusters, while also These steps might seem counterintuitive, but the act of changing one of the default values for the local cluster causes the Argo CD Web UI to create a new secret for this cluster. In the Argo CD namespace, you should now see a Secret resource named `cluster-(cluster suffix)` with label `argocd.argoproj.io/secret-type": "cluster"`. You may also create a local [cluster secret declaratively](../../declarative-setup/#clusters), or with the CLI using `argocd cluster add "(context name)" --in-cluster`, rather than through the Web UI. +### Fetch clusters based on their K8s version + +There is also the possibility to fetch clusters based upon their Kubernetes version. To do this, the label `argocd.argoproj.io/auto-label-cluster-info` needs to be set to `true` on the cluster secret. +Once that has been set, the controller will dynamically label the cluster secret with the Kubernetes version it is running on. To retrieve that value, you need to use the +`argocd.argoproj.io/kubernetes-version`, as the example below demonstrates: + +```yaml +spec: + goTemplate: true + generators: + - clusters: + selector: + matchLabels: + argocd.argoproj.io/kubernetes-version: 1.28 + # matchExpressions are also supported. + #matchExpressions: + # - key: argocd.argoproj.io/kubernetes-version + # operator: In + # values: + # - "1.27" + # - "1.28" +``` + ### Pass additional key-value pairs via `values` field You may pass additional, arbitrary string key-value pairs via the `values` field of the cluster generator. Values added via the `values` field are added as `values.(field)` diff --git a/docs/operator-manual/applicationset/Generators-SCM-Provider.md b/docs/operator-manual/applicationset/Generators-SCM-Provider.md index 6452e16b3bdc1b..f3c83392e30812 100644 --- a/docs/operator-manual/applicationset/Generators-SCM-Provider.md +++ b/docs/operator-manual/applicationset/Generators-SCM-Provider.md @@ -111,7 +111,7 @@ spec: * `tokenRef`: A `Secret` name and key containing the GitLab access token to use for requests. If not specified, will make anonymous requests which have a lower rate limit and can only see public repositories. * `insecure`: By default (false) - Skip checking the validity of the SCM's certificate - useful for self-signed TLS certificates. -For label filtering, the repository tags are used. +For label filtering, the repository topics are used. Available clone protocols are `ssh` and `https`. diff --git a/docs/operator-manual/applicationset/GoTemplate.md b/docs/operator-manual/applicationset/GoTemplate.md index 08c1f3feb035a3..1d62eeea9f93ae 100644 --- a/docs/operator-manual/applicationset/GoTemplate.md +++ b/docs/operator-manual/applicationset/GoTemplate.md @@ -12,6 +12,29 @@ An additional `normalize` function makes any string parameter usable as a valid with hyphens and truncating at 253 characters. This is useful when making parameters safe for things like Application names. +Another `slugify` function has been added which, by default, sanitizes and smart truncates (it doesn't cut a word into 2). This function accepts a couple of arguments: +- The first argument (if provided) is an integer specifying the maximum length of the slug. +- The second argument (if provided) is a boolean indicating whether smart truncation is enabled. +- The last argument (if provided) is the input name that needs to be slugified. + +#### Usage example + +``` +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: test-appset +spec: + ... + template: + metadata: + name: 'hellos3-{{.name}}-{{ cat .branch | slugify 23 }}' + annotations: + label-1: '{{ cat .branch | slugify }}' + label-2: '{{ cat .branch | slugify 23 }}' + label-3: '{{ cat .branch | slugify 50 false }}' +``` + If you want to customize [options defined by text/template](https://pkg.go.dev/text/template#Template.Option), you can add the `goTemplateOptions: ["opt1", "opt2", ...]` key to your ApplicationSet next to `goTemplate: true`. Note that at the time of writing, there is only one useful option defined, which is `missingkey=error`. @@ -183,6 +206,8 @@ ApplicationSet controller provides: 1. contains no more than 253 characters 2. contains only lowercase alphanumeric characters, '-' or '.' 3. starts and ends with an alphanumeric character + +- `slugify`: sanitizes like `normalize` and smart truncates (it doesn't cut a word into 2) like described in the [introduction](#introduction) section. - `toYaml` / `fromYaml` / `fromYamlArray` helm like functions diff --git a/docs/operator-manual/applicationset/Template.md b/docs/operator-manual/applicationset/Template.md index f66a403586bbd4..d96fb39252fedd 100644 --- a/docs/operator-manual/applicationset/Template.md +++ b/docs/operator-manual/applicationset/Template.md @@ -85,7 +85,7 @@ spec: spec: project: "default" source: - revision: HEAD + targetRevision: HEAD repoURL: https://github.com/argoproj/argo-cd.git # New path value is generated here: path: 'applicationset/examples/template-override/{{cluster}}-override' @@ -99,7 +99,7 @@ spec: source: repoURL: https://github.com/argoproj/argo-cd.git targetRevision: HEAD - # This 'default' value is not used: it is is replaced by the generator's template path, above + # This 'default' value is not used: it is replaced by the generator's template path, above path: applicationset/examples/template-override/default destination: server: '{{url}}' @@ -108,3 +108,71 @@ spec: (*The full example can be found [here](https://github.com/argoproj/argo-cd/tree/master/applicationset/examples/template-override).*) In this example, the ApplicationSet controller will generate an `Application` resource using the `path` generated by the List generator, rather than the `path` value defined in `.spec.template`. + +## Template Patch + +Templating is only available on string type. However, some use cases may require applying templating on other types. + +Example: + +- Conditionally set the automated sync policy. +- Conditionally switch prune boolean to `true`. +- Add multiple helm value files from a list. + +The `templatePatch` feature enables advanced templating, with support for `json` and `yaml`. + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: guestbook +spec: + goTemplate: true + generators: + - list: + elements: + - cluster: engineering-dev + url: https://kubernetes.default.svc + autoSync: true + prune: true + valueFiles: + - values.large.yaml + - values.debug.yaml + template: + metadata: + name: '{{.cluster}}-deployment' + spec: + project: "default" + source: + repoURL: https://github.com/infra-team/cluster-deployments.git + targetRevision: HEAD + path: guestbook/{{ .cluster }} + destination: + server: '{{.url}}' + namespace: guestbook + templatePatch: | + spec: + source: + helm: + valueFiles: + {{- range $valueFile := .valueFiles }} + - {{ $valueFile }} + {{- end }} + {{- if .autoSync }} + syncPolicy: + automated: + prune: {{ .prune }} + {{- end }} +``` + +!!! important + The `templatePatch` can apply arbitrary changes to the template. If parameters include untrustworthy user input, it + may be possible to inject malicious changes into the template. It is recommended to use `templatePatch` only with + trusted input or to carefully escape the input before using it in the template. Piping input to `toJson` should help + prevent, for example, a user from successfully injecting a string with newlines. + + The `spec.project` field is not supported in `templatePatch`. If you need to change the project, you can use the + `spec.project` field in the `template` field. + +!!! important + When writing a `templatePatch`, you're crafting a patch. So, if the patch includes an empty `spec: # nothing in here`, it will effectively clear out existing fields. See [#17040](https://github.com/argoproj/argo-cd/issues/17040) for an example of this behavior. diff --git a/docs/operator-manual/argocd-cm.yaml b/docs/operator-manual/argocd-cm.yaml index 5e4ed095be56dd..a291a57a4c9dd5 100644 --- a/docs/operator-manual/argocd-cm.yaml +++ b/docs/operator-manual/argocd-cm.yaml @@ -308,14 +308,22 @@ data: # have either a permanent banner or a regular closeable banner, and NOT both. eg. A user can't dismiss a # notification message (closeable) banner, to then immediately see a permanent banner. # ui.bannerpermanent: "true" - # An option to specify the position of the banner, either the top or bottom of the page. The default is at the top. - # Uncomment to make the banner appear at the bottom of the page. Any value other than "bottom" will make the banner appear at the top. + # An option to specify the position of the banner, either the top or bottom of the page, or both. The valid values + # are: "top", "bottom" and "both". The default (if the option is not provided), is "top". If "both" is specified, then + # the content appears both at the top and the bottom of the page. Uncomment the following line to make the banner appear + # at the bottom of the page. Change the value as needed. # ui.bannerposition: "bottom" # Application reconciliation timeout is the max amount of time required to discover if a new manifests version got # published to the repository. Reconciliation by timeout is disabled if timeout is set to 0. Three minutes by default. # > Note: argocd-repo-server deployment must be manually restarted after changing the setting. timeout.reconciliation: 180s + # With a large number of applications, the periodic refresh for each application can cause a spike in the refresh queue + # and can cause a spike in the repo-server component. To avoid this, you can set a jitter to the sync timeout, which will + # spread out the refreshes and give time to the repo-server to catch up. The jitter is the maximum duration that can be + # added to the sync timeout. So, if the sync timeout is 3 minutes and the jitter is 1 minute, then the actual timeout will + # be between 3 and 4 minutes. Disabled when the value is 0, defaults to 0. + timeout.reconciliation.jitter: 0 # cluster.inClusterEnabled indicates whether to allow in-cluster server address. This is enabled by default. cluster.inClusterEnabled: "true" @@ -405,4 +413,4 @@ data: # Mandatory if multiple services are specified. cluster: name: some-cluster - server: https://some-cluster + server: https://some-cluster \ No newline at end of file diff --git a/docs/operator-manual/argocd-cmd-params-cm.yaml b/docs/operator-manual/argocd-cmd-params-cm.yaml index bb55c6fb213f39..3cb79d85f3150b 100644 --- a/docs/operator-manual/argocd-cmd-params-cm.yaml +++ b/docs/operator-manual/argocd-cmd-params-cm.yaml @@ -17,7 +17,11 @@ data: redis.db: # Open-Telemetry collector address: (e.g. "otel-collector:4317") - otlp.address: + otlp.address: "" + # Open-Telemetry collector insecure: (e.g. "true") + otlp.insecure: "true" + # Open-Telemetry collector headers: (e.g. "key1=value1,key2=value2") + otlp.headers: "" # List of additional namespaces where applications may be created in and # reconciled from. The namespace where Argo CD is installed to will always @@ -64,6 +68,10 @@ data: controller.k8sclient.retry.base.backoff: "100" # Grace period in seconds for ignoring consecutive errors while communicating with repo server. controller.repo.error.grace.period.seconds: "180" + # Enables the server side diff feature at the application controller level. + # Diff calculation will be done by running a server side apply dryrun (when + # diff cache is unavailable). + controller.diff.server.side: "false" ## Server properties # Listen on given address for incoming connections (default "0.0.0.0") @@ -82,6 +90,9 @@ data: server.k8sclient.retry.max: "0" # The initial backoff delay on the first retry attempt in ms. Subsequent retries will double this backoff time up to a maximum threshold server.k8sclient.retry.base.backoff: "100" + # Semicolon-separated list of content types allowed on non-GET requests. Set an empty string to allow all. Be aware + # that allowing content types besides application/json may make your API more vulnerable to CSRF attacks. + server.api.content.types: "application/json" # Set the logging format. One of: text|json (default "text") server.log.format: "text" @@ -206,3 +217,5 @@ data: notificationscontroller.log.level: "info" # Set the logging format. One of: text|json (default "text") notificationscontroller.log.format: "text" + # Enable self-service notifications config. Used in conjunction with apps-in-any-namespace. (default "false") + notificationscontroller.selfservice.enabled: "false" diff --git a/docs/operator-manual/cluster-management.md b/docs/operator-manual/cluster-management.md new file mode 100644 index 00000000000000..bd0d28e08dba72 --- /dev/null +++ b/docs/operator-manual/cluster-management.md @@ -0,0 +1,23 @@ +# Cluster Management + +This guide is for operators looking to manage clusters on the CLI. If you want to use Kubernetes resources for this, check out [Declarative Setup](./declarative-setup.md#clusters). + +Not all commands are described here, see the [argocd cluster Command Reference](../user-guide/commands/argocd_cluster.md) for all available commands. + +## Adding a cluster + +Run `argocd cluster add context-name`. + +If you're unsure about the context names, run `kubectl config get-contexts` to get them all listed. + +This will connect to the cluster and install the necessary resources for ArgoCD to connect to it. +Note that you will need privileged access to the cluster. + +## Removing a cluster + +Run `argocd cluster rm context-name`. + +This removes the cluster with the specified name. + +!!!note "in-cluster cannot be removed" + The `in-cluster` cluster cannot be removed with this. If you want to disable the `in-cluster` configuration, you need to update your `argocd-cm` ConfigMap. Set [`cluster.inClusterEnabled`](./argocd-cm-yaml.md) to `"false"` diff --git a/docs/operator-manual/config-management-plugins.md b/docs/operator-manual/config-management-plugins.md index 5a831dc5e7c474..7c86075ff2f7fb 100644 --- a/docs/operator-manual/config-management-plugins.md +++ b/docs/operator-manual/config-management-plugins.md @@ -110,9 +110,9 @@ spec: # static parameter announcements list. command: [echo, '[{"name": "example-param", "string": "default-string-value"}]'] - # If set to `true` then the plugin receives repository files with original file mode. Dangerous since the repository - # might have executable files. Set to true only if you trust the CMP plugin authors. - preserveFileMode: false + # If set to `true` then the plugin receives repository files with original file mode. Dangerous since the repository + # might have executable files. Set to true only if you trust the CMP plugin authors. + preserveFileMode: false ``` !!! note diff --git a/docs/operator-manual/core.md b/docs/operator-manual/core.md index 01b394d6e9d8cc..3d6e0a322c423b 100644 --- a/docs/operator-manual/core.md +++ b/docs/operator-manual/core.md @@ -25,7 +25,7 @@ A few use-cases that justify running Argo CD Core are: - As a cluster admin, I want to rely on Kubernetes RBAC only. - As a devops engineer, I don't want to learn a new API or depend on - another CLI to automate my deployments. I want instead rely in + another CLI to automate my deployments. I want to rely on the Kubernetes API only. - As a cluster admin, I don't want to provide Argo CD UI or Argo CD CLI to developers. diff --git a/docs/operator-manual/custom-styles.md b/docs/operator-manual/custom-styles.md index 893720f7bc9c0b..6f68d5e23b1281 100644 --- a/docs/operator-manual/custom-styles.md +++ b/docs/operator-manual/custom-styles.md @@ -21,7 +21,7 @@ metadata: ... name: argocd-cm data: - ui.cssurl: "https://www.myhost.com/my-styles.css" + ui.cssurl: "https://www.example.com/my-styles.css" ``` ## Adding Styles Via Volume Mounts diff --git a/docs/operator-manual/declarative-setup.md b/docs/operator-manual/declarative-setup.md index 1694429ffc25cc..1f7f9ab76f273a 100644 --- a/docs/operator-manual/declarative-setup.md +++ b/docs/operator-manual/declarative-setup.md @@ -490,7 +490,7 @@ stringData: ### Legacy behaviour -In Argo CD version 2.0 and earlier, repositories where stored as part of the `argocd-cm` config map. For +In Argo CD version 2.0 and earlier, repositories were stored as part of the `argocd-cm` config map. For backward-compatibility, Argo CD will still honor repositories in the config map, but this style of repository configuration is deprecated and support for it will be removed in a future version. @@ -549,6 +549,7 @@ bearerToken: string awsAuthConfig: clusterName: string roleARN: string + profile: string # Configure external command to supply client credentials # See https://godoc.org/k8s.io/client-go/tools/clientcmd/api#ExecConfig execProviderConfig: @@ -590,8 +591,8 @@ metadata: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: - name: mycluster.com - server: https://mycluster.com + name: mycluster.example.com + server: https://mycluster.example.com config: | { "bearerToken": "", @@ -615,8 +616,8 @@ metadata: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: - name: "mycluster.com" - server: "https://mycluster.com" + name: "mycluster.example.com" + server: "https://mycluster.example.com" config: | { "awsAuthConfig": { @@ -676,8 +677,10 @@ extended to allow assumption of multiple roles, either as an explicit array of r } ``` -Example service account configs for `argocd-application-controller` and `argocd-server`. Note that once the annotations -have been set on the service accounts, both the application controller and server pods need to be restarted. +Example service account configs for `argocd-application-controller` and `argocd-server`. + +!!! warning + Once the annotations have been set on the service accounts, both the application controller and server pods need to be restarted. ```yaml apiVersion: v1 @@ -729,6 +732,140 @@ data: "rolearn": ":role/" "username": "" ``` + +#### Alternative EKS Authentication Methods +In some scenarios it may not be possible to use IRSA, such as when the Argo CD cluster is running on a different cloud +provider's platform. In this case, there are two options: +1. Use `execProviderConfig` to call the AWS authentication mechanism which enables the injection of environment variables to supply credentials +2. Leverage the new AWS profile option available in Argo CD release 2.10 + +Both of these options will require the steps involving IAM and the `aws-auth` config map (defined above) to provide the +principal with access to the cluster. + +##### Using execProviderConfig with Environment Variables +```yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: mycluster-secret + labels: + argocd.argoproj.io/secret-type: cluster +type: Opaque +stringData: + name: mycluster + server: https://mycluster.example.com + namespaces: "my,managed,namespaces" + clusterResources: "true" + config: | + { + "execProviderConfig": { + "command": "argocd-k8s-auth", + "args": ["aws", "--cluster-name", "my-eks-cluster"], + "apiVersion": "client.authentication.k8s.io/v1beta1", + "env": { + "AWS_REGION": "xx-east-1", + "AWS_ACCESS_KEY_ID": "{{ .aws_key_id }}", + "AWS_SECRET_ACCESS_KEY": "{{ .aws_key_secret }}", + "AWS_SESSION_TOKEN": "{{ .aws_token }}" + } + }, + "tlsClientConfig": { + "insecure": false, + "caData": "{{ .cluster_cert }}" + } + } +``` + +This example assumes that the role being attached to the credentials that have been supplied, if this is not the case +the role can be appended to the `args` section like so: + +```yaml +... + "args": ["aws", "--cluster-name", "my-eks-cluster", "--roleARN", "arn:aws:iam:::role/"], +... +``` +This construct can be used in conjunction with something like the External Secrets Operator to avoid storing the keys in +plain text and additionally helps to provide a foundation for key rotation. + +##### Using An AWS Profile For Authentication +The option to use profiles, added in release 2.10, provides a method for supplying credentials while still using the +standard Argo CD EKS cluster declaration with an additional command flag that points to an AWS credentials file: +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: mycluster-secret + labels: + argocd.argoproj.io/secret-type: cluster +type: Opaque +stringData: + name: "mycluster.com" + server: "https://mycluster.com" + config: | + { + "awsAuthConfig": { + "clusterName": "my-eks-cluster-name", + "roleARN": "arn:aws:iam:::role/", + "profile": "/mount/path/to/my-profile-file" + }, + "tlsClientConfig": { + "insecure": false, + "caData": "" + } + } +``` +This will instruct ArgoCD to read the file at the provided path and use the credentials defined within to authenticate to +AWS. The profile must be mounted in order for this to work. For example, the following values can be defined in a Helm +based ArgoCD deployment: + +```yaml +controller: + extraVolumes: + - name: my-profile-volume + secret: + secretName: my-aws-profile + items: + - key: my-profile-file + path: my-profile-file + extraVolumeMounts: + - name: my-profile-mount + mountPath: /mount/path/to + readOnly: true + +server: + extraVolumes: + - name: my-profile-volume + secret: + secretName: my-aws-profile + items: + - key: my-profile-file + path: my-profile-file + extraVolumeMounts: + - name: my-profile-mount + mountPath: /mount/path/to + readOnly: true +``` + +Where the secret is defined as follows: +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: my-aws-profile +type: Opaque +stringData: + my-profile-file: | + [default] + region = + aws_access_key_id = + aws_secret_access_key = + aws_session_token = +``` + +> ⚠️ Secret mounts are updated on an interval, not real time. If rotation is a requirement ensure the token lifetime outlives the mount update interval and the rotation process doesn't immediately invalidate the existing token + + ### GKE GKE cluster secret example using argocd-k8s-auth and [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity): @@ -742,8 +879,8 @@ metadata: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: - name: mycluster.com - server: https://mycluster.com + name: mycluster.example.com + server: https://mycluster.example.com config: | { "execProviderConfig": { @@ -786,6 +923,15 @@ In addition to the environment variables above, argocd-k8s-auth accepts two extr This is an example of using the [federated workload login flow](https://github.com/Azure/kubelogin#azure-workload-federated-identity-non-interactive). The federated token file needs to be mounted as a secret into argoCD, so it can be used in the flow. The location of the token file needs to be set in the environment variable AZURE_FEDERATED_TOKEN_FILE. +If your AKS cluster utilizes the [Mutating Admission Webhook](https://azure.github.io/azure-workload-identity/docs/installation/mutating-admission-webhook.html) from the Azure Workload Identity project, follow these steps to enable the `argocd-application-controller` and `argocd-server` pods to use the federated identity: + +1. **Label the Pods**: Add the `azure.workload.identity/use: "true"` label to the `argocd-application-controller` and `argocd-server` pods. + +2. **Create Federated Identity Credential**: Generate an Azure federated identity credential for the `argocd-application-controller` and `argocd-server` service accounts. Refer to the [Federated Identity Credential](https://azure.github.io/azure-workload-identity/docs/topics/federated-identity-credential.html) documentation for detailed instructions. + +3. **Set the AZURE_CLIENT_ID**: Update the `AZURE_CLIENT_ID` in the cluster secret to match the client id of the newly created federated identity credential. + + ```yaml apiVersion: v1 kind: Secret @@ -795,8 +941,8 @@ metadata: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: - name: mycluster.com - server: https://mycluster.com + name: mycluster.example.com + server: https://mycluster.example.com config: | { "execProviderConfig": { @@ -804,9 +950,9 @@ stringData: "env": { "AAD_ENVIRONMENT_NAME": "AzurePublicCloud", "AZURE_CLIENT_ID": "fill in client id", - "AZURE_TENANT_ID": "fill in tenant id", - "AZURE_FEDERATED_TOKEN_FILE": "/opt/path/to/federated_file.json", - "AZURE_AUTHORITY_HOST": "https://login.microsoftonline.com/", + "AZURE_TENANT_ID": "fill in tenant id", # optional, injected by workload identity mutating admission webhook if enabled + "AZURE_FEDERATED_TOKEN_FILE": "/opt/path/to/federated_file.json", # optional, injected by workload identity mutating admission webhook if enabled + "AZURE_AUTHORITY_HOST": "https://login.microsoftonline.com/", # optional, injected by workload identity mutating admission webhook if enabled "AAD_LOGIN_METHOD": "workloadidentity" }, "args": ["azure"], @@ -830,8 +976,8 @@ metadata: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: - name: mycluster.com - server: https://mycluster.com + name: mycluster.example.com + server: https://mycluster.example.com config: | { "execProviderConfig": { diff --git a/docs/operator-manual/dynamic-cluster-distribution.md b/docs/operator-manual/dynamic-cluster-distribution.md index a32258c3f2f0ad..9d5d2104a17953 100644 --- a/docs/operator-manual/dynamic-cluster-distribution.md +++ b/docs/operator-manual/dynamic-cluster-distribution.md @@ -17,16 +17,10 @@ which does not require a restart of the application controller pods. ## Enabling Dynamic Distribution of Clusters -This feature is disabled by default while it is in alpha. To enable it, you must set the environment `ARGOCD_ENABLE_DYNAMIC_CLUSTER_DISTRIBUTION` to true when running the Application Controller. - -In order to utilize the feature, the manifests `manifests/ha/base/controller-deployment/` can be applied as a Kustomize -overlay. This overlay sets the StatefulSet replicas to `0` and deploys the application controller as a Deployment. The -dynamic distribution code automatically kicks in when the controller is deployed as a Deployment. +This feature is disabled by default while it is in alpha. In order to utilize the feature, the manifests `manifests/ha/base/controller-deployment/` can be applied as a Kustomize overlay. This overlay sets the StatefulSet replicas to `0` and deploys the application controller as a Deployment. Also, you must set the environment `ARGOCD_ENABLE_DYNAMIC_CLUSTER_DISTRIBUTION` to true when running the Application Controller as a deployment. !!! important - The use of a Deployment instead of a StatefulSet is an implementation detail which may change in future versions of - this feature. Therefore, the directory name of the Kustomize overlay may change as well. Monitor the release notes - to avoid issues. + The use of a Deployment instead of a StatefulSet is an implementation detail which may change in future versions of this feature. Therefore, the directory name of the Kustomize overlay may change as well. Monitor the release notes to avoid issues. Note the introduction of new environment variable `ARGOCD_CONTROLLER_HEARTBEAT_TIME`. The environment variable is explained in [working of Dynamic Distribution Heartbeat Process](#working-of-dynamic-distribution) diff --git a/docs/operator-manual/high_availability.md b/docs/operator-manual/high_availability.md index 7de532d9632c38..a532200216d9ba 100644 --- a/docs/operator-manual/high_availability.md +++ b/docs/operator-manual/high_availability.md @@ -57,7 +57,7 @@ performance. For performance reasons the controller monitors and caches only the preferred version into a version of the resource stored in Git. If `kubectl convert` fails because the conversion is not supported then the controller falls back to Kubernetes API query which slows down reconciliation. In this case, we advise to use the preferred resource version in Git. -* The controller polls Git every 3m by default. You can change this duration using the `timeout.reconciliation` setting in the `argocd-cm` ConfigMap. The value of `timeout.reconciliation` is a duration string e.g `60s`, `1m`, `1h` or `1d`. +* The controller polls Git every 3m by default. You can change this duration using the `timeout.reconciliation` and `timeout.reconciliation.jitter` setting in the `argocd-cm` ConfigMap. The value of the fields is a duration string e.g `60s`, `1m`, `1h` or `1d`. * If the controller is managing too many clusters and uses too much memory then you can shard clusters across multiple controller replicas. To enable sharding, increase the number of replicas in `argocd-application-controller` `StatefulSet` @@ -98,8 +98,8 @@ metadata: type: Opaque stringData: shard: 1 - name: mycluster.com - server: https://mycluster.com + name: mycluster.example.com + server: https://mycluster.example.com config: | { "bearerToken": "", @@ -244,30 +244,41 @@ spec: # ... ``` +### Application Sync Timeout & Jitter + +Argo CD has a timeout for application syncs. It will trigger a refresh for each application periodically when the timeout expires. +With a large number of applications, this will cause a spike in the refresh queue and can cause a spike to the repo-server component. To avoid this, you can set a jitter to the sync timeout which will spread out the refreshes and give time to the repo-server to catch up. + +The jitter is the maximum duration that can be added to the sync timeout, so if the sync timeout is 5 minutes and the jitter is 1 minute, then the actual timeout will be between 5 and 6 minutes. + +To configure the jitter you can set the following environment variables: + +* `ARGOCD_RECONCILIATION_JITTER` - The jitter to apply to the sync timeout. Disabled when value is 0. Defaults to 0. + ## Rate Limiting Application Reconciliations -To prevent high controller resource usage or sync loops caused either due to misbehaving apps or other environment specific factors, +To prevent high controller resource usage or sync loops caused either due to misbehaving apps or other environment specific factors, we can configure rate limits on the workqueues used by the application controller. There are two types of rate limits that can be configured: * Global rate limits * Per item rate limits -The final rate limiter uses a combination of both and calculates the final backoff as `max(globalBackoff, perItemBackoff)`. +The final rate limiter uses a combination of both and calculates the final backoff as `max(globalBackoff, perItemBackoff)`. ### Global rate limits - This is enabled by default, it is a simple bucket based rate limiter that limits the number of items that can be queued per second. -This is useful to prevent a large number of apps from being queued at the same time. - + This is disabled by default, it is a simple bucket based rate limiter that limits the number of items that can be queued per second. +This is useful to prevent a large number of apps from being queued at the same time. + To configure the bucket limiter you can set the following environment variables: * `WORKQUEUE_BUCKET_SIZE` - The number of items that can be queued in a single burst. Defaults to 500. - * `WORKQUEUE_BUCKET_QPS` - The number of items that can be queued per second. Defaults to 50. + * `WORKQUEUE_BUCKET_QPS` - The number of items that can be queued per second. Defaults to MaxFloat64, which disables the limiter. -### Per item rate limits +### Per item rate limits - This by default returns a fixed base delay/backoff value but can be configured to return exponential values, read further to understand it's working. -Per item rate limiter limits the number of times a particular item can be queued. This is based on exponential backoff where the backoff time for an item keeps increasing exponentially + This by default returns a fixed base delay/backoff value but can be configured to return exponential values. +Per item rate limiter limits the number of times a particular item can be queued. This is based on exponential backoff where the backoff time for an item keeps increasing exponentially if it is queued multiple times in a short period, but the backoff is reset automatically if a configured `cool down` period has elapsed since the last time the item was queued. To configure the per item limiter you can set the following environment variables: @@ -277,16 +288,16 @@ To configure the per item limiter you can set the following environment variable * `WORKQUEUE_MAX_DELAY_NS` : The max delay in nanoseconds, this is the max backoff limit. Defaults to 3 * 10^9 (=3s) * `WORKQUEUE_BACKOFF_FACTOR` : The backoff factor, this is the factor by which the backoff is increased for each retry. Defaults to 1.5 -The formula used to calculate the backoff time for an item, where `numRequeue` is the number of times the item has been queued +The formula used to calculate the backoff time for an item, where `numRequeue` is the number of times the item has been queued and `lastRequeueTime` is the time at which the item was last queued: - When `WORKQUEUE_FAILURE_COOLDOWN_NS` != 0 : ``` -backoff = time.Since(lastRequeueTime) >= WORKQUEUE_FAILURE_COOLDOWN_NS ? - WORKQUEUE_BASE_DELAY_NS : +backoff = time.Since(lastRequeueTime) >= WORKQUEUE_FAILURE_COOLDOWN_NS ? + WORKQUEUE_BASE_DELAY_NS : min( - WORKQUEUE_MAX_DELAY_NS, + WORKQUEUE_MAX_DELAY_NS, WORKQUEUE_BASE_DELAY_NS * WORKQUEUE_BACKOFF_FACTOR ^ (numRequeue) ) ``` diff --git a/docs/operator-manual/ingress.md b/docs/operator-manual/ingress.md index 84b2bcaf34a67d..aad2208c218734 100644 --- a/docs/operator-manual/ingress.md +++ b/docs/operator-manual/ingress.md @@ -166,6 +166,43 @@ The argocd-server Service needs to be annotated with `projectcontour.io/upstream The API server should then be run with TLS disabled. Edit the `argocd-server` deployment to add the `--insecure` flag to the argocd-server command, or simply set `server.insecure: "true"` in the `argocd-cmd-params-cm` ConfigMap [as described here](server-commands/additional-configuration-method.md). +Contour httpproxy CRD: + +Using a contour httpproxy CRD allows you to use the same hostname for the GRPC and REST api. + +```yaml +apiVersion: projectcontour.io/v1 +kind: HTTPProxy +metadata: + name: argocd-server + namespace: argocd +spec: + ingressClassName: contour + virtualhost: + fqdn: path.to.argocd.io + tls: + secretName: wildcard-tls + routes: + - conditions: + - prefix: / + - header: + name: Content-Type + contains: application/grpc + services: + - name: argocd-server + port: 80 + protocol: h2c # allows for unencrypted http2 connections + timeoutPolicy: + response: 1h + idle: 600s + idleConnection: 600s + - conditions: + - prefix: / + services: + - name: argocd-server + port: 80 +``` + ## [kubernetes/ingress-nginx](https://github.com/kubernetes/ingress-nginx) ### Option 1: SSL-Passthrough @@ -661,9 +698,9 @@ metadata: networking.gke.io/v1beta1.FrontendConfig: argocd-frontend-config spec: tls: - - secretName: secret-yourdomain-com + - secretName: secret-example-com rules: - - host: argocd.yourdomain.com + - host: argocd.example.com http: paths: - pathType: ImplementationSpecific @@ -686,9 +723,9 @@ metadata: networking.gke.io/v1beta1.FrontendConfig: argocd-frontend-config spec: tls: - - secretName: secret-yourdomain-com + - secretName: secret-example-com rules: - - host: argocd.yourdomain.com + - host: argocd.example.com http: paths: - pathType: Prefix @@ -700,7 +737,7 @@ spec: number: 80 ``` -As you may know already, it can take some minutes to deploy the load balancer and become ready to accept connections. Once it's ready, get the public IP address for your Load Balancer, go to your DNS server (Google or third party) and point your domain or subdomain (i.e. argocd.yourdomain.com) to that IP address. +As you may know already, it can take some minutes to deploy the load balancer and become ready to accept connections. Once it's ready, get the public IP address for your Load Balancer, go to your DNS server (Google or third party) and point your domain or subdomain (i.e. argocd.example.com) to that IP address. You can get that IP address describing the Ingress object like this: diff --git a/docs/operator-manual/metrics.md b/docs/operator-manual/metrics.md index 2bde146a786a29..a3ddbfe9904d30 100644 --- a/docs/operator-manual/metrics.md +++ b/docs/operator-manual/metrics.md @@ -70,6 +70,8 @@ Scraped at the `argocd-server-metrics:8083/metrics` endpoint. | `argocd_redis_request_total` | counter | Number of Kubernetes requests executed during application reconciliation. | | `grpc_server_handled_total` | counter | Total number of RPCs completed on the server, regardless of success or failure. | | `grpc_server_msg_sent_total` | counter | Total number of gRPC stream messages sent by the server. | +| `argocd_proxy_extension_request_total` | counter | Number of requests sent to the configured proxy extensions. | +| `argocd_proxy_extension_request_duration_seconds` | histogram | Request duration in seconds between the Argo CD API server and the proxy extension backend. | ## Repo Server Metrics Metrics about the Repo Server. @@ -79,6 +81,7 @@ Scraped at the `argocd-repo-server:8084/metrics` endpoint. |--------|:----:|-------------| | `argocd_git_request_duration_seconds` | histogram | Git requests duration seconds. | | `argocd_git_request_total` | counter | Number of git requests performed by repo server | +| `argocd_git_fetch_fail_total` | counter | Number of git fetch requests failures by repo server | | `argocd_redis_request_duration_seconds` | histogram | Redis requests duration seconds. | | `argocd_redis_request_total` | counter | Number of Kubernetes requests executed during application reconciliation. | | `argocd_repo_pending_request_total` | gauge | Number of pending requests requiring repository lock | @@ -86,7 +89,7 @@ Scraped at the `argocd-repo-server:8084/metrics` endpoint. ## Prometheus Operator If using Prometheus Operator, the following ServiceMonitor example manifests can be used. -Change `metadata.labels.release` to the name of label selected by your Prometheus. +Add a namespace where Argo CD is installed and change `metadata.labels.release` to the name of label selected by your Prometheus. ```yaml apiVersion: monitoring.coreos.com/v1 @@ -148,6 +151,54 @@ spec: - port: metrics ``` +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: argocd-dex-server + labels: + release: prometheus-operator +spec: + selector: + matchLabels: + app.kubernetes.io/name: argocd-dex-server + endpoints: + - port: metrics +``` + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: argocd-redis-haproxy-metrics + labels: + release: prometheus-operator +spec: + selector: + matchLabels: + app.kubernetes.io/name: argocd-redis-ha-haproxy + endpoints: + - port: http-exporter-port +``` + +For notifications controller, you need to additionally add following: + +```yaml +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: argocd-notifications-controller + labels: + release: prometheus-operator +spec: + selector: + matchLabels: + app.kubernetes.io/name: argocd-notifications-controller-metrics + endpoints: + - port: metrics +``` + + ## Dashboards You can find an example Grafana dashboard [here](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard.json) or check demo instance diff --git a/docs/operator-manual/notifications/catalog.md b/docs/operator-manual/notifications/catalog.md index 8f413ac7eb5b38..add7084304b98e 100644 --- a/docs/operator-manual/notifications/catalog.md +++ b/docs/operator-manual/notifications/catalog.md @@ -62,8 +62,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -90,8 +89,7 @@ teams: "value": "{{.app.status.sync.revision}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -145,8 +143,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -169,8 +166,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -224,8 +220,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -252,8 +247,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -307,8 +301,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -335,8 +328,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -394,8 +386,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -418,8 +409,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -472,8 +462,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -500,8 +489,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" diff --git a/docs/operator-manual/notifications/functions.md b/docs/operator-manual/notifications/functions.md index 3d614e4e53a550..c50d122024b76e 100644 --- a/docs/operator-manual/notifications/functions.md +++ b/docs/operator-manual/notifications/functions.md @@ -48,6 +48,16 @@ Transforms given GIT URL into HTTPs format. Returns repository URL full name `(/)`. Currently supports only Github, GitLab and Bitbucket. +
+**`repo.QueryEscape(s string) string`** + +QueryEscape escapes the string, so it can be safely placed inside a URL + +Example: +``` +/projects/{{ call .repo.QueryEscape (call .repo.FullNameByRepoURL .app.status.RepoURL) }}/merge_requests +``` +
**`repo.GetCommitMetadata(sha string) CommitMetadata`** diff --git a/docs/operator-manual/notifications/index.md b/docs/operator-manual/notifications/index.md index c719d10e7611c6..eccca906ae91bd 100644 --- a/docs/operator-manual/notifications/index.md +++ b/docs/operator-manual/notifications/index.md @@ -45,3 +45,71 @@ So you can just use them instead of reinventing new ones. ``` Try syncing an application to get notified when the sync is completed. + +## Namespace based configuration + +A common installation method for Argo CD Notifications is to install it in a dedicated namespace to manage a whole cluster. In this case, the administrator is the only +person who can configure notifications in that namespace generally. However, in some cases, it is required to allow end-users to configure notifications +for their Argo CD applications. For example, the end-user can configure notifications for their Argo CD application in the namespace where they have access to and their Argo CD application is running in. + +This feature is based on applications in any namespace. See [applications in any namespace](../app-any-namespace.md) page for more information. + +In order to enable this feature, the Argo CD administrator must reconfigure the argocd-notification-controller workloads to add `--application-namespaces` and `--self-service-notification-enabled` parameters to the container's startup command. +`--application-namespaces` controls the list of namespaces that Argo CD applications are in. `--self-service-notification-enabled` turns on this feature. + +The startup parameters for both can also be conveniently set up and kept in sync by specifying +the `application.namespaces` and `notificationscontroller.selfservice.enabled` in the argocd-cmd-params-cm ConfigMap instead of changing the manifests for the respective workloads. For example: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cmd-params-cm +data: + application.namespaces: app-team-one, app-team-two + notificationscontroller.selfservice.enabled: "true" +``` + +To use this feature, you can deploy configmap named `argocd-notifications-cm` and possibly a secret `argocd-notifications-secret` in the namespace where the Argo CD application lives. + +When it is configured this way the controller will send notifications using both the controller level configuration (the configmap located in the same namespaces as the controller) as well as +the configuration located in the same namespace where the Argo CD application is at. + +Example: Application team wants to receive notifications using PagerDutyV2, when the controller level configuration is only supporting Slack. + +The following two resources are deployed in the namespace where the Argo CD application lives. +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-notifications-cm +data: + service.pagerdutyv2: | + serviceKeys: + my-service: $pagerduty-key-my-service +... +``` +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: argo-cd-notification-secret +type: Opaque +data: + pagerduty-key-my-service: +``` + +When an Argo CD application has the following subscriptions, user receives application sync failure message from pager duty. +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + notifications.argoproj.io/subscribe.on-sync-failed.pagerdutyv2: "" +``` + +!!! note + When the same notification service and trigger are defined in controller level configuration and application level configuration, + both notifications will be sent according to its own configuration. + +[Defining and using secrets within notification templates](templates.md/#defining-and-using-secrets-within-notification-templates) function is not available when flag `--self-service-notification-enable` is on. diff --git a/docs/operator-manual/notifications/services/alertmanager.md b/docs/operator-manual/notifications/services/alertmanager.md index e0f9d7e4e78890..033a76a29ea65d 100755 --- a/docs/operator-manual/notifications/services/alertmanager.md +++ b/docs/operator-manual/notifications/services/alertmanager.md @@ -43,7 +43,7 @@ You should turn off "send_resolved" or you will receive unnecessary recovery not apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.alertmanager: | targets: @@ -58,7 +58,7 @@ If your alertmanager has changed the default api, you can customize "apiPath". apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.alertmanager: | targets: @@ -89,7 +89,7 @@ stringData: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.alertmanager: | targets: @@ -110,7 +110,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.alertmanager: | targets: diff --git a/docs/operator-manual/notifications/services/awssqs.md b/docs/operator-manual/notifications/services/awssqs.md index 6bbc47cbbc0b51..5331533826348a 100755 --- a/docs/operator-manual/notifications/services/awssqs.md +++ b/docs/operator-manual/notifications/services/awssqs.md @@ -1,13 +1,13 @@ -# AWS SQS +# AWS SQS ## Parameters -This notification service is capable of sending simple messages to AWS SQS queue. +This notification service is capable of sending simple messages to AWS SQS queue. -* `queue` - name of the queue you are intending to send messages to. Can be overwriten with target destination annotation. +* `queue` - name of the queue you are intending to send messages to. Can be overridden with target destination annotation. * `region` - region of the sqs queue can be provided via env variable AWS_DEFAULT_REGION * `key` - optional, aws access key must be either referenced from a secret via variable or via env variable AWS_ACCESS_KEY_ID -* `secret` - optional, aws access secret must be either referenced from a secret via variableor via env variable AWS_SECRET_ACCESS_KEY +* `secret` - optional, aws access secret must be either referenced from a secret via variable or via env variable AWS_SECRET_ACCESS_KEY * `account` optional, external accountId of the queue * `endpointUrl` optional, useful for development with localstack @@ -30,7 +30,7 @@ metadata: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.awssqs: | region: "us-east-2" @@ -63,7 +63,7 @@ stringData: ### Minimal configuration using AWS Env variables -Ensure following list of enviromental variable is injected via OIDC, or other method. And assuming SQS is local to the account. +Ensure the following list of environment variables are injected via OIDC, or another method. And assuming SQS is local to the account. You may skip usage of secret for sensitive data and omit other parameters. (Setting parameters via ConfigMap takes precedent.) Variables: @@ -89,7 +89,7 @@ metadata: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.awssqs: | queue: "myqueue" @@ -104,3 +104,16 @@ data: - oncePer: obj.metadata.annotations["generation"] ``` + +## FIFO SQS Queues + +FIFO queues require a [MessageGroupId](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_SendMessage.html#SQS-SendMessage-request-MessageGroupId) to be sent along with every message, every message with a matching MessageGroupId will be processed one by one in order. + +To send to a FIFO SQS Queue you must include a `messageGroupId` in the template such as in the example below: + +```yaml +template.deployment-ready: | + message: | + Deployment {{.obj.metadata.name}} is ready! + messageGroupId: {{.obj.metadata.name}}-deployment +``` diff --git a/docs/operator-manual/notifications/services/email.md b/docs/operator-manual/notifications/services/email.md index b81ab6cde8b4cf..7fd3f0e22379c1 100755 --- a/docs/operator-manual/notifications/services/email.md +++ b/docs/operator-manual/notifications/services/email.md @@ -20,7 +20,7 @@ The following snippet contains sample Gmail service configuration: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.email.gmail: | username: $email-username @@ -36,7 +36,7 @@ Without authentication: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.email.example: | host: smtp.example.com @@ -52,7 +52,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: template.app-sync-succeeded: | email: diff --git a/docs/operator-manual/notifications/services/github.md b/docs/operator-manual/notifications/services/github.md index a3f89f8c87ef0f..1fa1a985d26824 100755 --- a/docs/operator-manual/notifications/services/github.md +++ b/docs/operator-manual/notifications/services/github.md @@ -12,7 +12,7 @@ The GitHub notification service changes commit status using [GitHub Apps](https: ## Configuration 1. Create a GitHub Apps using https://github.com/settings/apps/new -2. Change repository permissions to enable write commit statuses and/or deployments +2. Change repository permissions to enable write commit statuses and/or deployments and/or pull requests comments ![2](https://user-images.githubusercontent.com/18019529/108397381-3ca57980-725b-11eb-8d17-5b8992dc009e.png) 3. Generate a private key, and download it automatically ![3](https://user-images.githubusercontent.com/18019529/108397926-d4a36300-725b-11eb-83fe-74795c8c3e03.png) @@ -24,7 +24,7 @@ in `argocd-notifications-cm` ConfigMap apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.github: | appID: @@ -76,6 +76,11 @@ template.app-deployed: | logURL: "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true" requiredContexts: [] autoMerge: true + transientEnvironment: false + pullRequestComment: + content: | + Application {{.app.metadata.name}} is now running new version of deployments manifests. + See more here: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true ``` **Notes**: @@ -83,4 +88,5 @@ template.app-deployed: | - If `github.repoURLPath` and `github.revisionPath` are same as above, they can be omitted. - Automerge is optional and `true` by default for github deployments to ensure the requested ref is up to date with the default branch. Setting this option to `false` is required if you would like to deploy older refs in your default branch. - For more information see the [Github Deployment API Docs](https://docs.github.com/en/rest/deployments/deployments?apiVersion=2022-11-28#create-a-deployment). + For more information see the [GitHub Deployment API Docs](https://docs.github.com/en/rest/deployments/deployments?apiVersion=2022-11-28#create-a-deployment). +- If `github.pullRequestComment.content` is set to 65536 characters or more, it will be truncated. diff --git a/docs/operator-manual/notifications/services/googlechat.md b/docs/operator-manual/notifications/services/googlechat.md index 041ea6e022ef53..821c23023e863a 100755 --- a/docs/operator-manual/notifications/services/googlechat.md +++ b/docs/operator-manual/notifications/services/googlechat.md @@ -19,7 +19,7 @@ The Google Chat notification service send message notifications to a google chat apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.googlechat: | webhooks: @@ -59,24 +59,27 @@ A card message can be defined as follows: ```yaml template.app-sync-succeeded: | googlechat: - cards: | + cardsV2: | - header: title: ArgoCD Bot Notification sections: - widgets: - - textParagraph: + - decoratedText: text: The app {{ .app.metadata.name }} has successfully synced! - widgets: - - keyValue: + - decoratedText: topLabel: Repository - content: {{ call .repo.RepoURLToHTTPS .app.spec.source.repoURL }} - - keyValue: + text: {{ call .repo.RepoURLToHTTPS .app.spec.source.repoURL }} + - decoratedText: topLabel: Revision - content: {{ .app.spec.source.targetRevision }} - - keyValue: + text: {{ .app.spec.source.targetRevision }} + - decoratedText: topLabel: Author - content: {{ (call .repo.GetCommitMetadata .app.status.sync.revision).Author }} + text: {{ (call .repo.GetCommitMetadata .app.status.sync.revision).Author }} ``` +All [Card fields](https://developers.google.com/chat/api/reference/rest/v1/cards#Card_1) are supported and can be used +in notifications. It is also possible to use the previous (now deprecated) `cards` key to use the legacy card fields, +but this is not recommended as Google has deprecated this field and recommends using the newer `cardsV2`. The card message can be written in JSON too. @@ -86,7 +89,7 @@ It is possible send both simple text and card messages in a chat thread by speci ```yaml template.app-sync-succeeded: | - message: The app {{ .app.metadata.name }} has succesfully synced! + message: The app {{ .app.metadata.name }} has successfully synced! googlechat: threadKey: {{ .app.metadata.name }} ``` diff --git a/docs/operator-manual/notifications/services/grafana.md b/docs/operator-manual/notifications/services/grafana.md index a36672d0fa423e..1f3e77701f0448 100755 --- a/docs/operator-manual/notifications/services/grafana.md +++ b/docs/operator-manual/notifications/services/grafana.md @@ -21,7 +21,7 @@ Available parameters : apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.grafana: | apiUrl: https://grafana.example.com/api diff --git a/docs/operator-manual/notifications/services/mattermost.md b/docs/operator-manual/notifications/services/mattermost.md index 98e0d0fd7b82ff..d1f187e955b9ca 100755 --- a/docs/operator-manual/notifications/services/mattermost.md +++ b/docs/operator-manual/notifications/services/mattermost.md @@ -19,7 +19,7 @@ in `argocd-notifications-cm` ConfigMap apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.mattermost: | apiURL: diff --git a/docs/operator-manual/notifications/services/newrelic.md b/docs/operator-manual/notifications/services/newrelic.md index d98288a846422a..b0c7e340c9b28e 100755 --- a/docs/operator-manual/notifications/services/newrelic.md +++ b/docs/operator-manual/notifications/services/newrelic.md @@ -14,7 +14,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.newrelic: | apiURL: diff --git a/docs/operator-manual/notifications/services/opsgenie.md b/docs/operator-manual/notifications/services/opsgenie.md index 665d0081e7c73d..e92ee99756ab82 100755 --- a/docs/operator-manual/notifications/services/opsgenie.md +++ b/docs/operator-manual/notifications/services/opsgenie.md @@ -12,14 +12,15 @@ To be able to send notifications with argocd-notifications you have to create an 8. Give your integration a name, copy the "API key" and safe it somewhere for later 9. Make sure the checkboxes for "Create and Update Access" and "enable" are selected, disable the other checkboxes to remove unnecessary permissions 10. Click "Safe Integration" at the bottom -11. Check your browser for the correct server apiURL. If it is "app.opsgenie.com" then use the us/international api url `api.opsgenie.com` in the next step, otherwise use `api.eu.opsgenie.com` (european api). -12. You are finished with configuring opsgenie. Now you need to configure argocd-notifications. Use the apiUrl, the team name and the apiKey to configure the opsgenie integration in the `argocd-notifications-secret` secret. +11. Check your browser for the correct server apiURL. If it is "app.opsgenie.com" then use the US/international api url `api.opsgenie.com` in the next step, otherwise use `api.eu.opsgenie.com` (European API). +12. You are finished with configuring Opsgenie. Now you need to configure argocd-notifications. Use the apiUrl, the team name and the apiKey to configure the Opsgenie integration in the `argocd-notifications-secret` secret. + ```yaml apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.opsgenie: | apiUrl: diff --git a/docs/operator-manual/notifications/services/pagerduty.md b/docs/operator-manual/notifications/services/pagerduty.md index 0e1ab965332e18..c6e1e41dac81db 100755 --- a/docs/operator-manual/notifications/services/pagerduty.md +++ b/docs/operator-manual/notifications/services/pagerduty.md @@ -1,17 +1,17 @@ -# Pagerduty +# PagerDuty ## Parameters -The Pagerduty notification service is used to create pagerduty incidents and requires specifying the following settings: +The PagerDuty notification service is used to create PagerDuty incidents and requires specifying the following settings: -* `pagerdutyToken` - the pagerduty auth token +* `pagerdutyToken` - the PagerDuty auth token * `from` - email address of a valid user associated with the account making the request. * `serviceID` - The ID of the resource. ## Example -The following snippet contains sample Pagerduty service configuration: +The following snippet contains sample PagerDuty service configuration: ```yaml apiVersion: v1 @@ -26,7 +26,7 @@ stringData: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.pagerduty: | token: $pagerdutyToken @@ -35,13 +35,13 @@ data: ## Template -[Notification templates](../templates.md) support specifying subject for pagerduty notifications: +[Notification templates](../templates.md) support specifying subject for PagerDuty notifications: ```yaml apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: template.rollout-aborted: | message: Rollout {{.rollout.metadata.name}} is aborted. @@ -62,5 +62,5 @@ apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: annotations: - notifications.argoproj.io/subscribe.on-rollout-aborted.pagerduty: "" + notifications.argoproj.io/subscribe.on-rollout-aborted.pagerduty: "" ``` diff --git a/docs/operator-manual/notifications/services/pagerduty_v2.md b/docs/operator-manual/notifications/services/pagerduty_v2.md index 21e8d942e4e93a..549cdc937b1500 100755 --- a/docs/operator-manual/notifications/services/pagerduty_v2.md +++ b/docs/operator-manual/notifications/services/pagerduty_v2.md @@ -28,7 +28,7 @@ stringData: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.pagerdutyv2: | serviceKeys: @@ -43,7 +43,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: template.rollout-aborted: | message: Rollout {{.rollout.metadata.name}} is aborted. @@ -74,5 +74,5 @@ apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: annotations: - notifications.argoproj.io/subscribe.on-rollout-aborted.pagerdutyv2: "" + notifications.argoproj.io/subscribe.on-rollout-aborted.pagerdutyv2: "" ``` diff --git a/docs/operator-manual/notifications/services/pushover.md b/docs/operator-manual/notifications/services/pushover.md index 37cb20b277dcc9..a09b3660f92339 100755 --- a/docs/operator-manual/notifications/services/pushover.md +++ b/docs/operator-manual/notifications/services/pushover.md @@ -1,13 +1,13 @@ # Pushover 1. Create an app at [pushover.net](https://pushover.net/apps/build). -2. Store the API key in `` Secret and define the secret name in `` ConfigMap: +2. Store the API key in `` Secret and define the secret name in `argocd-notifications-cm` ConfigMap: ```yaml apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.pushover: | token: $pushover-token diff --git a/docs/operator-manual/notifications/services/rocketchat.md b/docs/operator-manual/notifications/services/rocketchat.md index f1157050139d01..20aaa405c80d01 100755 --- a/docs/operator-manual/notifications/services/rocketchat.md +++ b/docs/operator-manual/notifications/services/rocketchat.md @@ -43,7 +43,7 @@ stringData: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.rocketchat: | email: $rocketchat-email diff --git a/docs/operator-manual/notifications/services/slack.md b/docs/operator-manual/notifications/services/slack.md index 15937597c19f2b..41bdddd7617c4e 100755 --- a/docs/operator-manual/notifications/services/slack.md +++ b/docs/operator-manual/notifications/services/slack.md @@ -6,11 +6,16 @@ If you want to send message using incoming webhook, you can use [webhook](./webh The Slack notification service configuration includes following settings: -* `token` - the app token -* `apiURL` - optional, the server url, e.g. https://example.com/api -* `username` - optional, the app username -* `icon` - optional, the app icon, e.g. :robot_face: or https://example.com/image.png -* `insecureSkipVerify` - optional bool, true or false +| **Option** | **Required** | **Type** | **Description** | **Example** | +| -------------------- | ------------ | -------------- | --------------- | ----------- | +| `apiURL` | False | `string` | The server URL. | `https://example.com/api` | +| `channels` | False | `list[string]` | | `["my-channel-1", "my-channel-2"]` | +| `icon` | False | `string` | The app icon. | `:robot_face:` or `https://example.com/image.png` | +| `insecureSkipVerify` | False | `bool` | | `true` | +| `signingSecret` | False | `string` | | `8f742231b10e8888abcd99yyyzzz85a5` | +| `token` | **True** | `string` | The app's OAuth access token. | `xoxb-1234567890-1234567890123-5n38u5ed63fgzqlvuyxvxcx6` | +| `username` | False | `string` | The app username. | `argocd` | +| `disableUnfurl` | False | `bool` | Disable slack unfurling links in messages | `true` | ## Configuration @@ -44,7 +49,7 @@ The Slack notification service configuration includes following settings: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.slack: | token: $slack-token diff --git a/docs/operator-manual/notifications/services/teams.md b/docs/operator-manual/notifications/services/teams.md index b5b9a228c43eb5..0e44456d4de19f 100755 --- a/docs/operator-manual/notifications/services/teams.md +++ b/docs/operator-manual/notifications/services/teams.md @@ -18,7 +18,7 @@ The Teams notification service send message notifications using Teams bot and re apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.teams: | recipientUrls: @@ -113,7 +113,7 @@ template.app-sync-succeeded: | ### summary field -You can set a summary of the message that will be shown on Notifcation & Activity Feed +You can set a summary of the message that will be shown on Notification & Activity Feed ![](https://user-images.githubusercontent.com/6957724/116587921-84c4d480-a94d-11eb-9da4-f365151a12e7.jpg) diff --git a/docs/operator-manual/notifications/services/telegram.md b/docs/operator-manual/notifications/services/telegram.md index 953c2a9fca0bfa..8612a09d1ca842 100755 --- a/docs/operator-manual/notifications/services/telegram.md +++ b/docs/operator-manual/notifications/services/telegram.md @@ -2,13 +2,13 @@ 1. Get an API token using [@Botfather](https://t.me/Botfather). 2. Store token in `` Secret and configure telegram integration -in `` ConfigMap: +in `argocd-notifications-cm` ConfigMap: ```yaml apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.telegram: | token: $telegram-token diff --git a/docs/operator-manual/notifications/services/webex.md b/docs/operator-manual/notifications/services/webex.md index 440ed1ddc738f7..eba4c5e11b8dc7 100755 --- a/docs/operator-manual/notifications/services/webex.md +++ b/docs/operator-manual/notifications/services/webex.md @@ -24,7 +24,7 @@ The Webex Teams notification service configuration includes following settings: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webex: | token: $webex-token diff --git a/docs/operator-manual/notifications/services/webhook.md b/docs/operator-manual/notifications/services/webhook.md index bd45b1f69e40b1..4b8ca38a685adb 100755 --- a/docs/operator-manual/notifications/services/webhook.md +++ b/docs/operator-manual/notifications/services/webhook.md @@ -1,7 +1,7 @@ # Webhook The webhook notification service allows sending a generic HTTP request using the templatized request body and URL. -Using Webhook you might trigger a Jenkins job, update Github commit status. +Using Webhook you might trigger a Jenkins job, update GitHub commit status. ## Parameters @@ -9,8 +9,17 @@ The Webhook notification service configuration includes following settings: - `url` - the url to send the webhook to - `headers` - optional, the headers to pass along with the webhook -- `basicAuth` - optional, the basic authentication to pass along with the webook +- `basicAuth` - optional, the basic authentication to pass along with the webhook - `insecureSkipVerify` - optional bool, true or false +- `retryWaitMin` - Optional, the minimum wait time between retries. Default value: 1s. +- `retryWaitMax` - Optional, the maximum wait time between retries. Default value: 5s. +- `retryMax` - Optional, the maximum number of retries. Default value: 3. + +## Retry Behavior + +The webhook service will automatically retry the request if it fails due to network errors or if the server returns a 5xx status code. The number of retries and the wait time between retries can be configured using the `retryMax`, `retryWaitMin`, and `retryWaitMax` parameters. + +The wait time between retries is between `retryWaitMin` and `retryWaitMax`. If all retries fail, the `Send` method will return an error. ## Configuration @@ -22,7 +31,7 @@ Use the following steps to configure webhook: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.: | url: https:/// @@ -41,7 +50,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: template.github-commit-status: | webhook: @@ -67,13 +76,13 @@ metadata: ## Examples -### Set Github commit status +### Set GitHub commit status ```yaml apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.github: | url: https://api.github.com @@ -88,7 +97,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.github: | url: https://api.github.com @@ -119,7 +128,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.jenkins: | url: http:///job//build?token= @@ -136,7 +145,7 @@ type: Opaque apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.form: | url: https://form.example.com @@ -157,7 +166,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.slack_webhook: | url: https://hooks.slack.com/services/xxxxx diff --git a/docs/operator-manual/notifications/triggers.md b/docs/operator-manual/notifications/triggers.md index c3e2dc601296ba..49a62447779591 100644 --- a/docs/operator-manual/notifications/triggers.md +++ b/docs/operator-manual/notifications/triggers.md @@ -1,7 +1,7 @@ The trigger defines the condition when the notification should be sent. The definition includes name, condition and notification templates reference. The condition is a predicate expression that returns true if the notification should be sent. The trigger condition evaluation is powered by [antonmedv/expr](https://github.com/antonmedv/expr). -The condition language syntax is described at [Language-Definition.md](https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md). +The condition language syntax is described at [language-definition.md](https://github.com/antonmedv/expr/blob/master/docs/language-definition.md). The trigger is configured in the `argocd-notifications-cm` ConfigMap. For example the following trigger sends a notification when application sync status changes to `Unknown` using the `app-sync-status` template: @@ -71,7 +71,7 @@ When one repo is used to sync multiple applications, the `oncePer: app.status.sy ### oncePer -The `oncePer` filed is supported like as follows. +The `oncePer` field is supported like as follows. ```yaml apiVersion: argoproj.io/v1alpha1 diff --git a/docs/operator-manual/notifications/troubleshooting.md b/docs/operator-manual/notifications/troubleshooting.md index 6e144bb0c99857..616cd4b024e829 100644 --- a/docs/operator-manual/notifications/troubleshooting.md +++ b/docs/operator-manual/notifications/troubleshooting.md @@ -16,7 +16,7 @@ Additionally, you can specify `:empty` to use empty secret with no notification ```bash argocd admin notifications trigger get \ - --config-map ./argocd admin notifications-cm.yaml --secret :empty + --config-map ./argocd-notifications-cm.yaml --secret :empty ``` * Trigger notification using in-cluster config map and secret: diff --git a/docs/operator-manual/rbac.md b/docs/operator-manual/rbac.md index 0f15a18be19738..b1d386fb5eb8ec 100644 --- a/docs/operator-manual/rbac.md +++ b/docs/operator-manual/rbac.md @@ -159,6 +159,7 @@ data: g, your-github-org:your-team, role:org-admin ``` + ---- Another `policy.csv` example might look as follows: diff --git a/docs/operator-manual/secret-management.md b/docs/operator-manual/secret-management.md index ab06a46014b200..aa224e20ff7425 100644 --- a/docs/operator-manual/secret-management.md +++ b/docs/operator-manual/secret-management.md @@ -10,7 +10,7 @@ Here are some ways people are doing GitOps secrets: * [Bitnami Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) * [External Secrets Operator](https://github.com/external-secrets/external-secrets) * [Hashicorp Vault](https://www.vaultproject.io) -* [Bank-Vaults]((https://bank-vaults.dev/)) +* [Bank-Vaults](https://bank-vaults.dev/) * [Helm Secrets](https://github.com/jkroepke/helm-secrets) * [Kustomize secret generator plugins](https://github.com/kubernetes-sigs/kustomize/blob/fd7a353df6cece4629b8e8ad56b71e30636f38fc/examples/kvSourceGoPlugin.md#secret-values-from-anywhere) * [aws-secret-operator](https://github.com/mumoshu/aws-secret-operator) diff --git a/docs/operator-manual/server-commands/argocd-application-controller.md b/docs/operator-manual/server-commands/argocd-application-controller.md index e03cf7fc515367..61c0c321198957 100644 --- a/docs/operator-manual/server-commands/argocd-application-controller.md +++ b/docs/operator-manual/server-commands/argocd-application-controller.md @@ -17,6 +17,7 @@ argocd-application-controller [flags] ``` --app-hard-resync int Time period in seconds for application hard resync. --app-resync int Time period in seconds for application resync. (default 180) + --app-resync-jitter int Maximum time period in seconds to add as a delay jitter for application resync. --app-state-cache-expiration duration Cache expiration for app state (default 1h0m0s) --application-namespaces strings List of additional namespaces that applications are allowed to be reconciled from --as string Username to impersonate for the operation @@ -44,6 +45,8 @@ argocd-application-controller [flags] --operation-processors int Number of application operation processors (default 10) --otlp-address string OpenTelemetry collector address to send traces to --otlp-attrs strings List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value) + --otlp-headers stringToString List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2) (default []) + --otlp-insecure OpenTelemetry collector insecure mode (default true) --password string Password for basic authentication to the API server --persist-resource-health Enables storing the managed resources health in the Application CRD (default true) --proxy-url string If provided, this URL will be used to connect via proxy @@ -65,6 +68,7 @@ argocd-application-controller [flags] --sentinel stringArray Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). --sentinelmaster string Redis sentinel master group name. (default "master") --server string The address and port of the Kubernetes API server + --server-side-diff-enabled Feature flag to enable ServerSide diff. Default ("false") --sharding-method string Enables choice of sharding method. Supported sharding methods are : [legacy, round-robin] (default "legacy") --status-processors int Number of application status processors (default 20) --tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used. @@ -73,7 +77,7 @@ argocd-application-controller [flags] --username string Username for basic authentication to the API server --wq-backoff-factor float Set Workqueue Per Item Rate Limiter Backoff Factor, default is 1.5 (default 1.5) --wq-basedelay-ns duration Set Workqueue Per Item Rate Limiter Base Delay duration in nanoseconds, default 1000000 (1ms) (default 1ms) - --wq-bucket-qps int Set Workqueue Rate Limiter Bucket QPS, default 50 (default 50) + --wq-bucket-qps float Set Workqueue Rate Limiter Bucket QPS, default set to MaxFloat64 which disables the bucket limiter (default 1.7976931348623157e+308) --wq-bucket-size int Set Workqueue Rate Limiter Bucket Size, default 500 (default 500) --wq-cooldown-ns duration Set Workqueue Per Item Rate Limiter Cooldown duration in ns, default 0(per item rate limiter disabled) --wq-maxdelay-ns duration Set Workqueue Per Item Rate Limiter Max Delay duration in nanoseconds, default 1000000000 (1s) (default 1s) diff --git a/docs/operator-manual/server-commands/argocd-repo-server.md b/docs/operator-manual/server-commands/argocd-repo-server.md index 33ecaf7c76dd43..7be45fe18d26ff 100644 --- a/docs/operator-manual/server-commands/argocd-repo-server.md +++ b/docs/operator-manual/server-commands/argocd-repo-server.md @@ -29,6 +29,8 @@ argocd-repo-server [flags] --metrics-port int Start metrics server on given port (default 8084) --otlp-address string OpenTelemetry collector address to send traces to --otlp-attrs strings List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value) + --otlp-headers stringToString List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2) (default []) + --otlp-insecure OpenTelemetry collector insecure mode (default true) --parallelismlimit int Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit. --plugin-tar-exclude stringArray Globs to filter when sending tarballs to plugins. --port int Listen on given port for incoming connections (default 8081) diff --git a/docs/operator-manual/server-commands/argocd-server.md b/docs/operator-manual/server-commands/argocd-server.md index c33cf0bedcbcf7..a72cc041299ad6 100644 --- a/docs/operator-manual/server-commands/argocd-server.md +++ b/docs/operator-manual/server-commands/argocd-server.md @@ -25,71 +25,87 @@ argocd-server [flags] ### Options ``` - --address string Listen on given address (default "0.0.0.0") - --app-state-cache-expiration duration Cache expiration for app state (default 1h0m0s) - --application-namespaces strings List of additional namespaces where application resources can be managed in - --as string Username to impersonate for the operation - --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. - --as-uid string UID to impersonate for the operation - --basehref string Value for base href in index.html. Used if Argo CD is running behind reverse proxy under subpath different from / (default "/") - --certificate-authority string Path to a cert file for the certificate authority - --client-certificate string Path to a client certificate file for TLS - --client-key string Path to a client key file for TLS - --cluster string The name of the kubeconfig cluster to use - --connection-status-cache-expiration duration Cache expiration for cluster/repo connection status (default 1h0m0s) - --content-security-policy value Set Content-Security-Policy header in HTTP responses to value. To disable, set to "". (default "frame-ancestors 'self';") - --context string The name of the kubeconfig context to use - --default-cache-expiration duration Cache expiration default (default 24h0m0s) - --dex-server string Dex server address (default "argocd-dex-server:5556") - --dex-server-plaintext Use a plaintext client (non-TLS) to connect to dex server - --dex-server-strict-tls Perform strict validation of TLS certificates when connecting to dex server - --disable-auth Disable client authentication - --disable-compression If true, opt-out of response compression for all requests to the server - --enable-gzip Enable GZIP compression (default true) - --enable-proxy-extension Enable Proxy Extension feature - --gloglevel int Set the glog logging level - -h, --help help for argocd-server - --insecure Run server without TLS - --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure - --kubeconfig string Path to a kube config. Only required if out-of-cluster - --logformat string Set the logging format. One of: text|json (default "text") - --login-attempts-expiration duration Cache expiration for failed login attempts (default 24h0m0s) - --loglevel string Set the logging level. One of: debug|info|warn|error (default "info") - --metrics-address string Listen for metrics on given address (default "0.0.0.0") - --metrics-port int Start metrics on given port (default 8083) - -n, --namespace string If present, the namespace scope for this CLI request - --oidc-cache-expiration duration Cache expiration for OIDC state (default 3m0s) - --otlp-address string OpenTelemetry collector address to send traces to - --otlp-attrs strings List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value) - --password string Password for basic authentication to the API server - --port int Listen on given port (default 8080) - --proxy-url string If provided, this URL will be used to connect via proxy - --redis string Redis server hostname and port (e.g. argocd-redis:6379). - --redis-ca-certificate string Path to Redis server CA certificate (e.g. /etc/certs/redis/ca.crt). If not specified, system trusted CAs will be used for server certificate validation. - --redis-client-certificate string Path to Redis client certificate (e.g. /etc/certs/redis/client.crt). - --redis-client-key string Path to Redis client key (e.g. /etc/certs/redis/client.crt). - --redis-compress string Enable compression for data sent to Redis with the required compression algorithm. (possible values: gzip, none) (default "gzip") - --redis-insecure-skip-tls-verify Skip Redis server certificate validation. - --redis-use-tls Use TLS when connecting to Redis. - --redisdb int Redis database. - --repo-server string Repo server address (default "argocd-repo-server:8081") - --repo-server-plaintext Use a plaintext client (non-TLS) to connect to repository server - --repo-server-strict-tls Perform strict validation of TLS certificates when connecting to repo server - --repo-server-timeout-seconds int Repo server RPC call timeout seconds. (default 60) - --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") - --rootpath string Used if Argo CD is running behind reverse proxy under subpath different from / - --sentinel stringArray Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). - --sentinelmaster string Redis sentinel master group name. (default "master") - --server string The address and port of the Kubernetes API server - --staticassets string Directory path that contains additional static assets (default "/shared/app") - --tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used. - --tlsciphers string The list of acceptable ciphers to be used when establishing TLS connections. Use 'list' to list available ciphers. (default "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_RSA_WITH_AES_256_GCM_SHA384") - --tlsmaxversion string The maximum SSL/TLS version that is acceptable (one of: 1.0|1.1|1.2|1.3) (default "1.3") - --tlsminversion string The minimum SSL/TLS version that is acceptable (one of: 1.0|1.1|1.2|1.3) (default "1.2") - --token string Bearer token for authentication to the API server - --user string The name of the kubeconfig user to use - --username string Username for basic authentication to the API server - --x-frame-options value Set X-Frame-Options header in HTTP responses to value. To disable, set to "". (default "sameorigin") + --address string Listen on given address (default "0.0.0.0") + --api-content-types string Semicolon separated list of allowed content types for non GET api requests. Any content type is allowed if empty. (default "application/json") + --app-state-cache-expiration duration Cache expiration for app state (default 1h0m0s) + --application-namespaces strings List of additional namespaces where application resources can be managed in + --as string Username to impersonate for the operation + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation + --basehref string Value for base href in index.html. Used if Argo CD is running behind reverse proxy under subpath different from / (default "/") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --connection-status-cache-expiration duration Cache expiration for cluster/repo connection status (default 1h0m0s) + --content-security-policy value Set Content-Security-Policy header in HTTP responses to value. To disable, set to "". (default "frame-ancestors 'self';") + --context string The name of the kubeconfig context to use + --default-cache-expiration duration Cache expiration default (default 24h0m0s) + --dex-server string Dex server address (default "argocd-dex-server:5556") + --dex-server-plaintext Use a plaintext client (non-TLS) to connect to dex server + --dex-server-strict-tls Perform strict validation of TLS certificates when connecting to dex server + --disable-auth Disable client authentication + --disable-compression If true, opt-out of response compression for all requests to the server + --enable-gzip Enable GZIP compression (default true) + --enable-proxy-extension Enable Proxy Extension feature + --gloglevel int Set the glog logging level + -h, --help help for argocd-server + --insecure Run server without TLS + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to a kube config. Only required if out-of-cluster + --logformat string Set the logging format. One of: text|json (default "text") + --login-attempts-expiration duration Cache expiration for failed login attempts (default 24h0m0s) + --loglevel string Set the logging level. One of: debug|info|warn|error (default "info") + --metrics-address string Listen for metrics on given address (default "0.0.0.0") + --metrics-port int Start metrics on given port (default 8083) + -n, --namespace string If present, the namespace scope for this CLI request + --oidc-cache-expiration duration Cache expiration for OIDC state (default 3m0s) + --otlp-address string OpenTelemetry collector address to send traces to + --otlp-attrs strings List of OpenTelemetry collector extra attrs when send traces, each attribute is separated by a colon(e.g. key:value) + --otlp-headers stringToString List of OpenTelemetry collector extra headers sent with traces, headers are comma-separated key-value pairs(e.g. key1=value1,key2=value2) (default []) + --otlp-insecure OpenTelemetry collector insecure mode (default true) + --password string Password for basic authentication to the API server + --port int Listen on given port (default 8080) + --proxy-url string If provided, this URL will be used to connect via proxy + --redis string Redis server hostname and port (e.g. argocd-redis:6379). + --redis-ca-certificate string Path to Redis server CA certificate (e.g. /etc/certs/redis/ca.crt). If not specified, system trusted CAs will be used for server certificate validation. + --redis-client-certificate string Path to Redis client certificate (e.g. /etc/certs/redis/client.crt). + --redis-client-key string Path to Redis client key (e.g. /etc/certs/redis/client.crt). + --redis-compress string Enable compression for data sent to Redis with the required compression algorithm. (possible values: gzip, none) (default "gzip") + --redis-insecure-skip-tls-verify Skip Redis server certificate validation. + --redis-use-tls Use TLS when connecting to Redis. + --redisdb int Redis database. + --repo-cache-expiration duration Cache expiration for repo state, incl. app lists, app details, manifest generation, revision meta-data (default 24h0m0s) + --repo-server string Repo server address (default "argocd-repo-server:8081") + --repo-server-default-cache-expiration duration Cache expiration default (default 24h0m0s) + --repo-server-plaintext Use a plaintext client (non-TLS) to connect to repository server + --repo-server-redis string Redis server hostname and port (e.g. argocd-redis:6379). + --repo-server-redis-ca-certificate string Path to Redis server CA certificate (e.g. /etc/certs/redis/ca.crt). If not specified, system trusted CAs will be used for server certificate validation. + --repo-server-redis-client-certificate string Path to Redis client certificate (e.g. /etc/certs/redis/client.crt). + --repo-server-redis-client-key string Path to Redis client key (e.g. /etc/certs/redis/client.crt). + --repo-server-redis-compress string Enable compression for data sent to Redis with the required compression algorithm. (possible values: gzip, none) (default "gzip") + --repo-server-redis-insecure-skip-tls-verify Skip Redis server certificate validation. + --repo-server-redis-use-tls Use TLS when connecting to Redis. + --repo-server-redisdb int Redis database. + --repo-server-sentinel stringArray Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). + --repo-server-sentinelmaster string Redis sentinel master group name. (default "master") + --repo-server-strict-tls Perform strict validation of TLS certificates when connecting to repo server + --repo-server-timeout-seconds int Repo server RPC call timeout seconds. (default 60) + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + --revision-cache-expiration duration Cache expiration for cached revision (default 3m0s) + --rootpath string Used if Argo CD is running behind reverse proxy under subpath different from / + --sentinel stringArray Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). + --sentinelmaster string Redis sentinel master group name. (default "master") + --server string The address and port of the Kubernetes API server + --staticassets string Directory path that contains additional static assets (default "/shared/app") + --tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used. + --tlsciphers string The list of acceptable ciphers to be used when establishing TLS connections. Use 'list' to list available ciphers. (default "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:TLS_RSA_WITH_AES_256_GCM_SHA384") + --tlsmaxversion string The maximum SSL/TLS version that is acceptable (one of: 1.0|1.1|1.2|1.3) (default "1.3") + --tlsminversion string The minimum SSL/TLS version that is acceptable (one of: 1.0|1.1|1.2|1.3) (default "1.2") + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use + --username string Username for basic authentication to the API server + --x-frame-options value Set X-Frame-Options header in HTTP responses to value. To disable, set to "". (default "sameorigin") ``` ### SEE ALSO diff --git a/docs/operator-manual/signed-release-assets.md b/docs/operator-manual/signed-release-assets.md index 9aec6bb0710470..b4e4f3fc97418b 100644 --- a/docs/operator-manual/signed-release-assets.md +++ b/docs/operator-manual/signed-release-assets.md @@ -92,7 +92,7 @@ The attestation payload contains a non-forgeable provenance which is base64 enco ```bash slsa-verifier verify-image "$IMAGE" \ --source-uri github.com/argoproj/argo-cd \ - --source-tag v2.7.0 + --source-tag v2.7.0 \ --print-provenance | jq ``` diff --git a/docs/operator-manual/upgrading/2.10-2.11.md b/docs/operator-manual/upgrading/2.10-2.11.md new file mode 100644 index 00000000000000..4cf5c8ed02b0ba --- /dev/null +++ b/docs/operator-manual/upgrading/2.10-2.11.md @@ -0,0 +1,5 @@ +# v2.10 to 2.11 + +## initiatedBy added in Application CRD + +In order to address [argoproj/argo-cd#16612](https://github.com/argoproj/argo-cd/issues/16612), initiatedBy has been added in the Application CRD. \ No newline at end of file diff --git a/docs/operator-manual/upgrading/2.8-2.9.md b/docs/operator-manual/upgrading/2.8-2.9.md index 6a82684019ac4c..ef99e095878146 100644 --- a/docs/operator-manual/upgrading/2.8-2.9.md +++ b/docs/operator-manual/upgrading/2.8-2.9.md @@ -1,12 +1,5 @@ # v2.8 to 2.9 -## `managedNamespaceMetadata` no longer preserves client-side-applied labels or annotations +## Upgraded Kustomize Version -Argo CD 2.9 upgraded kubectl from 1.24 to 1.26. This upgrade introduced a change where client-side-applied labels and -annotations are no longer preserved when using a server-side kubectl apply. This change affects the -`managedNamespaceMetadata` field of the `Application` CRD. Previously, labels and annotations applied via a client-side -apply would be preserved when `managedNamespaceMetadata` was enabled. Now, those existing labels and annotation will be -removed. - -To avoid unexpected behavior, follow the [client-side to server-side resource upgrade guide](https://kubernetes.io/docs/reference/using-api/server-side-apply/#upgrading-from-client-side-apply-to-server-side-apply) -before enabling `managedNamespaceMetadata` on an existing namespace. +Note that bundled Kustomize version has been upgraded from 5.1.0 to 5.2.1. diff --git a/docs/operator-manual/upgrading/2.9-2.10.md b/docs/operator-manual/upgrading/2.9-2.10.md new file mode 100644 index 00000000000000..adb37c4babf4b9 --- /dev/null +++ b/docs/operator-manual/upgrading/2.9-2.10.md @@ -0,0 +1,16 @@ +# v2.9 to 2.10 + +## `managedNamespaceMetadata` no longer preserves client-side-applied labels or annotations + +Argo CD 2.10 upgraded kubectl from 1.24 to 1.26. This upgrade introduced a change where client-side-applied labels and +annotations are no longer preserved when using a server-side kubectl apply. This change affects the +`managedNamespaceMetadata` field of the `Application` CRD. Previously, labels and annotations applied via a client-side +apply would be preserved when `managedNamespaceMetadata` was enabled. Now, those existing labels and annotation will be +removed. + +To avoid unexpected behavior, follow the [client-side to server-side resource upgrade guide](https://kubernetes.io/docs/reference/using-api/server-side-apply/#upgrading-from-client-side-apply-to-server-side-apply) +before enabling `managedNamespaceMetadata` on an existing namespace. + +## Upgraded Helm Version + +Note that bundled Helm version has been upgraded from 3.13.2 to 3.14.2. diff --git a/docs/operator-manual/upgrading/overview.md b/docs/operator-manual/upgrading/overview.md index 67ab78076c648c..742c7b191b57a1 100644 --- a/docs/operator-manual/upgrading/overview.md +++ b/docs/operator-manual/upgrading/overview.md @@ -37,6 +37,7 @@ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/ +* [v2.9 to v2.10](./2.9-2.10.md) * [v2.8 to v2.9](./2.8-2.9.md) * [v2.7 to v2.8](./2.7-2.8.md) * [v2.6 to v2.7](./2.6-2.7.md) diff --git a/docs/operator-manual/user-management/index.md b/docs/operator-manual/user-management/index.md index 8e459202456ebf..c002b77ada5edb 100644 --- a/docs/operator-manual/user-management/index.md +++ b/docs/operator-manual/user-management/index.md @@ -201,7 +201,7 @@ data: id: acme-github name: Acme GitHub config: - hostName: github.acme.com + hostName: github.acme.example.com clientID: abcdefghijklmnopqrst clientSecret: $dex.acme.clientSecret # Alternatively $:dex.acme.clientSecret orgs: @@ -242,7 +242,7 @@ data: id: oidc name: OIDC config: - issuer: https://example-OIDC-provider.com + issuer: https://example-OIDC-provider.example.com clientID: aaaabbbbccccddddeee clientSecret: $dex.oidc.clientSecret ``` @@ -264,7 +264,7 @@ data: id: oidc name: OIDC config: - issuer: https://example-OIDC-provider.com + issuer: https://example-OIDC-provider.example.com clientID: aaaabbbbccccddddeee clientSecret: $dex.oidc.clientSecret insecureEnableGroups: true @@ -294,7 +294,7 @@ data: id: oidc name: OIDC config: - issuer: https://example-OIDC-provider.com + issuer: https://example-OIDC-provider.example.com clientID: aaaabbbbccccddddeee clientSecret: $dex.oidc.clientSecret insecureEnableGroups: true @@ -387,6 +387,20 @@ For a simple case this can be: oidc.config: | requestedIDTokenClaims: {"groups": {"essential": true}} ``` + +### Retrieving group claims when not in the token + +Some OIDC providers don't return the group information for a user in the ID token, even if explicitly requested using the `requestedIDTokenClaims` setting (Okta for example). They instead provide the groups on the user info endpoint. With the following config, Argo CD queries the user info endpoint during login for groups information of a user: + +```yaml +oidc.config: | + enableUserInfoGroups: true + userInfoPath: /userinfo + userInfoCacheExpiration: "5m" +``` + +**Note: If you omit the `userInfoCacheExpiration` setting or if it's greater than the expiration of the ID token, the argocd-server will cache group information as long as the ID token is valid!** + ### Configuring a custom logout URL for your OIDC provider Optionally, if your OIDC provider exposes a logout API and you wish to configure a custom logout URL for the purposes of invalidating @@ -395,18 +409,18 @@ any active session post logout, you can do so by specifying it as follows: ```yaml oidc.config: | name: example-OIDC-provider - issuer: https://example-OIDC-provider.com + issuer: https://example-OIDC-provider.example.com clientID: xxxxxxxxx clientSecret: xxxxxxxxx requestedScopes: ["openid", "profile", "email", "groups"] requestedIDTokenClaims: {"groups": {"essential": true}} - logoutURL: https://example-OIDC-provider.com/logout?id_token_hint={{token}} + logoutURL: https://example-OIDC-provider.example.com/logout?id_token_hint={{token}} ``` By default, this would take the user to their OIDC provider's login page after logout. If you also wish to redirect the user back to Argo CD after logout, you can specify the logout URL as follows: ```yaml ... - logoutURL: https://example-OIDC-provider.com/logout?id_token_hint={{token}}&post_logout_redirect_uri={{logoutRedirectURL}} + logoutURL: https://example-OIDC-provider.example.com/logout?id_token_hint={{token}}&post_logout_redirect_uri={{logoutRedirectURL}} ``` You are not required to specify a logoutRedirectURL as this is automatically generated by ArgoCD as your base ArgoCD url + Rootpath @@ -486,7 +500,7 @@ data: #### Alternative -If you want to store sensitive data in **another** Kubernetes `Secret`, instead of `argocd-secret`. ArgoCD knows to check the keys under `data` in your Kubernetes `Secret` for a corresponding key whenever a value in a configmap starts with `$`, then your Kubernetes `Secret` name and `:` (colon). +If you want to store sensitive data in **another** Kubernetes `Secret`, instead of `argocd-secret`. ArgoCD knows to check the keys under `data` in your Kubernetes `Secret` for a corresponding key whenever a value in a configmap or secret starts with `$`, then your Kubernetes `Secret` name and `:` (colon). Syntax: `$:` diff --git a/docs/operator-manual/user-management/microsoft.md b/docs/operator-manual/user-management/microsoft.md index 33a6b3e9459400..486d647fde3d05 100644 --- a/docs/operator-manual/user-management/microsoft.md +++ b/docs/operator-manual/user-management/microsoft.md @@ -1,13 +1,16 @@ # Microsoft -* [Azure AD SAML Enterprise App Auth using Dex](#azure-ad-saml-enterprise-app-auth-using-dex) -* [Azure AD App Registration Auth using OIDC](#azure-ad-app-registration-auth-using-oidc) -* [Azure AD App Registration Auth using Dex](#azure-ad-app-registration-auth-using-dex) +!!! note "" + Entra ID was formerly known as Azure AD. -## Azure AD SAML Enterprise App Auth using Dex -### Configure a new Azure AD Enterprise App +* [Entra ID SAML Enterprise App Auth using Dex](#entra-id-saml-enterprise-app-auth-using-dex) +* [Entra ID App Registration Auth using OIDC](#entra-id-app-registration-auth-using-oidc) +* [Entra ID App Registration Auth using Dex](#entra-id-app-registration-auth-using-dex) -1. From the `Azure Active Directory` > `Enterprise applications` menu, choose `+ New application` +## Entra ID SAML Enterprise App Auth using Dex +### Configure a new Entra ID Enterprise App + +1. From the `Microsoft Entra ID` > `Enterprise applications` menu, choose `+ New application` 2. Select `Non-gallery application` 3. Enter a `Name` for the application (e.g. `Argo CD`), then choose `Add` 4. Once the application is created, open it from the `Enterprise applications` menu. @@ -31,9 +34,9 @@ - *Keep a copy of the encoded output to be used in the next section.* 9. From the `Single sign-on` menu, copy the `Login URL` parameter, to be used in the next section. -### Configure Argo to use the new Azure AD Enterprise App +### Configure Argo to use the new Entra ID Enterprise App -1. Edit `argocd-cm` and add the following `dex.config` to the data section, replacing the `caData`, `my-argo-cd-url` and `my-login-url` your values from the Azure AD App: +1. Edit `argocd-cm` and add the following `dex.config` to the data section, replacing the `caData`, `my-argo-cd-url` and `my-login-url` your values from the Entra ID App: data: url: https://my-argo-cd-url @@ -56,7 +59,7 @@ groupsAttr: Group 2. Edit `argocd-rbac-cm` to configure permissions, similar to example below. - - Use Azure AD `Group IDs` for assigning roles. + - Use Entra ID `Group IDs` for assigning roles. - See [RBAC Configurations](../rbac.md) for more detailed scenarios. # example policy @@ -70,11 +73,11 @@ p, role:org-admin, repositories, delete, *, allow g, "84ce98d1-e359-4f3b-85af-985b458de3c6", role:org-admin # (azure group assigned to role) -## Azure AD App Registration Auth using OIDC -### Configure a new Azure AD App registration -#### Add a new Azure AD App registration +## Entra ID App Registration Auth using OIDC +### Configure a new Entra ID App registration +#### Add a new Entra ID App registration -1. From the `Azure Active Directory` > `App registrations` menu, choose `+ New registration` +1. From the `Microsoft Entra ID` > `App registrations` menu, choose `+ New registration` 2. Enter a `Name` for the application (e.g. `Argo CD`). 3. Specify who can use the application (e.g. `Accounts in this organizational directory only`). 4. Enter Redirect URI (optional) as follows (replacing `my-argo-cd-url` with your Argo URL), then choose `Add`. @@ -92,29 +95,29 @@ - **Redirect URI:** `http://localhost:8085/auth/callback` ![Azure App registration's Authentication](../../assets/azure-app-registration-authentication.png "Azure App registration's Authentication") -#### Add credentials a new Azure AD App registration +#### Add credentials a new Entra ID App registration 1. From the `Certificates & secrets` menu, choose `+ New client secret` 2. Enter a `Name` for the secret (e.g. `ArgoCD-SSO`). - Make sure to copy and save generated value. This is a value for the `client_secret`. ![Azure App registration's Secret](../../assets/azure-app-registration-secret.png "Azure App registration's Secret") -#### Setup permissions for Azure AD Application +#### Setup permissions for Entra ID Application 1. From the `API permissions` menu, choose `+ Add a permission` 2. Find `User.Read` permission (under `Microsoft Graph`) and grant it to the created application: - ![Azure AD API permissions](../../assets/azure-api-permissions.png "Azure AD API permissions") + ![Entra ID API permissions](../../assets/azure-api-permissions.png "Entra ID API permissions") 3. From the `Token Configuration` menu, choose `+ Add groups claim` - ![Azure AD token configuration](../../assets/azure-token-configuration.png "Azure AD token configuration") + ![Entra ID token configuration](../../assets/azure-token-configuration.png "Entra ID token configuration") -### Associate an Azure AD group to your Azure AD App registration +### Associate an Entra ID group to your Entra ID App registration -1. From the `Azure Active Directory` > `Enterprise applications` menu, search the App that you created (e.g. `Argo CD`). - - An Enterprise application with the same name of the Azure AD App registration is created when you add a new Azure AD App registration. +1. From the `Microsoft Entra ID` > `Enterprise applications` menu, search the App that you created (e.g. `Argo CD`). + - An Enterprise application with the same name of the Entra ID App registration is created when you add a new Entra ID App registration. 2. From the `Users and groups` menu of the app, add any users or groups requiring access to the service. ![Azure Enterprise SAML Users](../../assets/azure-enterprise-users.png "Azure Enterprise SAML Users") -### Configure Argo to use the new Azure AD App registration +### Configure Argo to use the new Entra ID App registration 1. Edit `argocd-cm` and configure the `data.oidc.config` and `data.url` section: @@ -173,7 +176,7 @@ Refer to [operator-manual/argocd-rbac-cm.yaml](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-rbac-cm.yaml) for all of the available variables. -## Azure AD App Registration Auth using Dex +## Entra ID App Registration Auth using Dex Configure a new AD App Registration, as above. Then, add the `dex.config` to `argocd-cm`: @@ -200,9 +203,9 @@ data: 1. Open a new browser tab and enter your ArgoCD URI: https://`` ![Azure SSO Web Log In](../../assets/azure-sso-web-log-in-via-azure.png "Azure SSO Web Log In") -3. Click `LOGIN VIA AZURE` button to log in with your Azure Active Directory account. You’ll see the ArgoCD applications screen. +3. Click `LOGIN VIA AZURE` button to log in with your Microsoft Entra ID account. You’ll see the ArgoCD applications screen. ![Azure SSO Web Application](../../assets/azure-sso-web-application.png "Azure SSO Web Application") -4. Navigate to User Info and verify Group ID. Groups will have your group’s Object ID that you added in the `Setup permissions for Azure AD Application` step. +4. Navigate to User Info and verify Group ID. Groups will have your group’s Object ID that you added in the `Setup permissions for Entra ID Application` step. ![Azure SSO Web User Info](../../assets/azure-sso-web-user-info.png "Azure SSO Web User Info") ### Log in to ArgoCD using CLI diff --git a/docs/operator-manual/user-management/okta.md b/docs/operator-manual/user-management/okta.md index 09d7099d199546..308254759de6ec 100644 --- a/docs/operator-manual/user-management/okta.md +++ b/docs/operator-manual/user-management/okta.md @@ -118,34 +118,81 @@ data: ## OIDC (without Dex) -!!! warning "Do you want groups for RBAC later?" - If you want `groups` scope returned from Okta you need to unfortunately contact support to enable [API Access Management with Okta](https://developer.okta.com/docs/concepts/api-access-management/) or [_just use SAML above!_](#saml-with-dex) +!!! warning "Okta groups for RBAC" + If you want `groups` scope returned from Okta, you will need to enable [API Access Management with Okta](https://developer.okta.com/docs/concepts/api-access-management/). This addon is free, and automatically enabled, on Okta developer edition. However, it's an optional add-on for production environments, with an additional associated cost. - Next you may need the API Access Management feature, which the support team can enable for your OktaPreview domain for testing, to enable "custom scopes" and a separate endpoint to use instead of the "public" `/oauth2/v1/authorize` API Access Management endpoint. This might be a paid feature if you want OIDC unfortunately. The free alternative I found was SAML. + You may alternately add a "groups" scope and claim to the default authorization server, and then filter the claim in the Okta application configuration. It's not clear if this requires the Authorization Server add-on. + + If this is not an option for you, use the [SAML (with Dex)](#saml-with-dex) option above instead. + +!!! note + These instructions and screenshots are of Okta version 2023.05.2 E. You can find the current version in the Okta website footer. + +First, create the OIDC integration: + +1. On the `Okta Admin` page, navigate to the Okta Applications at `Applications > Applications.` +1. Choose `Create App Integration`, and choose `OIDC`, and then `Web Application` in the resulting dialogues. + ![Okta OIDC app dialogue](../../assets/okta-create-oidc-app.png) +1. Update the following: + 1. `App Integration name` and `Logo` - set these to suit your needs; they'll be displayed in the Okta catalogue. + 1. `Sign-in redirect URLs`: Add `https://argocd.example.com/auth/callback`; replacing `argocd.example.com` with your ArgoCD web interface URL. Also add `http://localhost:8085/auth/callback` if you would like to be able to login with the CLI. + 1. `Sign-out redirect URIs`: Add `https://argocd.example.com`; substituting the correct domain name as above. + 1. Either assign groups, or choose to skip this step for now. + 1. Leave the rest of the options as-is, and save the integration. + ![Okta app settings](../../assets/okta-app.png) +1. Copy the `Client ID` and the `Client Secret` from the newly created app; you will need these later. + +Next, create a custom Authorization server: 1. On the `Okta Admin` page, navigate to the Okta API Management at `Security > API`. - ![Okta API Management](../../assets/api-management.png) -1. Choose your `default` authorization server. -1. Click `Scopes > Add Scope` - 1. Add a scope called `groups`. - ![Groups Scope](../../assets/groups-scope.png) -1. Click `Claims > Add Claim.` - 1. Add a claim called `groups` - 1. Choose the matching options you need, one example is: - * e.g. to match groups starting with `argocd-` you'd return an `ID Token` using your scope name from step 3 (e.g. `groups`) where the groups name `matches` the `regex` `argocd-.*` - ![Groups Claim](../../assets/groups-claim.png) -1. Edit the `argocd-cm` and configure the `data.oidc.config` section: +1. Click `Add Authorization Server`, and assign it a name and a description. The `Audience` should match your ArgoCD URL - `https://argocd.example.com` +1. Click `Scopes > Add Scope`: + 1. Add a scope called `groups`. Leave the rest of the options as default. + ![Groups Scope](../../assets/okta-groups-scope.png) +1. Click `Claims > Add Claim`: + 1. Add a claim called `groups`. + 1. Adjust the `Include in token type` to `ID Token`, `Always`. + 1. Adjust the `Value type` to `Groups`. + 1. Add a filter that will match the Okta groups you want passed on to ArgoCD; for example `Regex: argocd-.*`. + 1. Set `Include in` to `groups` (the scope you created above). + ![Groups Claim](../../assets/okta-groups-claim.png) +1. Click on `Access Policies` > `Add Policy.` This policy will restrict how this authorization server is used. + 1. Add a name and description. + 1. Assign the policy to the client (application integration) you created above. The field should auto-complete as you type. + 1. Create the policy. + ![Auth Policy](../../assets/okta-auth-policy.png) +1. Add a rule to the policy: + 1. Add a name; `default` is a reasonable name for this rule. + 1. Fine-tune the settings to suit your organization's security posture. Some ideas: + 1. uncheck all the grant types except the Authorization Code. + 1. Adjust the token lifetime to govern how long a session can last. + 1. Restrict refresh token lifetime, or completely disable it. + ![Default rule](../../assets/okta-auth-rule.png) +1. Finally, click `Back to Authorization Servers`, and copy the `Issuer URI`. You will need this later. + +If you haven't yet created Okta groups, and assigned them to the application integration, you should do that now: + +1. Go to `Directory > Groups` +1. For each group you wish to add: + 1. Click `Add Group`, and choose a meaningful name. It should match the regex or pattern you added to your custom `group` claim. + 1. Click on the group (refresh the page if the new group didn't show up in the list). + 1. Assign Okta users to the group. + 1. Click on `Applications` and assign the OIDC application integration you created to this group. + 1. Repeat as needed. + +Finally, configure ArgoCD itself. Edit the `argocd-cm` configmap: ```yaml +url: https://argocd.example.com oidc.config: | name: Okta - issuer: https://yourorganization.oktapreview.com - clientID: 0oaltaqg3oAIf2NOa0h3 - clientSecret: ZXF_CfUc-rtwNfzFecGquzdeJ_MxM4sGc8pDT2Tg6t + # this is the authorization server URI + issuer: https://example.okta.com/oauth2/aus9abcdefgABCDEFGd7 + clientID: 0oa9abcdefgh123AB5d7 + clientSecret: ABCDEFG1234567890abcdefg requestedScopes: ["openid", "profile", "email", "groups"] requestedIDTokenClaims: {"groups": {"essential": true}} ``` - - +You may want to store the `clientSecret` in a Kubernetes secret; see [how to deal with SSO secrets](./index.md/#sensitive-data-and-sso-client-secrets ) for more details. diff --git a/docs/operator-manual/webhook.md b/docs/operator-manual/webhook.md index eb15c4cb02369b..a0e6c8deba1b29 100644 --- a/docs/operator-manual/webhook.md +++ b/docs/operator-manual/webhook.md @@ -97,3 +97,13 @@ stringData: ``` After saving, the changes should take effect automatically. + +### Alternative + +If you want to store webhook data in **another** Kubernetes `Secret`, instead of `argocd-secret`. ArgoCD knows to check the keys under `data` in your Kubernetes `Secret` starts with `$`, then your Kubernetes `Secret` name and `:` (colon). + +Syntax: `$:` + +> NOTE: Secret must have label `app.kubernetes.io/part-of: argocd` + +For more information refer to the corresponding section in the [User Management Documentation](user-management/index.md#alternative). diff --git a/docs/proposals/applicationset-plugin-generator.md b/docs/proposals/applicationset-plugin-generator.md index 6a3b2ec484c8a1..616ef13efcd2b6 100644 --- a/docs/proposals/applicationset-plugin-generator.md +++ b/docs/proposals/applicationset-plugin-generator.md @@ -89,7 +89,7 @@ data: baseUrl: http://myplugin.plugin.svc.cluster.local ``` -- token is used a a bearer token in the RPC request. It could be a [sensitive reference](https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#sensitive-data-and-sso-client-secrets). +- token is used a bearer token in the RPC request. It could be a [sensitive reference](https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#sensitive-data-and-sso-client-secrets). ### Reconciliation logic diff --git a/docs/proposals/decouple-application-sync-user-using-impersonation.md b/docs/proposals/decouple-application-sync-user-using-impersonation.md new file mode 100644 index 00000000000000..e7e459a7059c0d --- /dev/null +++ b/docs/proposals/decouple-application-sync-user-using-impersonation.md @@ -0,0 +1,592 @@ +--- +title: Decouple Control plane and Application Sync privileges +authors: + - "@anandf" +sponsors: + - Red Hat +reviewers: + - "@blakepettersson" + - "@crenshaw-dev" + - "@jannfis" +approvers: + - "@alexmt" + - "@crenshaw-dev" + - "@jannfis" + +creation-date: 2023-06-23 +last-updated: 2024-02-06 +--- + +# Decouple Application Sync using Impersonation + +Application syncs in Argo CD have the same privileges as the Argo CD control plane. As a consequence, in a multi-tenant setup, the Argo CD control plane privileges needs to match the tenant that needs the highest privileges. As an example, if an Argo CD instance has 10 Applications and only one of them requires admin privileges, then the Argo CD control plane must have admin privileges in order to be able to sync that one Application. Argo CD provides a multi-tenancy model to restrict what each Application can do using `AppProjects`, even though the control plane has higher privileges, however that creates a large attack surface since if Argo CD is compromised, attackers would have cluster-admin access to the cluster. + +The goal of this proposal is to perform the Application sync as a different user using impersonation and use the service account provided in the cluster config purely for control plane operations. + +### What is Impersonation + +Impersonation is a feature in Kubernetes and enabled in the `kubectl` CLI client, using which, a user can act as another user through impersonation headers. For example, an admin could use this feature to debug an authorization policy by temporarily impersonating another user and seeing if a request was denied. + +Impersonation requests first authenticate as the requesting user, then switch to the impersonated user info. + +``` +kubectl --as ... +kubectl --as --as-group ... +``` + +## Open Questions [optional] + +- Should the restrictions imposed as part of the `AppProjects` be honored if the impersonation feature is enabled ? +>Yes, other restrictions implemented by `AppProject` related to whitelisting/blacklisting resources must continue to be honoured. +- Can an Application refer to a service account with elevated privileges like say `cluster-admin`, `admin`, and service accounts used for running the ArgoCD controllers itself ? +>Yes, this is possible as long as the ArgoCD admin user explicitly allows it through the `AppProject` configuration. +- Among the destinations configured in the `AppProject`, if there are multiple matches for a given destination, which destination option should be used ? +>If there are more than one matching destination, either with a glob pattern match or an exact match, then we use the first valid match to determine the service account to be used for the sync operation. +- Can the kubernetes audit trail events capture the impersonation. +>Yes, kubernetes audit trail events capture both the actual user and the impersonating user details and hence its possible to track who executed the commands and as which user permissions using the audit trails. +- Would the Sync hooks be using the impersonation service account. +>Yes, if the impersonation feature is enabled and customers use Sync hooks, then impersonation service account would be used for executing the hook jobs as well. +- If application resources have hardcoded namespaces in the git repository, would different service accounts be used for each resource during the sync operation ? +>The service account to be used for impersonation is determined on a per Application level rather than on per resource level. The value specified in `Application.spec.destination.namespace` would be used to determine the service account to be used for the sync operation of all resources present in the `Application`. + +## Summary + +In a multi team/multi tenant environment, an application team is typically granted access to a namespace to self-manage their Applications in a declarative way. Current implementation of ArgoCD requires the ArgoCD Administrator to create an `AppProject` with access settings configured to replicate the RBAC resources that are configured for each team. This approach requires duplication of effort and also requires syncing the access between both to maintain the security posture. It would be desirable for users to use the existing RBAC rules without having to revert to Argo CD API to create and manage these Applications. One namespace per team, or even one namespace per application is what we are looking to address as part of this proposal. + +## Motivation + +This proposal would allow ArgoCD administrators to manage the cluster permissions using kubernetes native RBAC implementation rather than using complex configurations in `AppProjects` to restrict access to individual applications. By decoupling the privileges required for application sync from the privileges required for ArgoCD control plane operations, the security requirement of providing least privileges can be achieved there by improving the security posture of ArgoCD. For implementing multi team/tenant use cases, this decoupling would be greatly beneficial. + +### Assumptions + +- Namespaces are pre-populated with one or more `ServiceAccounts` that define the permissions for each `AppProject`. +- Many users prefer to control access to kubernetes resources through kubernetes RBAC constructs instead of Argo specific constructs. +- Each tenant is generally given access to a specific namespace along with a service account, role or cluster role and role binding to control access to that namespace. +- `Applications` created by a tenant manage namespaced resources. +- An `AppProject` can either be mapped to a single tenant or multiple related tenants and the respective destinations that needs to be managed via the `AppProject`, needs to be configured. + + +### Goals +- Applications may only impersonate ServiceAccounts that live in the same namespace as the destination namespace configured in the application.If the service account is created in a different namespace, then the user can provide the service account name in the format `:` . ServiceAccount to be used for syncing each application is determined by the target destination configured in the `AppProject` associated with the `Application`. +- If impersonation feature is enabled, and no service account name is provided in the associated `AppProject`, then the default service account of the destination namespace of the `Application` should be used. +- Access restrictions implemented through properties in AppProject (if done) must have the existing behavior. From a security standpoint, any restrictions that were available before switching to a service account based approach should continue to exist even when the impersonation feature is enabled. + +### Non-Goals + +None + +## Proposal + +As part of this proposal, it would be possible for an ArgoCD Admin to specify a service account name in `AppProjects` CR for a single or a group of destinations. A destination is uniquely identified by a target cluster and a namespace combined. + +When applications gets synced, based on its destination (target cluster and namespace combination), the `defaultServiceAccount` configured in the `AppProject` will be selected and used for impersonation when executing the kubectl commands for the sync operation. + +We would be introducing a new element `destinationServiceAccounts` in `AppProject.spec`. This element is used for the sole purpose of specifying the impersonation configuration. The `defaultServiceAccount` configured for the `AppProject` would be used for the sync operation for a particular destination cluster and namespace. If impersonation feature is enabled and no specific service account is provided in the `AppProject` CR, then the `default` service account in the destination namespace would be used for impersonation. + +``` +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: my-project + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + description: Example Project + # Allow manifests to deploy from any Git repos + sourceRepos: + - '*' + destinations: + - * + destinationServiceAccounts: + - server: https://kubernetes.default.svc + namespace: guestbook + defaultServiceAccount: guestbook-deployer + - server: https://kubernetes.default.svc + namespace: guestbook-dev + defaultServiceAccount: guestbook-dev-deployer + - server: https://kubernetes.default.svc + namespace: guestbook-stage + defaultServiceAccount: guestbook-stage-deployer +``` + +### Structure of DestinationServiceAccount: +|Parameter| Type | Required/Optional| Description| +| ------ | ------ | ------- | -------- | +| server | string | Required | Server specifies the URL of the target cluster's Kubernetes control plane API. Glob patterns are supported. | +| namespace | string | Required | Namespace specifies the target namespace for the application's resources. Glob patterns are supported. | +| defaultServiceAccount | string | Required| DefaultServiceAccount specifies the service account to be impersonated when performing the `Application` sync operation.| + +**Note:** Only server URL for the target cluster is supported and target cluster name is not supported. + +### Future enhancements + +In a future release, we plan to support overriding of service accounts at the application level. In that case, we would be adding an element called `allowedServiceAccounts` to `AppProject.spec.destinationServiceAccounts[*]` + +### Use cases + +#### Use case 1: + +As a user, I would like to use kubernetes security constructs to restrict user access for application sync +So that, I can provide granular permissions based on the principle of least privilege required for syncing an application. + +#### Use case 2: + +As a user, I would like to configure a common service account for all applications associated to an AppProject +So that, I can use a generic convention of naming service accounts and avoid associating the service account per application. + +### Design considerations + +- Extending the `destinations` field under `AppProjects` was an option that was considered. But since the intent of it was to restrict the destinations that an associated `Application` can use, it was not used. Also the destination fields allowed negation operator (`!`) which would complicate the service account matching logic. The decision to create a new struct under `AppProject.Spec` for specifying the service account for each destination was considered a better alternative. + +- The field name `defaultServiceAccount` was chosen instead of `serviceAccount` as we wanted to support overriding of the service account at an `Application` at a later point in time and wanted to reserve the name `serviceAccount` for future extension. + +- Not supporting all impersonation options at the moment to keep the initial design to a minimum. Based on the need and feedback, support to impersonate users or groups can be added in future. + +### Implementation Details/Notes/Constraints + +#### Component : GitOps Engine + +- Fix GitOps Engine code to honor Impersonate configuration set in the Application sync context for all kubectl commands that are being executed. + +#### Component: ArgoCD API + +- Create a new struct type `DestinationServiceAccount` having fields `namespace`, `server` and `defaultServiceAccount` +- Create a new field `DestinationServiceAccounts` under a `AppProject.Spec` that takes in a list of `DestinationServiceAccount` objects. +- Add Documentation for newly introduced struct and its fields for `DestinationServiceAccount` and `DestinationServiceAccounts` under `AppProject.Spec` + +#### Component: ArgoCD Application Controller + +- Provide a configuration in `argocd-cm` which can be modified to enable the Impersonation feature. Set `applicationcontroller.enable.impersonation: true` in the Argo CD ConfigMap. Default value of `applicationcontroller.enable.impersonation` would be `false` and user has to explicitly override it to use this feature. +- Provide an option to override the Impersonation feature using environment variables. +Set `ARGOCD_APPLICATION_CONTROLLER_ENABLE_IMPERSONATION=true` in the Application controller environment variables. Default value of the environment variable must be `false` and user has to explicitly set it to `true` to use this feature. +- Provide an option to enable this feature using a command line flag `--enable-impersonation`. This new argument option needs to be added to the Application controller args. +- Fix Application Controller `sync.go` to set the Impersonate configuration from the AppProject CR to the `SyncContext` Object (rawConfig and restConfig field, need to understand which config is used for the actual sync and if both configs need to be impersonated.) + +#### Component: ArgoCD UI + +- Provide option to create `DestinationServiceAccount` with fields `namespace`, `server` and `defaultServiceAccount`. +- Provide option to add multiple `DestinationServiceAccounts` to an `AppProject` created/updated via the web console. +- Update the User Guide documentation on how to use these newly added fields from the web console. + +#### Component: ArgoCD CLI + +- Provide option to create `DestinationServiceAccount` with fields `namespace`, `server` and `defaultServiceAccount`. +- Provide option to add multiple `DestinationServiceAccounts` to an `AppProject` created/updated via the web console. +- Update the User Guide and other documentation where the CLI option usages are explained. + +#### Component: Documentation + +- Add note that this is a Beta feature in the documentation. +- Add a separate section for this feature under user-guide section. +- Update the ArgoCD CLI command reference documentation. +- Update the ArgoCD UI command reference documentation. + +### Detailed examples + +#### Example 1: Service account for application sync specified at the AppProject level for all namespaces + +In this specific scenario, service account name `generic-deployer` will get used for the application sync as the namespace `guestbook` matches the glob pattern `*`. + +- Install ArgoCD in the `argocd` namespace. +``` +kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/install.yaml -n argocd +``` + +- Enable the impersonation feature in ArgoCD. +``` +kubectl set env statefulset/argocd-application-controller ARGOCD_APPLICATION_CONTROLLER_ENABLE_IMPERSONATION=true +``` + +- Create a namespace called `guestbook` and a service account called `guestbook-deployer`. +``` +kubectl create namespace guestbook +kubectl create serviceaccount guestbook-deployer +``` + +- Create Role and RoleBindings and configure RBAC access for creating `Service` and `Deployment` objects in namespace `guestbook` for service account `guestbook-deployer`. +``` +kubectl create role guestbook-deployer-role --verb get,list,update,delete --resource pods,deployment,service +kubectl create rolebinding guestbook-deployer-rb --serviceaccount guestbook-deployer --role guestbook-deployer-role +``` + +- Create the `Application` in the `argocd` namespace and the required `AppProject` as below +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: guestbook + namespace: argocd +spec: + project: my-project + source: + repoURL: https://github.com/argoproj/argocd-example-apps.git + targetRevision: HEAD + path: guestbook + destination: + server: https://kubernetes.default.svc + namespace: guestbook +--- +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: my-project + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + description: Example Project + # Allow manifests to deploy from any Git repos + sourceRepos: + - '*' + destinations: + - namespace: * + server: https://kubernetes.default.svc + destinationServiceAccounts: + - namespace: * + server: https://kubernetes.default.svc + defaultServiceAccount: generic-deployer +``` + +#### Example 2: Service account for application sync specified at the AppProject level for specific namespaces + +In this specific scenario, service account name `guestbook-deployer` will get used for the application sync as the namespace `guestbook` matches the target namespace `guestbook`. + +- Install ArgoCD in the `argocd` namespace. +``` +kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/install.yaml -n argocd +``` + +- Enable the impersonation feature in ArgoCD. +``` +kubectl set env statefulset/argocd-application-controller ARGOCD_APPLICATION_CONTROLLER_ENABLE_IMPERSONATION=true +``` + +- Create a namespace called `guestbook` and a service account called `guestbook-deployer`. +``` +kubectl create namespace guestbook +kubectl create serviceaccount guestbook-deployer +``` +- Create Role and RoleBindings and configure RBAC access for creating `Service` and `Deployment` objects in namespace `guestbook` for service account `guestbook-deployer`. +``` +kubectl create role guestbook-deployer-role --verb get,list,update,delete --resource pods,deployment,service +kubectl create rolebinding guestbook-deployer-rb --serviceaccount guestbook-deployer --role guestbook-deployer-role +``` + +In this specific scenario, service account name `guestbook-deployer` will get used as it matches to the specific namespace `guestbook`. +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: guestbook + namespace: argocd +spec: + project: my-project + source: + repoURL: https://github.com/argoproj/argocd-example-apps.git + targetRevision: HEAD + path: guestbook + destination: + server: https://kubernetes.default.svc + namespace: guestbook +--- +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: my-project + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + description: Example Project + # Allow manifests to deploy from any Git repos + sourceRepos: + - '*' + destinations: + - namespace: guestbook + server: https://kubernetes.default.svc + - namespace: guestbook-ui + server: https://kubernetes.default.svc + destinationServiceAccounts: + - namespace: guestbook + server: https://kubernetes.default.svc + defaultServiceAccount: guestbook-deployer + - namespace: guestbook-ui + server: https://kubernetes.default.svc + defaultServiceAccount: guestbook-ui-deployer +``` + +#### Example 3: Remote destination with cluster-admin access and using different service account for the sync operation + +**Note**: In this example, we are relying on the default service account `argocd-manager` with `cluster-admin` privileges which gets created when adding a remote cluster destination using the ArgoCD CLI. + +- Install ArgoCD in the `argocd` namespace. +``` +kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/install.yaml -n argocd +``` + +- Enable the impersonation feature in ArgoCD. +``` +kubectl set env statefulset/argocd-application-controller ARGOCD_APPLICATION_CONTROLLER_ENABLE_IMPERSONATION=true +``` + +- Add the remote cluster as a destination to argocd +``` +argocd cluster add remote-cluster --name remote-cluster +``` +**Note:** The above command would create a service account named `argocd-manager` in `kube-system` namespace and `ClusterRole` named `argocd-manager-role` with full cluster admin access and a `ClusterRoleBinding` named `argocd-manager-role-binding` mapping the `argocd-manager-role` to the service account `remote-cluster` + +- In the remote cluster, create a namespace called `guestbook` and a service account called `guestbook-deployer`. +``` +kubectl ctx remote-cluster +kubectl create namespace guestbook +kubectl create serviceaccount guestbook-deployer +``` + +- In the remote cluster, create `Role` and `RoleBindings` and configure RBAC access for creating `Service` and `Deployment` objects in namespace `guestbook` for service account `guestbook-deployer`. + +``` +kubectl ctx remote-cluster +kubectl create role guestbook-deployer-role --verb get,list,update,delete --resource pods,deployment,service +kubectl create rolebinding guestbook-deployer-rb --serviceaccount guestbook-deployer --role guestbook-deployer-role +``` + +- Create the `Application` and `AppProject` for the `guestbook` application. +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: guestbook + namespace: argocd +spec: + project: my-project + source: + repoURL: https://github.com/argoproj/argocd-example-apps.git + targetRevision: HEAD + path: guestbook + destination: + server: https://kubernetes.default.svc + namespace: guestbook +--- +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: my-project + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + description: Example Project + # Allow manifests to deploy from any Git repos + sourceRepos: + - '*' + destinations: + - namespace: guestbook + server: https://kubernetes.default.svc + serviceAccountName: guestbook-deployer + destinationServiceAccounts: + - namespace: guestbook + server: https://kubernetes.default.svc + defaultServiceAccount: guestbook-deployer +``` + +#### Example 4: Remote destination with a custom service account for the sync operation + +**Note**: In this example, we are relying on a non default service account `guestbook` created in the target cluster and namespace for the sync operation. This use case is for handling scenarios where the remote cluster is managed by a different administrator and providing a service account with `cluster-admin` level access is not feasible. + +- Install ArgoCD in the `argocd` namespace. +``` +kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/install.yaml -n argocd +``` + +- Enable the impersonation feature in ArgoCD. +``` +kubectl set env statefulset/argocd-application-controller ARGOCD_APPLICATION_CONTROLLER_ENABLE_IMPERSONATION=true +``` + +- In the remote cluster, create a service account called `argocd-admin` +``` +kubectl ctx remote-cluster +kubectl create serviceaccount argocd-admin +kubectl create clusterrole argocd-admin-role --verb=impersonate --resource="users,groups,serviceaccounts" +kubectl create clusterrole argocd-admin-role-access-review --verb=create --resource="selfsubjectaccessreviews" +kubectl create clusterrolebinding argocd-admin-role-binding --serviceaccount argocd-admin --clusterrole argocd-admin-role +kubectl create clusterrolebinding argocd-admin-access-review-role-binding --serviceaccount argocd-admin --clusterrole argocd-admin-role +``` + +- In the remote cluster, create a namespace called `guestbook` and a service account called `guestbook-deployer`. +``` +kubectl ctx remote-cluster +kubectl create namespace guestbook +kubectl create serviceaccount guestbook-deployer +``` + +- In the remote cluster, create `Role` and `RoleBindings` and configure RBAC access for creating `Service` and `Deployment` objects in namespace `guestbook` for service account `guestbook-deployer`. +``` +kubectl create role guestbook-deployer-role --verb get,list,update,delete --resource pods,deployment,service +kubectl create rolebinding guestbook-deployer-rb --serviceaccount guestbook-deployer --role guestbook-deployer-role +``` + +In this specific scenario, service account name `guestbook-deployer` will get used as it matches to the specific namespace `guestbook`. +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: guestbook + namespace: argocd +spec: + project: my-project + source: + repoURL: https://github.com/argoproj/argocd-example-apps.git + targetRevision: HEAD + path: guestbook + destination: + server: https://kubernetes.default.svc + namespace: guestbook +--- +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: my-project + namespace: argocd + finalizers: + - resources-finalizer.argocd.argoproj.io +spec: + description: Example Project + # Allow manifests to deploy from any Git repos + sourceRepos: + - '*' + destinations: + - namespace: guestbook + server: https://kubernetes.default.svc + - namespace: guestbook-ui + server: https://kubernetes.default.svc + destinationServiceAccounts: + - namespace: guestbook + server: https://kubernetes.default.svc + defaultServiceAccount: guestbook-deployer + - namespace: guestbook-ui + server: https://kubernetes.default.svc + defaultServiceAccount: guestbook-ui-deployer +``` + +### Special cases + +#### Specifying service account in a different namespace + +By default, the service account would be looked up in the Application's destination namespace configured through `Application.Spec.Destination.Namespace` field. If the service account is in a different namespace, then users can provide the namespace of the service account explicitly in the format : +eg: +``` + ... + destinationServiceAccounts: + - server: https://kubernetes.default.svc + namespace: * + defaultServiceAccount: mynamespace:guestbook-deployer + ... +``` + +#### Multiple matches of destinations + +If there are multiple matches for a given destination, the first valid match in the list of `destinationServiceAccounts` would be used. + +eg: +Lets assume that the `AppProject` has the below `destinationServiceAccounts` configured. +``` + ... + destinationServiceAccounts: + - server: https://kubernetes.default.svc + namespace: guestbook-prod + defaultServiceAccount: guestbook-prod-deployer + - server: https://kubernetes.default.svc + namespace: guestbook-* + defaultServiceAccount: guestbook-generic-deployer + - server: https://kubernetes.default.svc + namespace: * + defaultServiceAccount: generic-deployer + ... +``` +- If the application destination namespace is `myns`, then the service account `generic-deployer` would be used as the first valid match is the glob pattern `*` and there are no other valid matches in the list. +- If the application destination namespace is `guestbook-dev` or `guestbook-stage`, then both glob patterns `*` and `guestbook-*` are valid matches, however `guestbook-*` pattern appears first and hence, the service account `guestbook-generic-deployer` would be used for the impersonation. +- If the application destination namespace is `guestbook-prod`, then there are three candidates, however the first valid match in the list is the one with service account `guestbook-prod-deployer` and that would be used for the impersonation. + +#### Application resources referring to multiple namespaces +If application resources have hardcoded namespaces in the git repository, would different service accounts be used for each resource during the sync operation ? + +The service account to be used for impersonation is determined on a per Application level rather than on per resource level. The value specified in `Application.spec.destination.namespace` would be used to determine the service account to be used for the sync operation of all resources present in the `Application`. + +### Security Considerations + +* How does this proposal impact the security aspects of Argo CD workloads ? +* Are there any unresolved follow-ups that need to be done to make the enhancement more robust ? + +### Risks and Mitigations + +#### Privilege Escalation + +There could be an issue of privilege escalation, if we allow users to impersonate without restrictions. This is mitigated by only allowing admin users to configure service account used for the sync operation at the `AppProject` level. + +Instead of allowing users to impersonate all possible users, administrators can restrict the users a particular service account can impersonate using the `resourceNames` field in the RBAC spec. + + +### Upgrade / Downgrade Strategy + +If applicable, how will the component be upgraded and downgraded? Make sure this is in the test +plan. + +Consider the following in developing an upgrade/downgrade strategy for this enhancement: + +- What changes (in invocations, configurations, API use, etc.) is an existing cluster required to + make on upgrade in order to keep previous behavior? +- What changes (in invocations, configurations, API use, etc.) is an existing cluster required to + make on upgrade in order to make use of the enhancement? + +- This feature would be implemented on an `opt-in` based on a feature flag and disabled by default. +- The new struct being added to `AppProject.Spec` would be introduced as an optional field and would be enabled only if the feature is enabled explicitly by a feature flag. If new property is used in the CR, but the feature flag is not enabled, then a warning message would be displayed during reconciliation of such CRs. + + +## Drawbacks + +- When using this feature, there is an overhead in creating namespaces, service accounts and the required RBAC policies and mapping the service accounts with the corresponding `AppProject` configuration. + +## Alternatives + +### Option 1 +Allow all options available in the `ImpersonationConfig` available to the user through the `AppProject` CRs. + +``` +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: my-project + namespace: argocd +spec: + description: Example Project + # Allow manifests to deploy from any Git repos + sourceRepos: + - '*' + destinations: + - namespace: * + server: https://kubernetes.default.svc + namespace: guestbook + impersonate: + user: system:serviceaccount:dev_ns:admin + uid: 1234 + groups: + - admin + - view + - edit +``` + +### Related issue + +https://github.com/argoproj/argo-cd/issues/7689 + + +### Related links + +https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation + +### Prior art + +https://github.com/argoproj/argo-cd/pull/3377 +https://github.com/argoproj/argo-cd/pull/7651 \ No newline at end of file diff --git a/docs/proposals/native-ocp-support.md b/docs/proposals/native-oci-support.md similarity index 99% rename from docs/proposals/native-ocp-support.md rename to docs/proposals/native-oci-support.md index 64918fde8904e8..7ec0053729c2ee 100644 --- a/docs/proposals/native-ocp-support.md +++ b/docs/proposals/native-oci-support.md @@ -126,10 +126,10 @@ Consider the following in developing an upgrade/downgrade strategy for this enha ## Drawbacks -* Sourcing content from an OCI registry may be perceived to be against GitOps principles as content is not sourced from a Git repository. This concern could be mitigated by attaching additional details related to the content (such as original Git source [URL, revision]). Though it should be noted that the GitOps principles only require a source of truth to be visioned and immutable which OCI registires support. +* Sourcing content from an OCI registry may be perceived to be against GitOps principles as content is not sourced from a Git repository. This concern could be mitigated by attaching additional details related to the content (such as original Git source [URL, revision]). Though it should be noted that the GitOps principles only require a source of truth to be visioned and immutable which OCI registries support. ## Alternatives ### Config Management Plugin -Content stored within OCI artifacts could be sourced using a Config Management Plugin which would not require changes to the core capabilities provided by Argo CD. However, this would be hacky and not represent itself within the Argo CD UI. \ No newline at end of file +Content stored within OCI artifacts could be sourced using a Config Management Plugin which would not require changes to the core capabilities provided by Argo CD. However, this would be hacky and not represent itself within the Argo CD UI. diff --git a/docs/proposals/parameterized-config-management-plugins.md b/docs/proposals/parameterized-config-management-plugins.md index fa3061b2c36863..749f4efe636875 100644 --- a/docs/proposals/parameterized-config-management-plugins.md +++ b/docs/proposals/parameterized-config-management-plugins.md @@ -256,7 +256,7 @@ spec: array: [values.yaml] - name: helm-parameters map: - image.repository: my.company.com/gcr-proxy/heptio-images/ks-guestbook-demo + image.repository: my.example.com/gcr-proxy/heptio-images/ks-guestbook-demo image.tag: "0.1" ``` @@ -283,7 +283,7 @@ That command, when run by a CMP with the above Application manifest, will print { "name": "helm-parameters", "map": { - "image.repository": "my.company.com/gcr-proxy/heptio-images/ks-guestbook-demo", + "image.repository": "my.example.com/gcr-proxy/heptio-images/ks-guestbook-demo", "image.tag": "0.1" } } @@ -398,7 +398,7 @@ like this: "title": "Helm Parameters", "tooltip": "Parameters to override when generating manifests with Helm", "map": { - "image.repository": "my.company.com/gcr-proxy/heptio-images/ks-guestbook-demo", + "image.repository": "my.example.com/gcr-proxy/heptio-images/ks-guestbook-demo", "image.tag": "0.1" } } @@ -423,7 +423,7 @@ readability.) "title": "Helm Parameters", "tooltip": "Parameters to override when generating manifests with Helm", "map": { - "image.repository": "my.company.com/gcr-proxy/heptio-images/ks-guestbook-demo", + "image.repository": "my.example.com/gcr-proxy/heptio-images/ks-guestbook-demo", "image.tag": "0.1" } } @@ -493,11 +493,11 @@ type ParametersAnnouncement []ParameterAnnouncement - name: images collectionType: map array: # this gets ignored because collectionType is 'map' - - ubuntu:latest=docker.company.com/proxy/ubuntu:latest - - guestbook:v0.1=docker.company.com/proxy/guestbook:v0.1 + - ubuntu:latest=docker.example.com/proxy/ubuntu:latest + - guestbook:v0.1=docker.example.com/proxy/guestbook:v0.1 map: - ubuntu:latest: docker.company.com/proxy/ubuntu:latest - guestbook:v0.1: docker.company.com/proxy/guestbook:v0.1 + ubuntu:latest: docker.example.com/proxy/ubuntu:latest + guestbook:v0.1: docker.example.com/proxy/guestbook:v0.1 ``` 2. **Question**: What do we do if the CMP user sets more than one of `value`/`array`/`map` in the Application spec? @@ -513,11 +513,11 @@ type ParametersAnnouncement []ParameterAnnouncement parameters: - name: images array: # this gets sent to the CMP, but the CMP should ignore it - - ubuntu:latest=docker.company.com/proxy/ubuntu:latest - - guestbook:v0.1=docker.company.com/proxy/guestbook:v0.1 + - ubuntu:latest=docker.example.com/proxy/ubuntu:latest + - guestbook:v0.1=docker.example.com/proxy/guestbook:v0.1 map: - ubuntu:latest: docker.company.com/proxy/ubuntu:latest - guestbook:v0.1: docker.company.com/proxy/guestbook:v0.1 + ubuntu:latest: docker.example.com/proxy/ubuntu:latest + guestbook:v0.1: docker.example.com/proxy/guestbook:v0.1 ``` 3. **Question**: How will the UI know that adding more items to an array or a map is allowed? @@ -528,17 +528,17 @@ type ParametersAnnouncement []ParameterAnnouncement - name: images collectionType: map # users will be allowed to add new items, because this is a map map: - ubuntu:latest: docker.company.com/proxy/ubuntu:latest - guestbook:v0.1: docker.company.com/proxy/guestbook:v0.1 + ubuntu:latest: docker.example.com/proxy/ubuntu:latest + guestbook:v0.1: docker.example.com/proxy/guestbook:v0.1 ``` If the CMP author wants an immutable array or map, they should just break it into individual parameters. ```yaml - name: ubuntu:latest - string: docker.company.com/proxy/ubuntu:latest + string: docker.example.com/proxy/ubuntu:latest - name: guestbook:v0.1 - string: docker.company.com/proxy/guestbook:v0.1 + string: docker.example.com/proxy/guestbook:v0.1 ``` 4. **Question**: What do we do if a CMP announcement doesn't include a `collectionType`? @@ -799,8 +799,8 @@ spec: "title": "Image Overrides", "collectionType": "map", "map": { - "quay.io/argoproj/argocd": "docker.company.com/proxy/argoproj/argocd", - "ubuntu:latest": "docker.company.com/proxy/argoproj/argocd" + "quay.io/argoproj/argocd": "docker.example.com/proxy/argoproj/argocd", + "ubuntu:latest": "docker.example.com/proxy/argoproj/argocd" } } ] diff --git a/docs/proposals/project-repos-and-clusters.md b/docs/proposals/project-repos-and-clusters.md index 1f8258f47a72ba..514c3890482188 100644 --- a/docs/proposals/project-repos-and-clusters.md +++ b/docs/proposals/project-repos-and-clusters.md @@ -102,7 +102,7 @@ p, proj:my-project:admin, repositories, update, my-project/*, allow This provides extra flexibility so that admin can have stricter rules. e.g.: ``` -p, proj:my-project:admin, repositories, update, my-project/"https://github.my-company.com/*", allow +p, proj:my-project:admin, repositories, update, my-project/"https://github.example.com/*", allow ``` #### UI/CLI Changes diff --git a/docs/requirements.txt b/docs/requirements.txt index 5ffcd4ff0221b7..d350ac4870ee22 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,6 @@ mkdocs==1.3.0 +# Strict mode has been disabled in latest versions of mkdocs-material. +# Thus pointing to the older version of mkdocs-material. mkdocs-material==7.1.8 markdown_include==0.6.0 pygments==2.15.0 diff --git a/docs/user-guide/ci_automation.md b/docs/user-guide/ci_automation.md index 14d35dc3cb2cd3..433483eba7a3f2 100644 --- a/docs/user-guide/ci_automation.md +++ b/docs/user-guide/ci_automation.md @@ -43,7 +43,7 @@ useful so that the CLI used in the CI pipeline is always kept in-sync and uses a that is always compatible with the Argo CD API server. ```bash -export ARGOCD_SERVER=argocd.mycompany.com +export ARGOCD_SERVER=argocd.example.com export ARGOCD_AUTH_TOKEN= curl -sSL -o /usr/local/bin/argocd https://${ARGOCD_SERVER}/download/argocd-linux-amd64 argocd app sync guestbook diff --git a/docs/user-guide/commands/argocd_admin_app_generate-spec.md b/docs/user-guide/commands/argocd_admin_app_generate-spec.md index 78213de5c170c7..af171470f4343e 100644 --- a/docs/user-guide/commands/argocd_admin_app_generate-spec.md +++ b/docs/user-guide/commands/argocd_admin_app_generate-spec.md @@ -67,6 +67,7 @@ argocd admin app generate-spec APPNAME [flags] --kustomize-force-common-annotation Force common annotations in Kustomize --kustomize-force-common-label Force common labels in Kustomize --kustomize-image stringArray Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d) + --kustomize-label-without-selector Do not apply common label to selectors or templates --kustomize-namespace string Kustomize namespace --kustomize-replica stringArray Kustomize replicas (e.g. --kustomize-replica my-development=2 --kustomize-replica my-statefulset=4) --kustomize-version string Kustomize version @@ -79,6 +80,7 @@ argocd admin app generate-spec APPNAME [flags] --path string Path in repository to the app directory, ignored if a file is set --plugin-env stringArray Additional plugin envs --project string Application project name + --ref string Ref is reference to another source within sources field --release-name string Helm release-name --repo string Repository URL, ignored if a file is set --revision string The tracking source branch, tag, commit or Helm chart version the application will sync to diff --git a/docs/user-guide/commands/argocd_admin_app_get-reconcile-results.md b/docs/user-guide/commands/argocd_admin_app_get-reconcile-results.md index 3290098999b7c7..29fa5d54d93883 100644 --- a/docs/user-guide/commands/argocd_admin_app_get-reconcile-results.md +++ b/docs/user-guide/commands/argocd_admin_app_get-reconcile-results.md @@ -32,6 +32,7 @@ argocd admin app get-reconcile-results PATH [flags] --repo-server string Repo server address. --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") --server string The address and port of the Kubernetes API server + --server-side-diff If set to "true" will use server-side diff while comparing resources. Default ("false") --tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used. --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use diff --git a/docs/user-guide/commands/argocd_admin_cluster.md b/docs/user-guide/commands/argocd_admin_cluster.md index 1a469c3f818ca9..544c0de08959c9 100644 --- a/docs/user-guide/commands/argocd_admin_cluster.md +++ b/docs/user-guide/commands/argocd_admin_cluster.md @@ -8,6 +8,20 @@ Manage clusters configuration argocd admin cluster [flags] ``` +### Examples + +``` + +#Generate declarative config for a cluster +argocd admin cluster generate-spec my-cluster -o yaml + +#Generate a kubeconfig for a cluster named "my-cluster" and display it in the console +argocd admin cluster kubeconfig my-cluster + +#Print information namespaces which Argo CD manages in each cluster +argocd admin cluster namespaces my-cluster +``` + ### Options ``` @@ -48,6 +62,6 @@ argocd admin cluster [flags] * [argocd admin cluster generate-spec](argocd_admin_cluster_generate-spec.md) - Generate declarative config for a cluster * [argocd admin cluster kubeconfig](argocd_admin_cluster_kubeconfig.md) - Generates kubeconfig for the specified cluster * [argocd admin cluster namespaces](argocd_admin_cluster_namespaces.md) - Print information namespaces which Argo CD manages in each cluster. -* [argocd admin cluster shards](argocd_admin_cluster_shards.md) - Print information about each controller shard and portion of Kubernetes resources it is responsible for. +* [argocd admin cluster shards](argocd_admin_cluster_shards.md) - Print information about each controller shard and the estimated portion of Kubernetes resources it is responsible for. * [argocd admin cluster stats](argocd_admin_cluster_stats.md) - Prints information cluster statistics and inferred shard number diff --git a/docs/user-guide/commands/argocd_admin_cluster_generate-spec.md b/docs/user-guide/commands/argocd_admin_cluster_generate-spec.md index cc24418b023f84..79f88233fab323 100644 --- a/docs/user-guide/commands/argocd_admin_cluster_generate-spec.md +++ b/docs/user-guide/commands/argocd_admin_cluster_generate-spec.md @@ -13,6 +13,7 @@ argocd admin cluster generate-spec CONTEXT [flags] ``` --annotation stringArray Set metadata annotations (e.g. --annotation key=value) --aws-cluster-name string AWS Cluster name if set then aws cli eks token command will be used to access cluster + --aws-profile string Optional AWS profile. If set then AWS IAM Authenticator uses this profile to perform cluster operations instead of the default AWS credential provider chain. --aws-role-arn string Optional AWS role arn. If set then AWS IAM Authenticator assumes a role to perform cluster operations instead of the default AWS credential provider chain. --bearer-token string Authentication token that should be used to access K8S API server --cluster-endpoint string Cluster endpoint to use. Can be one of the following: 'kubeconfig', 'kube-public', or 'internal'. diff --git a/docs/user-guide/commands/argocd_admin_cluster_kubeconfig.md b/docs/user-guide/commands/argocd_admin_cluster_kubeconfig.md index 8105605e80cd03..38f61ce5cd8a23 100644 --- a/docs/user-guide/commands/argocd_admin_cluster_kubeconfig.md +++ b/docs/user-guide/commands/argocd_admin_cluster_kubeconfig.md @@ -8,6 +8,23 @@ Generates kubeconfig for the specified cluster argocd admin cluster kubeconfig CLUSTER_URL OUTPUT_PATH [flags] ``` +### Examples + +``` + +#Generate a kubeconfig for a cluster named "my-cluster" on console +argocd admin cluster kubeconfig my-cluster + +#Listing available kubeconfigs for clusters managed by argocd +argocd admin cluster kubeconfig + +#Removing a specific kubeconfig file +argocd admin cluster kubeconfig my-cluster --delete + +#Generate a Kubeconfig for a Cluster with TLS Verification Disabled +argocd admin cluster kubeconfig https://cluster-api-url:6443 /path/to/output/kubeconfig.yaml --insecure-skip-tls-verify +``` + ### Options ``` diff --git a/docs/user-guide/commands/argocd_admin_cluster_shards.md b/docs/user-guide/commands/argocd_admin_cluster_shards.md index 6648b91b2199e0..48f6138d47b4a4 100644 --- a/docs/user-guide/commands/argocd_admin_cluster_shards.md +++ b/docs/user-guide/commands/argocd_admin_cluster_shards.md @@ -2,7 +2,7 @@ ## argocd admin cluster shards -Print information about each controller shard and portion of Kubernetes resources it is responsible for. +Print information about each controller shard and the estimated portion of Kubernetes resources it is responsible for. ``` argocd admin cluster shards [flags] @@ -43,6 +43,7 @@ argocd admin cluster shards [flags] --sentinelmaster string Redis sentinel master group name. (default "master") --server string The address and port of the Kubernetes API server --shard int Cluster shard filter (default -1) + --sharding-method string Sharding method. Defaults: legacy. Supported sharding methods are : [legacy, round-robin] (default "legacy") --tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used. --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use diff --git a/docs/user-guide/commands/argocd_admin_cluster_stats.md b/docs/user-guide/commands/argocd_admin_cluster_stats.md index 9e916288adf7e0..c5297ce7e35ed8 100644 --- a/docs/user-guide/commands/argocd_admin_cluster_stats.md +++ b/docs/user-guide/commands/argocd_admin_cluster_stats.md @@ -8,6 +8,20 @@ Prints information cluster statistics and inferred shard number argocd admin cluster stats [flags] ``` +### Examples + +``` + +#Display stats and shards for clusters +argocd admin cluster stats + +#Display Cluster Statistics for a Specific Shard +argocd admin cluster stats --shard=1 + +#In a multi-cluster environment to print stats for a specific cluster say(target-cluster) +argocd admin cluster stats target-cluster +``` + ### Options ``` @@ -43,6 +57,7 @@ argocd admin cluster stats [flags] --sentinelmaster string Redis sentinel master group name. (default "master") --server string The address and port of the Kubernetes API server --shard int Cluster shard filter (default -1) + --sharding-method string Sharding method. Defaults: legacy. Supported sharding methods are : [legacy, round-robin] (default "legacy") --tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used. --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use diff --git a/docs/user-guide/commands/argocd_admin_dashboard.md b/docs/user-guide/commands/argocd_admin_dashboard.md index be4d5c18468ab0..71e11a173906a8 100644 --- a/docs/user-guide/commands/argocd_admin_dashboard.md +++ b/docs/user-guide/commands/argocd_admin_dashboard.md @@ -44,6 +44,7 @@ $ argocd admin dashboard --redis-compress gzip --proxy-url string If provided, this URL will be used to connect via proxy --redis-compress string Enable this if the application controller is configured with redis compression enabled. (possible values: gzip, none) (default "gzip") --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + --server string The address and port of the Kubernetes API server --tls-server-name string If provided, this name will be used to validate server certificate. If this is not provided, hostname used to contact the server is used. --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use @@ -73,7 +74,6 @@ $ argocd admin dashboard --redis-compress gzip --redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy") --redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis") --repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server") - --server string Argo CD server address --server-crt string Server certificate file --server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server") ``` diff --git a/docs/user-guide/commands/argocd_app.md b/docs/user-guide/commands/argocd_app.md index 543fcd96035ecd..ff8fe0d4a01b6b 100644 --- a/docs/user-guide/commands/argocd_app.md +++ b/docs/user-guide/commands/argocd_app.md @@ -78,6 +78,7 @@ argocd app [flags] * [argocd](argocd.md) - argocd controls a Argo CD server * [argocd app actions](argocd_app_actions.md) - Manage Resource actions +* [argocd app add-source](argocd_app_add-source.md) - Adds a source to the list of sources in the application * [argocd app create](argocd_app_create.md) - Create an application * [argocd app delete](argocd_app_delete.md) - Delete an application * [argocd app delete-resource](argocd_app_delete-resource.md) - Delete resource in an application @@ -90,6 +91,7 @@ argocd app [flags] * [argocd app manifests](argocd_app_manifests.md) - Print manifests of an application * [argocd app patch](argocd_app_patch.md) - Patch application * [argocd app patch-resource](argocd_app_patch-resource.md) - Patch resource in an application +* [argocd app remove-source](argocd_app_remove-source.md) - Remove a source from multiple sources application. Index starts with 0. * [argocd app resources](argocd_app_resources.md) - List resource of application * [argocd app rollback](argocd_app_rollback.md) - Rollback application to a previous deployed version by History ID, omitted will Rollback to the previous version * [argocd app set](argocd_app_set.md) - Set application parameters diff --git a/docs/user-guide/commands/argocd_app_add-source.md b/docs/user-guide/commands/argocd_app_add-source.md new file mode 100644 index 00000000000000..9ce5ce5a941c79 --- /dev/null +++ b/docs/user-guide/commands/argocd_app_add-source.md @@ -0,0 +1,108 @@ +# `argocd app add-source` Command Reference + +## argocd app add-source + +Adds a source to the list of sources in the application + +``` +argocd app add-source APPNAME [flags] +``` + +### Examples + +``` + # Append a source to the list of sources in the application + argocd app add-source guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook +``` + +### Options + +``` + --allow-empty Set allow zero live resources when sync is automated + --auto-prune Set automatic pruning when sync is automated + --config-management-plugin string Config management plugin name + --dest-name string K8s cluster Name (e.g. minikube) + --dest-namespace string K8s target namespace + --dest-server string K8s cluster URL (e.g. https://kubernetes.default.svc) + --directory-exclude string Set glob expression used to exclude files from application source path + --directory-include string Set glob expression used to include files from application source path + --directory-recurse Recurse directory + --env string Application environment to monitor + --helm-chart string Helm Chart name + --helm-pass-credentials Pass credentials to all domain + --helm-set stringArray Helm set values on the command line (can be repeated to set several values: --helm-set key1=val1 --helm-set key2=val2) + --helm-set-file stringArray Helm set values from respective files specified via the command line (can be repeated to set several values: --helm-set-file key1=path1 --helm-set-file key2=path2) + --helm-set-string stringArray Helm set STRING values on the command line (can be repeated to set several values: --helm-set-string key1=val1 --helm-set-string key2=val2) + --helm-skip-crds Skip helm crd installation step + --helm-version string Helm version + -h, --help help for add-source + --ignore-missing-value-files Ignore locally missing valueFiles when setting helm template --values + --jsonnet-ext-var-code stringArray Jsonnet ext var + --jsonnet-ext-var-str stringArray Jsonnet string ext var + --jsonnet-libs stringArray Additional jsonnet libs (prefixed by repoRoot) + --jsonnet-tla-code stringArray Jsonnet top level code arguments + --jsonnet-tla-str stringArray Jsonnet top level string arguments + --kustomize-common-annotation stringArray Set common labels in Kustomize + --kustomize-common-label stringArray Set common labels in Kustomize + --kustomize-force-common-annotation Force common annotations in Kustomize + --kustomize-force-common-label Force common labels in Kustomize + --kustomize-image stringArray Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d) + --kustomize-label-without-selector Do not apply common label to selectors or templates + --kustomize-namespace string Kustomize namespace + --kustomize-replica stringArray Kustomize replicas (e.g. --kustomize-replica my-development=2 --kustomize-replica my-statefulset=4) + --kustomize-version string Kustomize version + --nameprefix string Kustomize nameprefix + --namesuffix string Kustomize namesuffix + -p, --parameter stringArray set a parameter override (e.g. -p guestbook=image=example/guestbook:latest) + --path string Path in repository to the app directory, ignored if a file is set + --plugin-env stringArray Additional plugin envs + --project string Application project name + --ref string Ref is reference to another source within sources field + --release-name string Helm release-name + --repo string Repository URL, ignored if a file is set + --revision string The tracking source branch, tag, commit or Helm chart version the application will sync to + --revision-history-limit int How many items to keep in revision history (default 10) + --self-heal Set self healing when sync is automated + --sync-option Prune=false Add or remove a sync option, e.g add Prune=false. Remove using `!` prefix, e.g. `!Prune=false` + --sync-policy string Set the sync policy (one of: none, automated (aliases of automated: auto, automatic)) + --sync-retry-backoff-duration duration Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h) (default 5s) + --sync-retry-backoff-factor int Factor multiplies the base duration after each failed sync retry (default 2) + --sync-retry-backoff-max-duration duration Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h) (default 3m0s) + --sync-retry-limit int Max number of allowed sync retries + --validate Validation of repo and cluster (default true) + --values stringArray Helm values file(s) to use + --values-literal-file string Filename or URL to import as a literal Helm values block +``` + +### Options inherited from parent commands + +``` + --auth-token string Authentication token + --client-crt string Client certificate file + --client-crt-key string Client certificate key file + --config string Path to Argo CD config (default "/home/user/.config/argocd/config") + --controller-name string Name of the Argo CD Application controller; set this or the ARGOCD_APPLICATION_CONTROLLER_NAME environment variable when the controller's name label differs from the default, for example when installing via the Helm chart (default "argocd-application-controller") + --core If set to true then CLI talks directly to Kubernetes instead of talking to Argo CD API server + --grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. + --grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root. + -H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers) + --http-retry-max int Maximum number of retries to establish http connection to Argo CD server + --insecure Skip server certificate and domain verification + --kube-context string Directs the command to the given kube-context + --logformat string Set the logging format. One of: text|json (default "text") + --loglevel string Set the logging level. One of: debug|info|warn|error (default "info") + --plaintext Disable TLS + --port-forward Connect to a random argocd-server port using port forwarding + --port-forward-namespace string Namespace name which should be used for port forwarding + --redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy") + --redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis") + --repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server") + --server string Argo CD server address + --server-crt string Server certificate file + --server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server") +``` + +### SEE ALSO + +* [argocd app](argocd_app.md) - Manage applications + diff --git a/docs/user-guide/commands/argocd_app_create.md b/docs/user-guide/commands/argocd_app_create.md index 41a671f3efdcd3..0171f257c671c2 100644 --- a/docs/user-guide/commands/argocd_app_create.md +++ b/docs/user-guide/commands/argocd_app_create.md @@ -65,6 +65,7 @@ argocd app create APPNAME [flags] --kustomize-force-common-annotation Force common annotations in Kustomize --kustomize-force-common-label Force common labels in Kustomize --kustomize-image stringArray Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d) + --kustomize-label-without-selector Do not apply common label to selectors or templates --kustomize-namespace string Kustomize namespace --kustomize-replica stringArray Kustomize replicas (e.g. --kustomize-replica my-development=2 --kustomize-replica my-statefulset=4) --kustomize-version string Kustomize version @@ -76,6 +77,7 @@ argocd app create APPNAME [flags] --path string Path in repository to the app directory, ignored if a file is set --plugin-env stringArray Additional plugin envs --project string Application project name + --ref string Ref is reference to another source within sources field --release-name string Helm release-name --repo string Repository URL, ignored if a file is set --revision string The tracking source branch, tag, commit or Helm chart version the application will sync to diff --git a/docs/user-guide/commands/argocd_app_delete.md b/docs/user-guide/commands/argocd_app_delete.md index f4ff666a4b919f..827eeaab4ce7a7 100644 --- a/docs/user-guide/commands/argocd_app_delete.md +++ b/docs/user-guide/commands/argocd_app_delete.md @@ -28,10 +28,12 @@ argocd app delete APPNAME [flags] ### Options ``` + -N, --app-namespace string Namespace where the application will be deleted from --cascade Perform a cascaded deletion of all application resources (default true) -h, --help help for delete -p, --propagation-policy string Specify propagation policy for deletion of application's resources. One of: foreground|background (default "foreground") -l, --selector string Delete all apps with matching label. Supports '=', '==', '!=', in, notin, exists & not exists. Matching apps must satisfy all of the specified label constraints. + --wait Wait until deletion of the application(s) completes -y, --yes Turn off prompting to confirm cascaded deletion of application resources ``` diff --git a/docs/user-guide/commands/argocd_app_diff.md b/docs/user-guide/commands/argocd_app_diff.md index 139584d4fead58..18cc8f4751324c 100644 --- a/docs/user-guide/commands/argocd_app_diff.md +++ b/docs/user-guide/commands/argocd_app_diff.md @@ -17,6 +17,7 @@ argocd app diff APPNAME [flags] ### Options ``` + -N, --app-namespace string Only render the difference in namespace --exit-code Return non-zero exit code when there is a diff (default true) --hard-refresh Refresh application data as well as target manifests cache -h, --help help for diff diff --git a/docs/user-guide/commands/argocd_app_edit.md b/docs/user-guide/commands/argocd_app_edit.md index 204e96cb76c0fd..e581677b79c128 100644 --- a/docs/user-guide/commands/argocd_app_edit.md +++ b/docs/user-guide/commands/argocd_app_edit.md @@ -11,7 +11,8 @@ argocd app edit APPNAME [flags] ### Options ``` - -h, --help help for edit + -N, --app-namespace string Only edit application in namespace + -h, --help help for edit ``` ### Options inherited from parent commands diff --git a/docs/user-guide/commands/argocd_app_get.md b/docs/user-guide/commands/argocd_app_get.md index cf766ed9eb0d79..d0bf744054c38b 100644 --- a/docs/user-guide/commands/argocd_app_get.md +++ b/docs/user-guide/commands/argocd_app_get.md @@ -42,12 +42,13 @@ argocd app get APPNAME [flags] ### Options ``` - --hard-refresh Refresh application data as well as target manifests cache - -h, --help help for get - -o, --output string Output format. One of: json|yaml|wide|tree (default "wide") - --refresh Refresh application data when retrieving - --show-operation Show application operation - --show-params Show application parameters and overrides + -N, --app-namespace string Only get application from namespace + --hard-refresh Refresh application data as well as target manifests cache + -h, --help help for get + -o, --output string Output format. One of: json|yaml|wide|tree (default "wide") + --refresh Refresh application data when retrieving + --show-operation Show application operation + --show-params Show application parameters and overrides ``` ### Options inherited from parent commands diff --git a/docs/user-guide/commands/argocd_app_history.md b/docs/user-guide/commands/argocd_app_history.md index 253a1dec64dd57..eefadef01f417e 100644 --- a/docs/user-guide/commands/argocd_app_history.md +++ b/docs/user-guide/commands/argocd_app_history.md @@ -11,8 +11,9 @@ argocd app history APPNAME [flags] ### Options ``` - -h, --help help for history - -o, --output string Output format. One of: wide|id (default "wide") + -N, --app-namespace string Only show application deployment history in namespace + -h, --help help for history + -o, --output string Output format. One of: wide|id (default "wide") ``` ### Options inherited from parent commands diff --git a/docs/user-guide/commands/argocd_app_patch.md b/docs/user-guide/commands/argocd_app_patch.md index 01147f022c755f..0c453ea159e649 100644 --- a/docs/user-guide/commands/argocd_app_patch.md +++ b/docs/user-guide/commands/argocd_app_patch.md @@ -21,9 +21,10 @@ argocd app patch APPNAME [flags] ### Options ``` - -h, --help help for patch - --patch string Patch body - --type string The type of patch being provided; one of [json merge] (default "json") + -N, --app-namespace string Only patch application in namespace + -h, --help help for patch + --patch string Patch body + --type string The type of patch being provided; one of [json merge] (default "json") ``` ### Options inherited from parent commands diff --git a/docs/user-guide/commands/argocd_app_remove-source.md b/docs/user-guide/commands/argocd_app_remove-source.md new file mode 100644 index 00000000000000..a334cbd37b5b16 --- /dev/null +++ b/docs/user-guide/commands/argocd_app_remove-source.md @@ -0,0 +1,56 @@ +# `argocd app remove-source` Command Reference + +## argocd app remove-source + +Remove a source from multiple sources application. Index starts with 0. + +``` +argocd app remove-source APPNAME [flags] +``` + +### Examples + +``` + # Remove the source at index 1 from application's sources + argocd app remove-source myapplication --source-index 1 +``` + +### Options + +``` + -h, --help help for remove-source + --source-index int Index of the source from the list of sources of the app. Index starts from 0. (default -1) +``` + +### Options inherited from parent commands + +``` + --auth-token string Authentication token + --client-crt string Client certificate file + --client-crt-key string Client certificate key file + --config string Path to Argo CD config (default "/home/user/.config/argocd/config") + --controller-name string Name of the Argo CD Application controller; set this or the ARGOCD_APPLICATION_CONTROLLER_NAME environment variable when the controller's name label differs from the default, for example when installing via the Helm chart (default "argocd-application-controller") + --core If set to true then CLI talks directly to Kubernetes instead of talking to Argo CD API server + --grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. + --grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root. + -H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers) + --http-retry-max int Maximum number of retries to establish http connection to Argo CD server + --insecure Skip server certificate and domain verification + --kube-context string Directs the command to the given kube-context + --logformat string Set the logging format. One of: text|json (default "text") + --loglevel string Set the logging level. One of: debug|info|warn|error (default "info") + --plaintext Disable TLS + --port-forward Connect to a random argocd-server port using port forwarding + --port-forward-namespace string Namespace name which should be used for port forwarding + --redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy") + --redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis") + --repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server") + --server string Argo CD server address + --server-crt string Server certificate file + --server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server") +``` + +### SEE ALSO + +* [argocd app](argocd_app.md) - Manage applications + diff --git a/docs/user-guide/commands/argocd_app_rollback.md b/docs/user-guide/commands/argocd_app_rollback.md index bfcbf89631854d..923023e35a2e86 100644 --- a/docs/user-guide/commands/argocd_app_rollback.md +++ b/docs/user-guide/commands/argocd_app_rollback.md @@ -11,10 +11,11 @@ argocd app rollback APPNAME [ID] [flags] ### Options ``` - -h, --help help for rollback - -o, --output string Output format. One of: json|yaml|wide|tree|tree=detailed (default "wide") - --prune Allow deleting unexpected resources - --timeout uint Time out after this many seconds + -N, --app-namespace string Rollback application in namespace + -h, --help help for rollback + -o, --output string Output format. One of: json|yaml|wide|tree|tree=detailed (default "wide") + --prune Allow deleting unexpected resources + --timeout uint Time out after this many seconds ``` ### Options inherited from parent commands diff --git a/docs/user-guide/commands/argocd_app_set.md b/docs/user-guide/commands/argocd_app_set.md index 18096e16f256a7..75a50a77f33799 100644 --- a/docs/user-guide/commands/argocd_app_set.md +++ b/docs/user-guide/commands/argocd_app_set.md @@ -59,6 +59,7 @@ argocd app set APPNAME [flags] --kustomize-force-common-annotation Force common annotations in Kustomize --kustomize-force-common-label Force common labels in Kustomize --kustomize-image stringArray Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d) + --kustomize-label-without-selector Do not apply common label to selectors or templates --kustomize-namespace string Kustomize namespace --kustomize-replica stringArray Kustomize replicas (e.g. --kustomize-replica my-development=2 --kustomize-replica my-statefulset=4) --kustomize-version string Kustomize version @@ -68,6 +69,7 @@ argocd app set APPNAME [flags] --path string Path in repository to the app directory, ignored if a file is set --plugin-env stringArray Additional plugin envs --project string Application project name + --ref string Ref is reference to another source within sources field --release-name string Helm release-name --repo string Repository URL, ignored if a file is set --revision string The tracking source branch, tag, commit or Helm chart version the application will sync to diff --git a/docs/user-guide/commands/argocd_app_sync.md b/docs/user-guide/commands/argocd_app_sync.md index 81ce3fd024c5ca..a0a8f8459eeaa1 100644 --- a/docs/user-guide/commands/argocd_app_sync.md +++ b/docs/user-guide/commands/argocd_app_sync.md @@ -38,6 +38,7 @@ argocd app sync [APPNAME... | -l selector | --project project-name] [flags] ### Options ``` + -N, --app-namespace string Only sync an application in namespace --apply-out-of-sync-only Sync only out-of-sync resources --assumeYes Assume yes as answer for all user queries or prompts --async Do not wait for application to sync before continuing diff --git a/docs/user-guide/commands/argocd_app_wait.md b/docs/user-guide/commands/argocd_app_wait.md index 99e422167b76f6..e2d3886f4d3abc 100644 --- a/docs/user-guide/commands/argocd_app_wait.md +++ b/docs/user-guide/commands/argocd_app_wait.md @@ -38,7 +38,9 @@ argocd app wait [APPNAME.. | -l selector] [flags] ### Options ``` + -N, --app-namespace string Only wait for an application in namespace --degraded Wait for degraded + --delete Wait for delete --health Wait for health -h, --help help for wait --operation Wait for pending operations diff --git a/docs/user-guide/commands/argocd_cluster_add.md b/docs/user-guide/commands/argocd_cluster_add.md index 6d3a094b4bf839..8a80a12f5a4d53 100644 --- a/docs/user-guide/commands/argocd_cluster_add.md +++ b/docs/user-guide/commands/argocd_cluster_add.md @@ -13,6 +13,7 @@ argocd cluster add CONTEXT [flags] ``` --annotation stringArray Set metadata annotations (e.g. --annotation key=value) --aws-cluster-name string AWS Cluster name if set then aws cli eks token command will be used to access cluster + --aws-profile string Optional AWS profile. If set then AWS IAM Authenticator uses this profile to perform cluster operations instead of the default AWS credential provider chain. --aws-role-arn string Optional AWS role arn. If set then AWS IAM Authenticator assumes a role to perform cluster operations instead of the default AWS credential provider chain. --cluster-endpoint string Cluster endpoint to use. Can be one of the following: 'kubeconfig', 'kube-public', or 'internal'. --cluster-resources Indicates if cluster level resources should be managed. The setting is used only if list of managed namespaces is not empty. diff --git a/docs/user-guide/commands/argocd_proj.md b/docs/user-guide/commands/argocd_proj.md index 17aeef0cdfc277..5586463adee6e4 100644 --- a/docs/user-guide/commands/argocd_proj.md +++ b/docs/user-guide/commands/argocd_proj.md @@ -84,6 +84,7 @@ argocd proj [flags] * [argocd proj add-orphaned-ignore](argocd_proj_add-orphaned-ignore.md) - Add a resource to orphaned ignore list * [argocd proj add-signature-key](argocd_proj_add-signature-key.md) - Add GnuPG signature key to project * [argocd proj add-source](argocd_proj_add-source.md) - Add project source repository +* [argocd proj add-source-namespace](argocd_proj_add-source-namespace.md) - Add source namespace to the AppProject * [argocd proj allow-cluster-resource](argocd_proj_allow-cluster-resource.md) - Adds a cluster-scoped API resource to the allow list and removes it from deny list * [argocd proj allow-namespace-resource](argocd_proj_allow-namespace-resource.md) - Removes a namespaced API resource from the deny list or add a namespaced API resource to the allow list * [argocd proj create](argocd_proj_create.md) - Create a project @@ -97,6 +98,7 @@ argocd proj [flags] * [argocd proj remove-orphaned-ignore](argocd_proj_remove-orphaned-ignore.md) - Remove a resource from orphaned ignore list * [argocd proj remove-signature-key](argocd_proj_remove-signature-key.md) - Remove GnuPG signature key from project * [argocd proj remove-source](argocd_proj_remove-source.md) - Remove project source repository +* [argocd proj remove-source-namespace](argocd_proj_remove-source-namespace.md) - Removes the source namespace from the AppProject * [argocd proj role](argocd_proj_role.md) - Manage a project's roles * [argocd proj set](argocd_proj_set.md) - Set project parameters * [argocd proj windows](argocd_proj_windows.md) - Manage a project's sync windows diff --git a/docs/user-guide/commands/argocd_proj_add-source-namespace.md b/docs/user-guide/commands/argocd_proj_add-source-namespace.md new file mode 100644 index 00000000000000..ced1f6fa3c67d6 --- /dev/null +++ b/docs/user-guide/commands/argocd_proj_add-source-namespace.md @@ -0,0 +1,55 @@ +# `argocd proj add-source-namespace` Command Reference + +## argocd proj add-source-namespace + +Add source namespace to the AppProject + +``` +argocd proj add-source-namespace PROJECT NAMESPACE [flags] +``` + +### Examples + +``` + # Add Kubernetes namespace as source namespace to the AppProject where application resources are allowed to be created in. + argocd proj add-source-namespace PROJECT NAMESPACE +``` + +### Options + +``` + -h, --help help for add-source-namespace +``` + +### Options inherited from parent commands + +``` + --auth-token string Authentication token + --client-crt string Client certificate file + --client-crt-key string Client certificate key file + --config string Path to Argo CD config (default "/home/user/.config/argocd/config") + --controller-name string Name of the Argo CD Application controller; set this or the ARGOCD_APPLICATION_CONTROLLER_NAME environment variable when the controller's name label differs from the default, for example when installing via the Helm chart (default "argocd-application-controller") + --core If set to true then CLI talks directly to Kubernetes instead of talking to Argo CD API server + --grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. + --grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root. + -H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers) + --http-retry-max int Maximum number of retries to establish http connection to Argo CD server + --insecure Skip server certificate and domain verification + --kube-context string Directs the command to the given kube-context + --logformat string Set the logging format. One of: text|json (default "text") + --loglevel string Set the logging level. One of: debug|info|warn|error (default "info") + --plaintext Disable TLS + --port-forward Connect to a random argocd-server port using port forwarding + --port-forward-namespace string Namespace name which should be used for port forwarding + --redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy") + --redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis") + --repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server") + --server string Argo CD server address + --server-crt string Server certificate file + --server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server") +``` + +### SEE ALSO + +* [argocd proj](argocd_proj.md) - Manage projects + diff --git a/docs/user-guide/commands/argocd_proj_remove-source-namespace.md b/docs/user-guide/commands/argocd_proj_remove-source-namespace.md new file mode 100644 index 00000000000000..6a0ee319c7b9b3 --- /dev/null +++ b/docs/user-guide/commands/argocd_proj_remove-source-namespace.md @@ -0,0 +1,55 @@ +# `argocd proj remove-source-namespace` Command Reference + +## argocd proj remove-source-namespace + +Removes the source namespace from the AppProject + +``` +argocd proj remove-source-namespace PROJECT NAMESPACE [flags] +``` + +### Examples + +``` + # Remove source NAMESPACE in PROJECT + argocd proj remove-source-namespace PROJECT NAMESPACE +``` + +### Options + +``` + -h, --help help for remove-source-namespace +``` + +### Options inherited from parent commands + +``` + --auth-token string Authentication token + --client-crt string Client certificate file + --client-crt-key string Client certificate key file + --config string Path to Argo CD config (default "/home/user/.config/argocd/config") + --controller-name string Name of the Argo CD Application controller; set this or the ARGOCD_APPLICATION_CONTROLLER_NAME environment variable when the controller's name label differs from the default, for example when installing via the Helm chart (default "argocd-application-controller") + --core If set to true then CLI talks directly to Kubernetes instead of talking to Argo CD API server + --grpc-web Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. + --grpc-web-root-path string Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root. + -H, --header strings Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers) + --http-retry-max int Maximum number of retries to establish http connection to Argo CD server + --insecure Skip server certificate and domain verification + --kube-context string Directs the command to the given kube-context + --logformat string Set the logging format. One of: text|json (default "text") + --loglevel string Set the logging level. One of: debug|info|warn|error (default "info") + --plaintext Disable TLS + --port-forward Connect to a random argocd-server port using port forwarding + --port-forward-namespace string Namespace name which should be used for port forwarding + --redis-haproxy-name string Name of the Redis HA Proxy; set this or the ARGOCD_REDIS_HAPROXY_NAME environment variable when the HA Proxy's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis-ha-haproxy") + --redis-name string Name of the Redis deployment; set this or the ARGOCD_REDIS_NAME environment variable when the Redis's name label differs from the default, for example when installing via the Helm chart (default "argocd-redis") + --repo-server-name string Name of the Argo CD Repo server; set this or the ARGOCD_REPO_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-repo-server") + --server string Argo CD server address + --server-crt string Server certificate file + --server-name string Name of the Argo CD API server; set this or the ARGOCD_SERVER_NAME environment variable when the server's name label differs from the default, for example when installing via the Helm chart (default "argocd-server") +``` + +### SEE ALSO + +* [argocd proj](argocd_proj.md) - Manage projects + diff --git a/docs/user-guide/commands/argocd_proj_windows.md b/docs/user-guide/commands/argocd_proj_windows.md index dc1b68bf0191b1..0b22c2098dc824 100644 --- a/docs/user-guide/commands/argocd_proj_windows.md +++ b/docs/user-guide/commands/argocd_proj_windows.md @@ -8,6 +8,23 @@ Manage a project's sync windows argocd proj windows [flags] ``` +### Examples + +``` + +#Add a sync window to a project +argocd proj windows add my-project \ +--schedule "0 0 * * 1-5" \ +--duration 3600 \ +--prune + +#Delete a sync window from a project +argocd proj windows delete + +#List project sync windows +argocd proj windows list +``` + ### Options ``` diff --git a/docs/user-guide/commands/argocd_proj_windows_add.md b/docs/user-guide/commands/argocd_proj_windows_add.md index 7f9eb50af8f5b4..52fd3a8354ee39 100644 --- a/docs/user-guide/commands/argocd_proj_windows_add.md +++ b/docs/user-guide/commands/argocd_proj_windows_add.md @@ -11,14 +11,15 @@ argocd proj windows add PROJECT [flags] ### Examples ``` -# Add a 1 hour allow sync window + +#Add a 1 hour allow sync window argocd proj windows add PROJECT \ --kind allow \ --schedule "0 22 * * *" \ --duration 1h \ --applications "*" -# Add a deny sync window with the ability to manually sync. +#Add a deny sync window with the ability to manually sync. argocd proj windows add PROJECT \ --kind deny \ --schedule "30 10 * * *" \ diff --git a/docs/user-guide/commands/argocd_proj_windows_delete.md b/docs/user-guide/commands/argocd_proj_windows_delete.md index 316b25041fde25..6faf7dbeedc19a 100644 --- a/docs/user-guide/commands/argocd_proj_windows_delete.md +++ b/docs/user-guide/commands/argocd_proj_windows_delete.md @@ -8,6 +8,17 @@ Delete a sync window from a project. Requires ID which can be found by running " argocd proj windows delete PROJECT ID [flags] ``` +### Examples + +``` + +#Delete a sync window from a project (default) with ID 0 +argocd proj windows delete default 0 + +#Delete a sync window from a project (new-project) with ID 1 +argocd proj windows delete new-project 1 +``` + ### Options ``` diff --git a/docs/user-guide/commands/argocd_proj_windows_disable-manual-sync.md b/docs/user-guide/commands/argocd_proj_windows_disable-manual-sync.md index 8951ad9371c908..e3b84ac38cc0e1 100644 --- a/docs/user-guide/commands/argocd_proj_windows_disable-manual-sync.md +++ b/docs/user-guide/commands/argocd_proj_windows_disable-manual-sync.md @@ -12,6 +12,17 @@ Disable manual sync for a sync window. Requires ID which can be found by running argocd proj windows disable-manual-sync PROJECT ID [flags] ``` +### Examples + +``` + +#Disable manual sync for a sync window for the Project +argocd proj windows disable-manual-sync PROJECT ID + +#Disbaling manual sync for a windows set on the default project with Id 0 +argocd proj windows disable-manual-sync default 0 +``` + ### Options ``` diff --git a/docs/user-guide/commands/argocd_proj_windows_enable-manual-sync.md b/docs/user-guide/commands/argocd_proj_windows_enable-manual-sync.md index a1ca162840f7a2..7ecbb50e6ac1b4 100644 --- a/docs/user-guide/commands/argocd_proj_windows_enable-manual-sync.md +++ b/docs/user-guide/commands/argocd_proj_windows_enable-manual-sync.md @@ -12,6 +12,20 @@ Enable manual sync for a sync window. Requires ID which can be found by running argocd proj windows enable-manual-sync PROJECT ID [flags] ``` +### Examples + +``` + +#Enabling manual sync for a general case +argocd proj windows enable-manual-sync PROJECT ID + +#Enabling manual sync for a windows set on the default project with Id 2 +argocd proj windows enable-manual-sync default 2 + +#Enabling manual sync with a custom message +argocd proj windows enable-manual-sync my-app-project --message "Manual sync initiated by admin +``` + ### Options ``` diff --git a/docs/user-guide/commands/argocd_proj_windows_list.md b/docs/user-guide/commands/argocd_proj_windows_list.md index 94073db4775b84..3c361f90d2a683 100644 --- a/docs/user-guide/commands/argocd_proj_windows_list.md +++ b/docs/user-guide/commands/argocd_proj_windows_list.md @@ -11,12 +11,15 @@ argocd proj windows list PROJECT [flags] ### Examples ``` -# List project windows + +#List project windows argocd proj windows list PROJECT - -# List project windows in yaml format + +#List project windows in yaml format argocd proj windows list PROJECT -o yaml +#List project windows info for a project name (test-project) +argocd proj windows list test-project ``` ### Options diff --git a/docs/user-guide/commands/argocd_repo_add.md b/docs/user-guide/commands/argocd_repo_add.md index 263dda07af7dcd..8399d48302509f 100644 --- a/docs/user-guide/commands/argocd_repo_add.md +++ b/docs/user-guide/commands/argocd_repo_add.md @@ -17,6 +17,12 @@ argocd repo add REPOURL [flags] # Add a Git repository via SSH on a non-default port - need to use ssh:// style URLs here argocd repo add ssh://git@git.example.com:2222/repos/repo --ssh-private-key-path ~/id_rsa + # Add a Git repository via SSH using socks5 proxy with no proxy credentials + argocd repo add ssh://git@github.com/argoproj/argocd-example-apps --ssh-private-key-path ~/id_rsa --proxy socks5://your.proxy.server.ip:1080 + + # Add a Git repository via SSH using socks5 proxy with proxy credentials + argocd repo add ssh://git@github.com/argoproj/argocd-example-apps --ssh-private-key-path ~/id_rsa --proxy socks5://username:password@your.proxy.server.ip:1080 + # Add a private Git repository via HTTPS using username/password and TLS client certificates: argocd repo add https://git.example.com/repos/repo --username git --password secret --tls-client-cert-path ~/mycert.crt --tls-client-cert-key-path ~/mycert.key diff --git a/docs/user-guide/diff-strategies.md b/docs/user-guide/diff-strategies.md new file mode 100644 index 00000000000000..2890fe64cbb0ea --- /dev/null +++ b/docs/user-guide/diff-strategies.md @@ -0,0 +1,131 @@ +# Diff Strategies + +Argo CD calculates the diff between the desired state and the live +state in order to define if an Application is out-of-sync. This same +logic is also used in Argo CD UI to display the differences between +live and desired states for all resources belonging to an application. + +Argo CD currently has 3 different strategies to calculate diffs: + +- **Legacy**: This is the main diff strategy used by default. It + applies a 3-way diff based on live state, desired state and + last-applied-configuration (annotation). +- **Structured-Merge Diff**: Strategy automatically applied when + enabling Server-Side Apply sync option. +- **Server-Side Diff**: New strategy that invokes a Server-Side Apply + in dryrun mode in order to generate the predicted live state. + +## Structured-Merge Diff +*Current Status: [Beta][1] (Since v2.5.0)* + +This is diff strategy is automatically used when Server-Side Apply +sync option is enabled. It uses the [structured-merge-diff][2] library +used by Kubernetes to calculate diffs based on fields ownership. There +are some challenges using this strategy to calculate diffs for CRDs +that define default values. After different issues were identified by +the community, this strategy is being discontinued in favour of +Server-Side Diff. + +## Server-Side Diff +*Current Status: [Beta][1] (Since v2.10.0)* + +This diff strategy will execute a Server-Side Apply in dryrun mode for +each resource of the application. The response of this operation is then +compared with the live state in order to provide the diff results. The +diff results are cached and new Server-Side Apply requests to Kube API +are only triggered when: + +- An Application refresh or hard-refresh is requested. +- There is a new revision in the repo which the Argo CD Application is + targeting. +- The Argo CD Application spec changed. + +One advantage of Server-Side Diff is that Kubernetes Admission +Controllers will participate in the diff calculation. If for example +a validation webhook identifies a resource to be invalid, that will be +informed to Argo CD during the diff stage rather than during the sync +stage. + +### Enabling it + +Server-Side Diff can be enabled at the Argo CD Controller level or per +Application. + +**Enabling Server-Side Diff for all Applications** + +Add the following entry in the argocd-cmd-params-cm configmap: + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cmd-params-cm +data: + controller.diff.server.side: "true" +... +``` + +Note: It is necessary to restart the `argocd-application-controller` +after applying this configuration. + +**Enabling Server-Side Diff for one application** + +Add the following annotation in the Argo CD Application resource: + +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + argocd.argoproj.io/compare-options: ServerSideDiff=true +... +``` + +**Disabling Server-Side Diff for one application** + +If Server-Side Diff is enabled globally in your Argo CD instance, it +is possible to disable it at the application level. In order to do so, +add the following annotation in the Application resource: + +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + argocd.argoproj.io/compare-options: ServerSideDiff=false +... +``` + +*Note: Please report any issues that forced you to disable the +Server-Side Diff feature* + +### Mutation Webhooks + +Server-Side Diff does not include changes made by mutation webhooks by +default. If you want to include mutation webhooks in Argo CD diffs add +the following annotation in the Argo CD Application resource: + +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + argocd.argoproj.io/compare-options: IncludeMutationWebhook=true +... +``` + +Note: This annoation is only effective when Server-Side Diff is +enabled. To enable both options for a given application add the +following annotation in the Argo CD Application resource: + +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + annotations: + argocd.argoproj.io/compare-options: ServerSideDiff=true,IncludeMutationWebhook=true +... +``` + +[1]: https://github.com/argoproj/argoproj/blob/main/community/feature-status.md#beta +[2]: https://github.com/kubernetes-sigs/structured-merge-diff diff --git a/docs/user-guide/diffing.md b/docs/user-guide/diffing.md index 5a056b9c3769b0..61f799e514d6a4 100644 --- a/docs/user-guide/diffing.md +++ b/docs/user-guide/diffing.md @@ -181,4 +181,7 @@ data: type: core/v1/PodSpec ``` -The list of supported Kubernetes types is available in [diffing_known_types.txt](https://raw.githubusercontent.com/argoproj/argo-cd/master/util/argo/normalizers/diffing_known_types.txt) +The list of supported Kubernetes types is available in [diffing_known_types.txt](https://raw.githubusercontent.com/argoproj/argo-cd/master/util/argo/normalizers/diffing_known_types.txt) and additionally: + +* `core/Quantity` +* `meta/v1/duration` diff --git a/docs/user-guide/environment-variables.md b/docs/user-guide/environment-variables.md index c990073363f04c..cff6446617fa3f 100644 --- a/docs/user-guide/environment-variables.md +++ b/docs/user-guide/environment-variables.md @@ -4,7 +4,7 @@ The following environment variables can be used with `argocd` CLI: | Environment Variable | Description | |--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `ARGOCD_SERVER` | the address of the Argo CD server without `https://` prefix
(instead of specifying `--server` for every command)
eg. `ARGOCD_SERVER=argocd.mycompany.com` if served through an ingress with DNS | +| `ARGOCD_SERVER` | the address of the Argo CD server without `https://` prefix
(instead of specifying `--server` for every command)
eg. `ARGOCD_SERVER=argocd.example.com` if served through an ingress with DNS | | `ARGOCD_AUTH_TOKEN` | the Argo CD `apiKey` for your Argo CD user to be able to authenticate | | `ARGOCD_OPTS` | command-line options to pass to `argocd` CLI
eg. `ARGOCD_OPTS="--grpc-web"` | | `ARGOCD_SERVER_NAME` | the Argo CD API Server name (default "argocd-server") | diff --git a/docs/user-guide/external-url.md b/docs/user-guide/external-url.md index 792b8465b233bc..7f08ea6c80bf47 100644 --- a/docs/user-guide/external-url.md +++ b/docs/user-guide/external-url.md @@ -12,7 +12,7 @@ kind: Deployment metadata: name: my-svc annotations: - link.argocd.argoproj.io/external-link: http://my-grafana.com/pre-generated-link + link.argocd.argoproj.io/external-link: http://my-grafana.example.com/pre-generated-link ``` ![External link](../assets/external-link.png) diff --git a/docs/user-guide/helm.md b/docs/user-guide/helm.md index 76853480a6def2..7a763336abcc85 100644 --- a/docs/user-guide/helm.md +++ b/docs/user-guide/helm.md @@ -25,9 +25,28 @@ spec: namespace: kubeseal ``` +Another example using a public OCI helm chart: +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: nginx +spec: + project: default + source: + chart: nginx + repoURL: registry-1.docker.io/bitnamicharts # note: the oci:// syntax is not included. + targetRevision: 15.9.0 + destination: + name: "in-cluster" + namespace: nginx +``` + !!! note "When using multiple ways to provide values" Order of precedence is `parameters > valuesObject > values > valueFiles > helm repository values.yaml` (see [Here](./helm.md#helm-value-precedence) for a more detailed example) +See [here](../operator-manual/declarative-setup.md#helm-chart-repositories) for more info about how to configure private Helm repositories. + ## Values Files Helm has the ability to use a different, or even multiple "values.yaml" files to derive its @@ -210,7 +229,7 @@ is any normal Kubernetes resource annotated with the `helm.sh/hook` annotation. Argo CD supports many (most?) Helm hooks by mapping the Helm annotations onto Argo CD's own hook annotations: | Helm Annotation | Notes | -| ------------------------------- | --------------------------------------------------------------------------------------------- | +| ------------------------------- |-----------------------------------------------------------------------------------------------| | `helm.sh/hook: crd-install` | Supported as equivalent to `argocd.argoproj.io/hook: PreSync`. | | `helm.sh/hook: pre-delete` | Not supported. In Helm stable there are 3 cases used to clean up CRDs and 3 to clean-up jobs. | | `helm.sh/hook: pre-rollback` | Not supported. Never used in Helm stable. | @@ -218,7 +237,7 @@ Argo CD supports many (most?) Helm hooks by mapping the Helm annotations onto Ar | `helm.sh/hook: pre-upgrade` | Supported as equivalent to `argocd.argoproj.io/hook: PreSync`. | | `helm.sh/hook: post-upgrade` | Supported as equivalent to `argocd.argoproj.io/hook: PostSync`. | | `helm.sh/hook: post-install` | Supported as equivalent to `argocd.argoproj.io/hook: PostSync`. | -| `helm.sh/hook: post-delete` | Not supported. Never used in Helm stable. | +| `helm.sh/hook: post-delete` | Supported as equivalent to `argocd.argoproj.io/hook: PostDelete`. | | `helm.sh/hook: post-rollback` | Not supported. Never used in Helm stable. | | `helm.sh/hook: test-success` | Not supported. No equivalent in Argo CD. | | `helm.sh/hook: test-failure` | Not supported. No equivalent in Argo CD. | diff --git a/docs/user-guide/kustomize.md b/docs/user-guide/kustomize.md index e97b4576669580..4e45eb685e75f0 100644 --- a/docs/user-guide/kustomize.md +++ b/docs/user-guide/kustomize.md @@ -7,6 +7,7 @@ The following configuration options are available for Kustomize: * `images` is a list of Kustomize image overrides * `replicas` is a list of Kustomize replica overrides * `commonLabels` is a string map of additional labels +* `labelWithoutSelector` is a boolean value which defines if the common label(s) should be applied to resource selectors and templates. * `forceCommonLabels` is a boolean value which defines if it's allowed to override existing labels * `commonAnnotations` is a string map of additional annotations * `namespace` is a Kubernetes resources namespace @@ -21,9 +22,9 @@ To use Kustomize with an overlay, point your path to the overlay. If you're generating resources, you should read up how to ignore those generated resources using the [`IgnoreExtraneous` compare option](compare-options.md). ## Patches -Patches are a way to kustomize resources using inline configurations in Argo CD applications. This allows for kustomizing without kustomization file. `patches` follow the same logic as the corresponding Kustomization. Any patches that target existing Kustomization file will be merged. +Patches are a way to kustomize resources using inline configurations in Argo CD applications. `patches` follow the same logic as the corresponding Kustomization. Any patches that target existing Kustomization file will be merged. -The following Kustomization can be done similarly in an Argo CD application. +This Kustomize example sources manifests from the `/kustomize-guestbook` folder of the `argoproj/argocd-example-apps` repository, and patches the `Deployment` to use port `443` on the container. ```yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization @@ -31,8 +32,7 @@ metadata: name: kustomize-inline-example namespace: test1 resources: - - https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/guestbook/guestbook-ui-deployment.yaml - - https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/guestbook/guestbook-ui-svc.yaml + - https://raw.githubusercontent.com/argoproj/argocd-example-apps/master/kustomize-guestbook/ patches: - target: kind: Deployment @@ -42,7 +42,8 @@ patches: path: /spec/template/spec/containers/0/ports/0/containerPort value: 443 ``` -Application will clone the repository, use the specified path, then kustomize using inline patches configuration. + +This `Application` does the equivalent using the inline `kustomize.patches` configuration. ```yaml apiVersion: argoproj.io/v1alpha1 kind: Application @@ -57,7 +58,7 @@ spec: server: https://kubernetes.default.svc project: default source: - path: guestbook + path: kustomize-guestbook repoURL: https://github.com/argoproj/argocd-example-apps.git targetRevision: master kustomize: @@ -71,6 +72,72 @@ spec: value: 443 ``` +The inline kustomize patches work well with `ApplicationSets`, too. Instead of maintaining a patch or overlay for each cluster, patches can now be done in the `Application` template and utilize attributes from the generators. For example, with [`external-dns`](https://github.com/kubernetes-sigs/external-dns/) to set the [`txt-owner-id`](https://github.com/kubernetes-sigs/external-dns/blob/e1adc9079b12774cccac051966b2c6a3f18f7872/docs/registry/registry.md?plain=1#L6) to the cluster name. + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: external-dns +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - clusters: {} + template: + metadata: + name: 'external-dns' + spec: + project: default + source: + repoURL: https://github.com/kubernetes-sigs/external-dns/ + targetRevision: v0.14.0 + path: kustomize + kustomize: + patches: + - target: + kind: Deployment + name: external-dns + patch: |- + - op: add + path: /spec/template/spec/containers/0/args/3 + value: --txt-owner-id={{.name}} # patch using attribute from generator + destination: + name: 'in-cluster' + namespace: default +``` + +## Components +Kustomize [components](https://github.com/kubernetes-sigs/kustomize/blob/master/examples/components.md) encapsulate both resources and patches together. They provide a powerful way to modularize and reuse configuration in Kubernetes applications. + +Outside of Argo CD, to utilize components, you must add the following to the `kustomization.yaml` that the Application references. For example: +```yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +... +components: +- ../component +``` + +With support added for components in `v2.10.0`, you can now reference a component directly in the Application: +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: application-kustomize-components +spec: + ... + source: + path: examples/application-kustomize-components/base + repoURL: https://github.com/my-user/my-repo + targetRevision: main + + # This! + kustomize: + components: + - ../component # relative to the kustomization.yaml (`source.path`). +``` + ## Private Remote Bases If you have remote bases that are either (a) HTTPS and need username/password (b) SSH and need SSH private key, then they'll inherit that from the app's repo. @@ -96,6 +163,9 @@ data: kustomize.buildOptions: --load-restrictor LoadRestrictionsNone kustomize.buildOptions.v4.4.0: --output /tmp ``` + +After modifying `kustomize.buildOptions`, you may need to restart ArgoCD for the changes to take effect. + ## Custom Kustomize versions Argo CD supports using multiple Kustomize versions simultaneously and specifies required version per application. diff --git a/docs/user-guide/projects.md b/docs/user-guide/projects.md index 9876405febdebe..f5979cf3c47b3a 100644 --- a/docs/user-guide/projects.md +++ b/docs/user-guide/projects.md @@ -292,7 +292,7 @@ p, proj:my-project:admin, repositories, update, my-project/*, allow This provides extra flexibility so that admins can have stricter rules. e.g.: ``` -p, proj:my-project:admin, repositories, update, my-project/https://github.my-company.com/*, allow +p, proj:my-project:admin, repositories, update, my-project/https://github.example.com/*, allow ``` Once the appropriate RBAC rules are in place, developers can create their own Git repositories and (assuming @@ -330,9 +330,9 @@ metadata: argocd.argoproj.io/secret-type: cluster type: Opaque stringData: - name: mycluster.com + name: mycluster.example.com project: my-project1 # Project scoped - server: https://mycluster.com + server: https://mycluster.example.com config: | { "bearerToken": "", diff --git a/docs/user-guide/resource_hooks.md b/docs/user-guide/resource_hooks.md index d705f8d21423df..6e15a55bb20c22 100644 --- a/docs/user-guide/resource_hooks.md +++ b/docs/user-guide/resource_hooks.md @@ -8,7 +8,9 @@ and after a Sync operation. Hooks can also be run if a Sync operation fails at a * Using a `Sync` hook to orchestrate a complex deployment requiring more sophistication than the Kubernetes rolling update strategy. * Using a `PostSync` hook to run integration and health checks after a deployment. -* Using a `SyncFail` hook to run clean-up or finalizer logic if a Sync operation fails. _`SyncFail` hooks are only available starting in v1.2_ +* Using a `SyncFail` hook to run clean-up or finalizer logic if a Sync operation fails. +* Using a `PostDelete` hook to run clean-up or finalizer logic after all Application resources are deleted. Please note that + `PostDelete` hooks are only deleted if the delete policy matches the aggregated deletion hooks status and not garbage collected after the application is deleted. ## Usage @@ -37,7 +39,8 @@ The following hooks are defined: | `Sync` | Executes after all `PreSync` hooks completed and were successful, at the same time as the application of the manifests. | | `Skip` | Indicates to Argo CD to skip the application of the manifest. | | `PostSync` | Executes after all `Sync` hooks completed and were successful, a successful application, and all resources in a `Healthy` state. | -| `SyncFail` | Executes when the sync operation fails. _Available starting in v1.2_ | +| `SyncFail` | Executes when the sync operation fails. | +| `PostDelete` | Executes after all Application resources are deleted. _Available starting in v2.10._ | ### Generate Name @@ -60,6 +63,7 @@ metadata: argocd.argoproj.io/hook: PostSync argocd.argoproj.io/hook-delete-policy: HookSucceeded ``` +Multiple hook delete policies can be specified as a comma separated list. The following policies define when the hook will be deleted. diff --git a/docs/user-guide/resource_tracking.md b/docs/user-guide/resource_tracking.md index 79eda63ce5d5ad..e62a7c094f4e26 100644 --- a/docs/user-guide/resource_tracking.md +++ b/docs/user-guide/resource_tracking.md @@ -65,6 +65,11 @@ metadata: The advantages of using the tracking id annotation is that there are no clashes any more with other Kubernetes tools and Argo CD is never confused about the owner of a resource. The `annotation+label` can also be used if you want other tools to understand resources managed by Argo CD. +### Non self-referencing annotations +When using the tracking method `annotation` or `annotation+label`, Argo CD will consider the resource properties in the annotation (name, namespace, group and kind) to determine whether the resource should be compared against the desired state. If the tracking annotation does not reference the resource it is applied to, the resource will neither affect the application's sync status nor be marked for pruning. + +This allows other kubernetes tools (e.g. [HNC](https://github.com/kubernetes-sigs/hierarchical-namespaces)) to copy a resource to a different namespace without impacting the Argo CD application's sync status. Copied resources will be visible on the UI at top level. They will have no sync status and won't impact the application's sync status. + ## Choosing a tracking method To actually select your preferred tracking method edit the `resourceTrackingMethod` value contained inside the `argocd-cm` configmap. diff --git a/docs/user-guide/status-badge.md b/docs/user-guide/status-badge.md index 8355be458f026d..33632279973098 100644 --- a/docs/user-guide/status-badge.md +++ b/docs/user-guide/status-badge.md @@ -9,7 +9,12 @@ To show this badge, use the following URL format `${argoCdBaseUrl}/api/badge?nam The URLs for status image are available on application details page: 1. Navigate to application details page and click on 'Details' button. -1. Scroll down to 'Status Badge' section. -1. Select required template such as URL, Markdown etc. +2. Scroll down to 'Status Badge' section. +3. Select required template such as URL, Markdown etc. for the status image URL in markdown, html, etc are available . -1. Copy the text and paste it into your README or website. \ No newline at end of file +4. Copy the text and paste it into your README or website. + +The application name may optionally be displayed in the status badge by adding the `?showAppName=true` query parameter. + +For example, `${argoCdBaseUrl}/api/badge?name=${appName}&showAppName=true`. +To remove the application name from the badge, remove the query parameter from the URL or set it to `false`. \ No newline at end of file diff --git a/docs/user-guide/sync-options.md b/docs/user-guide/sync-options.md index e5b1fe55e8e666..985f9fcf3c9749 100644 --- a/docs/user-guide/sync-options.md +++ b/docs/user-guide/sync-options.md @@ -270,7 +270,7 @@ spec: - RespectIgnoreDifferences=true ``` -The example above shows how an Argo CD Application can be configured so it will ignore the `spec.replicas` field from the desired state (git) during the sync stage. This is achieve by calculating and pre-patching the desired state before applying it in the cluster. Note that the `RespectIgnoreDifferences` sync option is only effective when the resource is already created in the cluster. If the Application is being created and no live state exists, the desired state is applied as-is. +The example above shows how an Argo CD Application can be configured so it will ignore the `spec.replicas` field from the desired state (git) during the sync stage. This is achieved by calculating and pre-patching the desired state before applying it in the cluster. Note that the `RespectIgnoreDifferences` sync option is only effective when the resource is already created in the cluster. If the Application is being created and no live state exists, the desired state is applied as-is. ## Create Namespace diff --git a/docs/user-guide/sync-waves.md b/docs/user-guide/sync-waves.md index 932ba396d68d26..8b17237c875717 100644 --- a/docs/user-guide/sync-waves.md +++ b/docs/user-guide/sync-waves.md @@ -37,7 +37,7 @@ Hooks and resources are assigned to wave zero by default. The wave can be negati When Argo CD starts a sync, it orders the resources in the following precedence: * The phase -* The wave they are in (lower values first) +* The wave they are in (lower values first for creation & updation and higher values first for deletion) * By kind (e.g. [namespaces first and then other Kubernetes resources, followed by custom resources](https://github.com/argoproj/gitops-engine/blob/bc9ce5764fa306f58cf59199a94f6c968c775a2d/pkg/sync/sync_tasks.go#L27-L66)) * By name @@ -49,6 +49,8 @@ It repeats this process until all phases and waves are in-sync and healthy. Because an application can have resources that are unhealthy in the first wave, it may be that the app can never get to healthy. +During pruning of resources, resources from higher waves are processed first before moving to lower waves. If, for any reason, a resource isn't removed/pruned in a wave, the resources in next waves won't be processed. This is to ensure proper resource cleanup between waves. + Note that there's currently a delay between each sync wave in order give other controllers a chance to react to the spec change that we just applied. This also prevent Argo CD from assessing resource health too quickly (against the stale object), causing hooks to fire prematurely. The current delay between each sync wave is 2 seconds and can be configured via environment diff --git a/examples/dashboard.json b/examples/dashboard.json index 7e992a53633244..108ac81918ba36 100644 --- a/examples/dashboard.json +++ b/examples/dashboard.json @@ -3,7 +3,10 @@ "list": [ { "builtIn": 1, - "datasource": "-- Grafana --", + "datasource": { + "type": "datasource", + "uid": "grafana" + }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", @@ -13,15 +16,17 @@ ] }, "editable": true, - "gnetId": null, + "fiscalYearStartMonth": 0, "graphTooltip": 0, - "id": 1, - "iteration": 1605574886303, + "id": 28, "links": [], + "liveNow": false, "panels": [ { "collapsed": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 1, "w": 24, @@ -30,12 +35,21 @@ }, "id": 68, "panels": [], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], "title": "Overview", "type": "row" }, { - "content": "![argoimage](https://avatars1.githubusercontent.com/u/30269780?s=110&v=4)", - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 4, "w": 2, @@ -44,29 +58,64 @@ }, "id": 26, "links": [], - "mode": "markdown", - "options": {}, - "title": "", + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "![argoimage](https://avatars1.githubusercontent.com/u/30269780?s=110&v=4)", + "mode": "markdown" + }, + "pluginVersion": "10.3.1", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], "transparent": true, "type": "text" }, { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": false, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], - "datasource": "$datasource", - "format": "dtdurations", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "dtdurations", + "unitScale": true + }, + "overrides": [] }, "gridPos": { "h": 4, @@ -75,79 +124,77 @@ "y": 1 }, "id": 32, - "interval": null, "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "options": {}, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": false + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "tableColumn": "", + "pluginVersion": "10.3.1", "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "time() - max(process_start_time_seconds{job=\"argocd-server-metrics\",namespace=~\"$namespace\"})", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], - "thresholds": "", "title": "Uptime", - "type": "singlestat", - "valueFontSize": "70%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "current" + "type": "stat" }, { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": false, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], - "datasource": "$datasource", - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "rgb(31, 120, 193)", + "mode": "fixed" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "0" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none", + "unitScale": true + }, + "overrides": [] }, "gridPos": { "h": 4, @@ -156,43 +203,30 @@ "y": 1 }, "id": 94, - "interval": null, "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "options": {}, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": true + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "tableColumn": "", + "pluginVersion": "10.3.1", "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "count(count by (server) (argocd_cluster_info{namespace=~\"$namespace\"}))", "format": "time_series", "instant": false, @@ -200,40 +234,47 @@ "refId": "A" } ], - "thresholds": "", - "timeFrom": null, - "timeShift": null, "title": "Clusters", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "0", - "value": "null" - } - ], - "valueName": "current" + "type": "stat" }, { - "cacheTimeout": null, - "colorBackground": false, - "colorPostfix": false, - "colorPrefix": false, - "colorValue": false, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], - "datasource": "$datasource", - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "rgb(31, 120, 193)", + "mode": "fixed" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none", + "unitScale": true + }, + "overrides": [] }, "gridPos": { "h": 4, @@ -242,45 +283,31 @@ "y": 1 }, "id": 75, - "interval": null, "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "options": {}, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "repeat": null, - "repeatDirection": "h", - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": true + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "tableColumn": "", + "pluginVersion": "10.3.1", + "repeatDirection": "h", "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(argocd_app_info{namespace=~\"$namespace\",dest_server=~\"$cluster\",health_status=~\"$health_status\",sync_status=~\"$sync_status\"})", "format": "time_series", "instant": false, @@ -288,38 +315,47 @@ "refId": "A" } ], - "thresholds": "", - "timeFrom": null, - "timeShift": null, "title": "Applications", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "N/A", - "value": "null" - } - ], - "valueName": "current" + "type": "stat" }, { - "cacheTimeout": null, - "colorBackground": false, - "colorValue": false, - "colors": [ - "#299c46", - "rgba(237, 129, 40, 0.89)", - "#d44a3a" - ], - "datasource": "$datasource", - "format": "none", - "gauge": { - "maxValue": 100, - "minValue": 0, - "show": false, - "thresholdLabels": false, - "thresholdMarkers": true + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "fixedColor": "rgb(31, 120, 193)", + "mode": "fixed" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "0" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none", + "unitScale": true + }, + "overrides": [] }, "gridPos": { "h": 4, @@ -328,43 +364,30 @@ "y": 1 }, "id": 107, - "interval": null, "links": [], - "mappingType": 1, - "mappingTypes": [ - { - "name": "value to text", - "value": 1 - }, - { - "name": "range to text", - "value": 2 - } - ], "maxDataPoints": 100, - "nullPointMode": "connected", - "nullText": null, - "options": {}, - "postfix": "", - "postfixFontSize": "50%", - "prefix": "", - "prefixFontSize": "50%", - "rangeMaps": [ - { - "from": "null", - "text": "N/A", - "to": "null" - } - ], - "sparkline": { - "fillColor": "rgba(31, 118, 189, 0.18)", - "full": false, - "lineColor": "rgb(31, 120, 193)", - "show": true + "options": { + "colorMode": "none", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true }, - "tableColumn": "", + "pluginVersion": "10.3.1", "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "count(count by (repo) (argocd_app_info{namespace=~\"$namespace\"}))", "format": "time_series", "instant": false, @@ -372,24 +395,47 @@ "refId": "A" } ], - "thresholds": "", - "timeFrom": null, - "timeShift": null, "title": "Repositories", - "type": "singlestat", - "valueFontSize": "80%", - "valueMaps": [ - { - "op": "=", - "text": "0", - "value": "null" - } - ], - "valueName": "current" + "type": "stat" }, { - "cacheTimeout": null, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "id": 0, + "op": "=", + "text": "0", + "type": 1, + "value": "null" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none", + "unitScale": true + }, + "overrides": [] + }, "gridPos": { "h": 4, "w": 3, @@ -399,47 +445,27 @@ "id": 100, "links": [], "options": { - "fieldOptions": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "horizontal", + "reduceOptions": { "calcs": [ "lastNotNull" ], - "defaults": { - "mappings": [ - { - "id": 0, - "op": "=", - "text": "0", - "type": 1, - "value": "null" - } - ], - "max": 100, - "min": 0, - "nullValueMode": "connected", - "thresholds": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ], - "unit": "none" - }, - "override": {}, - "overrides": [], + "fields": "", "values": false }, - "orientation": "horizontal", "showThresholdLabels": false, - "showThresholdMarkers": true + "showThresholdMarkers": true, + "sizing": "auto" }, - "pluginVersion": "6.5.2", + "pluginVersion": "10.3.1", "repeatDirection": "h", "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(argocd_app_info{namespace=~\"$namespace\",dest_server=~\"$cluster\",operation!=\"\"})", "format": "time_series", "instant": true, @@ -448,19 +474,24 @@ "refId": "A" } ], - "timeFrom": null, - "timeShift": null, "title": "Operations", "type": "gauge" }, { "aliasColors": {}, "bars": false, - "cacheTimeout": null, "dashLength": 10, "dashes": false, - "datasource": "$datasource", - "decimals": null, + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -491,10 +522,11 @@ "links": [], "nullPointMode": "connected", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 2, "points": false, "renderer": "flot", @@ -504,6 +536,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(argocd_app_info{namespace=~\"$namespace\",dest_server=~\"$cluster\",health_status=~\"$health_status\",sync_status=~\"$sync_status\"}) by (namespace)", "format": "time_series", "instant": false, @@ -513,9 +548,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Applications", "tooltip": { "shared": false, @@ -524,9 +557,7 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, @@ -534,29 +565,24 @@ { "decimals": 0, "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { "collapsed": true, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 1, "w": 24, @@ -575,11 +601,18 @@ "Unknown": "rgb(255, 255, 255)" }, "bars": false, - "cacheTimeout": null, "dashLength": 10, "dashes": false, - "datasource": "$datasource", - "decimals": null, + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -601,7 +634,6 @@ "min": false, "rightSide": true, "show": true, - "sideWidth": null, "sort": "current", "sortDesc": true, "total": false, @@ -612,10 +644,11 @@ "links": [], "nullPointMode": "null as zero", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 2, "points": false, "renderer": "flot", @@ -625,6 +658,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(argocd_app_info{namespace=~\"$namespace\",dest_server=~\"$cluster\",health_status=~\"$health_status\",sync_status=~\"$sync_status\",health_status!=\"\"}) by (health_status)", "format": "time_series", "instant": false, @@ -634,9 +670,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Health Status", "tooltip": { "shared": true, @@ -645,33 +679,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 2, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -686,11 +711,18 @@ "Unknown": "rgb(255, 255, 255)" }, "bars": false, - "cacheTimeout": null, "dashLength": 10, "dashes": false, - "datasource": "$datasource", - "decimals": null, + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -712,7 +744,6 @@ "min": false, "rightSide": true, "show": true, - "sideWidth": null, "sort": "current", "sortDesc": true, "total": false, @@ -723,10 +754,11 @@ "links": [], "nullPointMode": "null as zero", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 2, "points": false, "renderer": "flot", @@ -736,6 +768,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(argocd_app_info{namespace=~\"$namespace\",dest_server=~\"$cluster\",health_status=~\"$health_status\",sync_status=~\"$sync_status\",health_status!=\"\"}) by (sync_status)", "format": "time_series", "instant": false, @@ -745,9 +780,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Sync Status", "tooltip": { "shared": true, @@ -756,42 +789,43 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 2, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } } ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], "title": "Application Status", "type": "row" }, { "collapsed": true, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 1, "w": 24, @@ -805,8 +839,9 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", - "decimals": null, + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -851,6 +886,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(round(increase(argocd_app_sync_total{namespace=~\"$namespace\",dest_server=~\"$cluster\"}[$interval]))) by ($grouping)", "format": "time_series", "intervalFactor": 1, @@ -859,9 +897,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Sync Activity", "tooltip": { "shared": true, @@ -870,9 +906,7 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, @@ -882,7 +916,6 @@ "format": "short", "label": "", "logBase": 1, - "max": null, "min": "0", "show": true }, @@ -891,14 +924,11 @@ "format": "short", "label": "", "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -906,8 +936,9 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", - "decimals": null, + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -951,6 +982,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(round(increase(argocd_app_sync_total{namespace=~\"$namespace\",phase=~\"Error|Failed\",dest_server=~\"$cluster\"}[$interval]))) by ($grouping, phase)", "format": "time_series", "intervalFactor": 1, @@ -959,9 +993,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Sync Failures", "tooltip": { "shared": true, @@ -970,9 +1002,7 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, @@ -982,7 +1012,6 @@ "format": "none", "label": "", "logBase": 1, - "max": null, "min": "0", "show": true }, @@ -990,23 +1019,30 @@ "format": "short", "label": "", "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } } ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], "title": "Sync Stats", "type": "row" }, { "collapsed": true, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 1, "w": 24, @@ -1020,7 +1056,9 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -1062,6 +1100,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(argocd_app_reconcile_count{namespace=~\"$namespace\",dest_server=~\"$cluster\"}[$interval])) by ($grouping)", "format": "time_series", "intervalFactor": 1, @@ -1070,9 +1111,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Reconciliation Activity", "tooltip": { "shared": false, @@ -1081,50 +1120,39 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { - "cards": { - "cardPadding": null, - "cardRound": null - }, + "cards": {}, "color": { "cardColor": "#b4ff00", "colorScale": "sqrt", "colorScheme": "interpolateSpectral", "exponent": 0.5, - "min": null, "mode": "spectrum" }, "dataFormat": "tsbuckets", - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 7, "w": 24, @@ -1143,6 +1171,9 @@ "reverseYBuckets": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(argocd_app_reconcile_bucket{namespace=~\"$namespace\"}[$interval])) by (le)", "format": "heatmap", "instant": false, @@ -1151,8 +1182,6 @@ "refId": "A" } ], - "timeFrom": null, - "timeShift": null, "title": "Reconciliation Performance", "tooltip": { "show": true, @@ -1163,27 +1192,21 @@ "xAxis": { "show": true }, - "xBucketNumber": null, - "xBucketSize": null, "yAxis": { - "decimals": null, "format": "short", "logBase": 1, - "max": null, - "min": null, - "show": true, - "splitFactor": null + "show": true }, - "yBucketBound": "auto", - "yBucketNumber": null, - "yBucketSize": null + "yBucketBound": "auto" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -1204,7 +1227,6 @@ "min": false, "rightSide": true, "show": true, - "sideWidth": null, "sort": "current", "sortDesc": true, "total": false, @@ -1215,11 +1237,315 @@ "links": [], "nullPointMode": "null as zero", "options": { - "dataLinks": [] + "dataLinks": [] + }, + "paceLength": 10, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(increase(argocd_app_k8s_request_total{namespace=~\"$namespace\",server=~\"$cluster\"}[$interval])) by (verb, resource_kind)", + "format": "time_series", + "instant": false, + "intervalFactor": 1, + "legendFormat": "{{verb}} {{resource_kind}}", + "refId": "A" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "K8s API Activity", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "uid": "$datasource" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 31 + }, + "hiddenSeries": false, + "id": 96, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideZero": true, + "max": true, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(workqueue_depth{namespace=~\"$namespace\",name=~\"app_.*\"}) by (name)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{name}}", + "refId": "A" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Workqueue Depth", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + }, + { + "format": "short", + "logBase": 1, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "uid": "$datasource" + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 31 + }, + "hiddenSeries": false, + "id": 98, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideZero": false, + "max": true, + "min": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "dataLinks": [] + }, + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(argocd_kubectl_exec_pending{namespace=~\"$namespace\"}) by (command)", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{command}}", + "refId": "A" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Pending kubectl run", + "tooltip": { + "shared": true, + "sort": 2, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": 0, + "format": "short", + "label": "", + "logBase": 1, + "min": "0", + "show": true + }, + { + "decimals": 0, + "format": "short", + "label": "", + "logBase": 1, + "min": "0", + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Controller Stats", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 102, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 9 + }, + "hiddenSeries": false, + "id": 34, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": false, + "rightSide": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "connected", + "options": { + "alertThreshold": true }, "paceLength": 10, "percentage": false, - "pointradius": 2, + "pluginVersion": "10.3.1", + "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -1228,53 +1554,44 @@ "steppedLine": false, "targets": [ { - "expr": "sum(increase(argocd_app_k8s_request_total{namespace=~\"$namespace\",server=~\"$cluster\"}[$interval])) by (verb, resource_kind)", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_memstats_heap_alloc_bytes{job=\"argocd-server-metrics\",namespace=~\"$namespace\"}", "format": "time_series", - "instant": false, "intervalFactor": 1, - "legendFormat": "{{verb}} {{resource_kind}}", + "legendFormat": "{{namespace}}", "refId": "A" } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, - "title": "K8s API Activity", + "title": "Memory Usage", "tooltip": { - "shared": true, + "shared": false, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { - "format": "short", - "label": null, + "format": "bytes", "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -1282,39 +1599,52 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, - "w": 12, + "w": 24, "x": 0, - "y": 31 + "y": 16 }, "hiddenSeries": false, - "id": 96, + "id": 108, "legend": { "alignAsTable": true, "avg": true, "current": true, + "hideEmpty": true, "hideZero": true, "max": true, "min": false, - "rightSide": false, + "rightSide": true, "show": true, - "sideWidth": null, + "sort": "avg", + "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], - "nullPointMode": "null", + "nullPointMode": "connected", "options": { - "dataLinks": [] + "alertThreshold": true }, + "paceLength": 10, "percentage": false, - "pointradius": 2, + "pluginVersion": "10.3.1", + "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -1323,52 +1653,45 @@ "steppedLine": false, "targets": [ { - "expr": "sum(workqueue_depth{namespace=~\"$namespace\",name=~\"app_.*\"}) by (name)", + "datasource": { + "uid": "$datasource" + }, + "expr": "irate(process_cpu_seconds_total{job=\"argocd-server-metrics\",namespace=~\"$namespace\"}[1m])", "format": "time_series", "intervalFactor": 1, - "legendFormat": "{{name}}", + "legendFormat": "{{namespace}}", "refId": "A" } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, - "title": "Workqueue Depth", + "title": "CPU Usage", "tooltip": { - "shared": true, + "shared": false, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { - "format": "short", - "label": null, + "decimals": 1, + "format": "none", "logBase": 1, - "max": null, - "min": "0", "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": "0", "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -1376,26 +1699,38 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", - "decimals": null, + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, - "w": 12, - "x": 12, - "y": 31 + "w": 24, + "x": 0, + "y": 23 }, "hiddenSeries": false, - "id": 98, + "id": 62, "legend": { "alignAsTable": true, "avg": true, "current": true, + "hideEmpty": false, "hideZero": false, "max": true, "min": false, + "rightSide": true, "show": true, + "sort": "current", + "sortDesc": true, "total": false, "values": true }, @@ -1404,10 +1739,12 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, + "paceLength": 10, "percentage": false, - "pointradius": 2, + "pluginVersion": "10.3.1", + "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], @@ -1416,63 +1753,64 @@ "steppedLine": false, "targets": [ { - "expr": "sum(argocd_kubectl_exec_pending{namespace=~\"$namespace\"}) by (command)", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_goroutines{job=\"argocd-server-metrics\",namespace=~\"$namespace\"}", "format": "time_series", "intervalFactor": 1, - "legendFormat": "{{command}}", + "legendFormat": "{{namespace}}", "refId": "A" } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, - "title": "Pending kubectl run", + "title": "Goroutines", "tooltip": { - "shared": true, + "shared": false, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { - "decimals": 0, "format": "short", - "label": "", "logBase": 1, - "max": null, - "min": "0", "show": true }, { - "decimals": 0, "format": "short", - "label": "", "logBase": 1, - "max": null, - "min": "0", "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } } ], - "title": "Controller Stats", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Controller Telemetry", "type": "row" }, + { "collapsed": true, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 1, "w": 24, @@ -1486,14 +1824,23 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, - "y": 26 + "y": 9 }, "hiddenSeries": false, "id": 34, @@ -1515,10 +1862,11 @@ "links": [], "nullPointMode": "connected", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 5, "points": false, "renderer": "flot", @@ -1528,7 +1876,10 @@ "steppedLine": false, "targets": [ { - "expr": "go_memstats_heap_alloc_bytes{job=\"argocd-metrics\",namespace=~\"$namespace\"}", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_memstats_heap_alloc_bytes{job=\"argocd-applicationset-controller-metrics\",namespace=~\"$namespace\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{namespace}}", @@ -1536,9 +1887,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Memory Usage", "tooltip": { "shared": false, @@ -1547,33 +1896,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -1581,14 +1921,23 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, - "y": 33 + "y": 16 }, "hiddenSeries": false, "id": 108, @@ -1612,10 +1961,11 @@ "links": [], "nullPointMode": "connected", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 5, "points": false, "renderer": "flot", @@ -1625,7 +1975,10 @@ "steppedLine": false, "targets": [ { - "expr": "irate(process_cpu_seconds_total{job=\"argocd-metrics\",namespace=~\"$namespace\"}[1m])", + "datasource": { + "uid": "$datasource" + }, + "expr": "irate(process_cpu_seconds_total{job=\"argocd-applicationset-controller-metrics\",namespace=~\"$namespace\"}[1m])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{namespace}}", @@ -1633,9 +1986,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "CPU Usage", "tooltip": { "shared": false, @@ -1644,9 +1995,7 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, @@ -1654,24 +2003,17 @@ { "decimals": 1, "format": "none", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -1679,14 +2021,23 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, - "y": 40 + "y": 23 }, "hiddenSeries": false, "id": 62, @@ -1710,10 +2061,11 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 5, "points": false, "renderer": "flot", @@ -1723,7 +2075,10 @@ "steppedLine": false, "targets": [ { - "expr": "go_goroutines{job=\"argocd-metrics\",namespace=~\"$namespace\"}", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_goroutines{job=\"argocd-applicationset-controller-metrics\",namespace=~\"$namespace\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{namespace}}", @@ -1731,9 +2086,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Goroutines", "tooltip": { "shared": false, @@ -1742,42 +2095,43 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } } ], - "title": "Controller Telemetry", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "AppSet Controller Telemetry", "type": "row" }, { "collapsed": true, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 1, "w": 24, @@ -1791,7 +2145,9 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -1832,6 +2188,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(argocd_cluster_api_resource_objects{namespace=~\"$namespace\",server=~\"$cluster\"}) by (server)", "format": "time_series", "intervalFactor": 1, @@ -1840,9 +2199,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Resource Objects Count", "tooltip": { "shared": false, @@ -1851,33 +2208,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -1885,7 +2233,9 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -1927,6 +2277,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": " sum(argocd_cluster_api_resources{namespace=~\"$namespace\",server=~\"$cluster\"}) by (server)", "format": "time_series", "intervalFactor": 1, @@ -1935,9 +2288,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "API Resources Count", "tooltip": { "shared": false, @@ -1946,33 +2297,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -1980,7 +2322,9 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { @@ -2021,6 +2365,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(argocd_cluster_events_total{namespace=~\"$namespace\",server=~\"$cluster\"}[$interval])) by (server)", "format": "time_series", "intervalFactor": 1, @@ -2029,9 +2376,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Cluster Events Count", "tooltip": { "shared": false, @@ -2040,42 +2385,43 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } } ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], "title": "Cluster Stats", "type": "row" }, { "collapsed": true, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 1, "w": 24, @@ -2089,14 +2435,23 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 7 + "y": 11 }, "hiddenSeries": false, "id": 82, @@ -2114,9 +2469,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 2, "points": false, "renderer": "flot", @@ -2126,6 +2482,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(argocd_git_request_total{request_type=\"ls-remote\", namespace=~\"$namespace\"}[10m])) by (namespace)", "format": "time_series", "intervalFactor": 1, @@ -2134,9 +2493,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Git Requests (ls-remote)", "tooltip": { "shared": true, @@ -2145,33 +2502,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -2179,14 +2527,23 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 7 + "y": 11 }, "hiddenSeries": false, "id": 84, @@ -2204,9 +2561,10 @@ "links": [], "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 2, "points": false, "renderer": "flot", @@ -2216,6 +2574,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(argocd_git_request_total{request_type=\"fetch\", namespace=~\"$namespace\"}[10m])) by (namespace)", "format": "time_series", "intervalFactor": 1, @@ -2224,9 +2585,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Git Requests (checkout)", "tooltip": { "shared": true, @@ -2235,9 +2594,7 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, @@ -2246,29 +2603,20 @@ "format": "short", "label": "", "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { - "cards": { - "cardPadding": null, - "cardRound": null - }, + "cards": {}, "color": { "cardColor": "#b4ff00", "colorScale": "sqrt", @@ -2277,12 +2625,30 @@ "mode": "spectrum" }, "dataFormat": "tsbuckets", - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + }, + "unitScale": true + }, + "overrides": [] + }, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 15 + "y": 19 }, "heatmap": {}, "hideZeroBuckets": false, @@ -2291,10 +2657,51 @@ "legend": { "show": false }, - "options": {}, + "options": { + "calculate": false, + "calculation": {}, + "cellGap": 2, + "cellValues": {}, + "color": { + "exponent": 0.5, + "fill": "#b4ff00", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Spectral", + "steps": 128 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": false + }, + "rowsFrame": { + "layout": "auto" + }, + "showValue": "never", + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "short" + } + }, + "pluginVersion": "10.3.1", "reverseYBuckets": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(argocd_git_request_duration_seconds_bucket{request_type=\"fetch\", namespace=~\"$namespace\"}[$interval])) by (le)", "format": "heatmap", "intervalFactor": 10, @@ -2302,8 +2709,6 @@ "refId": "A" } ], - "timeFrom": null, - "timeShift": null, "title": "Git Fetch Performance", "tooltip": { "show": true, @@ -2313,26 +2718,15 @@ "xAxis": { "show": true }, - "xBucketNumber": null, - "xBucketSize": null, "yAxis": { - "decimals": null, "format": "short", "logBase": 1, - "max": null, - "min": null, - "show": true, - "splitFactor": null + "show": true }, - "yBucketBound": "auto", - "yBucketNumber": null, - "yBucketSize": null + "yBucketBound": "auto" }, { - "cards": { - "cardPadding": null, - "cardRound": null - }, + "cards": {}, "color": { "cardColor": "#b4ff00", "colorScale": "sqrt", @@ -2341,12 +2735,30 @@ "mode": "spectrum" }, "dataFormat": "tsbuckets", - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "scaleDistribution": { + "type": "linear" + } + }, + "unitScale": true + }, + "overrides": [] + }, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 15 + "y": 19 }, "heatmap": {}, "hideZeroBuckets": false, @@ -2355,10 +2767,51 @@ "legend": { "show": false }, - "options": {}, + "options": { + "calculate": false, + "calculation": {}, + "cellGap": 2, + "cellValues": {}, + "color": { + "exponent": 0.5, + "fill": "#b4ff00", + "mode": "scheme", + "reverse": false, + "scale": "exponential", + "scheme": "Spectral", + "steps": 128 + }, + "exemplars": { + "color": "rgba(255,0,255,0.7)" + }, + "filterValues": { + "le": 1e-9 + }, + "legend": { + "show": false + }, + "rowsFrame": { + "layout": "auto" + }, + "showValue": "never", + "tooltip": { + "mode": "single", + "showColorScale": false, + "yHistogram": false + }, + "yAxis": { + "axisPlacement": "left", + "reverse": false, + "unit": "short" + } + }, + "pluginVersion": "10.3.1", "reverseYBuckets": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(argocd_git_request_duration_seconds_bucket{request_type=\"ls-remote\", namespace=~\"$namespace\"}[$interval])) by (le)", "format": "heatmap", "intervalFactor": 10, @@ -2366,8 +2819,6 @@ "refId": "A" } ], - "timeFrom": null, - "timeShift": null, "title": "Git Ls-Remote Performance", "tooltip": { "show": true, @@ -2377,34 +2828,28 @@ "xAxis": { "show": true }, - "xBucketNumber": null, - "xBucketSize": null, "yAxis": { - "decimals": null, "format": "short", "logBase": 1, - "max": null, - "min": null, - "show": true, - "splitFactor": null + "show": true }, - "yBucketBound": "auto", - "yBucketNumber": null, - "yBucketSize": null + "yBucketBound": "auto" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, - "y": 23 + "y": 27 }, "hiddenSeries": false, "id": 71, @@ -2435,7 +2880,10 @@ "steppedLine": false, "targets": [ { - "expr": "go_memstats_heap_alloc_bytes{job=\"argocd-repo-server\",namespace=~\"$namespace\"}", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_memstats_heap_alloc_bytes{job=\"argocd-repo-server-metrics\",namespace=~\"$namespace\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{pod}}", @@ -2443,9 +2891,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Memory Used", "tooltip": { "shared": true, @@ -2454,33 +2900,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -2488,14 +2925,16 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, - "y": 31 + "y": 35 }, "hiddenSeries": false, "id": 72, @@ -2526,7 +2965,10 @@ "steppedLine": false, "targets": [ { - "expr": "go_goroutines{job=\"argocd-repo-server\",namespace=~\"$namespace\"}", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_goroutines{job=\"argocd-repo-server-metrics\",namespace=~\"$namespace\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{pod}}", @@ -2534,9 +2976,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Goroutines", "tooltip": { "shared": true, @@ -2545,42 +2985,43 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } } ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], "title": "Repo Server Stats", "type": "row" }, { "collapsed": true, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "gridPos": { "h": 1, "w": 24, @@ -2594,14 +3035,24 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "unitScale": true + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, - "y": 89 + "y": 12 }, + "hiddenSeries": false, "id": 61, "legend": { "avg": false, @@ -2616,8 +3067,12 @@ "linewidth": 1, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 5, "points": false, "renderer": "flot", @@ -2627,7 +3082,10 @@ "steppedLine": false, "targets": [ { - "expr": "go_memstats_heap_alloc_bytes{job=\"argocd-server-metrics\",namespace=~\"$namespace\"}", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_memstats_heap_alloc_bytes{job=\"argocd-repo-server-metrics\",namespace=~\"$namespace\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{pod}}", @@ -2635,9 +3093,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Memory Used", "tooltip": { "shared": true, @@ -2646,33 +3102,25 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", - "label": null, "logBase": 1, - "max": null, "min": "0", "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -2680,14 +3128,24 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "unitScale": true + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 24, "x": 0, - "y": 97 + "y": 20 }, + "hiddenSeries": false, "id": 36, "legend": { "avg": false, @@ -2702,8 +3160,12 @@ "linewidth": 1, "links": [], "nullPointMode": "null", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 5, "points": false, "renderer": "flot", @@ -2713,7 +3175,10 @@ "steppedLine": false, "targets": [ { - "expr": "go_goroutines{job=\"argocd-server-metrics\",namespace=~\"$namespace\"}", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_goroutines{job=\"argocd-repo-server-metrics\",namespace=~\"$namespace\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{pod}}", @@ -2721,9 +3186,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Goroutines", "tooltip": { "shared": true, @@ -2732,33 +3195,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -2766,14 +3220,24 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "unitScale": true + }, + "overrides": [] + }, "fill": 1, + "fillGradient": 0, "gridPos": { "h": 9, "w": 24, "x": 0, - "y": 106 + "y": 29 }, + "hiddenSeries": false, "id": 38, "legend": { "avg": false, @@ -2788,8 +3252,12 @@ "linewidth": 1, "links": [], "nullPointMode": "connected", + "options": { + "alertThreshold": true + }, "paceLength": 10, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 5, "points": false, "renderer": "flot", @@ -2799,7 +3267,10 @@ "steppedLine": false, "targets": [ { - "expr": "go_gc_duration_seconds{job=\"argocd-server-metrics\", quantile=\"1\", namespace=~\"$namespace\"}", + "datasource": { + "uid": "$datasource" + }, + "expr": "go_gc_duration_seconds{job=\"argocd-repo-server-metrics\", quantile=\"1\", namespace=~\"$namespace\"}", "format": "time_series", "intervalFactor": 2, "legendFormat": "{{pod}}", @@ -2807,9 +3278,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "GC Time Quantiles", "tooltip": { "shared": true, @@ -2818,33 +3287,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -2853,12 +3313,11 @@ "h": 2, "w": 24, "x": 0, - "y": 115 + "y": 38 }, "id": 54, "links": [], "mode": "markdown", - "title": "", "transparent": true, "type": "text" }, @@ -2867,14 +3326,15 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", - "decimals": null, + "datasource": { + "uid": "$datasource" + }, "fill": 1, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 117 + "y": 40 }, "id": 40, "legend": { @@ -2907,6 +3367,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"application.ApplicationService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", "format": "time_series", "intervalFactor": 1, @@ -2915,9 +3378,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "ApplicationService Requests", "tooltip": { "shared": false, @@ -2926,33 +3387,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -2960,13 +3412,15 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 117 + "y": 40 }, "id": 42, "legend": { @@ -2997,6 +3451,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"cluster.ClusterService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", "format": "time_series", "intervalFactor": 1, @@ -3005,9 +3462,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "ClusterService Requests", "tooltip": { "shared": false, @@ -3016,33 +3471,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -3050,13 +3496,15 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 126 + "y": 49 }, "id": 44, "legend": { @@ -3087,6 +3535,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"project.ProjectService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", "format": "time_series", "intervalFactor": 1, @@ -3095,9 +3546,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "ProjectService Requests", "tooltip": { "shared": true, @@ -3106,33 +3555,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -3140,13 +3580,15 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 126 + "y": 49 }, "id": 46, "legend": { @@ -3176,6 +3618,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"repository.RepositoryService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", "format": "time_series", "intervalFactor": 1, @@ -3192,9 +3637,7 @@ "yaxis": "left" } ], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "RepositoryService Requests", "tooltip": { "shared": true, @@ -3203,33 +3646,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -3237,13 +3671,15 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 135 + "y": 58 }, "id": 48, "legend": { @@ -3273,6 +3709,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"session.SessionService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", "format": "time_series", "intervalFactor": 1, @@ -3281,9 +3720,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "SessionService Requests", "tooltip": { "shared": true, @@ -3292,33 +3729,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -3326,13 +3754,15 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 135 + "y": 58 }, "id": 49, "legend": { @@ -3362,6 +3792,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"version.VersionService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", "format": "time_series", "intervalFactor": 1, @@ -3370,9 +3803,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "VersionService Requests", "tooltip": { "shared": true, @@ -3381,33 +3812,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -3415,13 +3837,15 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "gridPos": { "h": 9, "w": 12, "x": 0, - "y": 144 + "y": 67 }, "id": 50, "legend": { @@ -3451,6 +3875,9 @@ "steppedLine": false, "targets": [ { + "datasource": { + "uid": "$datasource" + }, "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"account.AccountService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", "format": "time_series", "intervalFactor": 1, @@ -3459,9 +3886,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "AccountService Requests", "tooltip": { "shared": true, @@ -3470,33 +3895,24 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } }, { @@ -3504,13 +3920,15 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "$datasource", + "datasource": { + "uid": "$datasource" + }, "fill": 1, "gridPos": { "h": 9, "w": 12, "x": 12, - "y": 144 + "y": 67 }, "id": 99, "legend": { @@ -3540,7 +3958,10 @@ "steppedLine": false, "targets": [ { - "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"settings.SettingsService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(increase(grpc_server_handled_total{job=\"argocd-server-metrics\",grpc_service=\"cluster.SettingsService\",namespace=~\"$namespace\"}[$interval])) by (grpc_code, grpc_method)", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{grpc_code}},{{grpc_method}}", @@ -3548,9 +3969,7 @@ } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "SettingsService Requests", "tooltip": { "shared": true, @@ -3559,42 +3978,44 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } } ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], "title": "Server Stats", "type": "row" }, { "collapsed": true, - "datasource": null, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, "gridPos": { "h": 1, "w": 24, @@ -3608,14 +4029,24 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": null, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "links": [], + "unitScale": true + }, + "overrides": [] + }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, - "y": 9 + "y": 13 }, "hiddenSeries": false, "id": 112, @@ -3632,9 +4063,10 @@ "linewidth": 1, "nullPointMode": "null", "options": { - "dataLinks": [] + "alertThreshold": true }, "percentage": false, + "pluginVersion": "10.3.1", "pointradius": 2, "points": false, "renderer": "flot", @@ -3644,14 +4076,16 @@ "steppedLine": false, "targets": [ { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, "expr": "sum(increase(argocd_redis_request_total{namespace=~\"$namespace\"}[$interval])) by (failed)", "refId": "A" } ], "thresholds": [], - "timeFrom": null, "timeRegions": [], - "timeShift": null, "title": "Requests by result", "tooltip": { "shared": true, @@ -3660,58 +4094,58 @@ }, "type": "graph", "xaxis": { - "buckets": null, "mode": "time", - "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true }, { "format": "short", - "label": null, "logBase": 1, - "max": null, - "min": null, "show": true } ], "yaxis": { - "align": false, - "alignLevel": null + "align": false } } ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A" + } + ], "title": "Redis Stats", "type": "row" } ], - "refresh": false, - "schemaVersion": 21, - "style": "dark", + "refresh": "", + "schemaVersion": 39, "tags": [], "templating": { "list": [ { "current": { + "selected": false, "text": "Prometheus", - "value": "Prometheus" + "value": "prometheus" }, "hide": 0, "includeAll": false, - "label": null, "multi": false, "name": "datasource", "options": [], "query": "prometheus", + "queryValue": "", "refresh": 1, "regex": "", "skipUrlSync": false, @@ -3724,11 +4158,13 @@ "text": "All", "value": "$__all" }, - "datasource": "$datasource", + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, "definition": "label_values(kube_pod_info, namespace)", "hide": 0, "includeAll": true, - "label": null, "multi": false, "name": "namespace", "options": [], @@ -3738,7 +4174,6 @@ "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", - "tags": [], "tagsQuery": "", "type": "query", "useTags": false @@ -3753,7 +4188,6 @@ "value": "$__auto_interval_interval" }, "hide": 0, - "label": null, "name": "interval", "options": [ { @@ -3810,13 +4244,12 @@ { "allValue": "", "current": { - "selected": true, + "selected": false, "text": "namespace", "value": "namespace" }, "hide": 0, "includeAll": false, - "label": null, "multi": false, "name": "grouping", "options": [ @@ -3837,6 +4270,7 @@ } ], "query": "namespace,name,project", + "queryValue": "", "skipUrlSync": false, "type": "custom" }, @@ -3847,11 +4281,13 @@ "text": "All", "value": "$__all" }, - "datasource": "$datasource", + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, "definition": "label_values(argocd_cluster_info, server)", "hide": 0, "includeAll": true, - "label": null, "multi": false, "name": "cluster", "options": [], @@ -3861,7 +4297,6 @@ "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", - "tags": [], "tagsQuery": "", "type": "query", "useTags": false @@ -3869,13 +4304,12 @@ { "allValue": ".*", "current": { - "selected": true, + "selected": false, "text": "All", "value": "$__all" }, "hide": 0, "includeAll": true, - "label": null, "multi": false, "name": "health_status", "options": [ @@ -3922,13 +4356,12 @@ { "allValue": ".*", "current": { - "selected": true, + "selected": false, "text": "All", "value": "$__all" }, "hide": 0, "includeAll": true, - "label": null, "multi": false, "name": "sync_status", "options": [ @@ -3991,5 +4424,6 @@ "timezone": "", "title": "ArgoCD", "uid": "LCAgc9rWz", - "version": 1 + "version": 2, + "weekStart": "" } \ No newline at end of file diff --git a/examples/k8s-rbac/argocd-server-applications/argocd-notifications-controller-rbac-clusterrole.yaml b/examples/k8s-rbac/argocd-server-applications/argocd-notifications-controller-rbac-clusterrole.yaml index 05f92abb117173..ecbf6de3efb01d 100644 --- a/examples/k8s-rbac/argocd-server-applications/argocd-notifications-controller-rbac-clusterrole.yaml +++ b/examples/k8s-rbac/argocd-server-applications/argocd-notifications-controller-rbac-clusterrole.yaml @@ -16,4 +16,13 @@ rules: - list - watch - update - - patch \ No newline at end of file + - patch +- apiGroups: + - "" + resources: + - secrets + - configmaps + verbs: + - get + - list + - watch \ No newline at end of file diff --git a/go.mod b/go.mod index 2985d9a693d169..bc92aeaea190f0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/argoproj/argo-cd/v2 -go 1.19 +go 1.21 + +toolchain go1.21.0 require ( code.gitea.io/sdk/gitea v0.15.1 @@ -11,10 +13,10 @@ require ( github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d github.com/alicebob/miniredis/v2 v2.30.4 github.com/antonmedv/expr v1.15.2 - github.com/argoproj/gitops-engine v0.7.1-0.20231102154024-c0c2dd1f6f48 - github.com/argoproj/notifications-engine v0.4.1-0.20230905144632-9dcecdc3eebf + github.com/argoproj/gitops-engine v0.7.1-0.20240124052710-5fd9f449e757 + github.com/argoproj/notifications-engine v0.4.1-0.20240206192038-2daee6022f41 github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1 - github.com/aws/aws-sdk-go v1.44.317 + github.com/aws/aws-sdk-go v1.50.8 github.com/bmatcuk/doublestar/v4 v4.6.0 github.com/bombsimon/logrusr/v2 v2.0.1 github.com/bradleyfalzon/ghinstallation/v2 v2.6.0 @@ -23,11 +25,13 @@ require ( github.com/coreos/go-oidc/v3 v3.6.0 github.com/cyphar/filepath-securejoin v0.2.4 github.com/dustin/go-humanize v1.0.1 - github.com/evanphx/json-patch v5.6.0+incompatible + github.com/evanphx/json-patch v5.9.0+incompatible + github.com/felixge/httpsnoop v1.0.3 github.com/fsnotify/fsnotify v1.6.0 github.com/gfleury/go-bitbucket-v1 v0.0.0-20220301131131-8e7ed04b843e - github.com/go-git/go-git/v5 v5.8.1 - github.com/go-logr/logr v1.2.4 + github.com/go-git/go-git/v5 v5.11.0 + github.com/go-jose/go-jose/v3 v3.0.1 + github.com/go-logr/logr v1.3.0 github.com/go-openapi/loads v0.21.2 github.com/go-openapi/runtime v0.26.0 github.com/go-playground/webhooks/v6 v6.3.0 @@ -41,7 +45,7 @@ require ( github.com/google/go-github/v35 v35.3.0 github.com/google/go-jsonnet v0.20.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 github.com/gorilla/handlers v1.5.1 github.com/gorilla/websocket v1.5.0 github.com/gosimple/slug v1.13.1 @@ -76,34 +80,33 @@ require ( github.com/xanzy/go-gitlab v0.91.1 github.com/yuin/gopher-lua v1.1.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0 - go.opentelemetry.io/otel v1.16.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 - go.opentelemetry.io/otel/sdk v1.16.0 - golang.org/x/crypto v0.14.0 + go.opentelemetry.io/otel v1.21.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 + go.opentelemetry.io/otel/sdk v1.21.0 + golang.org/x/crypto v0.17.0 golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/oauth2 v0.11.0 golang.org/x/sync v0.3.0 - golang.org/x/term v0.13.0 - google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 - google.golang.org/grpc v1.58.3 + golang.org/x/term v0.15.0 + google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d + google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 - gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.26.4 - k8s.io/apiextensions-apiserver v0.26.4 - k8s.io/apimachinery v0.26.4 - k8s.io/apiserver v0.26.4 - k8s.io/client-go v0.26.4 - k8s.io/code-generator v0.26.4 + k8s.io/api v0.26.11 + k8s.io/apiextensions-apiserver v0.26.10 + k8s.io/apimachinery v0.26.11 + k8s.io/apiserver v0.26.11 + k8s.io/client-go v0.26.11 + k8s.io/code-generator v0.26.11 k8s.io/klog/v2 v2.100.1 k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f k8s.io/kubectl v0.26.4 k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 layeh.com/gopher-json v0.0.0-20190114024228-97fed8db8427 oras.land/oras-go/v2 v2.3.0 - sigs.k8s.io/controller-runtime v0.14.6 - sigs.k8s.io/structured-merge-diff/v4 v4.3.0 + sigs.k8s.io/controller-runtime v0.14.7 + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 sigs.k8s.io/yaml v1.3.0 ) @@ -113,34 +116,40 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v0.5.2 // indirect - github.com/aws/aws-sdk-go-v2 v1.17.3 // indirect - github.com/aws/aws-sdk-go-v2/config v1.18.8 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.8 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect - github.com/aws/aws-sdk-go-v2/service/sqs v1.20.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 // indirect - github.com/aws/smithy-go v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect + github.com/aws/aws-sdk-go-v2/config v1.25.12 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.29.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect + github.com/aws/smithy-go v1.19.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/google/s2a-go v0.1.4 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect + go.opencensus.io v0.24.0 // indirect + google.golang.org/api v0.132.0 // indirect + google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect gopkg.in/retry.v1 v1.0.3 // indirect k8s.io/klog v1.0.0 // indirect nhooyr.io/websocket v1.8.7 // indirect ) require ( - cloud.google.com/go/compute v1.21.0 // indirect + cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect @@ -153,9 +162,8 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/PagerDuty/go-pagerduty v1.7.0 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20210112200207-10ab4d695d60 // indirect - github.com/acomagu/bufpipe v1.0.4 // indirect github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -173,13 +181,11 @@ require ( github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect github.com/fatih/camelcase v1.0.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fvbommel/sortorder v1.0.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.4.1 // indirect - github.com/go-jose/go-jose/v3 v3.0.0 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/analysis v0.21.4 // indirect github.com/go-openapi/errors v0.20.3 // indirect @@ -190,7 +196,7 @@ require ( github.com/go-openapi/swag v0.22.3 // indirect github.com/go-openapi/validate v0.22.1 // indirect github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect - github.com/golang/glog v1.1.0 // indirect + github.com/golang/glog v1.1.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic v0.6.9 // indirect @@ -201,7 +207,7 @@ require ( github.com/gosimple/unidecode v1.0.1 // indirect github.com/gregdel/pushover v1.2.1 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-version v1.2.1 // indirect github.com/huandu/xstrings v1.3.3 // indirect @@ -237,7 +243,7 @@ require ( github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect + github.com/prometheus/client_model v0.3.0 github.com/prometheus/common v0.42.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect @@ -245,7 +251,7 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect - github.com/skeema/knownhosts v1.2.0 // indirect + github.com/skeema/knownhosts v1.2.1 // indirect github.com/slack-go/slack v0.12.2 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/stretchr/objx v0.5.0 // indirect @@ -256,18 +262,17 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xlab/treeprint v1.1.0 // indirect go.mongodb.org/mongo-driver v1.11.3 // indirect - go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect - go.opentelemetry.io/otel/metric v1.16.0 // indirect - go.opentelemetry.io/otel/trace v1.16.0 // indirect - go.opentelemetry.io/proto/otlp v0.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd // indirect - golang.org/x/mod v0.9.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/net v0.19.0 + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 - golang.org/x/tools v0.7.0 // indirect + golang.org/x/tools v0.13.0 // indirect gomodules.xyz/envconfig v1.3.1-0.20190308184047-426f31af0d45 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect gomodules.xyz/notify v0.1.1 // indirect @@ -276,12 +281,12 @@ require ( gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - k8s.io/cli-runtime v0.26.4 // indirect - k8s.io/component-base v0.26.4 // indirect - k8s.io/component-helpers v0.26.4 // indirect + k8s.io/cli-runtime v0.26.11 // indirect + k8s.io/component-base v0.26.11 // indirect + k8s.io/component-helpers v0.26.11 // indirect k8s.io/gengo v0.0.0-20220902162205-c0856e24416d // indirect k8s.io/kube-aggregator v0.26.4 // indirect - k8s.io/kubernetes v1.26.4 // indirect + k8s.io/kubernetes v1.26.11 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.12.1 // indirect sigs.k8s.io/kustomize/kyaml v0.13.9 // indirect @@ -300,34 +305,34 @@ replace ( // Avoid CVE-2022-28948 gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 - k8s.io/api => k8s.io/api v0.26.4 - k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.26.4 - k8s.io/apimachinery => k8s.io/apimachinery v0.26.4 - k8s.io/apiserver => k8s.io/apiserver v0.26.4 - k8s.io/cli-runtime => k8s.io/cli-runtime v0.26.4 - k8s.io/client-go => k8s.io/client-go v0.26.4 - k8s.io/cloud-provider => k8s.io/cloud-provider v0.26.4 - k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.26.4 - k8s.io/code-generator => k8s.io/code-generator v0.26.4 - k8s.io/component-base => k8s.io/component-base v0.26.4 - k8s.io/component-helpers => k8s.io/component-helpers v0.26.4 - k8s.io/controller-manager => k8s.io/controller-manager v0.26.4 - k8s.io/cri-api => k8s.io/cri-api v0.26.4 - k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.26.4 - k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.26.4 - k8s.io/kms => k8s.io/kms v0.26.4 - k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.26.4 - k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.26.4 - k8s.io/kube-proxy => k8s.io/kube-proxy v0.26.4 - k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.26.4 - k8s.io/kubectl => k8s.io/kubectl v0.26.4 - k8s.io/kubelet => k8s.io/kubelet v0.26.4 - k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.26.4 - k8s.io/metrics => k8s.io/metrics v0.26.4 - k8s.io/mount-utils => k8s.io/mount-utils v0.26.4 - k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.26.4 - k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.26.4 - k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.26.4 - k8s.io/sample-controller => k8s.io/sample-controller v0.26.4 + k8s.io/api => k8s.io/api v0.26.11 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.26.11 + k8s.io/apimachinery => k8s.io/apimachinery v0.26.11 + k8s.io/apiserver => k8s.io/apiserver v0.26.11 + k8s.io/cli-runtime => k8s.io/cli-runtime v0.26.11 + k8s.io/client-go => k8s.io/client-go v0.26.11 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.26.11 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.26.11 + k8s.io/code-generator => k8s.io/code-generator v0.26.11 + k8s.io/component-base => k8s.io/component-base v0.26.11 + k8s.io/component-helpers => k8s.io/component-helpers v0.26.11 + k8s.io/controller-manager => k8s.io/controller-manager v0.26.11 + k8s.io/cri-api => k8s.io/cri-api v0.26.11 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.26.11 + k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.26.11 + k8s.io/kms => k8s.io/kms v0.26.11 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.26.11 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.26.11 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.26.11 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.26.11 + k8s.io/kubectl => k8s.io/kubectl v0.26.11 + k8s.io/kubelet => k8s.io/kubelet v0.26.11 + k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.26.11 + k8s.io/metrics => k8s.io/metrics v0.26.11 + k8s.io/mount-utils => k8s.io/mount-utils v0.26.11 + k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.26.11 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.26.11 + k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.26.11 + k8s.io/sample-controller => k8s.io/sample-controller v0.26.11 ) diff --git a/go.sum b/go.sum index bf38fae9acb7d5..f95399cf6c204c 100644 --- a/go.sum +++ b/go.sum @@ -174,8 +174,8 @@ cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOV cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= -cloud.google.com/go/compute v1.21.0 h1:JNBsyXVoOoNJtTQcnEY5uYpZIbeCTYIeDe0Xh1bySMk= -cloud.google.com/go/compute v1.21.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= +cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= +cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= @@ -657,8 +657,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/PagerDuty/go-pagerduty v1.7.0 h1:S1NcMKECxT5hJwV4VT+QzeSsSiv4oWl1s2821dUqG/8= github.com/PagerDuty/go-pagerduty v1.7.0/go.mod h1:PuFyJKRz1liIAH4h5KVXVD18Obpp1ZXRdxHvmGXooro= github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95 h1:KLq8BE0KwCL+mmXnjLWEAOYO+2l2AE4YMmqG1ZpZHBs= -github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 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/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20210112200207-10ab4d695d60 h1:prBTRx78AQnXzivNT9Crhu564W/zPPr3ibSlpT9xKcE= @@ -668,8 +668,6 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d h1:WtAMR0fPCOfK7TPGZ8ZpLLY18HRvL7XJ3xcs0wnREgo= github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d/go.mod h1:WML6KOYjeU8N6YyusMjj2qRvaPNUEvrQvaxuFcMRFJY= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= -github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= @@ -686,6 +684,7 @@ github.com/alicebob/miniredis/v2 v2.30.4 h1:8S4/o1/KoUArAGbGwPxcwf0krlzceva2XVOS github.com/alicebob/miniredis/v2 v2.30.4/go.mod h1:b25qWj4fCEsBeAAR2mlb0ufImGC6uH3VlUfb/HS5zKg= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antonmedv/expr v1.15.2 h1:afFXpDWIC2n3bF+kTZE1JvFo+c34uaM3sTqh8z0xfdU= github.com/antonmedv/expr v1.15.2/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= @@ -695,10 +694,10 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE= -github.com/argoproj/gitops-engine v0.7.1-0.20231102154024-c0c2dd1f6f48 h1:vnXMrNkBFC0H0KBkH1Jno31OVgJQR4KSd0ypEcfzQRA= -github.com/argoproj/gitops-engine v0.7.1-0.20231102154024-c0c2dd1f6f48/go.mod h1:1TchqKw9XmYYZluyEHa1dTJQoZgbV6PhabB/e8Wf3KY= -github.com/argoproj/notifications-engine v0.4.1-0.20230905144632-9dcecdc3eebf h1:4wliaBwd6iKvT/5huDTJntaYtTSdwPLs00SOQwDSK6A= -github.com/argoproj/notifications-engine v0.4.1-0.20230905144632-9dcecdc3eebf/go.mod h1:TuK0BNKo34DIUOyCCGOB9ij+smGCxeCgt9ZB+0fMWno= +github.com/argoproj/gitops-engine v0.7.1-0.20240124052710-5fd9f449e757 h1:5fKAhTQcTBom0vin56cz/UTPx2GMuvdb+lJRAUOPbHA= +github.com/argoproj/gitops-engine v0.7.1-0.20240124052710-5fd9f449e757/go.mod h1:gWE8uROi7hIkWGNAVM+8FWkMfo0vZ03SLx/aFw/DBzg= +github.com/argoproj/notifications-engine v0.4.1-0.20240206192038-2daee6022f41 h1:PQE8LbcbRHdtnQzeEWwVU2QHXACKOA30yS3No5HSoTQ= +github.com/argoproj/notifications-engine v0.4.1-0.20240206192038-2daee6022f41/go.mod h1:TsyusmXQWIL0ST7YMRG/ered7WlWDmbmnPpXnS2LJmM= github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1 h1:qsHwwOJ21K2Ao0xPju1sNuqphyMnMYkyB3ZLoLtxWpo= github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1/go.mod h1:CZHlkyAD1/+FbEn6cB2DQTj48IoLGvEYsWEvtzP3238= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -714,35 +713,37 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:W github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.44.289/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-sdk-go v1.44.317 h1:+8XWrLmGMwPPXSRSLPzhgcGnzJ2mYkgkrcB9C/GnSOU= -github.com/aws/aws-sdk-go v1.44.317/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.50.8 h1:gY0WoOW+/Wz6XmYSgDH9ge3wnAevYDSQWPxxJvqAkP4= +github.com/aws/aws-sdk-go v1.50.8/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY= -github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= -github.com/aws/aws-sdk-go-v2/config v1.18.8 h1:lDpy0WM8AHsywOnVrOHaSMfpaiV2igOw8D7svkFkXVA= -github.com/aws/aws-sdk-go-v2/config v1.18.8/go.mod h1:5XCmmyutmzzgkpk/6NYTjeWb6lgo9N170m1j6pQkIBs= -github.com/aws/aws-sdk-go-v2/credentials v1.13.8 h1:vTrwTvv5qAwjWIGhZDSBH/oQHuIQjGmD232k01FUh6A= -github.com/aws/aws-sdk-go-v2/credentials v1.13.8/go.mod h1:lVa4OHbvgjVot4gmh1uouF1ubgexSCN92P6CJQpT0t8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 h1:j9wi1kQ8b+e0FBVHxCqCGo4kxDU175hoDHcWAi0sauU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21/go.mod h1:ugwW57Z5Z48bpvUyZuaPy4Kv+vEfJWnIrky7RmkBvJg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 h1:KeTxcGdNnQudb46oOl4d90f2I33DF/c6q3RnZAmvQdQ= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28/go.mod h1:yRZVr/iT0AqyHeep00SZ4YfBAKojXz08w3XMBscdi0c= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 h1:5C6XgTViSb0bunmU57b3CT+MhxULqHH2721FVA+/kDM= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21/go.mod h1:lRToEJsn+DRA9lW4O9L9+/3hjTkUzlzyzHqn8MTds5k= -github.com/aws/aws-sdk-go-v2/service/sqs v1.20.0 h1:tQoMg8i4nFAB70cJ4wiAYEiZRYo2P6uDmU2D6ys/igo= -github.com/aws/aws-sdk-go-v2/service/sqs v1.20.0/go.mod h1:jQhN5f4p3PALMNlUtfb/0wGIFlV7vGtJlPDVfxfNfPY= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 h1:/2gzjhQowRLarkkBOGPXSRnb8sQ2RVsjdG1C/UliK/c= -github.com/aws/aws-sdk-go-v2/service/sso v1.12.0/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 h1:Jfly6mRxk2ZOSlbCvZfKNS7TukSx1mIzhSsqZ/IGSZI= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0/go.mod h1:TZSH7xLO7+phDtViY/KUp9WGCJMQkLJ/VpgkTFd5gh8= -github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 h1:kOO++CYo50RcTFISESluhWEi5Prhg+gaSs4whWabiZU= -github.com/aws/aws-sdk-go-v2/service/sts v1.18.0/go.mod h1:+lGbb3+1ugwKrNTWcf2RT05Xmp543B06zDFTwiTLp7I= -github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= -github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= +github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU= +github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4= +github.com/aws/aws-sdk-go-v2/config v1.25.12 h1:mF4cMuNh/2G+d19nWnm1vJ/ak0qK6SbqF0KtSX9pxu0= +github.com/aws/aws-sdk-go-v2/config v1.25.12/go.mod h1:lOvvqtZP9p29GIjOTuA/76HiVk0c/s8qRcFRq2+E2uc= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8= +github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1 h1:uR9lXYjdPX0xY+NhvaJ4dD8rpSRz5VY81ccIIoNG+lw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.7.1/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino= +github.com/aws/aws-sdk-go-v2/service/sqs v1.29.7 h1:tRNrFDGRm81e6nTX5Q4CFblea99eAfm0dxXazGpLceU= +github.com/aws/aws-sdk-go-v2/service/sqs v1.29.7/go.mod h1:8GWUDux5Z2h6z2efAtr54RdHXtLm8sq7Rg85ZNY/CZM= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow= +github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0= +github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U= +github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM= +github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/beevik/ntp v0.2.0/go.mod h1:hIHWr+l3+/clUnF44zdK+CWW7fO8dR5cIylAQ76NRpg= 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= @@ -761,7 +762,9 @@ github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/bradleyfalzon/ghinstallation/v2 v2.6.0 h1:IRY7Xy588KylkoycsUhFpW7cdGpy5Y5BPsz4IfuJtGk= github.com/bradleyfalzon/ghinstallation/v2 v2.6.0/go.mod h1:oQ3etOwN3TRH4EwgW5/7MxSVMGlMlzG/O8TU7eYdoSk= github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao= +github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= @@ -805,6 +808,7 @@ github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230310173818-32f1caf87195/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= @@ -833,6 +837,7 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZm github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= +github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -845,7 +850,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= @@ -868,10 +874,11 @@ github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= +github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 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/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= @@ -892,6 +899,7 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -907,6 +915,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= @@ -916,16 +925,17 @@ github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2H github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= -github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A= -github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= +github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= 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-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo= -github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= +github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA= +github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= @@ -942,11 +952,12 @@ github.com/go-logr/logr v1.0.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/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/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.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY= github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= @@ -1057,8 +1068,9 @@ github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 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/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -1148,19 +1160,23 @@ github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLe github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.0/go.mod h1:OJpEgntRZo8ugHpF9hkoLJbS5dSI20XZeXJ9JVywLlM= github.com/google/s2a-go v0.1.3/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= +github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM= +github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w= 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/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -1175,6 +1191,8 @@ github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38 github.com/googleapis/gax-go/v2 v2.8.0/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 h1:4EZlYQIiyecYJlUbVkFXCXHz1QPhVXcHnQKAzBTPfQo= @@ -1209,8 +1227,9 @@ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgf github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 h1:lLT7ZLSzGLI08vc9cpd+tYmNWjdKDqyr/2L+f6U12Fk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -1327,6 +1346,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 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= @@ -1357,8 +1377,6 @@ github.com/malexdev/utfutil v0.0.0-20180510171754-00c8d4a8e7a8 h1:A6SLdFpRzUUF5v github.com/malexdev/utfutil v0.0.0-20180510171754-00c8d4a8e7a8/go.mod h1:UtpLyb/EupVKXF/N0b4NRe1DNg+QYJsnsHQ038romhM= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -1478,8 +1496,9 @@ github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ github.com/onsi/gomega v1.23.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= -github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -1575,8 +1594,9 @@ github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +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/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -1606,8 +1626,8 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= -github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c h1:fyKiXKO1/I/B6Y2U8T7WdQGWzwehOuGIrljPtt7YTTI= github.com/skratchdot/open-golang v0.0.0-20160302144031-75fb7ed4208c/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= @@ -1726,27 +1746,27 @@ 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.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0 h1:ZOLJc06r4CB42laIXg/7udr0pbZyuAihN10A/XuiQRY= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0/go.mod h1:5z+/ZWJQKXa9YT34fQNx5K8Hd1EoIhvtUygUQPqEOgQ= -go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= -go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo= -go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= -go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= -go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= -go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= -go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= -go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd h1:Uo/x0Ir5vQJ+683GXB9Ug+4fcjsbp7z7Ul8UaZbhsRM= go.starlark.net v0.0.0-20220328144851-d1966c6b9fcd/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -1755,7 +1775,8 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +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.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= @@ -1765,6 +1786,7 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1794,8 +1816,9 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0 golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1857,8 +1880,9 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 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= @@ -1939,8 +1963,9 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -2111,8 +2136,9 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.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/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2126,8 +2152,9 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -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/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2146,13 +2173,13 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -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/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -2237,8 +2264,9 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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= @@ -2324,6 +2352,8 @@ google.golang.org/api v0.118.0/go.mod h1:76TtD3vkgmZ66zZzp72bUUklpmQmKlhh6sYtIjY google.golang.org/api v0.122.0/go.mod h1:gcitW0lvnyWjSp9nKxAbdHKIZ6vF4aajGueeslZOyms= google.golang.org/api v0.124.0/go.mod h1:xu2HQurE5gi/3t1aFCvhPD781p0a3p11sdunTJ2BlP4= google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.132.0 h1:8t2/+qZ26kAOGSmOiHwVycqVaDg7q3JDILrNi/Z6rvc= +google.golang.org/api v0.132.0/go.mod h1:AeTBC6GpJnJSRJjktDcPX0QwtS8pGYZOV6MSuSCusw0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2473,21 +2503,21 @@ google.golang.org/genproto v0.0.0-20230403163135-c38d8f061ccd/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230525234025-438c736192d0/go.mod h1:9ExIQyXL5hZrHzQceCwuSYwZZ5QZBazOcprJ5rgs3lY= google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g= -google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98/go.mod h1:S7mY02OqCJTD0E1OiQy1F72PWFB4bZJ87cAtLPYgDR0= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY= +google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= -google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d h1:DoPTO70H+bcDXcd39vOqb2viZxgqeBeSGtZ55yZU4/Q= +google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -2535,8 +2565,8 @@ google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5v google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= -google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= -google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= 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= @@ -2571,8 +2601,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/retry.v1 v1.0.3 h1:a9CArYczAVv6Qs6VGoLMio99GEs7kY9UzSF9+LD+iGs= gopkg.in/retry.v1 v1.0.3/go.mod h1:FJkXmWiMaAo7xB+xhvDF59zhfjDWyzmyAxiT4dB688g= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= @@ -2583,6 +2611,7 @@ 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-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -2592,24 +2621,24 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt 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= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -k8s.io/api v0.26.4 h1:qSG2PmtcD23BkYiWfoYAcak870eF/hE7NNYBYavTT94= -k8s.io/api v0.26.4/go.mod h1:WwKEXU3R1rgCZ77AYa7DFksd9/BAIKyOmRlbVxgvjCk= -k8s.io/apiextensions-apiserver v0.26.4 h1:9D2RTxYGxrG5uYg6D7QZRcykXvavBvcA59j5kTaedQI= -k8s.io/apiextensions-apiserver v0.26.4/go.mod h1:cd4uGFGIgzEqUghWpRsr9KE8j2KNTjY8Ji8pnMMazyw= -k8s.io/apimachinery v0.26.4 h1:rZccKdBLg9vP6J09JD+z8Yr99Ce8gk3Lbi9TCx05Jzs= -k8s.io/apimachinery v0.26.4/go.mod h1:ats7nN1LExKHvJ9TmwootT00Yz05MuYqPXEXaVeOy5I= -k8s.io/apiserver v0.26.4 h1:3Oq4mnJv0mzVX7BR/Nod+8KjlELf/3Ljvu9ZWDyLUoA= -k8s.io/apiserver v0.26.4/go.mod h1:yAY3O1vBM4/0OIGAGeWcdfzQvgdwJ188VirLcuSAVnw= -k8s.io/cli-runtime v0.26.4 h1:MgSU871KDzBDX7V9GtuqS6Ai9lhQCHgRzkurnXOWtZ0= -k8s.io/cli-runtime v0.26.4/go.mod h1:MjJ2DXMChw2zcG0/agzm17xwKpfVxOfuoCdfY9iOCOE= -k8s.io/client-go v0.26.4 h1:/7P/IbGBuT73A+G97trf44NTPSNqvuBREpOfdLbHvD4= -k8s.io/client-go v0.26.4/go.mod h1:6qOItWm3EwxJdl/8p5t7FWtWUOwyMdA8N9ekbW4idpI= -k8s.io/code-generator v0.26.4 h1:zgDD0qX13p/jtrAoYRRiYeQ5ibnriwmo2cMkMZAtJxc= -k8s.io/code-generator v0.26.4/go.mod h1:ryaiIKwfxEJEaywEzx3dhWOydpVctKYbqLajJf0O8dI= -k8s.io/component-base v0.26.4 h1:Bg2xzyXNKL3eAuiTEu3XE198d6z22ENgFgGQv2GGOUk= -k8s.io/component-base v0.26.4/go.mod h1:lTuWL1Xz/a4e80gmIC3YZG2JCO4xNwtKWHJWeJmsq20= -k8s.io/component-helpers v0.26.4 h1:qbZrh8QmfL+Yn7lWEI/BPrvITGgkBy33djP5Tzsu2hA= -k8s.io/component-helpers v0.26.4/go.mod h1:2Siz5eWmaKu0khASXMTCfJuASZAbCPX9mtjlCe5IWRs= +k8s.io/api v0.26.11 h1:hLhTZRdYc3vBBOY4wbEyTLWgMyieOAk2Ws9NG57QqO4= +k8s.io/api v0.26.11/go.mod h1:bSr/A0TKRt5W2OMDdexkM/ER1NxOxiQqNNFXW2nMZrM= +k8s.io/apiextensions-apiserver v0.26.11 h1:6/T0Jm9c+Aw1AYUflPOz2sAsty304/DDSkciTr8+HuE= +k8s.io/apiextensions-apiserver v0.26.11/go.mod h1:xMqWxAB+AvSTdmFRVWlpavY9bJl/3g6yWiPn/fwZbT0= +k8s.io/apimachinery v0.26.11 h1:w//840HHdwSRKqD15j9YX9HLlU6RPlfrvW0xEhLk2+0= +k8s.io/apimachinery v0.26.11/go.mod h1:2/HZp0l6coXtS26du1Bk36fCuAEr/lVs9Q9NbpBtd1Y= +k8s.io/apiserver v0.26.11 h1:JcrlATLu5xQVLV7/rfRFFl9ivvNLmZH0dM3DFFdFp+w= +k8s.io/apiserver v0.26.11/go.mod h1:htEG/Q3sI3+6Is3Z26QzBjaCGICsz/kFj+IhIP4oJuE= +k8s.io/cli-runtime v0.26.11 h1:HO3Sgf06XkT8/8wWnhskfz4+LMKrChRz+A13vDJSQrE= +k8s.io/cli-runtime v0.26.11/go.mod h1:D98GjQtDmqn7WDuKBgWivd6R8qEs3yzT19EmCM5pqBs= +k8s.io/client-go v0.26.11 h1:RjfZr5+vQjjTRmk4oCqHyC0cgrZXPjw+X+ge35sk4GI= +k8s.io/client-go v0.26.11/go.mod h1:+emNszw9va/uRJIM5ALTBtFnlZMTjwBrNjRfEh0iuw8= +k8s.io/code-generator v0.26.11 h1:S0PJxapUhG6LWYezYB/FVE5Gf4BxGY0fCwnLrwfQ/70= +k8s.io/code-generator v0.26.11/go.mod h1:Hjxj7hpvSxcNnYIWzCSuEdwN0/9aHlezQRKJXr0Kv8U= +k8s.io/component-base v0.26.11 h1:1/JmB6fexefGByfFyIK6aHksZZVtaDskttzXOzmZ6zA= +k8s.io/component-base v0.26.11/go.mod h1:jYNisnoM6iWFRUg51pxaQabzL5fBYTr5CMpsLjUYGp0= +k8s.io/component-helpers v0.26.11 h1:XD2/2lik/5n1WFepDvgHzIGL0tix/EU3GaxGJHdsgkA= +k8s.io/component-helpers v0.26.11/go.mod h1:lw3bchkI0NHMPmb+CE73GznPW0Mvqd/Y9UVMEqBkysE= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20220902162205-c0856e24416d h1:U9tB195lKdzwqicbJvyJeOXV7Klv+wNAWENRnXEGi08= k8s.io/gengo v0.0.0-20220902162205-c0856e24416d/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -2621,15 +2650,15 @@ k8s.io/klog/v2 v2.5.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-aggregator v0.26.4 h1:iGljhq5exQkbuc3bnkwUx95RPCBDExg7DkX9XaYhg6w= -k8s.io/kube-aggregator v0.26.4/go.mod h1:eWfg4tU0+l57ebWiS5THOANIJUrKRxudSVDJ+63bqvQ= +k8s.io/kube-aggregator v0.26.11 h1:P46aQPWOE+8bTbK2cqxUFP1XwH4ShZEHnlk1T5QFT8U= +k8s.io/kube-aggregator v0.26.11/go.mod h1:XNGLFzn4Ex7qFVqpCnvLUr354EM4QhMFuFSoB6JHmL4= k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= 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/kubectl v0.26.4 h1:A0Oa0u/po4KxXnXsNCOwLojAe9cQR3TJNJabEIf7U1w= -k8s.io/kubectl v0.26.4/go.mod h1:cWtp/+I4p+h5En3s2zO1zCry9v3/6h37EQ2tF3jNRnM= -k8s.io/kubernetes v1.26.4 h1:/c00/JjOltBwhNOBs+hO433Q3fRyeWWtyYF6z2xtmag= -k8s.io/kubernetes v1.26.4/go.mod h1:NxzR7U7mS+OGa3J/qweI86Pek//mlfHqDgt6NNGdz8g= +k8s.io/kubectl v0.26.11 h1:cVPzYA4HKefU3tPiVK7hZpJ+5Lm04XoyvCCY5ODznpQ= +k8s.io/kubectl v0.26.11/go.mod h1:xjEX/AHtEQrGj2AGqVopyHr/JU1hLy1k7Yn48JuK9LQ= +k8s.io/kubernetes v1.26.11 h1:g3r1IAUqsaHnOG2jdpoagJ5W9UCXkR2ljQ/7BmCzPNg= +k8s.io/kubernetes v1.26.11/go.mod h1:z1URAaBJ+XnOTr3Q/l4umxRUxn/OyD2fbkUgS0Bl7u4= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20221107191617-1a15be271d1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 h1:kmDqav+P+/5e1i9tFfHq1qcF3sOrDp+YEkVDAHu7Jwk= @@ -2679,8 +2708,8 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 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/controller-runtime v0.14.6 h1:oxstGVvXGNnMvY7TAESYk+lzr6S3V5VFxQ6d92KcwQA= -sigs.k8s.io/controller-runtime v0.14.6/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= +sigs.k8s.io/controller-runtime v0.14.7 h1:Vrnm2vk9ZFlRkXATHz0W0wXcqNl7kPat8q2JyxVy0Q8= +sigs.k8s.io/controller-runtime v0.14.7/go.mod h1:ErTs3SJCOujNUnTz4AS+uh8hp6DHMo1gj6fFndJT1X8= sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= @@ -2689,8 +2718,8 @@ sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCY sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk= -sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +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.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/hack/generate-proto.sh b/hack/generate-proto.sh index 8466993ebc5443..fa5d7322c7f81d 100755 --- a/hack/generate-proto.sh +++ b/hack/generate-proto.sh @@ -10,9 +10,13 @@ set -o nounset set -o pipefail # shellcheck disable=SC2128 -PROJECT_ROOT=$(cd "$(dirname "${BASH_SOURCE}")"/..; pwd) +PROJECT_ROOT=$( + cd "$(dirname "${BASH_SOURCE}")"/.. + pwd +) PATH="${PROJECT_ROOT}/dist:${PATH}" GOPATH=$(go env GOPATH) +GOPATH_PROJECT_ROOT="${GOPATH}/src/github.com/argoproj/argo-cd" # output tool versions go version @@ -41,6 +45,7 @@ APIMACHINERY_PKGS=( export GO111MODULE=on [ -e ./v2 ] || ln -s . v2 +[ -e "${GOPATH_PROJECT_ROOT}" ] || (mkdir -p "$(dirname "${GOPATH_PROJECT_ROOT}")" && ln -s "${PROJECT_ROOT}" "${GOPATH_PROJECT_ROOT}") # protoc_include is the include directory containing the .proto files distributed with protoc binary if [ -d /dist/protoc-include ]; then @@ -53,10 +58,17 @@ fi go-to-protobuf \ --go-header-file="${PROJECT_ROOT}"/hack/custom-boilerplate.go.txt \ - --packages="$(IFS=, ; echo "${PACKAGES[*]}")" \ - --apimachinery-packages="$(IFS=, ; echo "${APIMACHINERY_PKGS[*]}")" \ - --proto-import=./vendor \ - --proto-import="${protoc_include}" + --packages="$( + IFS=, + echo "${PACKAGES[*]}" + )" \ + --apimachinery-packages="$( + IFS=, + echo "${APIMACHINERY_PKGS[*]}" + )" \ + --proto-import="${PROJECT_ROOT}"/vendor \ + --proto-import="${protoc_include}" \ + --output-base="${GOPATH}/src/" # Either protoc-gen-go, protoc-gen-gofast, or protoc-gen-gogofast can be used to build # server/*/.pb.go from .proto files. golang/protobuf and gogo/protobuf can be used @@ -86,9 +98,11 @@ for i in ${PROTO_FILES}; do --${GOPROTOBINARY}_out=plugins=grpc:"$GOPATH"/src \ --grpc-gateway_out=logtostderr=true:"$GOPATH"/src \ --swagger_out=logtostderr=true:. \ - $i + "$i" done -[ -e ./v2 ] && rm -rf v2 + +[ -L "${GOPATH_PROJECT_ROOT}" ] && rm -rf "${GOPATH_PROJECT_ROOT}" +[ -L ./v2 ] && rm -rf v2 # collect_swagger gathers swagger files into a subdirectory collect_swagger() { @@ -97,7 +111,7 @@ collect_swagger() { PRIMARY_SWAGGER=$(mktemp) COMBINED_SWAGGER=$(mktemp) - cat < "${PRIMARY_SWAGGER}" + cat <"${PRIMARY_SWAGGER}" { "swagger": "2.0", "info": { @@ -111,7 +125,7 @@ EOF rm -f "${SWAGGER_OUT}" - find "${SWAGGER_ROOT}" -name '*.swagger.json' -exec swagger mixin --ignore-conflicts "${PRIMARY_SWAGGER}" '{}' \+ > "${COMBINED_SWAGGER}" + find "${SWAGGER_ROOT}" -name '*.swagger.json' -exec swagger mixin --ignore-conflicts "${PRIMARY_SWAGGER}" '{}' \+ >"${COMBINED_SWAGGER}" jq -r 'del(.definitions[].properties[]? | select(."$ref"!=null and .description!=null).description) | del(.definitions[].properties[]? | select(."$ref"!=null and .title!=null).title) | # The "array" and "map" fields have custom unmarshaling. Modify the swagger to reflect this. .definitions.v1alpha1ApplicationSourcePluginParameter.properties.array = {"description":"Array is the value of an array type parameter.","type":"array","items":{"type":"string"}} | @@ -120,10 +134,10 @@ EOF del(.definitions.v1alpha1OptionalMap) | # Output for int64 is incorrect, because it is based on proto definitions, where int64 is a string. In our JSON API, we expect int64 to be an integer. https://github.com/grpc-ecosystem/grpc-gateway/issues/219 (.definitions[]?.properties[]? | select(.type == "string" and .format == "int64")) |= (.type = "integer") - ' "${COMBINED_SWAGGER}" | \ - jq '.definitions.v1Time.type = "string" | .definitions.v1Time.format = "date-time" | del(.definitions.v1Time.properties)' | \ - jq '.definitions.v1alpha1ResourceNode.allOf = [{"$ref": "#/definitions/v1alpha1ResourceRef"}] | del(.definitions.v1alpha1ResourceNode.properties.resourceRef) ' \ - > "${SWAGGER_OUT}" + ' "${COMBINED_SWAGGER}" | + jq '.definitions.v1Time.type = "string" | .definitions.v1Time.format = "date-time" | del(.definitions.v1Time.properties)' | + jq '.definitions.v1alpha1ResourceNode.allOf = [{"$ref": "#/definitions/v1alpha1ResourceRef"}] | del(.definitions.v1alpha1ResourceNode.properties.resourceRef) ' \ + >"${SWAGGER_OUT}" /bin/rm "${PRIMARY_SWAGGER}" "${COMBINED_SWAGGER}" } @@ -139,4 +153,3 @@ clean_swagger server clean_swagger reposerver clean_swagger controller clean_swagger cmpserver - diff --git a/hack/installers/checksums/helm-v3.13.2-linux-amd64.tar.gz.sha256 b/hack/installers/checksums/helm-v3.13.2-linux-amd64.tar.gz.sha256 new file mode 100644 index 00000000000000..8908445e50510b --- /dev/null +++ b/hack/installers/checksums/helm-v3.13.2-linux-amd64.tar.gz.sha256 @@ -0,0 +1 @@ +55a8e6dce87a1e52c61e0ce7a89bf85b38725ba3e8deb51d4a08ade8a2c70b2d helm-v3.13.2-linux-amd64.tar.gz diff --git a/hack/installers/checksums/helm-v3.13.2-linux-arm64.tar.gz.sha256 b/hack/installers/checksums/helm-v3.13.2-linux-arm64.tar.gz.sha256 new file mode 100644 index 00000000000000..cf6b333b8d98bf --- /dev/null +++ b/hack/installers/checksums/helm-v3.13.2-linux-arm64.tar.gz.sha256 @@ -0,0 +1 @@ +f5654aaed63a0da72852776e1d3f851b2ea9529cb5696337202703c2e1ed2321 helm-v3.13.2-linux-arm64.tar.gz diff --git a/hack/installers/checksums/helm-v3.13.2-linux-ppc64le.tar.gz.sha256 b/hack/installers/checksums/helm-v3.13.2-linux-ppc64le.tar.gz.sha256 new file mode 100644 index 00000000000000..696df1bb8df5e9 --- /dev/null +++ b/hack/installers/checksums/helm-v3.13.2-linux-ppc64le.tar.gz.sha256 @@ -0,0 +1 @@ +11d96134cc4ec106c23cd8c163072e9aed6cd73e36a3da120e5876d426203f37 helm-v3.13.2-linux-ppc64le.tar.gz diff --git a/hack/installers/checksums/helm-v3.13.2-linux-s390x.tar.gz.sha256 b/hack/installers/checksums/helm-v3.13.2-linux-s390x.tar.gz.sha256 new file mode 100644 index 00000000000000..f539faf320db76 --- /dev/null +++ b/hack/installers/checksums/helm-v3.13.2-linux-s390x.tar.gz.sha256 @@ -0,0 +1 @@ +3ffc5b4a041e5306dc00905ebe5dfea449e34ada268a713d34c69709afd6a9a2 helm-v3.13.2-linux-s390x.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.0-linux-amd64.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.0-linux-amd64.tar.gz.sha256 new file mode 100644 index 00000000000000..6f9aaf5a270d59 --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.0-linux-amd64.tar.gz.sha256 @@ -0,0 +1 @@ +f43e1c3387de24547506ab05d24e5309c0ce0b228c23bd8aa64e9ec4b8206651 helm-v3.14.0-linux-amd64.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.0-linux-arm64.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.0-linux-arm64.tar.gz.sha256 new file mode 100644 index 00000000000000..d0e09bd4b41f7c --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.0-linux-arm64.tar.gz.sha256 @@ -0,0 +1 @@ +b29e61674731b15f6ad3d1a3118a99d3cc2ab25a911aad1b8ac8c72d5a9d2952 helm-v3.14.0-linux-arm64.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.0-linux-ppc64le.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.0-linux-ppc64le.tar.gz.sha256 new file mode 100644 index 00000000000000..d179322b99dd5a --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.0-linux-ppc64le.tar.gz.sha256 @@ -0,0 +1 @@ +f1f9d3561724863edd4c06d89acb2e2fd8ae0f1b72058ceb891fa1c346ce5dbc helm-v3.14.0-linux-ppc64le.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.0-linux-s390x.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.0-linux-s390x.tar.gz.sha256 new file mode 100644 index 00000000000000..31ff04397b29e4 --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.0-linux-s390x.tar.gz.sha256 @@ -0,0 +1 @@ +82298ef39936f1bef848959a29f77bff92d1309d8646657e3a7733702e81288c helm-v3.14.0-linux-s390x.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.1-linux-amd64.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.1-linux-amd64.tar.gz.sha256 new file mode 100644 index 00000000000000..cc06e129863111 --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.1-linux-amd64.tar.gz.sha256 @@ -0,0 +1 @@ +75496ea824f92305ff7d28af37f4af57536bf5138399c824dff997b9d239dd42 helm-v3.14.1-linux-amd64.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.1-linux-arm64.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.1-linux-arm64.tar.gz.sha256 new file mode 100644 index 00000000000000..63f791b234ec45 --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.1-linux-arm64.tar.gz.sha256 @@ -0,0 +1 @@ +f865b8ad4228fd0990bbc5b50615eb6cb9eb31c9a9ca7238401ed897bbbe9033 helm-v3.14.1-linux-arm64.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.1-linux-ppc64le.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.1-linux-ppc64le.tar.gz.sha256 new file mode 100644 index 00000000000000..17b9b1e625fac8 --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.1-linux-ppc64le.tar.gz.sha256 @@ -0,0 +1 @@ +4d853ab8fe3462287c7272fbadd5f73531ecdd6fa0db37d31630e41ae1ae21de helm-v3.14.1-linux-ppc64le.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.1-linux-s390x.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.1-linux-s390x.tar.gz.sha256 new file mode 100644 index 00000000000000..232ec10e03fc68 --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.1-linux-s390x.tar.gz.sha256 @@ -0,0 +1 @@ +19bf07999c7244bfeb0fd27152919b9faa1148cf43910edbb98efa9150058a98 helm-v3.14.1-linux-s390x.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.2-linux-amd64.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.2-linux-amd64.tar.gz.sha256 new file mode 100644 index 00000000000000..22049267fd24ea --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.2-linux-amd64.tar.gz.sha256 @@ -0,0 +1 @@ +0885a501d586c1e949e9b113bf3fb3290b0bbf74db9444a1d8c2723a143006a5 helm-v3.14.2-linux-amd64.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.2-linux-arm64.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.2-linux-arm64.tar.gz.sha256 new file mode 100644 index 00000000000000..17320419ee7e62 --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.2-linux-arm64.tar.gz.sha256 @@ -0,0 +1 @@ +c65d6a9557bb359abc2c0d26670de850b52327dc3976ad6f9e14c298ea3e1b61 helm-v3.14.2-linux-arm64.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.2-linux-ppc64le.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.2-linux-ppc64le.tar.gz.sha256 new file mode 100644 index 00000000000000..8ffe4ebe40e62c --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.2-linux-ppc64le.tar.gz.sha256 @@ -0,0 +1 @@ +f3bc8582ff151e619cd285d9cdf9fef1c5733ee5522d8bed2ef680ef07f87223 helm-v3.14.2-linux-ppc64le.tar.gz diff --git a/hack/installers/checksums/helm-v3.14.2-linux-s390x.tar.gz.sha256 b/hack/installers/checksums/helm-v3.14.2-linux-s390x.tar.gz.sha256 new file mode 100644 index 00000000000000..d14a74799e6a2f --- /dev/null +++ b/hack/installers/checksums/helm-v3.14.2-linux-s390x.tar.gz.sha256 @@ -0,0 +1 @@ +7bda34aa26638e5116b31385f3b781172572175bf4c1ae00c87d8b154458ed94 helm-v3.14.2-linux-s390x.tar.gz diff --git a/hack/tool-versions.sh b/hack/tool-versions.sh index 842af77c8a2aca..964dec3e6c8f19 100644 --- a/hack/tool-versions.sh +++ b/hack/tool-versions.sh @@ -11,7 +11,7 @@ # Use ./hack/installers/checksums/add-helm-checksums.sh and # add-kustomize-checksums.sh to help download checksums. ############################################################################### -helm3_version=3.13.1 +helm3_version=3.14.2 kubectl_version=1.17.8 kubectx_version=0.6.3 kustomize5_version=5.2.1 diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh index abee0493ead86b..9f6d15524d04d4 100755 --- a/hack/update-codegen.sh +++ b/hack/update-codegen.sh @@ -19,21 +19,31 @@ set -o errexit set -o nounset set -o pipefail -PROJECT_ROOT=$(cd $(dirname ${BASH_SOURCE})/..; pwd) +PROJECT_ROOT=$( + cd $(dirname ${BASH_SOURCE})/.. + pwd +) PATH="${PROJECT_ROOT}/dist:${PATH}" +GOPATH=$(go env GOPATH) +GOPATH_PROJECT_ROOT="${GOPATH}/src/github.com/argoproj/argo-cd" TARGET_SCRIPT=/tmp/generate-groups.sh # codegen utilities are installed outside of generate-groups.sh so remove the `go install` step in the script. -sed -e '/go install/d' ${PROJECT_ROOT}/vendor/k8s.io/code-generator/generate-groups.sh > ${TARGET_SCRIPT} +sed -e '/go install/d' ${PROJECT_ROOT}/vendor/k8s.io/code-generator/generate-groups.sh >${TARGET_SCRIPT} # generate-groups.sh assumes codegen utilities are installed to GOBIN, but we just ensure the CLIs # are in the path and invoke them without assumption of their location sed -i.bak -e 's#${gobin}/##g' ${TARGET_SCRIPT} [ -e ./v2 ] || ln -s . v2 +[ -e "${GOPATH_PROJECT_ROOT}" ] || (mkdir -p "$(dirname "${GOPATH_PROJECT_ROOT}")" && ln -s "${PROJECT_ROOT}" "${GOPATH_PROJECT_ROOT}") + bash -x ${TARGET_SCRIPT} "deepcopy,client,informer,lister" \ github.com/argoproj/argo-cd/v2/pkg/client github.com/argoproj/argo-cd/v2/pkg/apis \ "application:v1alpha1" \ - --go-header-file ${PROJECT_ROOT}/hack/custom-boilerplate.go.txt -[ -e ./v2 ] && rm -rf v2 \ No newline at end of file + --go-header-file "${PROJECT_ROOT}/hack/custom-boilerplate.go.txt" \ + --output-base "${GOPATH}/src" + +[ -L "${GOPATH_PROJECT_ROOT}" ] && rm -rf "${GOPATH_PROJECT_ROOT}" +[ -L ./v2 ] && rm -rf v2 diff --git a/hack/update-kubernetes-version.sh b/hack/update-kubernetes-version.sh new file mode 100755 index 00000000000000..8d52033a601fc8 --- /dev/null +++ b/hack/update-kubernetes-version.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [ -z "${1:-}" ]; then + echo "Example usage: ./hack/update-kubernetes-version.sh v1.26.11" + exit 1 +fi +VERSION=${1#"v"} +MODS=($( + curl -sS https://raw.githubusercontent.com/kubernetes/kubernetes/v${VERSION}/go.mod | + sed -n 's|.*k8s.io/\(.*\) => ./staging/src/k8s.io/.*|k8s.io/\1|p' +)) +for MOD in "${MODS[@]}"; do + echo "Updating $MOD..." >&2 + V=$( + go mod download -json "${MOD}@kubernetes-${VERSION}" | + sed -n 's|.*"Version": "\(.*\)".*|\1|p' + ) + go mod edit "-replace=${MOD}=${MOD}@${V}" +done +go get "k8s.io/kubernetes@v${VERSION}" +go mod tidy diff --git a/hack/update-openapi.sh b/hack/update-openapi.sh index 2db84ed5f62428..0250ed45b93ac0 100755 --- a/hack/update-openapi.sh +++ b/hack/update-openapi.sh @@ -5,20 +5,30 @@ set -o errexit set -o nounset set -o pipefail -PROJECT_ROOT=$(cd $(dirname "$0")/.. ; pwd) +PROJECT_ROOT=$( + cd $(dirname "$0")/.. + pwd +) PATH="${PROJECT_ROOT}/dist:${PATH}" +GOPATH=$(go env GOPATH) +GOPATH_PROJECT_ROOT="${GOPATH}/src/github.com/argoproj/argo-cd" + VERSION="v1alpha1" - + [ -e ./v2 ] || ln -s . v2 +[ -e "${GOPATH_PROJECT_ROOT}" ] || (mkdir -p "$(dirname "${GOPATH_PROJECT_ROOT}")" && ln -s "${PROJECT_ROOT}" "${GOPATH_PROJECT_ROOT}") + openapi-gen \ --go-header-file ${PROJECT_ROOT}/hack/custom-boilerplate.go.txt \ --input-dirs github.com/argoproj/argo-cd/v2/pkg/apis/application/${VERSION} \ --output-package github.com/argoproj/argo-cd/v2/pkg/apis/application/${VERSION} \ --report-filename pkg/apis/api-rules/violation_exceptions.list \ + --output-base "${GOPATH}/src" \ $@ -[ -e ./v2 ] && rm -rf v2 + +[ -L "${GOPATH_PROJECT_ROOT}" ] && rm -rf "${GOPATH_PROJECT_ROOT}" +[ -L ./v2 ] && rm -rf v2 export GO111MODULE=on -go build -o ./dist/gen-crd-spec ${PROJECT_ROOT}/hack/gen-crd-spec +go build -o ./dist/gen-crd-spec "${PROJECT_ROOT}/hack/gen-crd-spec" ./dist/gen-crd-spec - diff --git a/manifests/base/application-controller-deployment/argocd-application-controller-deployment.yaml b/manifests/base/application-controller-deployment/argocd-application-controller-deployment.yaml index 9b618d96367dc3..68dd75de2f47fe 100644 --- a/manifests/base/application-controller-deployment/argocd-application-controller-deployment.yaml +++ b/manifests/base/application-controller-deployment/argocd-application-controller-deployment.yaml @@ -20,8 +20,6 @@ spec: - args: - /usr/local/bin/argocd-application-controller env: - - name: ARGOCD_CONTROLLER_REPLICAS - value: "1" - name: ARGOCD_RECONCILIATION_TIMEOUT valueFrom: configMapKeyRef: @@ -34,24 +32,36 @@ spec: name: argocd-cm key: timeout.hard.reconciliation optional: true + - name: ARGOCD_RECONCILIATION_JITTER + valueFrom: + configMapKeyRef: + key: timeout.reconciliation.jitter + name: argocd-cm + optional: true + - name: ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.repo.error.grace.period.seconds + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: repo.server - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: repo.server + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_TIMEOUT_SECONDS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.repo.server.timeout.seconds - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.repo.server.timeout.seconds + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_STATUS_PROCESSORS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.status.processors - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.status.processors + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_OPERATION_PROCESSORS valueFrom: configMapKeyRef: @@ -78,22 +88,22 @@ spec: optional: true - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_TIMEOUT_SECONDS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.self.heal.timeout.seconds - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.self.heal.timeout.seconds + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_PLAINTEXT valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.repo.server.plaintext - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.repo.server.plaintext + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_STRICT_TLS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.repo.server.strict.tls - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.repo.server.strict.tls + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_PERSIST_RESOURCE_HEALTH valueFrom: configMapKeyRef: @@ -102,16 +112,16 @@ spec: optional: true - name: ARGOCD_APP_STATE_CACHE_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.app.state.cache.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.app.state.cache.expiration + optional: true - name: REDIS_SERVER valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.server - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.server + optional: true - name: REDIS_COMPRESSION valueFrom: configMapKeyRef: @@ -120,58 +130,70 @@ spec: optional: true - name: REDISDB valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.db - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.db + optional: true - name: ARGOCD_DEFAULT_CACHE_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.default.cache.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.default.cache.expiration + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_ADDRESS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: otlp.address - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.address + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.insecure + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.headers + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: application.namespaces - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: application.namespaces + optional: true - name: ARGOCD_CONTROLLER_SHARDING_ALGORITHM valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.sharding.algorithm - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.sharding.algorithm + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_KUBECTL_PARALLELISM_LIMIT - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.kubectl.parallelism.limit - optional: true - - name: ARGOCD_CONTROLLER_HEARTBEAT_TIME valueFrom: configMapKeyRef: name: argocd-cmd-params-cm - key: controller.heatbeat.time + key: controller.kubectl.parallelism.limit optional: true - name: ARGOCD_K8SCLIENT_RETRY_MAX valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.k8sclient.retry.max - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.k8sclient.retry.max + optional: true - name: ARGOCD_K8SCLIENT_RETRY_BASE_BACKOFF valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.k8sclient.retry.base.backoff - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.k8sclient.retry.base.backoff + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.diff.server.side + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always name: argocd-application-controller diff --git a/manifests/base/application-controller-deployment/argocd-application-controller-statefulset.yaml b/manifests/base/application-controller-deployment/argocd-application-controller-statefulset.yaml new file mode 100644 index 00000000000000..10e4ea2ac7e3e0 --- /dev/null +++ b/manifests/base/application-controller-deployment/argocd-application-controller-statefulset.yaml @@ -0,0 +1,15 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: argocd-application-controller +spec: + replicas: 0 + template: + spec: + containers: + - name: argocd-application-controller + args: + - /usr/local/bin/argocd-application-controller + env: + - name: ARGOCD_CONTROLLER_REPLICAS + value: "0" \ No newline at end of file diff --git a/manifests/base/application-controller-deployment/kustomization.yaml b/manifests/base/application-controller-deployment/kustomization.yaml index 8f35ec8bd388f0..733a378e013e04 100644 --- a/manifests/base/application-controller-deployment/kustomization.yaml +++ b/manifests/base/application-controller-deployment/kustomization.yaml @@ -2,5 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: +- ../application-controller-roles - argocd-application-controller-service.yaml +- argocd-application-controller-statefulset.yaml - argocd-application-controller-deployment.yaml + diff --git a/manifests/base/application-controller/argocd-application-controller-role.yaml b/manifests/base/application-controller-roles/argocd-application-controller-role.yaml similarity index 87% rename from manifests/base/application-controller/argocd-application-controller-role.yaml rename to manifests/base/application-controller-roles/argocd-application-controller-role.yaml index 27e0bc7bfe9cb2..a672268eb1dd9d 100644 --- a/manifests/base/application-controller/argocd-application-controller-role.yaml +++ b/manifests/base/application-controller-roles/argocd-application-controller-role.yaml @@ -36,3 +36,11 @@ rules: verbs: - create - list +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch diff --git a/manifests/base/application-controller/argocd-application-controller-rolebinding.yaml b/manifests/base/application-controller-roles/argocd-application-controller-rolebinding.yaml similarity index 100% rename from manifests/base/application-controller/argocd-application-controller-rolebinding.yaml rename to manifests/base/application-controller-roles/argocd-application-controller-rolebinding.yaml diff --git a/manifests/base/application-controller/argocd-application-controller-sa.yaml b/manifests/base/application-controller-roles/argocd-application-controller-sa.yaml similarity index 100% rename from manifests/base/application-controller/argocd-application-controller-sa.yaml rename to manifests/base/application-controller-roles/argocd-application-controller-sa.yaml diff --git a/manifests/base/application-controller-roles/kustomization.yaml b/manifests/base/application-controller-roles/kustomization.yaml new file mode 100644 index 00000000000000..f834d2ef3dbc46 --- /dev/null +++ b/manifests/base/application-controller-roles/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- argocd-application-controller-sa.yaml +- argocd-application-controller-role.yaml +- argocd-application-controller-rolebinding.yaml diff --git a/manifests/base/application-controller/argocd-application-controller-statefulset.yaml b/manifests/base/application-controller/argocd-application-controller-statefulset.yaml index 3c576a421a25c3..d974edffdd6185 100644 --- a/manifests/base/application-controller/argocd-application-controller-statefulset.yaml +++ b/manifests/base/application-controller/argocd-application-controller-statefulset.yaml @@ -35,6 +35,12 @@ spec: name: argocd-cm key: timeout.hard.reconciliation optional: true + - name: ARGOCD_RECONCILIATION_JITTER + valueFrom: + configMapKeyRef: + key: timeout.reconciliation.jitter + name: argocd-cm + optional: true - name: ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS valueFrom: configMapKeyRef: @@ -43,22 +49,22 @@ spec: optional: true - name: ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: repo.server - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: repo.server + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_TIMEOUT_SECONDS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.repo.server.timeout.seconds - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.repo.server.timeout.seconds + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_STATUS_PROCESSORS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.status.processors - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.status.processors + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_OPERATION_PROCESSORS valueFrom: configMapKeyRef: @@ -85,22 +91,22 @@ spec: optional: true - name: ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_TIMEOUT_SECONDS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.self.heal.timeout.seconds - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.self.heal.timeout.seconds + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_PLAINTEXT valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.repo.server.plaintext - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.repo.server.plaintext + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_STRICT_TLS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.repo.server.strict.tls - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.repo.server.strict.tls + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_PERSIST_RESOURCE_HEALTH valueFrom: configMapKeyRef: @@ -109,16 +115,16 @@ spec: optional: true - name: ARGOCD_APP_STATE_CACHE_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.app.state.cache.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.app.state.cache.expiration + optional: true - name: REDIS_SERVER valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.server - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.server + optional: true - name: REDIS_COMPRESSION valueFrom: configMapKeyRef: @@ -127,52 +133,70 @@ spec: optional: true - name: REDISDB valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.db - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.db + optional: true - name: ARGOCD_DEFAULT_CACHE_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.default.cache.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.default.cache.expiration + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_ADDRESS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: otlp.address - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.address + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.insecure + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.headers + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: application.namespaces - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: application.namespaces + optional: true - name: ARGOCD_CONTROLLER_SHARDING_ALGORITHM valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.sharding.algorithm - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.sharding.algorithm + optional: true - name: ARGOCD_APPLICATION_CONTROLLER_KUBECTL_PARALLELISM_LIMIT valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.kubectl.parallelism.limit - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.kubectl.parallelism.limit + optional: true - name: ARGOCD_K8SCLIENT_RETRY_MAX valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.k8sclient.retry.max - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.k8sclient.retry.max + optional: true - name: ARGOCD_K8SCLIENT_RETRY_BASE_BACKOFF valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: controller.k8sclient.retry.base.backoff - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.k8sclient.retry.base.backoff + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: controller.diff.server.side + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always name: argocd-application-controller diff --git a/manifests/base/application-controller/kustomization.yaml b/manifests/base/application-controller/kustomization.yaml index 9a801ad877bd24..616977fb9b08b3 100644 --- a/manifests/base/application-controller/kustomization.yaml +++ b/manifests/base/application-controller/kustomization.yaml @@ -2,9 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- argocd-application-controller-sa.yaml -- argocd-application-controller-role.yaml -- argocd-application-controller-rolebinding.yaml +- ../application-controller-roles - argocd-application-controller-statefulset.yaml - argocd-metrics.yaml - argocd-application-controller-network-policy.yaml \ No newline at end of file diff --git a/manifests/base/dex/argocd-dex-server-deployment.yaml b/manifests/base/dex/argocd-dex-server-deployment.yaml index 8d3b37d177913a..7ff5985f44a90a 100644 --- a/manifests/base/dex/argocd-dex-server-deployment.yaml +++ b/manifests/base/dex/argocd-dex-server-deployment.yaml @@ -37,7 +37,7 @@ spec: type: RuntimeDefault containers: - name: dex - image: ghcr.io/dexidp/dex:v2.37.0 + image: ghcr.io/dexidp/dex:v2.38.0 imagePullPolicy: Always command: [/shared/argocd-dex, rundex] env: diff --git a/manifests/base/notification/argocd-notifications-controller-deployment.yaml b/manifests/base/notification/argocd-notifications-controller-deployment.yaml index 9cd1a068808b11..876a207c16e427 100644 --- a/manifests/base/notification/argocd-notifications-controller-deployment.yaml +++ b/manifests/base/notification/argocd-notifications-controller-deployment.yaml @@ -54,6 +54,12 @@ spec: key: application.namespaces name: argocd-cmd-params-cm optional: true + - name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED + valueFrom: + configMapKeyRef: + key: notificationscontroller.selfservice.enabled + name: argocd-cmd-params-cm + optional: true workingDir: /app livenessProbe: tcpSocket: diff --git a/manifests/base/repo-server/argocd-repo-server-deployment.yaml b/manifests/base/repo-server/argocd-repo-server-deployment.yaml index 728c80c14bc2a4..907bc80a34e560 100644 --- a/manifests/base/repo-server/argocd-repo-server-deployment.yaml +++ b/manifests/base/repo-server/argocd-repo-server-deployment.yaml @@ -120,6 +120,18 @@ spec: name: argocd-cmd-params-cm key: otlp.address optional: true + - name: ARGOCD_REPO_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.insecure + optional: true + - name: ARGOCD_REPO_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.headers + optional: true - name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE valueFrom: configMapKeyRef: diff --git a/manifests/base/server/argocd-server-deployment.yaml b/manifests/base/server/argocd-server-deployment.yaml index b09891d26e5295..0ebeb70e085311 100644 --- a/manifests/base/server/argocd-server-deployment.yaml +++ b/manifests/base/server/argocd-server-deployment.yaml @@ -25,136 +25,136 @@ spec: env: - name: ARGOCD_SERVER_INSECURE valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.insecure - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.insecure + optional: true - name: ARGOCD_SERVER_BASEHREF valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.basehref - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.basehref + optional: true - name: ARGOCD_SERVER_ROOTPATH valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.rootpath - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.rootpath + optional: true - name: ARGOCD_SERVER_LOGFORMAT valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.log.format - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.log.format + optional: true - name: ARGOCD_SERVER_LOG_LEVEL valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.log.level - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.log.level + optional: true - name: ARGOCD_SERVER_REPO_SERVER valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: repo.server - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: repo.server + optional: true - name: ARGOCD_SERVER_DEX_SERVER valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.dex.server - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.dex.server + optional: true - name: ARGOCD_SERVER_DISABLE_AUTH valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.disable.auth - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.disable.auth + optional: true - name: ARGOCD_SERVER_ENABLE_GZIP valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.enable.gzip - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.enable.gzip + optional: true - name: ARGOCD_SERVER_REPO_SERVER_TIMEOUT_SECONDS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.repo.server.timeout.seconds - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.repo.server.timeout.seconds + optional: true - name: ARGOCD_SERVER_X_FRAME_OPTIONS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.x.frame.options - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.x.frame.options + optional: true - name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.content.security.policy - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.content.security.policy + optional: true - name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.repo.server.plaintext - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.repo.server.plaintext + optional: true - name: ARGOCD_SERVER_REPO_SERVER_STRICT_TLS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.repo.server.strict.tls - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.repo.server.strict.tls + optional: true - name: ARGOCD_SERVER_DEX_SERVER_PLAINTEXT valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.dex.server.plaintext - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.dex.server.plaintext + optional: true - name: ARGOCD_SERVER_DEX_SERVER_STRICT_TLS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.dex.server.strict.tls - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.dex.server.strict.tls + optional: true - name: ARGOCD_TLS_MIN_VERSION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.tls.minversion - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.tls.minversion + optional: true - name: ARGOCD_TLS_MAX_VERSION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.tls.maxversion - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.tls.maxversion + optional: true - name: ARGOCD_TLS_CIPHERS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.tls.ciphers - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.tls.ciphers + optional: true - name: ARGOCD_SERVER_CONNECTION_STATUS_CACHE_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.connection.status.cache.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.connection.status.cache.expiration + optional: true - name: ARGOCD_SERVER_OIDC_CACHE_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.oidc.cache.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.oidc.cache.expiration + optional: true - name: ARGOCD_SERVER_LOGIN_ATTEMPTS_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.login.attempts.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.login.attempts.expiration + optional: true - name: ARGOCD_SERVER_STATIC_ASSETS valueFrom: configMapKeyRef: @@ -163,16 +163,16 @@ spec: optional: true - name: ARGOCD_APP_STATE_CACHE_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.app.state.cache.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.app.state.cache.expiration + optional: true - name: REDIS_SERVER valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.server - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.server + optional: true - name: REDIS_COMPRESSION valueFrom: configMapKeyRef: @@ -181,64 +181,82 @@ spec: optional: true - name: REDISDB valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.db - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.db + optional: true - name: ARGOCD_DEFAULT_CACHE_EXPIRATION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.default.cache.expiration - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.default.cache.expiration + optional: true - name: ARGOCD_MAX_COOKIE_NUMBER valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.http.cookie.maxnumber - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.http.cookie.maxnumber + optional: true - name: ARGOCD_SERVER_LISTEN_ADDRESS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.listen.address - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.listen.address + optional: true - name: ARGOCD_SERVER_METRICS_LISTEN_ADDRESS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.metrics.listen.address - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.metrics.listen.address + optional: true - name: ARGOCD_SERVER_OTLP_ADDRESS valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: otlp.address - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.address + optional: true + - name: ARGOCD_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.insecure + optional: true + - name: ARGOCD_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.headers + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: application.namespaces - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: application.namespaces + optional: true - name: ARGOCD_SERVER_ENABLE_PROXY_EXTENSION valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.enable.proxy.extension - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.enable.proxy.extension + optional: true - name: ARGOCD_K8SCLIENT_RETRY_MAX valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.k8sclient.retry.max - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.k8sclient.retry.max + optional: true - name: ARGOCD_K8SCLIENT_RETRY_BASE_BACKOFF valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.k8sclient.retry.base.backoff - optional: true + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.k8sclient.retry.base.backoff + optional: true + - name: ARGOCD_API_CONTENT_TYPES + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.api.content.types + optional: true volumeMounts: - name: ssh-known-hosts mountPath: /app/config/ssh diff --git a/manifests/cluster-rbac/applicationset-controller/argocd-applicationset-controller-clusterrole.yaml b/manifests/cluster-rbac/applicationset-controller/argocd-applicationset-controller-clusterrole.yaml new file mode 100644 index 00000000000000..259a48e7aee9e7 --- /dev/null +++ b/manifests/cluster-rbac/applicationset-controller/argocd-applicationset-controller-clusterrole.yaml @@ -0,0 +1,88 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: argocd-applicationset-controller + app.kubernetes.io/part-of: argocd + app.kubernetes.io/component: applicationset-controller + name: argocd-applicationset-controller +rules: +- apiGroups: + - argoproj.io + resources: + - applications + - applicationsets + - applicationsets/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - argoproj.io + resources: + - applicationsets/status + verbs: + - get + - patch + - update +- apiGroups: + - argoproj.io + resources: + - appprojects + verbs: + - get +- apiGroups: + - "" + resources: + - events + verbs: + - create + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - update + - delete + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - apps + - extensions + resources: + - deployments + verbs: + - get + - list + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch diff --git a/manifests/cluster-rbac/applicationset-controller/argocd-applicationset-controller-clusterrolebinding.yaml b/manifests/cluster-rbac/applicationset-controller/argocd-applicationset-controller-clusterrolebinding.yaml new file mode 100644 index 00000000000000..820f16f472e4e1 --- /dev/null +++ b/manifests/cluster-rbac/applicationset-controller/argocd-applicationset-controller-clusterrolebinding.yaml @@ -0,0 +1,16 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/name: argocd-applicationset-controller + app.kubernetes.io/part-of: argocd + app.kubernetes.io/component: applicationset-controller + name: argocd-applicationset-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: argocd-applicationset-controller +subjects: +- kind: ServiceAccount + name: argocd-applicationset-controller + namespace: argocd diff --git a/manifests/cluster-rbac/applicationset-controller/kustomization.yaml b/manifests/cluster-rbac/applicationset-controller/kustomization.yaml new file mode 100644 index 00000000000000..b8f18c57a14f73 --- /dev/null +++ b/manifests/cluster-rbac/applicationset-controller/kustomization.yaml @@ -0,0 +1,6 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- argocd-applicationset-controller-clusterrole.yaml +- argocd-applicationset-controller-clusterrolebinding.yaml diff --git a/manifests/cluster-rbac/kustomization.yaml b/manifests/cluster-rbac/kustomization.yaml index 7f791905b661b0..55e6e2d72df9e9 100644 --- a/manifests/cluster-rbac/kustomization.yaml +++ b/manifests/cluster-rbac/kustomization.yaml @@ -3,4 +3,5 @@ kind: Kustomization resources: - ./application-controller +- ./applicationset-controller - ./server diff --git a/manifests/core-install.yaml b/manifests/core-install.yaml index a04328c5a70404..4855148349703c 100644 --- a/manifests/core-install.yaml +++ b/manifests/core-install.yaml @@ -343,6 +343,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to + apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -678,6 +682,10 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1127,6 +1135,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to apply + common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1452,6 +1464,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to apply + common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1726,6 +1742,19 @@ spec: description: ID is an auto incrementing identifier of the RevisionHistory format: int64 type: integer + initiatedBy: + description: InitiatedBy contains information about who initiated + the operations + properties: + automated: + description: Automated is set to true if operation was initiated + automatically by the application controller. + type: boolean + username: + description: Username contains the name of a user who started + operation + type: string + type: object revision: description: Revision holds the revision the sync was performed against @@ -1930,6 +1959,10 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -2268,6 +2301,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -2751,6 +2789,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors + or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3107,6 +3150,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies + whether to apply common labels to resource + selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3577,6 +3625,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3926,6 +3979,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -4418,6 +4476,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -4767,6 +4830,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -5196,6 +5264,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5410,6 +5480,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5783,6 +5855,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5997,6 +6071,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6374,6 +6450,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6588,6 +6666,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6945,6 +7025,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7159,6 +7241,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7540,6 +7624,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7754,6 +7840,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8127,6 +8215,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8341,6 +8431,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8718,6 +8810,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8932,6 +9026,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9289,6 +9385,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9503,6 +9601,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9870,6 +9970,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10084,6 +10186,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10656,6 +10760,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10870,6 +10976,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11433,6 +11541,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11647,6 +11757,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12018,6 +12130,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12232,6 +12346,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12613,6 +12729,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12827,6 +12945,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13200,6 +13320,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13414,6 +13536,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13791,6 +13915,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14005,6 +14131,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14362,6 +14490,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14576,6 +14706,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14943,6 +15075,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15157,6 +15291,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15729,6 +15865,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15943,6 +16081,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -16506,6 +16646,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -16720,6 +16862,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17095,6 +17239,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17309,6 +17455,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17673,6 +17821,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17887,6 +18037,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -18459,6 +18611,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -18673,6 +18827,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19236,6 +19392,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19450,6 +19608,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19896,6 +20056,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -20110,6 +20272,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -20261,6 +20425,8 @@ spec: - metadata - spec type: object + templatePatch: + type: string required: - generators - template @@ -20718,6 +20884,14 @@ rules: verbs: - create - list +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -21400,6 +21574,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE valueFrom: configMapKeyRef: @@ -21617,6 +21803,12 @@ spec: key: timeout.hard.reconciliation name: argocd-cm optional: true + - name: ARGOCD_RECONCILIATION_JITTER + valueFrom: + configMapKeyRef: + key: timeout.reconciliation.jitter + name: argocd-cm + optional: true - name: ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS valueFrom: configMapKeyRef: @@ -21725,6 +21917,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -21755,6 +21959,12 @@ spec: key: controller.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF + valueFrom: + configMapKeyRef: + key: controller.diff.server.side + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always name: argocd-application-controller diff --git a/manifests/crds/application-crd.yaml b/manifests/crds/application-crd.yaml index 962543f80eb97e..aaf1347f64dfb7 100644 --- a/manifests/crds/application-crd.yaml +++ b/manifests/crds/application-crd.yaml @@ -342,6 +342,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to + apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -677,6 +681,10 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1126,6 +1134,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to apply + common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1451,6 +1463,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to apply + common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1725,6 +1741,19 @@ spec: description: ID is an auto incrementing identifier of the RevisionHistory format: int64 type: integer + initiatedBy: + description: InitiatedBy contains information about who initiated + the operations + properties: + automated: + description: Automated is set to true if operation was initiated + automatically by the application controller. + type: boolean + username: + description: Username contains the name of a user who started + operation + type: string + type: object revision: description: Revision holds the revision the sync was performed against @@ -1929,6 +1958,10 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -2267,6 +2300,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -2750,6 +2788,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors + or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3106,6 +3149,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies + whether to apply common labels to resource + selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3576,6 +3624,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3925,6 +3978,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -4417,6 +4475,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -4766,6 +4829,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps diff --git a/manifests/crds/applicationset-crd.yaml b/manifests/crds/applicationset-crd.yaml index 3470ddcdcc63b8..1af353f436e27c 100644 --- a/manifests/crds/applicationset-crd.yaml +++ b/manifests/crds/applicationset-crd.yaml @@ -256,6 +256,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -470,6 +472,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -843,6 +847,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -1057,6 +1063,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -1434,6 +1442,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -1648,6 +1658,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -2005,6 +2017,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -2219,6 +2233,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -2600,6 +2616,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -2814,6 +2832,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -3187,6 +3207,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -3401,6 +3423,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -3778,6 +3802,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -3992,6 +4018,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -4349,6 +4377,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -4563,6 +4593,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -4930,6 +4962,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5144,6 +5178,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5716,6 +5752,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5930,6 +5968,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6493,6 +6533,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6707,6 +6749,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7078,6 +7122,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7292,6 +7338,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7673,6 +7721,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7887,6 +7937,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8260,6 +8312,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8474,6 +8528,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8851,6 +8907,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9065,6 +9123,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9422,6 +9482,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9636,6 +9698,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10003,6 +10067,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10217,6 +10283,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10789,6 +10857,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11003,6 +11073,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11566,6 +11638,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11780,6 +11854,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12155,6 +12231,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12369,6 +12447,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12733,6 +12813,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12947,6 +13029,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13519,6 +13603,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13733,6 +13819,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14296,6 +14384,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14510,6 +14600,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14956,6 +15048,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15170,6 +15264,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15321,6 +15417,8 @@ spec: - metadata - spec type: object + templatePatch: + type: string required: - generators - template diff --git a/manifests/ha/base/controller-deployment/kustomization.yaml b/manifests/ha/base/controller-deployment/kustomization.yaml index d6d20d99b45167..e98bd250d699e8 100644 --- a/manifests/ha/base/controller-deployment/kustomization.yaml +++ b/manifests/ha/base/controller-deployment/kustomization.yaml @@ -1,20 +1,17 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization - patches: +- path: argocd-application-controller-statefulset.yaml - path: argocd-repo-server-deployment.yaml - path: argocd-server-deployment.yaml -- path: argocd-application-controller-statefulset.yaml - path: argocd-cmd-params-cm.yaml - images: - name: quay.io/argoproj/argocd newName: quay.io/argoproj/argocd newTag: latest resources: -- ../../../base/application-controller - ../../../base/application-controller-deployment - ../../../base/applicationset-controller - ../../../base/dex diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml index 69def7daef6b08..db683d1b889bf0 100644 --- a/manifests/ha/install.yaml +++ b/manifests/ha/install.yaml @@ -343,6 +343,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to + apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -678,6 +682,10 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1127,6 +1135,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to apply + common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1452,6 +1464,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to apply + common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1726,6 +1742,19 @@ spec: description: ID is an auto incrementing identifier of the RevisionHistory format: int64 type: integer + initiatedBy: + description: InitiatedBy contains information about who initiated + the operations + properties: + automated: + description: Automated is set to true if operation was initiated + automatically by the application controller. + type: boolean + username: + description: Username contains the name of a user who started + operation + type: string + type: object revision: description: Revision holds the revision the sync was performed against @@ -1930,6 +1959,10 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -2268,6 +2301,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -2751,6 +2789,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors + or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3107,6 +3150,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies + whether to apply common labels to resource + selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3577,6 +3625,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3926,6 +3979,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -4418,6 +4476,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -4767,6 +4830,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -5196,6 +5264,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5410,6 +5480,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5783,6 +5855,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5997,6 +6071,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6374,6 +6450,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6588,6 +6666,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6945,6 +7025,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7159,6 +7241,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7540,6 +7624,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7754,6 +7840,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8127,6 +8215,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8341,6 +8431,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8718,6 +8810,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8932,6 +9026,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9289,6 +9385,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9503,6 +9601,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9870,6 +9970,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10084,6 +10186,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10656,6 +10760,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10870,6 +10976,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11433,6 +11541,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11647,6 +11757,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12018,6 +12130,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12232,6 +12346,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12613,6 +12729,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12827,6 +12945,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13200,6 +13320,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13414,6 +13536,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13791,6 +13915,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14005,6 +14131,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14362,6 +14490,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14576,6 +14706,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14943,6 +15075,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15157,6 +15291,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15729,6 +15865,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15943,6 +16081,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -16506,6 +16646,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -16720,6 +16862,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17095,6 +17239,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17309,6 +17455,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17673,6 +17821,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17887,6 +18037,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -18459,6 +18611,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -18673,6 +18827,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19236,6 +19392,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19450,6 +19608,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19896,6 +20056,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -20110,6 +20272,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -20261,6 +20425,8 @@ spec: - metadata - spec type: object + templatePatch: + type: string required: - generators - template @@ -20754,6 +20920,14 @@ rules: verbs: - create - list +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -20983,6 +21157,95 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: applicationset-controller + app.kubernetes.io/name: argocd-applicationset-controller + app.kubernetes.io/part-of: argocd + name: argocd-applicationset-controller +rules: +- apiGroups: + - argoproj.io + resources: + - applications + - applicationsets + - applicationsets/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - argoproj.io + resources: + - applicationsets/status + verbs: + - get + - patch + - update +- apiGroups: + - argoproj.io + resources: + - appprojects + verbs: + - get +- apiGroups: + - "" + resources: + - events + verbs: + - create + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - update + - delete + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - apps + - extensions + resources: + - deployments + verbs: + - get + - list + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: labels: app.kubernetes.io/component: server @@ -21164,6 +21427,23 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: applicationset-controller + app.kubernetes.io/name: argocd-applicationset-controller + app.kubernetes.io/part-of: argocd + name: argocd-applicationset-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: argocd-applicationset-controller +subjects: +- kind: ServiceAccount + name: argocd-applicationset-controller + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/component: server @@ -22505,7 +22785,7 @@ spec: key: dexserver.disable.tls name: argocd-cmd-params-cm optional: true - image: ghcr.io/dexidp/dex:v2.37.0 + image: ghcr.io/dexidp/dex:v2.38.0 imagePullPolicy: Always name: dex ports: @@ -22610,6 +22890,12 @@ spec: key: application.namespaces name: argocd-cmd-params-cm optional: true + - name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED + valueFrom: + configMapKeyRef: + key: notificationscontroller.selfservice.enabled + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: @@ -22887,6 +23173,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE valueFrom: configMapKeyRef: @@ -23282,6 +23580,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -23306,6 +23616,12 @@ spec: key: server.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_API_CONTENT_TYPES + valueFrom: + configMapKeyRef: + key: server.api.content.types + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: @@ -23432,6 +23748,12 @@ spec: key: timeout.hard.reconciliation name: argocd-cm optional: true + - name: ARGOCD_RECONCILIATION_JITTER + valueFrom: + configMapKeyRef: + key: timeout.reconciliation.jitter + name: argocd-cm + optional: true - name: ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS valueFrom: configMapKeyRef: @@ -23540,6 +23862,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -23570,6 +23904,12 @@ spec: key: controller.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF + valueFrom: + configMapKeyRef: + key: controller.diff.server.side + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always name: argocd-application-controller diff --git a/manifests/ha/namespace-install.yaml b/manifests/ha/namespace-install.yaml index 33ada40e37004b..044a061bf0cb1b 100644 --- a/manifests/ha/namespace-install.yaml +++ b/manifests/ha/namespace-install.yaml @@ -109,6 +109,14 @@ rules: verbs: - create - list +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -1754,7 +1762,7 @@ spec: key: dexserver.disable.tls name: argocd-cmd-params-cm optional: true - image: ghcr.io/dexidp/dex:v2.37.0 + image: ghcr.io/dexidp/dex:v2.38.0 imagePullPolicy: Always name: dex ports: @@ -1859,6 +1867,12 @@ spec: key: application.namespaces name: argocd-cmd-params-cm optional: true + - name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED + valueFrom: + configMapKeyRef: + key: notificationscontroller.selfservice.enabled + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: @@ -2136,6 +2150,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE valueFrom: configMapKeyRef: @@ -2531,6 +2557,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -2555,6 +2593,12 @@ spec: key: server.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_API_CONTENT_TYPES + valueFrom: + configMapKeyRef: + key: server.api.content.types + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: @@ -2681,6 +2725,12 @@ spec: key: timeout.hard.reconciliation name: argocd-cm optional: true + - name: ARGOCD_RECONCILIATION_JITTER + valueFrom: + configMapKeyRef: + key: timeout.reconciliation.jitter + name: argocd-cm + optional: true - name: ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS valueFrom: configMapKeyRef: @@ -2789,6 +2839,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -2819,6 +2881,12 @@ spec: key: controller.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF + valueFrom: + configMapKeyRef: + key: controller.diff.server.side + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always name: argocd-application-controller diff --git a/manifests/install.yaml b/manifests/install.yaml index 8ff73a868e464b..6b433b5622984c 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -343,6 +343,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to + apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -678,6 +682,10 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1127,6 +1135,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to apply + common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1452,6 +1464,10 @@ spec: definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether to apply + common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -1726,6 +1742,19 @@ spec: description: ID is an auto incrementing identifier of the RevisionHistory format: int64 type: integer + initiatedBy: + description: InitiatedBy contains information about who initiated + the operations + properties: + automated: + description: Automated is set to true if operation was initiated + automatically by the application controller. + type: boolean + username: + description: Username contains the name of a user who started + operation + type: string + type: object revision: description: Revision holds the revision the sync was performed against @@ -1930,6 +1959,10 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -2268,6 +2301,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -2751,6 +2789,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors + or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3107,6 +3150,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies + whether to apply common labels to resource + selectors or not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3577,6 +3625,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -3926,6 +3979,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -4418,6 +4476,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -4767,6 +4830,11 @@ spec: image definition in the format [old_image_name=]: type: string type: array + labelWithoutSelector: + description: LabelWithoutSelector specifies whether + to apply common labels to resource selectors or + not + type: boolean namePrefix: description: NamePrefix is a prefix appended to resources for Kustomize apps @@ -5196,6 +5264,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5410,6 +5480,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5783,6 +5855,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -5997,6 +6071,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6374,6 +6450,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6588,6 +6666,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -6945,6 +7025,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7159,6 +7241,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7540,6 +7624,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -7754,6 +7840,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8127,6 +8215,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8341,6 +8431,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8718,6 +8810,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -8932,6 +9026,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9289,6 +9385,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9503,6 +9601,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -9870,6 +9970,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10084,6 +10186,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10656,6 +10760,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -10870,6 +10976,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11433,6 +11541,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -11647,6 +11757,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12018,6 +12130,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12232,6 +12346,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12613,6 +12729,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -12827,6 +12945,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13200,6 +13320,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13414,6 +13536,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -13791,6 +13915,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14005,6 +14131,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14362,6 +14490,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14576,6 +14706,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -14943,6 +15075,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15157,6 +15291,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15729,6 +15865,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -15943,6 +16081,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -16506,6 +16646,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -16720,6 +16862,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17095,6 +17239,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17309,6 +17455,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17673,6 +17821,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -17887,6 +18037,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -18459,6 +18611,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -18673,6 +18827,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19236,6 +19392,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19450,6 +19608,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -19896,6 +20056,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -20110,6 +20272,8 @@ spec: items: type: string type: array + labelWithoutSelector: + type: boolean namePrefix: type: string nameSuffix: @@ -20261,6 +20425,8 @@ spec: - metadata - spec type: object + templatePatch: + type: string required: - generators - template @@ -20745,6 +20911,14 @@ rules: verbs: - create - list +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -20942,6 +21116,95 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + labels: + app.kubernetes.io/component: applicationset-controller + app.kubernetes.io/name: argocd-applicationset-controller + app.kubernetes.io/part-of: argocd + name: argocd-applicationset-controller +rules: +- apiGroups: + - argoproj.io + resources: + - applications + - applicationsets + - applicationsets/finalizers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - argoproj.io + resources: + - applicationsets/status + verbs: + - get + - patch + - update +- apiGroups: + - argoproj.io + resources: + - appprojects + verbs: + - get +- apiGroups: + - "" + resources: + - events + verbs: + - create + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - update + - delete + - get + - list + - patch + - watch +- apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - watch +- apiGroups: + - apps + - extensions + resources: + - deployments + verbs: + - get + - list + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: labels: app.kubernetes.io/component: server @@ -21091,6 +21354,23 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding +metadata: + labels: + app.kubernetes.io/component: applicationset-controller + app.kubernetes.io/name: argocd-applicationset-controller + app.kubernetes.io/part-of: argocd + name: argocd-applicationset-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: argocd-applicationset-controller +subjects: +- kind: ServiceAccount + name: argocd-applicationset-controller + namespace: argocd +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/component: server @@ -21600,7 +21880,7 @@ spec: key: dexserver.disable.tls name: argocd-cmd-params-cm optional: true - image: ghcr.io/dexidp/dex:v2.37.0 + image: ghcr.io/dexidp/dex:v2.38.0 imagePullPolicy: Always name: dex ports: @@ -21705,6 +21985,12 @@ spec: key: application.namespaces name: argocd-cmd-params-cm optional: true + - name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED + valueFrom: + configMapKeyRef: + key: notificationscontroller.selfservice.enabled + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: @@ -21933,6 +22219,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE valueFrom: configMapKeyRef: @@ -22326,6 +22624,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -22350,6 +22660,12 @@ spec: key: server.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_API_CONTENT_TYPES + valueFrom: + configMapKeyRef: + key: server.api.content.types + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: @@ -22476,6 +22792,12 @@ spec: key: timeout.hard.reconciliation name: argocd-cm optional: true + - name: ARGOCD_RECONCILIATION_JITTER + valueFrom: + configMapKeyRef: + key: timeout.reconciliation.jitter + name: argocd-cm + optional: true - name: ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS valueFrom: configMapKeyRef: @@ -22584,6 +22906,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -22614,6 +22948,12 @@ spec: key: controller.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF + valueFrom: + configMapKeyRef: + key: controller.diff.server.side + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always name: argocd-application-controller diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml index 9e6f98030d89bf..cb58228423c11f 100644 --- a/manifests/namespace-install.yaml +++ b/manifests/namespace-install.yaml @@ -100,6 +100,14 @@ rules: verbs: - create - list +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role @@ -849,7 +857,7 @@ spec: key: dexserver.disable.tls name: argocd-cmd-params-cm optional: true - image: ghcr.io/dexidp/dex:v2.37.0 + image: ghcr.io/dexidp/dex:v2.38.0 imagePullPolicy: Always name: dex ports: @@ -954,6 +962,12 @@ spec: key: application.namespaces name: argocd-cmd-params-cm optional: true + - name: ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED + valueFrom: + configMapKeyRef: + key: notificationscontroller.selfservice.enabled + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: @@ -1182,6 +1196,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_REPO_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_REPO_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_REPO_SERVER_MAX_COMBINED_DIRECTORY_MANIFESTS_SIZE valueFrom: configMapKeyRef: @@ -1575,6 +1601,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -1599,6 +1637,12 @@ spec: key: server.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_API_CONTENT_TYPES + valueFrom: + configMapKeyRef: + key: server.api.content.types + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: @@ -1725,6 +1769,12 @@ spec: key: timeout.hard.reconciliation name: argocd-cm optional: true + - name: ARGOCD_RECONCILIATION_JITTER + valueFrom: + configMapKeyRef: + key: timeout.reconciliation.jitter + name: argocd-cm + optional: true - name: ARGOCD_REPO_ERROR_GRACE_PERIOD_SECONDS valueFrom: configMapKeyRef: @@ -1833,6 +1883,18 @@ spec: key: otlp.address name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + key: otlp.insecure + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + key: otlp.headers + name: argocd-cmd-params-cm + optional: true - name: ARGOCD_APPLICATION_NAMESPACES valueFrom: configMapKeyRef: @@ -1863,6 +1925,12 @@ spec: key: controller.k8sclient.retry.base.backoff name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF + valueFrom: + configMapKeyRef: + key: controller.diff.server.side + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always name: argocd-application-controller diff --git a/mkdocs.yml b/mkdocs.yml index 61abeae5f8725f..a7e8f86e216ccb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -128,6 +128,9 @@ nav: - operator-manual/server-commands/additional-configuration-method.md - Upgrading: - operator-manual/upgrading/overview.md + - operator-manual/upgrading/2.10-2.11.md + - operator-manual/upgrading/2.9-2.10.md + - operator-manual/upgrading/2.8-2.9.md - operator-manual/upgrading/2.7-2.8.md - operator-manual/upgrading/2.6-2.7.md - operator-manual/upgrading/2.5-2.6.md @@ -160,7 +163,9 @@ nav: - user-guide/multiple_sources.md - GnuPG verification: user-guide/gpg-verification.md - user-guide/auto_sync.md - - user-guide/diffing.md + - Diffing: + - Diff Strategies: user-guide/diff-strategies.md + - Diff Customization: user-guide/diffing.md - user-guide/orphaned-resources.md - user-guide/compare-options.md - user-guide/sync-options.md diff --git a/notification_controller/controller/controller.go b/notification_controller/controller/controller.go index 32dfac2b75a3b3..7d871af4c44a32 100644 --- a/notification_controller/controller/controller.go +++ b/notification_controller/controller/controller.go @@ -63,19 +63,27 @@ func NewController( registry *controller.MetricsRegistry, secretName string, configMapName string, + selfServiceNotificationEnabled bool, ) *notificationController { var appClient dynamic.ResourceInterface + namespaceableAppClient := client.Resource(applications) appClient = namespaceableAppClient + if len(applicationNamespaces) == 0 { appClient = namespaceableAppClient.Namespace(namespace) } - appInformer := newInformer(appClient, namespace, applicationNamespaces, appLabelSelector) appProjInformer := newInformer(newAppProjClient(client, namespace), namespace, []string{namespace}, "") - secretInformer := k8s.NewSecretInformer(k8sClient, namespace, secretName) - configMapInformer := k8s.NewConfigMapInformer(k8sClient, namespace, configMapName) - apiFactory := api.NewFactory(settings.GetFactorySettings(argocdService, secretName, configMapName), namespace, secretInformer, configMapInformer) + var notificationConfigNamespace string + if selfServiceNotificationEnabled { + notificationConfigNamespace = v1.NamespaceAll + } else { + notificationConfigNamespace = namespace + } + secretInformer := k8s.NewSecretInformer(k8sClient, notificationConfigNamespace, secretName) + configMapInformer := k8s.NewConfigMapInformer(k8sClient, notificationConfigNamespace, configMapName) + apiFactory := api.NewFactory(settings.GetFactorySettings(argocdService, secretName, configMapName, selfServiceNotificationEnabled), namespace, secretInformer, configMapInformer) res := ¬ificationController{ secretInformer: secretInformer, @@ -83,19 +91,30 @@ func NewController( appInformer: appInformer, appProjInformer: appProjInformer, apiFactory: apiFactory} - res.ctrl = controller.NewController(namespaceableAppClient, appInformer, apiFactory, - controller.WithSkipProcessing(func(obj v1.Object) (bool, string) { - app, ok := (obj).(*unstructured.Unstructured) - if !ok { - return false, "" - } - if checkAppNotInAdditionalNamespaces(app, namespace, applicationNamespaces) { - return true, "app is not in one of the application-namespaces, nor the notification controller namespace" - } - return !isAppSyncStatusRefreshed(app, log.WithField("app", obj.GetName())), "sync status out of date" - }), - controller.WithMetricsRegistry(registry), - controller.WithAlterDestinations(res.alterDestinations)) + skipProcessingOpt := controller.WithSkipProcessing(func(obj v1.Object) (bool, string) { + app, ok := (obj).(*unstructured.Unstructured) + if !ok { + return false, "" + } + if checkAppNotInAdditionalNamespaces(app, namespace, applicationNamespaces) { + return true, "app is not in one of the application-namespaces, nor the notification controller namespace" + } + return !isAppSyncStatusRefreshed(app, log.WithField("app", obj.GetName())), "sync status out of date" + }) + metricsRegistryOpt := controller.WithMetricsRegistry(registry) + alterDestinationsOpt := controller.WithAlterDestinations(res.alterDestinations) + + if !selfServiceNotificationEnabled { + res.ctrl = controller.NewController(namespaceableAppClient, appInformer, apiFactory, + skipProcessingOpt, + metricsRegistryOpt, + alterDestinationsOpt) + } else { + res.ctrl = controller.NewControllerWithNamespaceSupport(namespaceableAppClient, appInformer, apiFactory, + skipProcessingOpt, + metricsRegistryOpt, + alterDestinationsOpt) + } return res } @@ -118,6 +137,7 @@ func (c *notificationController) alterDestinations(obj v1.Object, destinations s } func newInformer(resClient dynamic.ResourceInterface, controllerNamespace string, applicationNamespaces []string, selector string) cache.SharedIndexInformer { + informer := cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { diff --git a/notification_controller/controller/controller_test.go b/notification_controller/controller/controller_test.go index 5ad1e520502a3f..4eedb28f5e0011 100644 --- a/notification_controller/controller/controller_test.go +++ b/notification_controller/controller/controller_test.go @@ -110,26 +110,30 @@ func TestInit(t *testing.T) { k8sClient := k8sfake.NewSimpleClientset() appLabelSelector := "app=test" - nc := NewController( - k8sClient, - dynamicClient, - nil, - "default", - []string{}, - appLabelSelector, - nil, - "my-secret", - "my-configmap", - ) - - assert.NotNil(t, nc) - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - err = nc.Init(ctx) - - assert.NoError(t, err) + selfServiceNotificationEnabledFlags := []bool{false, true} + for _, selfServiceNotificationEnabled := range selfServiceNotificationEnabledFlags { + nc := NewController( + k8sClient, + dynamicClient, + nil, + "default", + []string{}, + appLabelSelector, + nil, + "my-secret", + "my-configmap", + selfServiceNotificationEnabled, + ) + + assert.NotNil(t, nc) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + err = nc.Init(ctx) + + assert.NoError(t, err) + } } func TestInitTimeout(t *testing.T) { @@ -152,6 +156,7 @@ func TestInitTimeout(t *testing.T) { nil, "my-secret", "my-configmap", + false, ) assert.NotNil(t, nc) diff --git a/notifications_catalog/install.yaml b/notifications_catalog/install.yaml index 59b3665b9a2e3a..7457b25ddad897 100644 --- a/notifications_catalog/install.yaml +++ b/notifications_catalog/install.yaml @@ -40,8 +40,7 @@ data: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -68,8 +67,7 @@ data: "value": "{{.app.status.sync.revision}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -119,8 +117,7 @@ data: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -143,8 +140,7 @@ data: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -194,8 +190,7 @@ data: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -222,8 +217,7 @@ data: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -273,8 +267,7 @@ data: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -301,8 +294,7 @@ data: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -356,8 +348,7 @@ data: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -380,8 +371,7 @@ data: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" @@ -430,8 +420,7 @@ data: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -458,8 +447,7 @@ data: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" diff --git a/notifications_catalog/templates/app-deployed.yaml b/notifications_catalog/templates/app-deployed.yaml index 843bf57e21a89c..ee58c775f1fd80 100644 --- a/notifications_catalog/templates/app-deployed.yaml +++ b/notifications_catalog/templates/app-deployed.yaml @@ -25,8 +25,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -52,8 +51,7 @@ teams: "value": "{{.app.status.sync.revision}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" diff --git a/notifications_catalog/templates/app-health-degraded.yaml b/notifications_catalog/templates/app-health-degraded.yaml index 46c39b2e9ca0cf..59115c9a14935f 100644 --- a/notifications_catalog/templates/app-health-degraded.yaml +++ b/notifications_catalog/templates/app-health-degraded.yaml @@ -21,8 +21,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -44,8 +43,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" diff --git a/notifications_catalog/templates/app-sync-failed.yaml b/notifications_catalog/templates/app-sync-failed.yaml index 4a5ece85ba541d..a4c23787dde8b2 100644 --- a/notifications_catalog/templates/app-sync-failed.yaml +++ b/notifications_catalog/templates/app-sync-failed.yaml @@ -21,8 +21,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -48,8 +47,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" diff --git a/notifications_catalog/templates/app-sync-running.yaml b/notifications_catalog/templates/app-sync-running.yaml index b2a86042e3ce2c..434132ad86d896 100644 --- a/notifications_catalog/templates/app-sync-running.yaml +++ b/notifications_catalog/templates/app-sync-running.yaml @@ -21,8 +21,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -47,8 +46,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" diff --git a/notifications_catalog/templates/app-sync-status-unknown.yaml b/notifications_catalog/templates/app-sync-status-unknown.yaml index b1af244fb6d2d3..c893070bfcc63a 100644 --- a/notifications_catalog/templates/app-sync-status-unknown.yaml +++ b/notifications_catalog/templates/app-sync-status-unknown.yaml @@ -26,8 +26,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -48,8 +47,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" diff --git a/notifications_catalog/templates/app-sync-succeeded.yaml b/notifications_catalog/templates/app-sync-succeeded.yaml index d791de55149a49..76e467bd1c37dd 100644 --- a/notifications_catalog/templates/app-sync-succeeded.yaml +++ b/notifications_catalog/templates/app-sync-succeeded.yaml @@ -21,8 +21,7 @@ slack: "short": true } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "title": "{{$c.type}}", "value": "{{$c.message}}", @@ -48,8 +47,7 @@ teams: "value": "{{.app.spec.source.repoURL}}" } {{range $index, $c := .app.status.conditions}} - {{if not $index}},{{end}} - {{if $index}},{{end}} + , { "name": "{{$c.type}}", "value": "{{$c.message}}" diff --git a/pkg/apiclient/apiclient_test.go b/pkg/apiclient/apiclient_test.go index 7bb3b36befdde8..b4b35d0b80d483 100644 --- a/pkg/apiclient/apiclient_test.go +++ b/pkg/apiclient/apiclient_test.go @@ -7,10 +7,34 @@ import ( ) func Test_parseHeaders(t *testing.T) { - headerString := []string{"foo:", "foo1:bar1", "foo2:bar2:bar2"} - headers, err := parseHeaders(headerString) - assert.NoError(t, err) - assert.Equal(t, headers.Get("foo"), "") - assert.Equal(t, headers.Get("foo1"), "bar1") - assert.Equal(t, headers.Get("foo2"), "bar2:bar2") + t.Run("Header parsed successfully", func(t *testing.T) { + headerString := []string{"foo:", "foo1:bar1", "foo2:bar2:bar2"} + headers, err := parseHeaders(headerString) + assert.NoError(t, err) + assert.Equal(t, headers.Get("foo"), "") + assert.Equal(t, headers.Get("foo1"), "bar1") + assert.Equal(t, headers.Get("foo2"), "bar2:bar2") + }) + + t.Run("Header parsed error", func(t *testing.T) { + headerString := []string{"foo"} + _, err := parseHeaders(headerString) + assert.ErrorContains(t, err, "additional headers must be colon(:)-separated: foo") + }) +} + +func Test_parseGRPCHeaders(t *testing.T) { + t.Run("Header parsed successfully", func(t *testing.T) { + headerStrings := []string{"origin: https://foo.bar", "content-length: 123"} + headers, err := parseGRPCHeaders(headerStrings) + assert.NoError(t, err) + assert.Equal(t, headers.Get("origin"), []string{" https://foo.bar"}) + assert.Equal(t, headers.Get("content-length"), []string{" 123"}) + }) + + t.Run("Header parsed error", func(t *testing.T) { + headerString := []string{"foo"} + _, err := parseGRPCHeaders(headerString) + assert.ErrorContains(t, err, "additional headers must be colon(:)-separated: foo") + }) } diff --git a/pkg/apiclient/grpcproxy.go b/pkg/apiclient/grpcproxy.go index 28af7b62783df4..72fea42efee3f2 100644 --- a/pkg/apiclient/grpcproxy.go +++ b/pkg/apiclient/grpcproxy.go @@ -131,14 +131,14 @@ func (c *client) startGRPCProxy() (*grpc.Server, net.Listener, error) { } md, _ := metadata.FromIncomingContext(stream.Context()) + headersMD, err := parseGRPCHeaders(c.Headers) - for _, kv := range c.Headers { - if len(strings.Split(kv, ":"))%2 == 1 { - return fmt.Errorf("additional headers key/values must be separated by a colon(:): %s", kv) - } - md.Append(strings.Split(kv, ":")[0], strings.Split(kv, ":")[1]) + if err != nil { + return err } + md = metadata.Join(md, headersMD) + resp, err := c.executeRequest(fullMethodName, msg, md) if err != nil { return err @@ -216,3 +216,16 @@ func (c *client) useGRPCProxy() (net.Addr, io.Closer, error) { return nil }), nil } + +func parseGRPCHeaders(headerStrings []string) (metadata.MD, error) { + md := metadata.New(map[string]string{}) + for _, kv := range headerStrings { + i := strings.IndexByte(kv, ':') + // zero means meaningless empty header name + if i <= 0 { + return nil, fmt.Errorf("additional headers must be colon(:)-separated: %s", kv) + } + md.Append(kv[0:i], kv[i+1:]) + } + return md, nil +} diff --git a/pkg/apis/application/v1alpha1/application_defaults.go b/pkg/apis/application/v1alpha1/application_defaults.go index 2bc9b1bd0d7448..ad8112af8c88d2 100644 --- a/pkg/apis/application/v1alpha1/application_defaults.go +++ b/pkg/apis/application/v1alpha1/application_defaults.go @@ -9,6 +9,9 @@ const ( // ResourcesFinalizerName is the finalizer value which we inject to finalize deletion of an application ResourcesFinalizerName string = "resources-finalizer.argocd.argoproj.io" + // PostDeleteFinalizerName is the finalizer that controls post-delete hooks execution + PostDeleteFinalizerName string = "post-delete-finalizer.argocd.argoproj.io" + // ForegroundPropagationPolicyFinalizer is the finalizer we inject to delete application with foreground propagation policy ForegroundPropagationPolicyFinalizer string = "resources-finalizer.argocd.argoproj.io/foreground" diff --git a/pkg/apis/application/v1alpha1/applicationset_types.go b/pkg/apis/application/v1alpha1/applicationset_types.go index 357aa494ced6ad..4e27c35be71f35 100644 --- a/pkg/apis/application/v1alpha1/applicationset_types.go +++ b/pkg/apis/application/v1alpha1/applicationset_types.go @@ -65,6 +65,7 @@ type ApplicationSetSpec struct { // ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators ApplyNestedSelectors bool `json:"applyNestedSelectors,omitempty" protobuf:"bytes,8,name=applyNestedSelectors"` IgnoreApplicationDifferences ApplicationSetIgnoreDifferences `json:"ignoreApplicationDifferences,omitempty" protobuf:"bytes,9,name=ignoreApplicationDifferences"` + TemplatePatch *string `json:"templatePatch,omitempty" protobuf:"bytes,10,name=templatePatch"` } type ApplicationPreservedFields struct { diff --git a/pkg/apis/application/v1alpha1/generated.pb.go b/pkg/apis/application/v1alpha1/generated.pb.go index 432f0e10c9a265..f6a253d23ed7d4 100644 --- a/pkg/apis/application/v1alpha1/generated.pb.go +++ b/pkg/apis/application/v1alpha1/generated.pb.go @@ -3009,38 +3009,10 @@ func (m *PullRequestGeneratorGithub) XXX_DiscardUnknown() { var xxx_messageInfo_PullRequestGeneratorGithub proto.InternalMessageInfo -func (m *PullRequestGeneratorScmManager) Reset() { *m = PullRequestGeneratorScmManager{} } -func (*PullRequestGeneratorScmManager) ProtoMessage() {} -func (*PullRequestGeneratorScmManager) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{106} -} -func (m *PullRequestGeneratorScmManager) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *PullRequestGeneratorScmManager) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *PullRequestGeneratorScmManager) XXX_Merge(src proto.Message) { - xxx_messageInfo_PullRequestGeneratorScmManager.Merge(m, src) -} -func (m *PullRequestGeneratorScmManager) XXX_Size() int { - return m.Size() -} -func (m *PullRequestGeneratorScmManager) XXX_DiscardUnknown() { - xxx_messageInfo_PullRequestGeneratorScmManager.DiscardUnknown(m) -} - -var xxx_messageInfo_PullRequestGeneratorScmManager proto.InternalMessageInfo - func (m *RefTarget) Reset() { *m = RefTarget{} } func (*RefTarget) ProtoMessage() {} func (*RefTarget) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{107} + return fileDescriptor_030104ce3b95bcac, []int{106} } func (m *RefTarget) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3068,7 +3040,7 @@ var xxx_messageInfo_RefTarget proto.InternalMessageInfo func (m *RepoCreds) Reset() { *m = RepoCreds{} } func (*RepoCreds) ProtoMessage() {} func (*RepoCreds) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{108} + return fileDescriptor_030104ce3b95bcac, []int{107} } func (m *RepoCreds) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3096,7 +3068,7 @@ var xxx_messageInfo_RepoCreds proto.InternalMessageInfo func (m *RepoCredsList) Reset() { *m = RepoCredsList{} } func (*RepoCredsList) ProtoMessage() {} func (*RepoCredsList) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{109} + return fileDescriptor_030104ce3b95bcac, []int{108} } func (m *RepoCredsList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3124,7 +3096,7 @@ var xxx_messageInfo_RepoCredsList proto.InternalMessageInfo func (m *Repository) Reset() { *m = Repository{} } func (*Repository) ProtoMessage() {} func (*Repository) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{110} + return fileDescriptor_030104ce3b95bcac, []int{109} } func (m *Repository) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3152,7 +3124,7 @@ var xxx_messageInfo_Repository proto.InternalMessageInfo func (m *RepositoryCertificate) Reset() { *m = RepositoryCertificate{} } func (*RepositoryCertificate) ProtoMessage() {} func (*RepositoryCertificate) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{111} + return fileDescriptor_030104ce3b95bcac, []int{110} } func (m *RepositoryCertificate) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3180,7 +3152,7 @@ var xxx_messageInfo_RepositoryCertificate proto.InternalMessageInfo func (m *RepositoryCertificateList) Reset() { *m = RepositoryCertificateList{} } func (*RepositoryCertificateList) ProtoMessage() {} func (*RepositoryCertificateList) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{112} + return fileDescriptor_030104ce3b95bcac, []int{111} } func (m *RepositoryCertificateList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3208,7 +3180,7 @@ var xxx_messageInfo_RepositoryCertificateList proto.InternalMessageInfo func (m *RepositoryList) Reset() { *m = RepositoryList{} } func (*RepositoryList) ProtoMessage() {} func (*RepositoryList) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{113} + return fileDescriptor_030104ce3b95bcac, []int{112} } func (m *RepositoryList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3236,7 +3208,7 @@ var xxx_messageInfo_RepositoryList proto.InternalMessageInfo func (m *ResourceAction) Reset() { *m = ResourceAction{} } func (*ResourceAction) ProtoMessage() {} func (*ResourceAction) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{114} + return fileDescriptor_030104ce3b95bcac, []int{113} } func (m *ResourceAction) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3264,7 +3236,7 @@ var xxx_messageInfo_ResourceAction proto.InternalMessageInfo func (m *ResourceActionDefinition) Reset() { *m = ResourceActionDefinition{} } func (*ResourceActionDefinition) ProtoMessage() {} func (*ResourceActionDefinition) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{115} + return fileDescriptor_030104ce3b95bcac, []int{114} } func (m *ResourceActionDefinition) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3292,7 +3264,7 @@ var xxx_messageInfo_ResourceActionDefinition proto.InternalMessageInfo func (m *ResourceActionParam) Reset() { *m = ResourceActionParam{} } func (*ResourceActionParam) ProtoMessage() {} func (*ResourceActionParam) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{116} + return fileDescriptor_030104ce3b95bcac, []int{115} } func (m *ResourceActionParam) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3320,7 +3292,7 @@ var xxx_messageInfo_ResourceActionParam proto.InternalMessageInfo func (m *ResourceActions) Reset() { *m = ResourceActions{} } func (*ResourceActions) ProtoMessage() {} func (*ResourceActions) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{117} + return fileDescriptor_030104ce3b95bcac, []int{116} } func (m *ResourceActions) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3348,7 +3320,7 @@ var xxx_messageInfo_ResourceActions proto.InternalMessageInfo func (m *ResourceDiff) Reset() { *m = ResourceDiff{} } func (*ResourceDiff) ProtoMessage() {} func (*ResourceDiff) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{118} + return fileDescriptor_030104ce3b95bcac, []int{117} } func (m *ResourceDiff) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3376,7 +3348,7 @@ var xxx_messageInfo_ResourceDiff proto.InternalMessageInfo func (m *ResourceIgnoreDifferences) Reset() { *m = ResourceIgnoreDifferences{} } func (*ResourceIgnoreDifferences) ProtoMessage() {} func (*ResourceIgnoreDifferences) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{119} + return fileDescriptor_030104ce3b95bcac, []int{118} } func (m *ResourceIgnoreDifferences) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3404,7 +3376,7 @@ var xxx_messageInfo_ResourceIgnoreDifferences proto.InternalMessageInfo func (m *ResourceNetworkingInfo) Reset() { *m = ResourceNetworkingInfo{} } func (*ResourceNetworkingInfo) ProtoMessage() {} func (*ResourceNetworkingInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{120} + return fileDescriptor_030104ce3b95bcac, []int{119} } func (m *ResourceNetworkingInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3432,7 +3404,7 @@ var xxx_messageInfo_ResourceNetworkingInfo proto.InternalMessageInfo func (m *ResourceNode) Reset() { *m = ResourceNode{} } func (*ResourceNode) ProtoMessage() {} func (*ResourceNode) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{121} + return fileDescriptor_030104ce3b95bcac, []int{120} } func (m *ResourceNode) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3460,7 +3432,7 @@ var xxx_messageInfo_ResourceNode proto.InternalMessageInfo func (m *ResourceOverride) Reset() { *m = ResourceOverride{} } func (*ResourceOverride) ProtoMessage() {} func (*ResourceOverride) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{122} + return fileDescriptor_030104ce3b95bcac, []int{121} } func (m *ResourceOverride) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3488,7 +3460,7 @@ var xxx_messageInfo_ResourceOverride proto.InternalMessageInfo func (m *ResourceRef) Reset() { *m = ResourceRef{} } func (*ResourceRef) ProtoMessage() {} func (*ResourceRef) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{123} + return fileDescriptor_030104ce3b95bcac, []int{122} } func (m *ResourceRef) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3516,7 +3488,7 @@ var xxx_messageInfo_ResourceRef proto.InternalMessageInfo func (m *ResourceResult) Reset() { *m = ResourceResult{} } func (*ResourceResult) ProtoMessage() {} func (*ResourceResult) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{124} + return fileDescriptor_030104ce3b95bcac, []int{123} } func (m *ResourceResult) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3544,7 +3516,7 @@ var xxx_messageInfo_ResourceResult proto.InternalMessageInfo func (m *ResourceStatus) Reset() { *m = ResourceStatus{} } func (*ResourceStatus) ProtoMessage() {} func (*ResourceStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{125} + return fileDescriptor_030104ce3b95bcac, []int{124} } func (m *ResourceStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3572,7 +3544,7 @@ var xxx_messageInfo_ResourceStatus proto.InternalMessageInfo func (m *RetryStrategy) Reset() { *m = RetryStrategy{} } func (*RetryStrategy) ProtoMessage() {} func (*RetryStrategy) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{126} + return fileDescriptor_030104ce3b95bcac, []int{125} } func (m *RetryStrategy) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3600,7 +3572,7 @@ var xxx_messageInfo_RetryStrategy proto.InternalMessageInfo func (m *RevisionHistory) Reset() { *m = RevisionHistory{} } func (*RevisionHistory) ProtoMessage() {} func (*RevisionHistory) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{127} + return fileDescriptor_030104ce3b95bcac, []int{126} } func (m *RevisionHistory) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3628,7 +3600,7 @@ var xxx_messageInfo_RevisionHistory proto.InternalMessageInfo func (m *RevisionMetadata) Reset() { *m = RevisionMetadata{} } func (*RevisionMetadata) ProtoMessage() {} func (*RevisionMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{128} + return fileDescriptor_030104ce3b95bcac, []int{127} } func (m *RevisionMetadata) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3656,7 +3628,7 @@ var xxx_messageInfo_RevisionMetadata proto.InternalMessageInfo func (m *SCMProviderGenerator) Reset() { *m = SCMProviderGenerator{} } func (*SCMProviderGenerator) ProtoMessage() {} func (*SCMProviderGenerator) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{129} + return fileDescriptor_030104ce3b95bcac, []int{128} } func (m *SCMProviderGenerator) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3684,7 +3656,7 @@ var xxx_messageInfo_SCMProviderGenerator proto.InternalMessageInfo func (m *SCMProviderGeneratorAWSCodeCommit) Reset() { *m = SCMProviderGeneratorAWSCodeCommit{} } func (*SCMProviderGeneratorAWSCodeCommit) ProtoMessage() {} func (*SCMProviderGeneratorAWSCodeCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{130} + return fileDescriptor_030104ce3b95bcac, []int{129} } func (m *SCMProviderGeneratorAWSCodeCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3712,7 +3684,7 @@ var xxx_messageInfo_SCMProviderGeneratorAWSCodeCommit proto.InternalMessageInfo func (m *SCMProviderGeneratorAzureDevOps) Reset() { *m = SCMProviderGeneratorAzureDevOps{} } func (*SCMProviderGeneratorAzureDevOps) ProtoMessage() {} func (*SCMProviderGeneratorAzureDevOps) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{131} + return fileDescriptor_030104ce3b95bcac, []int{130} } func (m *SCMProviderGeneratorAzureDevOps) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3740,7 +3712,7 @@ var xxx_messageInfo_SCMProviderGeneratorAzureDevOps proto.InternalMessageInfo func (m *SCMProviderGeneratorBitbucket) Reset() { *m = SCMProviderGeneratorBitbucket{} } func (*SCMProviderGeneratorBitbucket) ProtoMessage() {} func (*SCMProviderGeneratorBitbucket) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{132} + return fileDescriptor_030104ce3b95bcac, []int{131} } func (m *SCMProviderGeneratorBitbucket) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3768,7 +3740,7 @@ var xxx_messageInfo_SCMProviderGeneratorBitbucket proto.InternalMessageInfo func (m *SCMProviderGeneratorBitbucketServer) Reset() { *m = SCMProviderGeneratorBitbucketServer{} } func (*SCMProviderGeneratorBitbucketServer) ProtoMessage() {} func (*SCMProviderGeneratorBitbucketServer) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{133} + return fileDescriptor_030104ce3b95bcac, []int{132} } func (m *SCMProviderGeneratorBitbucketServer) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3796,7 +3768,7 @@ var xxx_messageInfo_SCMProviderGeneratorBitbucketServer proto.InternalMessageInf func (m *SCMProviderGeneratorFilter) Reset() { *m = SCMProviderGeneratorFilter{} } func (*SCMProviderGeneratorFilter) ProtoMessage() {} func (*SCMProviderGeneratorFilter) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{134} + return fileDescriptor_030104ce3b95bcac, []int{133} } func (m *SCMProviderGeneratorFilter) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3824,7 +3796,7 @@ var xxx_messageInfo_SCMProviderGeneratorFilter proto.InternalMessageInfo func (m *SCMProviderGeneratorGitea) Reset() { *m = SCMProviderGeneratorGitea{} } func (*SCMProviderGeneratorGitea) ProtoMessage() {} func (*SCMProviderGeneratorGitea) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{135} + return fileDescriptor_030104ce3b95bcac, []int{134} } func (m *SCMProviderGeneratorGitea) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3852,7 +3824,7 @@ var xxx_messageInfo_SCMProviderGeneratorGitea proto.InternalMessageInfo func (m *SCMProviderGeneratorGithub) Reset() { *m = SCMProviderGeneratorGithub{} } func (*SCMProviderGeneratorGithub) ProtoMessage() {} func (*SCMProviderGeneratorGithub) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{136} + return fileDescriptor_030104ce3b95bcac, []int{135} } func (m *SCMProviderGeneratorGithub) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3880,7 +3852,7 @@ var xxx_messageInfo_SCMProviderGeneratorGithub proto.InternalMessageInfo func (m *SCMProviderGeneratorGitlab) Reset() { *m = SCMProviderGeneratorGitlab{} } func (*SCMProviderGeneratorGitlab) ProtoMessage() {} func (*SCMProviderGeneratorGitlab) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{137} + return fileDescriptor_030104ce3b95bcac, []int{136} } func (m *SCMProviderGeneratorGitlab) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3905,38 +3877,10 @@ func (m *SCMProviderGeneratorGitlab) XXX_DiscardUnknown() { var xxx_messageInfo_SCMProviderGeneratorGitlab proto.InternalMessageInfo -func (m *SCMProviderGeneratorScmManager) Reset() { *m = SCMProviderGeneratorScmManager{} } -func (*SCMProviderGeneratorScmManager) ProtoMessage() {} -func (*SCMProviderGeneratorScmManager) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{138} -} -func (m *SCMProviderGeneratorScmManager) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *SCMProviderGeneratorScmManager) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil -} -func (m *SCMProviderGeneratorScmManager) XXX_Merge(src proto.Message) { - xxx_messageInfo_SCMProviderGeneratorScmManager.Merge(m, src) -} -func (m *SCMProviderGeneratorScmManager) XXX_Size() int { - return m.Size() -} -func (m *SCMProviderGeneratorScmManager) XXX_DiscardUnknown() { - xxx_messageInfo_SCMProviderGeneratorScmManager.DiscardUnknown(m) -} - -var xxx_messageInfo_SCMProviderGeneratorScmManager proto.InternalMessageInfo - func (m *SecretRef) Reset() { *m = SecretRef{} } func (*SecretRef) ProtoMessage() {} func (*SecretRef) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{139} + return fileDescriptor_030104ce3b95bcac, []int{137} } func (m *SecretRef) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3964,7 +3908,7 @@ var xxx_messageInfo_SecretRef proto.InternalMessageInfo func (m *SignatureKey) Reset() { *m = SignatureKey{} } func (*SignatureKey) ProtoMessage() {} func (*SignatureKey) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{140} + return fileDescriptor_030104ce3b95bcac, []int{138} } func (m *SignatureKey) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3992,7 +3936,7 @@ var xxx_messageInfo_SignatureKey proto.InternalMessageInfo func (m *SyncOperation) Reset() { *m = SyncOperation{} } func (*SyncOperation) ProtoMessage() {} func (*SyncOperation) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{141} + return fileDescriptor_030104ce3b95bcac, []int{139} } func (m *SyncOperation) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4020,7 +3964,7 @@ var xxx_messageInfo_SyncOperation proto.InternalMessageInfo func (m *SyncOperationResource) Reset() { *m = SyncOperationResource{} } func (*SyncOperationResource) ProtoMessage() {} func (*SyncOperationResource) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{142} + return fileDescriptor_030104ce3b95bcac, []int{140} } func (m *SyncOperationResource) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4048,7 +3992,7 @@ var xxx_messageInfo_SyncOperationResource proto.InternalMessageInfo func (m *SyncOperationResult) Reset() { *m = SyncOperationResult{} } func (*SyncOperationResult) ProtoMessage() {} func (*SyncOperationResult) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{143} + return fileDescriptor_030104ce3b95bcac, []int{141} } func (m *SyncOperationResult) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4076,7 +4020,7 @@ var xxx_messageInfo_SyncOperationResult proto.InternalMessageInfo func (m *SyncPolicy) Reset() { *m = SyncPolicy{} } func (*SyncPolicy) ProtoMessage() {} func (*SyncPolicy) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{144} + return fileDescriptor_030104ce3b95bcac, []int{142} } func (m *SyncPolicy) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4104,7 +4048,7 @@ var xxx_messageInfo_SyncPolicy proto.InternalMessageInfo func (m *SyncPolicyAutomated) Reset() { *m = SyncPolicyAutomated{} } func (*SyncPolicyAutomated) ProtoMessage() {} func (*SyncPolicyAutomated) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{145} + return fileDescriptor_030104ce3b95bcac, []int{143} } func (m *SyncPolicyAutomated) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4132,7 +4076,7 @@ var xxx_messageInfo_SyncPolicyAutomated proto.InternalMessageInfo func (m *SyncStatus) Reset() { *m = SyncStatus{} } func (*SyncStatus) ProtoMessage() {} func (*SyncStatus) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{146} + return fileDescriptor_030104ce3b95bcac, []int{144} } func (m *SyncStatus) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4160,7 +4104,7 @@ var xxx_messageInfo_SyncStatus proto.InternalMessageInfo func (m *SyncStrategy) Reset() { *m = SyncStrategy{} } func (*SyncStrategy) ProtoMessage() {} func (*SyncStrategy) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{147} + return fileDescriptor_030104ce3b95bcac, []int{145} } func (m *SyncStrategy) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4188,7 +4132,7 @@ var xxx_messageInfo_SyncStrategy proto.InternalMessageInfo func (m *SyncStrategyApply) Reset() { *m = SyncStrategyApply{} } func (*SyncStrategyApply) ProtoMessage() {} func (*SyncStrategyApply) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{148} + return fileDescriptor_030104ce3b95bcac, []int{146} } func (m *SyncStrategyApply) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4216,7 +4160,7 @@ var xxx_messageInfo_SyncStrategyApply proto.InternalMessageInfo func (m *SyncStrategyHook) Reset() { *m = SyncStrategyHook{} } func (*SyncStrategyHook) ProtoMessage() {} func (*SyncStrategyHook) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{149} + return fileDescriptor_030104ce3b95bcac, []int{147} } func (m *SyncStrategyHook) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4244,7 +4188,7 @@ var xxx_messageInfo_SyncStrategyHook proto.InternalMessageInfo func (m *SyncWindow) Reset() { *m = SyncWindow{} } func (*SyncWindow) ProtoMessage() {} func (*SyncWindow) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{150} + return fileDescriptor_030104ce3b95bcac, []int{148} } func (m *SyncWindow) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4272,7 +4216,7 @@ var xxx_messageInfo_SyncWindow proto.InternalMessageInfo func (m *TLSClientConfig) Reset() { *m = TLSClientConfig{} } func (*TLSClientConfig) ProtoMessage() {} func (*TLSClientConfig) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{151} + return fileDescriptor_030104ce3b95bcac, []int{149} } func (m *TLSClientConfig) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4300,7 +4244,7 @@ var xxx_messageInfo_TLSClientConfig proto.InternalMessageInfo func (m *TagFilter) Reset() { *m = TagFilter{} } func (*TagFilter) ProtoMessage() {} func (*TagFilter) Descriptor() ([]byte, []int) { - return fileDescriptor_030104ce3b95bcac, []int{152} + return fileDescriptor_030104ce3b95bcac, []int{150} } func (m *TagFilter) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -4449,7 +4393,6 @@ func init() { proto.RegisterType((*PullRequestGeneratorGitLab)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.PullRequestGeneratorGitLab") proto.RegisterType((*PullRequestGeneratorGitea)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.PullRequestGeneratorGitea") proto.RegisterType((*PullRequestGeneratorGithub)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.PullRequestGeneratorGithub") - proto.RegisterType((*PullRequestGeneratorScmManager)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.PullRequestGeneratorScmManager") proto.RegisterType((*RefTarget)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.RefTarget") proto.RegisterType((*RepoCreds)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.RepoCreds") proto.RegisterType((*RepoCredsList)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.RepoCredsList") @@ -4484,7 +4427,6 @@ func init() { proto.RegisterType((*SCMProviderGeneratorGitea)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.SCMProviderGeneratorGitea") proto.RegisterType((*SCMProviderGeneratorGithub)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.SCMProviderGeneratorGithub") proto.RegisterType((*SCMProviderGeneratorGitlab)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.SCMProviderGeneratorGitlab") - proto.RegisterType((*SCMProviderGeneratorScmManager)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.SCMProviderGeneratorScmManager") proto.RegisterType((*SecretRef)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.SecretRef") proto.RegisterType((*SignatureKey)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.SignatureKey") proto.RegisterType((*SyncOperation)(nil), "github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.SyncOperation") @@ -4506,698 +4448,697 @@ func init() { } var fileDescriptor_030104ce3b95bcac = []byte{ - // 11045 bytes of a gzipped FileDescriptorProto + // 11030 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x7d, 0x6d, 0x70, 0x1c, 0xc9, - 0x75, 0x98, 0x66, 0x3f, 0x80, 0xdd, 0x87, 0x0f, 0x92, 0x4d, 0xf2, 0x0e, 0x47, 0xdd, 0x11, 0xf4, - 0x5c, 0x7c, 0x3a, 0x47, 0x77, 0x80, 0x8f, 0xbe, 0x53, 0x2e, 0x3e, 0x5b, 0x32, 0x3e, 0x48, 0x10, - 0x24, 0x40, 0xe0, 0x1a, 0x20, 0x29, 0x9d, 0x74, 0x3a, 0x0d, 0x66, 0x1b, 0x8b, 0x21, 0x66, 0x67, - 0xf6, 0x66, 0x66, 0x41, 0xe0, 0x2c, 0xc9, 0x92, 0x25, 0xdb, 0x4a, 0xf4, 0x71, 0x8a, 0x94, 0x94, - 0xce, 0x49, 0xe4, 0xc8, 0x1f, 0x49, 0xd9, 0x95, 0xa8, 0xe2, 0xd8, 0x3f, 0xe2, 0xc4, 0x49, 0xa5, - 0x6c, 0xa7, 0x52, 0x4a, 0x29, 0x89, 0x5d, 0x29, 0x97, 0xe5, 0x7c, 0x18, 0x91, 0x98, 0x72, 0x25, - 0x95, 0xaa, 0xb8, 0xca, 0x89, 0x7f, 0x24, 0x4c, 0x7e, 0xa4, 0xfa, 0xbb, 0x67, 0x76, 0x16, 0x58, - 0x00, 0x03, 0x92, 0x52, 0xee, 0xdf, 0x6e, 0xbf, 0x37, 0xef, 0xf5, 0xf4, 0x74, 0xbf, 0xf7, 0xfa, - 0xf5, 0x7b, 0xaf, 0x61, 0xa1, 0xe9, 0x25, 0x1b, 0x9d, 0xb5, 0x09, 0x37, 0x6c, 0x4d, 0x3a, 0x51, - 0x33, 0x6c, 0x47, 0xe1, 0x6d, 0xf6, 0xe3, 0x59, 0xb7, 0x31, 0xb9, 0x75, 0x71, 0xb2, 0xbd, 0xd9, - 0x9c, 0x74, 0xda, 0x5e, 0x3c, 0xe9, 0xb4, 0xdb, 0xbe, 0xe7, 0x3a, 0x89, 0x17, 0x06, 0x93, 0x5b, - 0xcf, 0x39, 0x7e, 0x7b, 0xc3, 0x79, 0x6e, 0xb2, 0x49, 0x02, 0x12, 0x39, 0x09, 0x69, 0x4c, 0xb4, - 0xa3, 0x30, 0x09, 0xd1, 0x8f, 0x68, 0x6a, 0x13, 0x92, 0x1a, 0xfb, 0xf1, 0x9a, 0xdb, 0x98, 0xd8, - 0xba, 0x38, 0xd1, 0xde, 0x6c, 0x4e, 0x50, 0x6a, 0x13, 0x06, 0xb5, 0x09, 0x49, 0xed, 0xdc, 0xb3, - 0x46, 0x5f, 0x9a, 0x61, 0x33, 0x9c, 0x64, 0x44, 0xd7, 0x3a, 0xeb, 0xec, 0x1f, 0xfb, 0xc3, 0x7e, - 0x71, 0x66, 0xe7, 0xec, 0xcd, 0x17, 0xe3, 0x09, 0x2f, 0xa4, 0xdd, 0x9b, 0x74, 0xc3, 0x88, 0x4c, - 0x6e, 0x75, 0x75, 0xe8, 0xdc, 0x15, 0x8d, 0x43, 0xb6, 0x13, 0x12, 0xc4, 0x5e, 0x18, 0xc4, 0xcf, - 0xd2, 0x2e, 0x90, 0x68, 0x8b, 0x44, 0xe6, 0xeb, 0x19, 0x08, 0x79, 0x94, 0x9e, 0xd7, 0x94, 0x5a, - 0x8e, 0xbb, 0xe1, 0x05, 0x24, 0xda, 0xd1, 0x8f, 0xb7, 0x48, 0xe2, 0xe4, 0x3d, 0x35, 0xd9, 0xeb, - 0xa9, 0xa8, 0x13, 0x24, 0x5e, 0x8b, 0x74, 0x3d, 0xf0, 0x9e, 0xfd, 0x1e, 0x88, 0xdd, 0x0d, 0xd2, - 0x72, 0xba, 0x9e, 0xfb, 0xa1, 0x5e, 0xcf, 0x75, 0x12, 0xcf, 0x9f, 0xf4, 0x82, 0x24, 0x4e, 0xa2, - 0xec, 0x43, 0xf6, 0xeb, 0x30, 0x32, 0x75, 0x6b, 0x65, 0xaa, 0x93, 0x6c, 0xcc, 0x84, 0xc1, 0xba, - 0xd7, 0x44, 0x2f, 0xc0, 0x90, 0xeb, 0x77, 0xe2, 0x84, 0x44, 0xd7, 0x9d, 0x16, 0x19, 0xb3, 0x2e, - 0x58, 0x4f, 0xd7, 0xa7, 0x4f, 0x7f, 0x63, 0x77, 0xfc, 0x1d, 0x77, 0x77, 0xc7, 0x87, 0x66, 0x34, - 0x08, 0x9b, 0x78, 0xe8, 0x07, 0x60, 0x30, 0x0a, 0x7d, 0x32, 0x85, 0xaf, 0x8f, 0x95, 0xd8, 0x23, - 0x27, 0xc4, 0x23, 0x83, 0x98, 0x37, 0x63, 0x09, 0xb7, 0xff, 0xa0, 0x04, 0x30, 0xd5, 0x6e, 0x2f, - 0x47, 0xe1, 0x6d, 0xe2, 0x26, 0xe8, 0x23, 0x50, 0xa3, 0x43, 0xd7, 0x70, 0x12, 0x87, 0x71, 0x1b, - 0xba, 0xf8, 0x83, 0x13, 0xfc, 0x4d, 0x26, 0xcc, 0x37, 0xd1, 0x13, 0x87, 0x62, 0x4f, 0x6c, 0x3d, - 0x37, 0xb1, 0xb4, 0x46, 0x9f, 0x5f, 0x24, 0x89, 0x33, 0x8d, 0x04, 0x33, 0xd0, 0x6d, 0x58, 0x51, - 0x45, 0x01, 0x54, 0xe2, 0x36, 0x71, 0x59, 0xc7, 0x86, 0x2e, 0x2e, 0x4c, 0x1c, 0x65, 0x86, 0x4e, - 0xe8, 0x9e, 0xaf, 0xb4, 0x89, 0x3b, 0x3d, 0x2c, 0x38, 0x57, 0xe8, 0x3f, 0xcc, 0xf8, 0xa0, 0x2d, - 0x18, 0x88, 0x13, 0x27, 0xe9, 0xc4, 0x63, 0x65, 0xc6, 0xf1, 0x7a, 0x61, 0x1c, 0x19, 0xd5, 0xe9, - 0x51, 0xc1, 0x73, 0x80, 0xff, 0xc7, 0x82, 0x9b, 0xfd, 0x47, 0x16, 0x8c, 0x6a, 0xe4, 0x05, 0x2f, - 0x4e, 0xd0, 0x87, 0xba, 0x06, 0x77, 0xa2, 0xbf, 0xc1, 0xa5, 0x4f, 0xb3, 0xa1, 0x3d, 0x29, 0x98, - 0xd5, 0x64, 0x8b, 0x31, 0xb0, 0x2d, 0xa8, 0x7a, 0x09, 0x69, 0xc5, 0x63, 0xa5, 0x0b, 0xe5, 0xa7, - 0x87, 0x2e, 0x5e, 0x29, 0xea, 0x3d, 0xa7, 0x47, 0x04, 0xd3, 0xea, 0x3c, 0x25, 0x8f, 0x39, 0x17, - 0xfb, 0x57, 0x86, 0xcd, 0xf7, 0xa3, 0x03, 0x8e, 0x9e, 0x83, 0xa1, 0x38, 0xec, 0x44, 0x2e, 0xc1, - 0xa4, 0x1d, 0xc6, 0x63, 0xd6, 0x85, 0x32, 0x9d, 0x7a, 0x74, 0xa6, 0xae, 0xe8, 0x66, 0x6c, 0xe2, - 0xa0, 0x2f, 0x58, 0x30, 0xdc, 0x20, 0x71, 0xe2, 0x05, 0x8c, 0xbf, 0xec, 0xfc, 0xea, 0x91, 0x3b, - 0x2f, 0x1b, 0x67, 0x35, 0xf1, 0xe9, 0x33, 0xe2, 0x45, 0x86, 0x8d, 0xc6, 0x18, 0xa7, 0xf8, 0xd3, - 0x15, 0xd7, 0x20, 0xb1, 0x1b, 0x79, 0x6d, 0xfa, 0x9f, 0xcd, 0x19, 0x63, 0xc5, 0xcd, 0x6a, 0x10, - 0x36, 0xf1, 0x50, 0x00, 0x55, 0xba, 0xa2, 0xe2, 0xb1, 0x0a, 0xeb, 0xff, 0xfc, 0xd1, 0xfa, 0x2f, - 0x06, 0x95, 0x2e, 0x56, 0x3d, 0xfa, 0xf4, 0x5f, 0x8c, 0x39, 0x1b, 0xf4, 0x79, 0x0b, 0xc6, 0xc4, - 0x8a, 0xc7, 0x84, 0x0f, 0xe8, 0xad, 0x0d, 0x2f, 0x21, 0xbe, 0x17, 0x27, 0x63, 0x55, 0xd6, 0x87, - 0xc9, 0xfe, 0xe6, 0xd6, 0x5c, 0x14, 0x76, 0xda, 0xd7, 0xbc, 0xa0, 0x31, 0x7d, 0x41, 0x70, 0x1a, - 0x9b, 0xe9, 0x41, 0x18, 0xf7, 0x64, 0x89, 0xbe, 0x6c, 0xc1, 0xb9, 0xc0, 0x69, 0x91, 0xb8, 0xed, - 0xd0, 0x4f, 0xcb, 0xc1, 0xd3, 0xbe, 0xe3, 0x6e, 0xb2, 0x1e, 0x0d, 0x1c, 0xae, 0x47, 0xb6, 0xe8, - 0xd1, 0xb9, 0xeb, 0x3d, 0x49, 0xe3, 0x3d, 0xd8, 0xa2, 0x5f, 0xb4, 0xe0, 0x54, 0x18, 0xb5, 0x37, - 0x9c, 0x80, 0x34, 0x24, 0x34, 0x1e, 0x1b, 0x64, 0x4b, 0xef, 0xc3, 0x47, 0xfb, 0x44, 0x4b, 0x59, - 0xb2, 0x8b, 0x61, 0xe0, 0x25, 0x61, 0xb4, 0x42, 0x92, 0xc4, 0x0b, 0x9a, 0xf1, 0xf4, 0xd9, 0xbb, - 0xbb, 0xe3, 0xa7, 0xba, 0xb0, 0x70, 0x77, 0x7f, 0xd0, 0x8f, 0xc3, 0x50, 0xbc, 0x13, 0xb8, 0xb7, - 0xbc, 0xa0, 0x11, 0xde, 0x89, 0xc7, 0x6a, 0x45, 0x2c, 0xdf, 0x15, 0x45, 0x50, 0x2c, 0x40, 0xcd, - 0x00, 0x9b, 0xdc, 0xf2, 0x3f, 0x9c, 0x9e, 0x4a, 0xf5, 0xa2, 0x3f, 0x9c, 0x9e, 0x4c, 0x7b, 0xb0, - 0x45, 0x3f, 0x63, 0xc1, 0x48, 0xec, 0x35, 0x03, 0x27, 0xe9, 0x44, 0xe4, 0x1a, 0xd9, 0x89, 0xc7, - 0x80, 0x75, 0xe4, 0xea, 0x11, 0x47, 0xc5, 0x20, 0x39, 0x7d, 0x56, 0xf4, 0x71, 0xc4, 0x6c, 0x8d, - 0x71, 0x9a, 0x6f, 0xde, 0x42, 0xd3, 0xd3, 0x7a, 0xa8, 0xd8, 0x85, 0xa6, 0x27, 0x75, 0x4f, 0x96, - 0xe8, 0xc7, 0xe0, 0x24, 0x6f, 0x52, 0x23, 0x1b, 0x8f, 0x0d, 0x33, 0x41, 0x7b, 0xe6, 0xee, 0xee, - 0xf8, 0xc9, 0x95, 0x0c, 0x0c, 0x77, 0x61, 0xa3, 0xd7, 0x61, 0xbc, 0x4d, 0xa2, 0x96, 0x97, 0x2c, - 0x05, 0xfe, 0x8e, 0x14, 0xdf, 0x6e, 0xd8, 0x26, 0x0d, 0xd1, 0x9d, 0x78, 0x6c, 0xe4, 0x82, 0xf5, - 0x74, 0x6d, 0xfa, 0x5d, 0xa2, 0x9b, 0xe3, 0xcb, 0x7b, 0xa3, 0xe3, 0xfd, 0xe8, 0xd9, 0xff, 0xb2, - 0x04, 0x27, 0xb3, 0x8a, 0x13, 0xfd, 0x1d, 0x0b, 0x4e, 0xdc, 0xbe, 0x93, 0xac, 0x86, 0x9b, 0x24, - 0x88, 0xa7, 0x77, 0xa8, 0x78, 0x63, 0x2a, 0x63, 0xe8, 0xa2, 0x5b, 0xac, 0x8a, 0x9e, 0xb8, 0x9a, - 0xe6, 0x72, 0x29, 0x48, 0xa2, 0x9d, 0xe9, 0x47, 0xc5, 0xdb, 0x9d, 0xb8, 0x7a, 0x6b, 0xd5, 0x84, - 0xe2, 0x6c, 0xa7, 0xce, 0x7d, 0xd6, 0x82, 0x33, 0x79, 0x24, 0xd0, 0x49, 0x28, 0x6f, 0x92, 0x1d, - 0x6e, 0x95, 0x61, 0xfa, 0x13, 0xbd, 0x0a, 0xd5, 0x2d, 0xc7, 0xef, 0x10, 0x61, 0xdd, 0xcc, 0x1d, - 0xed, 0x45, 0x54, 0xcf, 0x30, 0xa7, 0xfa, 0xc3, 0xa5, 0x17, 0x2d, 0xfb, 0x77, 0xcb, 0x30, 0x64, - 0xe8, 0xb7, 0xfb, 0x60, 0xb1, 0x85, 0x29, 0x8b, 0x6d, 0xb1, 0x30, 0xd5, 0xdc, 0xd3, 0x64, 0xbb, - 0x93, 0x31, 0xd9, 0x96, 0x8a, 0x63, 0xb9, 0xa7, 0xcd, 0x86, 0x12, 0xa8, 0x87, 0x6d, 0x6a, 0x91, - 0x53, 0xd5, 0x5f, 0x29, 0xe2, 0x13, 0x2e, 0x49, 0x72, 0xd3, 0x23, 0x77, 0x77, 0xc7, 0xeb, 0xea, - 0x2f, 0xd6, 0x8c, 0xec, 0x6f, 0x59, 0x70, 0xc6, 0xe8, 0xe3, 0x4c, 0x18, 0x34, 0x3c, 0xf6, 0x69, - 0x2f, 0x40, 0x25, 0xd9, 0x69, 0x4b, 0xb3, 0x5f, 0x8d, 0xd4, 0xea, 0x4e, 0x9b, 0x60, 0x06, 0xa1, - 0x86, 0x7e, 0x8b, 0xc4, 0xb1, 0xd3, 0x24, 0x59, 0x43, 0x7f, 0x91, 0x37, 0x63, 0x09, 0x47, 0x11, - 0x20, 0xdf, 0x89, 0x93, 0xd5, 0xc8, 0x09, 0x62, 0x46, 0x7e, 0xd5, 0x6b, 0x11, 0x31, 0xc0, 0x7f, - 0xbe, 0xbf, 0x19, 0x43, 0x9f, 0x98, 0x7e, 0xe4, 0xee, 0xee, 0x38, 0x5a, 0xe8, 0xa2, 0x84, 0x73, - 0xa8, 0xdb, 0x5f, 0xb6, 0xe0, 0x91, 0x7c, 0x5b, 0x0c, 0x3d, 0x05, 0x03, 0x7c, 0xcb, 0x27, 0xde, - 0x4e, 0x7f, 0x12, 0xd6, 0x8a, 0x05, 0x14, 0x4d, 0x42, 0x5d, 0xe9, 0x09, 0xf1, 0x8e, 0xa7, 0x04, - 0x6a, 0x5d, 0x2b, 0x17, 0x8d, 0x43, 0x07, 0x8d, 0xfe, 0x11, 0x96, 0x9b, 0x1a, 0x34, 0xb6, 0x49, - 0x62, 0x10, 0xfb, 0x3f, 0x59, 0x70, 0xc2, 0xe8, 0xd5, 0x7d, 0x30, 0xcd, 0x83, 0xb4, 0x69, 0x3e, - 0x5f, 0xd8, 0x7c, 0xee, 0x61, 0x9b, 0x7f, 0xde, 0x82, 0x73, 0x06, 0xd6, 0xa2, 0x93, 0xb8, 0x1b, - 0x97, 0xb6, 0xdb, 0x11, 0x89, 0xe9, 0x76, 0x1a, 0x3d, 0x61, 0xc8, 0xad, 0xe9, 0x21, 0x41, 0xa1, - 0x7c, 0x8d, 0xec, 0x70, 0x21, 0xf6, 0x0c, 0xd4, 0xf8, 0xe4, 0x0c, 0x23, 0x31, 0xe2, 0xea, 0xdd, - 0x96, 0x44, 0x3b, 0x56, 0x18, 0xc8, 0x86, 0x01, 0x26, 0x9c, 0xe8, 0x62, 0xa5, 0x6a, 0x08, 0xe8, - 0x47, 0xbc, 0xc9, 0x5a, 0xb0, 0x80, 0xd8, 0x71, 0xaa, 0x3b, 0xcb, 0x11, 0x61, 0x1f, 0xb7, 0x71, - 0xd9, 0x23, 0x7e, 0x23, 0xa6, 0xdb, 0x06, 0x27, 0x08, 0xc2, 0x44, 0xec, 0x00, 0x8c, 0x6d, 0xc3, - 0x94, 0x6e, 0xc6, 0x26, 0x0e, 0x65, 0xea, 0x3b, 0x6b, 0xc4, 0xe7, 0x23, 0x2a, 0x98, 0x2e, 0xb0, - 0x16, 0x2c, 0x20, 0xf6, 0xdd, 0x12, 0xdb, 0xa0, 0xa8, 0xa5, 0x4f, 0xee, 0xc7, 0xee, 0x36, 0x4a, - 0xc9, 0xca, 0xe5, 0xe2, 0x04, 0x17, 0xe9, 0xbd, 0xc3, 0x7d, 0x23, 0x23, 0x2e, 0x71, 0xa1, 0x5c, - 0xf7, 0xde, 0xe5, 0xfe, 0x56, 0x09, 0xc6, 0xd3, 0x0f, 0x74, 0x49, 0x5b, 0xba, 0xa5, 0x32, 0x18, - 0x65, 0x9d, 0x18, 0x06, 0x3e, 0x36, 0xf1, 0x7a, 0x08, 0xac, 0xd2, 0x71, 0x0a, 0x2c, 0x53, 0x9e, - 0x96, 0xf7, 0x91, 0xa7, 0x4f, 0xa9, 0x51, 0xaf, 0x64, 0x04, 0x58, 0x5a, 0xa7, 0x5c, 0x80, 0x4a, - 0x9c, 0x90, 0xf6, 0x58, 0x35, 0x2d, 0x8f, 0x56, 0x12, 0xd2, 0xc6, 0x0c, 0x62, 0xff, 0xb7, 0x12, - 0x3c, 0x9a, 0x1e, 0x43, 0xad, 0x02, 0xde, 0x97, 0x52, 0x01, 0xef, 0x36, 0x55, 0xc0, 0xbd, 0xdd, - 0xf1, 0x77, 0xf6, 0x78, 0xec, 0xbb, 0x46, 0x43, 0xa0, 0xb9, 0xcc, 0x28, 0x4e, 0xa6, 0x47, 0xf1, - 0xde, 0xee, 0xf8, 0x13, 0x3d, 0xde, 0x31, 0x33, 0xcc, 0x4f, 0xc1, 0x40, 0x44, 0x9c, 0x38, 0x0c, - 0xc4, 0x40, 0xab, 0xcf, 0x81, 0x59, 0x2b, 0x16, 0x50, 0xfb, 0xdf, 0xd6, 0xb3, 0x83, 0x3d, 0xc7, - 0x9d, 0x70, 0x61, 0x84, 0x3c, 0xa8, 0x30, 0xb3, 0x9e, 0x8b, 0x86, 0x6b, 0x47, 0x5b, 0x46, 0x54, - 0x0d, 0x28, 0xd2, 0xd3, 0x35, 0xfa, 0xd5, 0x68, 0x13, 0x66, 0x2c, 0xd0, 0x36, 0xd4, 0x5c, 0x69, - 0x6d, 0x97, 0x8a, 0xf0, 0x4b, 0x09, 0x5b, 0x5b, 0x73, 0x1c, 0xa6, 0xf2, 0x5a, 0x99, 0xe8, 0x8a, - 0x1b, 0x22, 0x50, 0x6e, 0x7a, 0x89, 0xf8, 0xac, 0x47, 0xdc, 0x4f, 0xcd, 0x79, 0xc6, 0x2b, 0x0e, - 0x52, 0x25, 0x32, 0xe7, 0x25, 0x98, 0xd2, 0x47, 0x3f, 0x65, 0xc1, 0x50, 0xec, 0xb6, 0x96, 0xa3, - 0x70, 0xcb, 0x6b, 0x90, 0x48, 0x58, 0x53, 0x47, 0x14, 0x4d, 0x2b, 0x33, 0x8b, 0x92, 0xa0, 0xe6, - 0xcb, 0xf7, 0xb7, 0x1a, 0x82, 0x4d, 0xbe, 0x74, 0x97, 0xf1, 0xa8, 0x78, 0xf7, 0x59, 0xe2, 0x7a, - 0x54, 0xff, 0xc9, 0x4d, 0x15, 0x9b, 0x29, 0x47, 0xb6, 0x2e, 0x67, 0x3b, 0xee, 0x26, 0x5d, 0x6f, - 0xba, 0x43, 0xef, 0xbc, 0xbb, 0x3b, 0xfe, 0xe8, 0x4c, 0x3e, 0x4f, 0xdc, 0xab, 0x33, 0x6c, 0xc0, - 0xda, 0x1d, 0xdf, 0xc7, 0xe4, 0xf5, 0x0e, 0x61, 0x2e, 0x93, 0x02, 0x06, 0x6c, 0x59, 0x13, 0xcc, - 0x0c, 0x98, 0x01, 0xc1, 0x26, 0x5f, 0xf4, 0x3a, 0x0c, 0xb4, 0x9c, 0x24, 0xf2, 0xb6, 0x85, 0x9f, - 0xe4, 0x88, 0xf6, 0xfe, 0x22, 0xa3, 0xa5, 0x99, 0x33, 0x4d, 0xcd, 0x1b, 0xb1, 0x60, 0x84, 0x5a, - 0x50, 0x6d, 0x91, 0xa8, 0x49, 0xc6, 0x6a, 0x45, 0xf8, 0x84, 0x17, 0x29, 0x29, 0xcd, 0xb0, 0x4e, - 0xad, 0x23, 0xd6, 0x86, 0x39, 0x17, 0xf4, 0x2a, 0xd4, 0x62, 0xe2, 0x13, 0x97, 0xda, 0x37, 0x75, - 0xc6, 0xf1, 0x87, 0xfa, 0xb4, 0xf5, 0xa8, 0x61, 0xb1, 0x22, 0x1e, 0xe5, 0x0b, 0x4c, 0xfe, 0xc3, - 0x8a, 0x24, 0x1d, 0xc0, 0xb6, 0xdf, 0x69, 0x7a, 0xc1, 0x18, 0x14, 0x31, 0x80, 0xcb, 0x8c, 0x56, - 0x66, 0x00, 0x79, 0x23, 0x16, 0x8c, 0xec, 0x3f, 0xb6, 0x00, 0xa5, 0x85, 0xda, 0x7d, 0x30, 0x6a, - 0x5f, 0x4f, 0x1b, 0xb5, 0x0b, 0x45, 0x5a, 0x1d, 0x3d, 0xec, 0xda, 0xdf, 0xac, 0x43, 0x46, 0x1d, - 0x5c, 0x27, 0x71, 0x42, 0x1a, 0x6f, 0x8b, 0xf0, 0xb7, 0x45, 0xf8, 0xdb, 0x22, 0x5c, 0x89, 0xf0, - 0xb5, 0x8c, 0x08, 0x7f, 0xaf, 0xb1, 0xea, 0xf5, 0xa1, 0xea, 0x6b, 0xea, 0xd4, 0xd5, 0xec, 0x81, - 0x81, 0x40, 0x25, 0xc1, 0xd5, 0x95, 0xa5, 0xeb, 0xb9, 0x32, 0xfb, 0xb5, 0xb4, 0xcc, 0x3e, 0x2a, - 0x8b, 0xff, 0x1f, 0xa4, 0xf4, 0xbf, 0xb0, 0xe0, 0x5d, 0x69, 0xe9, 0x25, 0x67, 0xce, 0x7c, 0x33, - 0x08, 0x23, 0x32, 0xeb, 0xad, 0xaf, 0x93, 0x88, 0x04, 0x2e, 0x89, 0x95, 0x17, 0xc3, 0xea, 0xe5, - 0xc5, 0x40, 0xcf, 0xc3, 0xf0, 0xed, 0x38, 0x0c, 0x96, 0x43, 0x2f, 0x10, 0x22, 0x88, 0x6e, 0x84, - 0x4f, 0xde, 0xdd, 0x1d, 0x1f, 0xa6, 0x23, 0x2a, 0xdb, 0x71, 0x0a, 0x0b, 0xcd, 0xc0, 0xa9, 0xdb, - 0xaf, 0x2f, 0x3b, 0x89, 0xe1, 0x0e, 0x90, 0x1b, 0x77, 0x76, 0x60, 0x71, 0xf5, 0xe5, 0x0c, 0x10, - 0x77, 0xe3, 0xdb, 0x7f, 0xa3, 0x04, 0x8f, 0x65, 0x5e, 0x24, 0xf4, 0xfd, 0xb0, 0x93, 0xd0, 0x4d, - 0x0d, 0xfa, 0x39, 0x0b, 0x4e, 0xb6, 0xd2, 0x1e, 0x87, 0x58, 0x38, 0x76, 0xdf, 0x5f, 0x98, 0x8e, - 0xc8, 0xb8, 0x34, 0xa6, 0xc7, 0xc4, 0x08, 0x9d, 0xcc, 0x00, 0x62, 0xdc, 0xd5, 0x17, 0xf4, 0x2a, - 0xd4, 0x5b, 0xce, 0xf6, 0x8d, 0x76, 0xc3, 0x49, 0xe4, 0x7e, 0xb2, 0xb7, 0x1b, 0xa0, 0x93, 0x78, - 0xfe, 0x04, 0x3f, 0xae, 0x9f, 0x98, 0x0f, 0x92, 0xa5, 0x68, 0x25, 0x89, 0xbc, 0xa0, 0xc9, 0xdd, - 0x79, 0x8b, 0x92, 0x0c, 0xd6, 0x14, 0xed, 0xaf, 0x5a, 0x59, 0x25, 0xa5, 0x46, 0x27, 0x72, 0x12, - 0xd2, 0xdc, 0x41, 0x1f, 0x85, 0x2a, 0xdd, 0xf8, 0xc9, 0x51, 0xb9, 0x55, 0xa4, 0xe6, 0x34, 0xbe, - 0x84, 0x56, 0xa2, 0xf4, 0x5f, 0x8c, 0x39, 0x53, 0xfb, 0x8f, 0x6b, 0x59, 0x63, 0x81, 0x1d, 0xde, - 0x5e, 0x04, 0x68, 0x86, 0xab, 0xa4, 0xd5, 0xf6, 0xe9, 0xb0, 0x58, 0xec, 0x04, 0x40, 0xf9, 0x3a, - 0xe6, 0x14, 0x04, 0x1b, 0x58, 0xe8, 0x2f, 0x59, 0x00, 0x4d, 0x39, 0xe7, 0xa5, 0x21, 0x70, 0xa3, - 0xc8, 0xd7, 0xd1, 0x2b, 0x4a, 0xf7, 0x45, 0x31, 0xc4, 0x06, 0x73, 0xf4, 0x93, 0x16, 0xd4, 0x12, - 0xd9, 0x7d, 0xae, 0x1a, 0x57, 0x8b, 0xec, 0x89, 0x7c, 0x69, 0x6d, 0x13, 0xa9, 0x21, 0x51, 0x7c, - 0xd1, 0x4f, 0x5b, 0x00, 0xf1, 0x4e, 0xe0, 0x2e, 0x87, 0xbe, 0xe7, 0xee, 0x08, 0x8d, 0x79, 0xb3, - 0x50, 0x7f, 0x8c, 0xa2, 0x3e, 0x3d, 0x4a, 0x47, 0x43, 0xff, 0xc7, 0x06, 0x67, 0xf4, 0x71, 0xa8, - 0xc5, 0x62, 0xba, 0x09, 0x1d, 0xb9, 0x5a, 0xac, 0x57, 0x88, 0xd3, 0x16, 0xe2, 0x55, 0xfc, 0xc3, - 0x8a, 0x27, 0xfa, 0x8a, 0x05, 0x27, 0xda, 0x69, 0x3f, 0x9f, 0x50, 0x87, 0xc5, 0xc9, 0x80, 0x8c, - 0x1f, 0x71, 0xfa, 0xf4, 0xdd, 0xdd, 0xf1, 0x13, 0x99, 0x46, 0x9c, 0xed, 0x05, 0x95, 0x80, 0x7a, - 0x06, 0x2f, 0xb5, 0xb9, 0xcf, 0x71, 0x50, 0x4b, 0xc0, 0xb9, 0x2c, 0x10, 0x77, 0xe3, 0xa3, 0x65, - 0x38, 0x43, 0x7b, 0xb7, 0xc3, 0xcd, 0x4f, 0xa9, 0x5e, 0x62, 0xa6, 0x0c, 0x6b, 0xd3, 0x8f, 0x8b, - 0x19, 0xc2, 0xbc, 0xfa, 0x59, 0x1c, 0x9c, 0xfb, 0x24, 0xfa, 0x5d, 0x0b, 0x1e, 0xf7, 0x98, 0x1a, - 0x30, 0x1d, 0xe6, 0x5a, 0x23, 0x88, 0x93, 0x58, 0x52, 0xa8, 0xac, 0xe8, 0xa5, 0x7e, 0xa6, 0xff, - 0x9c, 0x78, 0x83, 0xc7, 0xe7, 0xf7, 0xe8, 0x12, 0xde, 0xb3, 0xc3, 0xf6, 0x37, 0x4b, 0xa9, 0x63, - 0x0d, 0xe5, 0x4b, 0x64, 0x52, 0xc3, 0x95, 0x6e, 0x1c, 0x29, 0x04, 0x0b, 0x95, 0x1a, 0xca, 0x49, - 0xa4, 0xa5, 0x86, 0x6a, 0x8a, 0xb1, 0xc1, 0x9c, 0xda, 0x96, 0xa7, 0x9c, 0xac, 0xc7, 0x52, 0x08, - 0xb2, 0x57, 0x8b, 0xec, 0x52, 0xf7, 0x21, 0xd4, 0x63, 0xa2, 0x6b, 0xa7, 0xba, 0x40, 0xb8, 0xbb, - 0x4b, 0xf6, 0x37, 0xd3, 0x47, 0x29, 0xc6, 0x1a, 0xec, 0xe3, 0x98, 0xe8, 0x0b, 0x16, 0x0c, 0x45, - 0xa1, 0xef, 0x7b, 0x41, 0x93, 0xca, 0x0b, 0xa1, 0xf4, 0x3e, 0x78, 0x2c, 0x7a, 0x47, 0x08, 0x06, - 0x66, 0xa1, 0x62, 0xcd, 0x13, 0x9b, 0x1d, 0xb0, 0xff, 0xc8, 0x82, 0xb1, 0x5e, 0x72, 0x0d, 0x11, - 0x78, 0xa7, 0x5c, 0xb4, 0x2a, 0x48, 0x62, 0x29, 0x98, 0x25, 0x3e, 0x51, 0xfe, 0xe3, 0xda, 0xf4, - 0x93, 0xe2, 0x35, 0xdf, 0xb9, 0xdc, 0x1b, 0x15, 0xef, 0x45, 0x07, 0xbd, 0x02, 0x27, 0x8d, 0xf7, - 0x8a, 0xd5, 0xc0, 0xd4, 0xa7, 0x27, 0xa8, 0x21, 0x31, 0x95, 0x81, 0xdd, 0xdb, 0x1d, 0x7f, 0x24, - 0xdb, 0x26, 0x04, 0x6f, 0x17, 0x1d, 0xfb, 0x97, 0x4a, 0xd9, 0xaf, 0xa5, 0x74, 0xe6, 0x5b, 0x56, - 0xd7, 0xae, 0xfc, 0xfd, 0xc7, 0xa1, 0xa7, 0xd8, 0xfe, 0x5d, 0xc5, 0x61, 0xf4, 0xc6, 0x79, 0x80, - 0x07, 0xbd, 0xf6, 0xbf, 0xaa, 0xc0, 0x1e, 0x3d, 0xeb, 0xc3, 0x08, 0x3e, 0xf0, 0xe9, 0xe0, 0xe7, - 0x2c, 0x75, 0x72, 0x54, 0x66, 0x8b, 0xbc, 0x71, 0x5c, 0x63, 0xcf, 0xf7, 0x21, 0x31, 0x0f, 0x36, - 0x50, 0xde, 0xe8, 0xf4, 0x19, 0x15, 0xfa, 0x9a, 0x95, 0x3e, 0xfb, 0xe2, 0xd1, 0x63, 0xde, 0xb1, - 0xf5, 0xc9, 0x38, 0x50, 0xe3, 0x1d, 0xd3, 0xc7, 0x30, 0xbd, 0x8e, 0xda, 0x26, 0x00, 0xd6, 0xbd, - 0xc0, 0xf1, 0xbd, 0x37, 0xe8, 0x2e, 0xa3, 0xca, 0x14, 0x25, 0xb3, 0x3c, 0x2e, 0xab, 0x56, 0x6c, - 0x60, 0x9c, 0xfb, 0x8b, 0x30, 0x64, 0xbc, 0x79, 0x4e, 0x8c, 0xc4, 0x19, 0x33, 0x46, 0xa2, 0x6e, - 0x84, 0x36, 0x9c, 0x7b, 0x2f, 0x9c, 0xcc, 0x76, 0xf0, 0x20, 0xcf, 0xdb, 0xff, 0x6b, 0x30, 0x7b, - 0x18, 0xb5, 0x4a, 0xa2, 0x16, 0xed, 0xda, 0xdb, 0x0e, 0xa2, 0xb7, 0x1d, 0x44, 0x6f, 0x3b, 0x88, - 0x4c, 0x1f, 0xbf, 0x70, 0x7e, 0x0c, 0xde, 0x27, 0xe7, 0x47, 0xca, 0x9d, 0x53, 0x2b, 0xdc, 0x9d, - 0x63, 0xdf, 0xad, 0x42, 0xca, 0x8e, 0xe2, 0xe3, 0xfd, 0x03, 0x30, 0x18, 0x91, 0x76, 0x78, 0x03, - 0x2f, 0x08, 0x1d, 0xa2, 0xe3, 0xe0, 0x79, 0x33, 0x96, 0x70, 0xaa, 0x6b, 0xda, 0x4e, 0xb2, 0x21, - 0x94, 0x88, 0xd2, 0x35, 0xcb, 0x4e, 0xb2, 0x81, 0x19, 0x04, 0xbd, 0x17, 0x46, 0x13, 0x27, 0x6a, - 0x52, 0xb3, 0x79, 0x8b, 0x7d, 0x56, 0x71, 0x64, 0xf9, 0x88, 0xc0, 0x1d, 0x5d, 0x4d, 0x41, 0x71, - 0x06, 0x1b, 0xbd, 0x0e, 0x95, 0x0d, 0xe2, 0xb7, 0xc4, 0x90, 0xaf, 0x14, 0x27, 0xe3, 0xd9, 0xbb, - 0x5e, 0x21, 0x7e, 0x8b, 0x4b, 0x20, 0xfa, 0x0b, 0x33, 0x56, 0x74, 0xbe, 0xd5, 0x37, 0x3b, 0x71, - 0x12, 0xb6, 0xbc, 0x37, 0xa4, 0xa7, 0xee, 0xfd, 0x05, 0x33, 0xbe, 0x26, 0xe9, 0x73, 0x97, 0x88, - 0xfa, 0x8b, 0x35, 0x67, 0xd6, 0x8f, 0x86, 0x17, 0xb1, 0x4f, 0xb5, 0x23, 0x1c, 0x6e, 0x45, 0xf7, - 0x63, 0x56, 0xd2, 0xe7, 0xfd, 0x50, 0x7f, 0xb1, 0xe6, 0x8c, 0x76, 0xd4, 0xbc, 0x1f, 0x62, 0x7d, - 0xb8, 0x51, 0x70, 0x1f, 0xf8, 0x9c, 0xcf, 0x9d, 0xff, 0x4f, 0x42, 0xd5, 0xdd, 0x70, 0xa2, 0x64, - 0x6c, 0x98, 0x4d, 0x1a, 0xe5, 0x9a, 0x99, 0xa1, 0x8d, 0x98, 0xc3, 0xd0, 0x13, 0x50, 0x8e, 0xc8, - 0x3a, 0x0b, 0xbf, 0x34, 0x02, 0x73, 0x30, 0x59, 0xc7, 0xb4, 0xdd, 0xfe, 0xf9, 0x52, 0xda, 0x5c, - 0x4a, 0xbf, 0x37, 0x9f, 0xed, 0x6e, 0x27, 0x8a, 0xa5, 0xfb, 0xc6, 0x98, 0xed, 0xac, 0x19, 0x4b, - 0x38, 0xfa, 0xa4, 0x05, 0x83, 0xb7, 0xe3, 0x30, 0x08, 0x48, 0x22, 0x54, 0xd3, 0xcd, 0x82, 0x87, - 0xe2, 0x2a, 0xa7, 0xae, 0xfb, 0x20, 0x1a, 0xb0, 0xe4, 0x4b, 0xbb, 0x4b, 0xb6, 0x5d, 0xbf, 0xd3, - 0xe8, 0x8a, 0xb5, 0xb8, 0xc4, 0x9b, 0xb1, 0x84, 0x53, 0x54, 0x2f, 0xe0, 0xa8, 0x95, 0x34, 0xea, - 0x7c, 0x20, 0x50, 0x05, 0xdc, 0xfe, 0x6b, 0x03, 0x70, 0x36, 0x77, 0x71, 0x50, 0x43, 0x86, 0x99, - 0x0a, 0x97, 0x3d, 0x9f, 0xc8, 0x28, 0x23, 0x66, 0xc8, 0xdc, 0x54, 0xad, 0xd8, 0xc0, 0x40, 0x3f, - 0x01, 0xd0, 0x76, 0x22, 0xa7, 0x45, 0x94, 0x7b, 0xf5, 0xc8, 0xf6, 0x02, 0xed, 0xc7, 0xb2, 0xa4, - 0xa9, 0xf7, 0xa6, 0xaa, 0x29, 0xc6, 0x06, 0x4b, 0xf4, 0x02, 0x0c, 0x45, 0xc4, 0x27, 0x4e, 0xcc, - 0xa2, 0x77, 0xb3, 0xa9, 0x08, 0x58, 0x83, 0xb0, 0x89, 0x87, 0x9e, 0x52, 0x01, 0x59, 0x99, 0xc0, - 0x94, 0x74, 0x50, 0x16, 0x7a, 0xd3, 0x82, 0xd1, 0x75, 0xcf, 0x27, 0x9a, 0xbb, 0x48, 0x1c, 0x58, - 0x3a, 0xfa, 0x4b, 0x5e, 0x36, 0xe9, 0x6a, 0x09, 0x99, 0x6a, 0x8e, 0x71, 0x86, 0x3d, 0xfd, 0xcc, - 0x5b, 0x24, 0x62, 0xa2, 0x75, 0x20, 0xfd, 0x99, 0x6f, 0xf2, 0x66, 0x2c, 0xe1, 0x68, 0x0a, 0x4e, - 0xb4, 0x9d, 0x38, 0x9e, 0x89, 0x48, 0x83, 0x04, 0x89, 0xe7, 0xf8, 0x3c, 0xac, 0xbf, 0xa6, 0xc3, - 0x7a, 0x97, 0xd3, 0x60, 0x9c, 0xc5, 0x47, 0x1f, 0x80, 0x47, 0xb9, 0xff, 0x62, 0xd1, 0x8b, 0x63, - 0x2f, 0x68, 0xea, 0x69, 0x20, 0xdc, 0x38, 0xe3, 0x82, 0xd4, 0xa3, 0xf3, 0xf9, 0x68, 0xb8, 0xd7, - 0xf3, 0xe8, 0x19, 0xa8, 0xc5, 0x9b, 0x5e, 0x7b, 0x26, 0x6a, 0xc4, 0xec, 0xec, 0xa2, 0xa6, 0x9d, - 0x86, 0x2b, 0xa2, 0x1d, 0x2b, 0x0c, 0xe4, 0xc2, 0x30, 0xff, 0x24, 0x3c, 0xa2, 0x4c, 0xc8, 0xc7, - 0x67, 0x7b, 0xaa, 0x47, 0x91, 0x79, 0x36, 0x81, 0x9d, 0x3b, 0x97, 0xe4, 0x49, 0x0a, 0x77, 0xfc, - 0xdf, 0x34, 0xc8, 0xe0, 0x14, 0x51, 0xfb, 0x67, 0x4b, 0xe9, 0x1d, 0xb7, 0xb9, 0x48, 0x51, 0x4c, - 0x97, 0x62, 0x72, 0xd3, 0x89, 0xa4, 0x37, 0xe6, 0x88, 0xd9, 0x07, 0x82, 0xee, 0x4d, 0x27, 0x32, - 0x17, 0x35, 0x63, 0x80, 0x25, 0x27, 0x74, 0x1b, 0x2a, 0x89, 0xef, 0x14, 0x94, 0xae, 0x64, 0x70, - 0xd4, 0x0e, 0x90, 0x85, 0xa9, 0x18, 0x33, 0x1e, 0xe8, 0x71, 0x6a, 0xf5, 0xaf, 0xc9, 0x93, 0x0e, - 0x61, 0xa8, 0xaf, 0xc5, 0x98, 0xb5, 0xda, 0xbf, 0x0c, 0x39, 0x72, 0x55, 0x29, 0x32, 0x74, 0x11, - 0x80, 0x6e, 0x20, 0x97, 0x23, 0xb2, 0xee, 0x6d, 0x0b, 0x43, 0x42, 0xad, 0xdd, 0xeb, 0x0a, 0x82, - 0x0d, 0x2c, 0xf9, 0xcc, 0x4a, 0x67, 0x9d, 0x3e, 0x53, 0xea, 0x7e, 0x86, 0x43, 0xb0, 0x81, 0x85, - 0x9e, 0x87, 0x01, 0xaf, 0xe5, 0x34, 0x55, 0x24, 0xe5, 0xe3, 0x74, 0xd1, 0xce, 0xb3, 0x96, 0x7b, - 0xbb, 0xe3, 0xa3, 0xaa, 0x43, 0xac, 0x09, 0x0b, 0x5c, 0xf4, 0x4b, 0x16, 0x0c, 0xbb, 0x61, 0xab, - 0x15, 0x06, 0x7c, 0xdb, 0x25, 0xf6, 0x90, 0xb7, 0x8f, 0x4b, 0xcd, 0x4f, 0xcc, 0x18, 0xcc, 0xf8, - 0x26, 0x52, 0xe5, 0x55, 0x99, 0x20, 0x9c, 0xea, 0x95, 0xb9, 0xb6, 0xab, 0xfb, 0xac, 0xed, 0xdf, - 0xb0, 0xe0, 0x14, 0x7f, 0xd6, 0xd8, 0x0d, 0x8a, 0x14, 0xa2, 0xf0, 0x98, 0x5f, 0xab, 0x6b, 0x83, - 0xac, 0xbc, 0x74, 0x5d, 0x70, 0xdc, 0xdd, 0x49, 0x34, 0x07, 0xa7, 0xd6, 0xc3, 0xc8, 0x25, 0xe6, - 0x40, 0x08, 0xc1, 0xa4, 0x08, 0x5d, 0xce, 0x22, 0xe0, 0xee, 0x67, 0xd0, 0x4d, 0x78, 0xc4, 0x68, - 0x34, 0xc7, 0x81, 0xcb, 0xa6, 0xf3, 0x82, 0xda, 0x23, 0x97, 0x73, 0xb1, 0x70, 0x8f, 0xa7, 0xd3, - 0x0e, 0x93, 0x7a, 0x1f, 0x0e, 0x93, 0xd7, 0xe0, 0x31, 0xb7, 0x7b, 0x64, 0xb6, 0xe2, 0xce, 0x5a, - 0xcc, 0x25, 0x55, 0x6d, 0xfa, 0xfb, 0x04, 0x81, 0xc7, 0x66, 0x7a, 0x21, 0xe2, 0xde, 0x34, 0xd0, - 0x47, 0xa1, 0x16, 0x11, 0xf6, 0x55, 0x62, 0x91, 0x4f, 0x73, 0xc4, 0x5d, 0xb2, 0xb6, 0x40, 0x39, - 0x59, 0x2d, 0x7b, 0x45, 0x43, 0x8c, 0x15, 0x47, 0x74, 0x07, 0x06, 0xdb, 0x4e, 0xe2, 0x6e, 0x88, - 0x2c, 0x9a, 0x23, 0x87, 0xb1, 0x28, 0xe6, 0xcb, 0x94, 0xaa, 0x9e, 0xe4, 0xcb, 0x9c, 0x09, 0x96, - 0xdc, 0xa8, 0x35, 0xe2, 0x86, 0xad, 0x76, 0x18, 0x90, 0x20, 0x89, 0xc7, 0x46, 0xb4, 0x35, 0x32, - 0xa3, 0x5a, 0xb1, 0x81, 0x71, 0xee, 0x7d, 0x70, 0xaa, 0x6b, 0xe1, 0x1d, 0xc8, 0xb9, 0x32, 0x0b, - 0x8f, 0xe4, 0x4f, 0xf1, 0x03, 0xb9, 0x58, 0x7e, 0x3d, 0x13, 0xab, 0x6a, 0x98, 0xbd, 0x7d, 0xb8, - 0xeb, 0x1c, 0x28, 0x93, 0x60, 0x4b, 0x48, 0xfc, 0xcb, 0x47, 0x1b, 0xe9, 0x4b, 0xc1, 0x16, 0x5f, - 0xa1, 0xcc, 0x27, 0x71, 0x29, 0xd8, 0xc2, 0x94, 0x36, 0xfa, 0x92, 0x95, 0x32, 0xdb, 0xb8, 0x93, - 0xef, 0xc3, 0xc7, 0x62, 0xe7, 0xf7, 0x6d, 0xc9, 0xd9, 0xff, 0xba, 0x04, 0x17, 0xf6, 0x23, 0xd2, - 0xc7, 0xf0, 0x3d, 0x09, 0x03, 0x31, 0x3b, 0x7d, 0x16, 0x22, 0x74, 0x88, 0xce, 0x2c, 0x7e, 0x1e, - 0xfd, 0x1a, 0x16, 0x20, 0xe4, 0x43, 0xb9, 0xe5, 0xb4, 0x85, 0xef, 0x67, 0xfe, 0xa8, 0xd9, 0x2b, - 0xf4, 0xbf, 0xe3, 0x2f, 0x3a, 0x6d, 0xee, 0x51, 0x30, 0x1a, 0x30, 0x65, 0x83, 0x12, 0xa8, 0x3a, - 0x51, 0xe4, 0xc8, 0xa3, 0xce, 0x6b, 0xc5, 0xf0, 0x9b, 0xa2, 0x24, 0xa7, 0x4f, 0xdd, 0xdd, 0x1d, - 0x1f, 0x49, 0x35, 0x61, 0xce, 0xcc, 0xfe, 0xdc, 0x60, 0x2a, 0x83, 0x83, 0x9d, 0x5f, 0xc7, 0x30, - 0x20, 0x5c, 0x3e, 0x56, 0xd1, 0x49, 0x43, 0x3c, 0x05, 0x8f, 0xed, 0xea, 0x44, 0x22, 0xb3, 0x60, - 0x85, 0x3e, 0x6b, 0xb1, 0x74, 0x61, 0x99, 0xd5, 0x22, 0xf6, 0x52, 0xc7, 0x93, 0xbd, 0x6c, 0x26, - 0x21, 0xcb, 0x46, 0x6c, 0x72, 0xa7, 0x3a, 0xb6, 0xcd, 0x13, 0xdf, 0xb2, 0x3b, 0x2a, 0x99, 0x50, - 0x2c, 0xe1, 0x68, 0x3b, 0xe7, 0x9c, 0xba, 0x80, 0x94, 0xd3, 0x3e, 0x4e, 0xa6, 0xbf, 0x66, 0xc1, - 0x29, 0x2f, 0x7b, 0xe0, 0x28, 0x76, 0x1e, 0x47, 0x8c, 0x84, 0xe8, 0x7d, 0x9e, 0xa9, 0x94, 0x6f, - 0x17, 0x08, 0x77, 0x77, 0x06, 0x35, 0xa0, 0xe2, 0x05, 0xeb, 0xa1, 0x30, 0x39, 0xa6, 0x8f, 0xd6, - 0xa9, 0xf9, 0x60, 0x3d, 0xd4, 0xab, 0x99, 0xfe, 0xc3, 0x8c, 0x3a, 0x5a, 0x80, 0x33, 0x91, 0xf0, - 0x0d, 0x5d, 0xf1, 0x62, 0xba, 0x83, 0x5f, 0xf0, 0x5a, 0x5e, 0xc2, 0xcc, 0x85, 0xf2, 0xf4, 0xd8, - 0xdd, 0xdd, 0xf1, 0x33, 0x38, 0x07, 0x8e, 0x73, 0x9f, 0x42, 0x6f, 0xc0, 0xa0, 0xcc, 0x6f, 0xae, - 0x15, 0xb1, 0x8b, 0xeb, 0x9e, 0xff, 0x6a, 0x32, 0xad, 0x88, 0x54, 0x66, 0xc9, 0xd0, 0x7e, 0x73, - 0x08, 0xba, 0x0f, 0x31, 0xd1, 0xc7, 0xa0, 0x1e, 0xa9, 0x9c, 0x6b, 0xab, 0x08, 0xe5, 0x2a, 0xbf, - 0xaf, 0x38, 0x40, 0x55, 0x86, 0x8b, 0xce, 0xae, 0xd6, 0x1c, 0xe9, 0xf6, 0x22, 0xd6, 0x67, 0x9d, - 0x05, 0xcc, 0x6d, 0xc1, 0x55, 0x9f, 0x63, 0xed, 0x04, 0x2e, 0x66, 0x3c, 0x50, 0x04, 0x03, 0x1b, - 0xc4, 0xf1, 0x93, 0x8d, 0x62, 0x5c, 0xee, 0x57, 0x18, 0xad, 0x6c, 0xe6, 0x0d, 0x6f, 0xc5, 0x82, - 0x13, 0xda, 0x86, 0xc1, 0x0d, 0x3e, 0x01, 0x84, 0xc5, 0xbf, 0x78, 0xd4, 0xc1, 0x4d, 0xcd, 0x2a, - 0xfd, 0xb9, 0x45, 0x03, 0x96, 0xec, 0x58, 0x90, 0x8b, 0x71, 0x7e, 0xcf, 0x97, 0x6e, 0x71, 0x49, - 0x47, 0xfd, 0x1f, 0xde, 0x7f, 0x04, 0x86, 0x23, 0xe2, 0x86, 0x81, 0xeb, 0xf9, 0xa4, 0x31, 0x25, - 0xdd, 0xe9, 0x07, 0x49, 0x55, 0x61, 0xbb, 0x66, 0x6c, 0xd0, 0xc0, 0x29, 0x8a, 0xe8, 0x33, 0x16, - 0x8c, 0xaa, 0x44, 0x4d, 0xfa, 0x41, 0x88, 0x70, 0xdf, 0x2e, 0x14, 0x94, 0x16, 0xca, 0x68, 0x4e, - 0xa3, 0xbb, 0xbb, 0xe3, 0xa3, 0xe9, 0x36, 0x9c, 0xe1, 0x8b, 0x5e, 0x01, 0x08, 0xd7, 0x78, 0x24, - 0xcb, 0x54, 0x22, 0x7c, 0xb9, 0x07, 0x79, 0xd5, 0x51, 0x9e, 0xb3, 0x26, 0x29, 0x60, 0x83, 0x1a, - 0xba, 0x06, 0xc0, 0x97, 0xcd, 0xea, 0x4e, 0x5b, 0x6e, 0x0b, 0x64, 0xae, 0x11, 0xac, 0x28, 0xc8, - 0xbd, 0xdd, 0xf1, 0x6e, 0xdf, 0x1a, 0x0b, 0x33, 0x30, 0x1e, 0x47, 0x3f, 0x0e, 0x83, 0x71, 0xa7, - 0xd5, 0x72, 0x94, 0xa7, 0xb7, 0xc0, 0x2c, 0x38, 0x4e, 0xd7, 0x10, 0x45, 0xbc, 0x01, 0x4b, 0x8e, - 0xe8, 0x36, 0x15, 0xaa, 0xb1, 0x70, 0xfa, 0xb1, 0x55, 0xc4, 0x6d, 0x82, 0x21, 0xf6, 0x4e, 0xef, - 0x91, 0x81, 0x39, 0x38, 0x07, 0xe7, 0xde, 0xee, 0xf8, 0x23, 0xe9, 0xf6, 0x85, 0x50, 0xe4, 0xa5, - 0xe5, 0xd2, 0x44, 0x57, 0x65, 0xb9, 0x13, 0xfa, 0xda, 0x32, 0x0b, 0xff, 0x69, 0x5d, 0xee, 0x84, - 0x35, 0xf7, 0x1e, 0x33, 0xf3, 0x61, 0xb4, 0x08, 0xa7, 0xdd, 0x30, 0x48, 0xa2, 0xd0, 0xf7, 0x79, - 0x0d, 0x1f, 0xbe, 0x43, 0xe3, 0x9e, 0xe0, 0x77, 0x8a, 0x6e, 0x9f, 0x9e, 0xe9, 0x46, 0xc1, 0x79, - 0xcf, 0xd9, 0x41, 0x3a, 0xc4, 0x4f, 0x0c, 0xce, 0xf3, 0x30, 0x4c, 0xb6, 0x13, 0x12, 0x05, 0x8e, - 0x7f, 0x03, 0x2f, 0x48, 0x1f, 0x28, 0x5b, 0x03, 0x97, 0x8c, 0x76, 0x9c, 0xc2, 0x42, 0xb6, 0x72, - 0x4b, 0x18, 0xb9, 0x96, 0xdc, 0x2d, 0x21, 0x9d, 0x10, 0xf6, 0xff, 0x2e, 0xa5, 0x0c, 0xb2, 0xd5, - 0x88, 0x10, 0x14, 0x42, 0x35, 0x08, 0x1b, 0x4a, 0xf6, 0x5f, 0x2d, 0x46, 0xf6, 0x5f, 0x0f, 0x1b, - 0x46, 0x4d, 0x14, 0xfa, 0x2f, 0xc6, 0x9c, 0x0f, 0x2b, 0x1a, 0x21, 0xab, 0x6b, 0x30, 0x80, 0xd8, - 0x68, 0x14, 0xc9, 0x59, 0x15, 0x8d, 0x58, 0x32, 0x19, 0xe1, 0x34, 0x5f, 0xb4, 0x09, 0xd5, 0x8d, - 0x30, 0x4e, 0xe4, 0xf6, 0xe3, 0x88, 0x3b, 0x9d, 0x2b, 0x61, 0x9c, 0x30, 0x2b, 0x42, 0xbd, 0x36, - 0x6d, 0x89, 0x31, 0xe7, 0x61, 0xff, 0x17, 0x2b, 0xe5, 0xf1, 0xbe, 0xc5, 0xc2, 0x5d, 0xb7, 0x48, - 0x40, 0x97, 0xb5, 0x19, 0x18, 0xf4, 0x17, 0x32, 0xc9, 0x83, 0xef, 0xea, 0x55, 0xa1, 0xea, 0x0e, - 0xa5, 0x30, 0xc1, 0x48, 0x18, 0x31, 0x44, 0x9f, 0xb0, 0xd2, 0x69, 0x9c, 0xa5, 0x22, 0x36, 0x18, - 0x66, 0x2a, 0xf3, 0xbe, 0x19, 0xa1, 0xf6, 0x97, 0x2c, 0x18, 0x9c, 0x76, 0xdc, 0xcd, 0x70, 0x7d, - 0x1d, 0x3d, 0x03, 0xb5, 0x46, 0x27, 0x32, 0x33, 0x4a, 0xd5, 0x36, 0x7f, 0x56, 0xb4, 0x63, 0x85, - 0x41, 0xe7, 0xf0, 0xba, 0xe3, 0xca, 0x84, 0xe6, 0x32, 0x9f, 0xc3, 0x97, 0x59, 0x0b, 0x16, 0x10, - 0xf4, 0x02, 0x0c, 0xb5, 0x9c, 0x6d, 0xf9, 0x70, 0xd6, 0xdd, 0xbe, 0xa8, 0x41, 0xd8, 0xc4, 0xb3, - 0xff, 0xb9, 0x05, 0x63, 0xd3, 0x4e, 0xec, 0xb9, 0x53, 0x9d, 0x64, 0x63, 0xda, 0x4b, 0xd6, 0x3a, - 0xee, 0x26, 0x49, 0x78, 0x16, 0x3b, 0xed, 0x65, 0x27, 0xa6, 0x4b, 0x49, 0xed, 0xeb, 0x54, 0x2f, - 0x6f, 0x88, 0x76, 0xac, 0x30, 0xd0, 0x1b, 0x30, 0xd4, 0x76, 0xe2, 0xf8, 0x4e, 0x18, 0x35, 0x30, - 0x59, 0x2f, 0xa6, 0x86, 0xc4, 0x0a, 0x71, 0x23, 0x92, 0x60, 0xb2, 0x2e, 0x8e, 0x84, 0x35, 0x7d, - 0x6c, 0x32, 0xb3, 0xbf, 0x60, 0xc1, 0x63, 0xd3, 0xc4, 0x89, 0x48, 0xc4, 0x4a, 0x4e, 0xa8, 0x17, - 0x99, 0xf1, 0xc3, 0x4e, 0x03, 0xbd, 0x0e, 0xb5, 0x84, 0x36, 0xd3, 0x6e, 0x59, 0xc5, 0x76, 0x8b, - 0x9d, 0xe8, 0xae, 0x0a, 0xe2, 0x58, 0xb1, 0xb1, 0xff, 0xba, 0x05, 0xc3, 0xec, 0x70, 0x6c, 0x96, - 0x24, 0x8e, 0xe7, 0x77, 0x55, 0x66, 0xb2, 0xfa, 0xac, 0xcc, 0x74, 0x01, 0x2a, 0x1b, 0x61, 0x8b, - 0x64, 0x0f, 0x76, 0xaf, 0x84, 0x74, 0x5b, 0x4d, 0x21, 0xe8, 0x39, 0xfa, 0xe1, 0xbd, 0x20, 0x71, - 0xe8, 0x12, 0x90, 0xce, 0xd7, 0x13, 0xfc, 0xa3, 0xab, 0x66, 0x6c, 0xe2, 0xd8, 0xbf, 0x55, 0x87, - 0x41, 0x71, 0xfa, 0xdf, 0x77, 0x25, 0x03, 0xb9, 0xbf, 0x2f, 0xf5, 0xdc, 0xdf, 0xc7, 0x30, 0xe0, - 0xb2, 0xba, 0x6f, 0xc2, 0x8c, 0xbc, 0x56, 0x48, 0xb8, 0x08, 0x2f, 0x25, 0xa7, 0xbb, 0xc5, 0xff, - 0x63, 0xc1, 0x0a, 0x7d, 0xd1, 0x82, 0x13, 0x6e, 0x18, 0x04, 0xc4, 0xd5, 0x36, 0x4e, 0xa5, 0x88, - 0xa8, 0x80, 0x99, 0x34, 0x51, 0x7d, 0x32, 0x93, 0x01, 0xe0, 0x2c, 0x7b, 0xf4, 0x12, 0x8c, 0xf0, - 0x31, 0xbb, 0x99, 0xf2, 0x18, 0xeb, 0x82, 0x3d, 0x26, 0x10, 0xa7, 0x71, 0xd1, 0x04, 0xf7, 0xbc, - 0x8b, 0xd2, 0x38, 0x03, 0xda, 0xb1, 0x66, 0x14, 0xc5, 0x31, 0x30, 0x50, 0x04, 0x28, 0x22, 0xeb, - 0x11, 0x89, 0x37, 0x44, 0x74, 0x04, 0xb3, 0xaf, 0x06, 0x0f, 0x97, 0xf5, 0x8c, 0xbb, 0x28, 0xe1, - 0x1c, 0xea, 0x68, 0x53, 0x6c, 0x30, 0x6b, 0x45, 0xc8, 0x50, 0xf1, 0x99, 0x7b, 0xee, 0x33, 0xc7, - 0xa1, 0x1a, 0x6f, 0x38, 0x51, 0x83, 0xd9, 0x75, 0x65, 0x9e, 0x69, 0xb3, 0x42, 0x1b, 0x30, 0x6f, - 0x47, 0xb3, 0x70, 0x32, 0x53, 0x6e, 0x28, 0x16, 0x9e, 0x5d, 0x95, 0x55, 0x91, 0x29, 0x54, 0x14, - 0xe3, 0xae, 0x27, 0x4c, 0xe7, 0xc3, 0xd0, 0x3e, 0xce, 0x87, 0x1d, 0x15, 0x83, 0xc7, 0x7d, 0xae, - 0x2f, 0x17, 0x32, 0x00, 0x7d, 0x05, 0xdc, 0x7d, 0x3e, 0x13, 0x70, 0x37, 0xc2, 0x3a, 0x70, 0xb3, - 0x98, 0x0e, 0x1c, 0x3c, 0xba, 0xee, 0x41, 0x46, 0xcb, 0xfd, 0x99, 0x05, 0xf2, 0xbb, 0xce, 0x38, - 0xee, 0x06, 0xa1, 0x53, 0x06, 0xbd, 0x17, 0x46, 0xd5, 0x16, 0x7a, 0x26, 0xec, 0x04, 0x3c, 0x50, - 0xae, 0xac, 0x8f, 0x70, 0x71, 0x0a, 0x8a, 0x33, 0xd8, 0x68, 0x12, 0xea, 0x74, 0x9c, 0xf8, 0xa3, - 0x5c, 0xd7, 0xaa, 0x6d, 0xfa, 0xd4, 0xf2, 0xbc, 0x78, 0x4a, 0xe3, 0xa0, 0x10, 0x4e, 0xf9, 0x4e, - 0x9c, 0xb0, 0x1e, 0xd0, 0x1d, 0xf5, 0x21, 0x6b, 0x0e, 0xb0, 0xd0, 0xfd, 0x85, 0x2c, 0x21, 0xdc, - 0x4d, 0xdb, 0xfe, 0x56, 0x05, 0x46, 0x52, 0x92, 0xf1, 0x80, 0x4a, 0xfa, 0x19, 0xa8, 0x49, 0xbd, - 0x99, 0xad, 0x8e, 0xa2, 0x94, 0xab, 0xc2, 0xa0, 0x4a, 0x6b, 0x4d, 0x6b, 0xd5, 0xac, 0x51, 0x61, - 0x28, 0x5c, 0x6c, 0xe2, 0x31, 0xa1, 0x9c, 0xf8, 0xf1, 0x8c, 0xef, 0x91, 0x20, 0xe1, 0xdd, 0x2c, - 0x46, 0x28, 0xaf, 0x2e, 0xac, 0x98, 0x44, 0xb5, 0x50, 0xce, 0x00, 0x70, 0x96, 0x3d, 0xfa, 0xb4, - 0x05, 0x23, 0xce, 0x9d, 0x58, 0x17, 0x27, 0x15, 0xa1, 0x75, 0x47, 0x54, 0x52, 0xa9, 0x7a, 0xa7, - 0xdc, 0xe5, 0x9b, 0x6a, 0xc2, 0x69, 0xa6, 0xe8, 0x2d, 0x0b, 0x10, 0xd9, 0x26, 0xae, 0x0c, 0xfe, - 0x13, 0x7d, 0x19, 0x28, 0x62, 0xa7, 0x79, 0xa9, 0x8b, 0x2e, 0x97, 0xea, 0xdd, 0xed, 0x38, 0xa7, - 0x0f, 0xf6, 0x3f, 0x2e, 0xab, 0x05, 0xa5, 0xe3, 0x4d, 0x1d, 0x23, 0xee, 0xcd, 0x3a, 0x7c, 0xdc, - 0x9b, 0x8e, 0x1f, 0xe8, 0x4e, 0x65, 0x4c, 0x65, 0x3e, 0x95, 0x1e, 0x50, 0xe6, 0xd3, 0x4f, 0x5a, - 0xa9, 0x3a, 0x40, 0x43, 0x17, 0x5f, 0x29, 0x36, 0xd6, 0x75, 0x82, 0xc7, 0x36, 0x64, 0xa4, 0x7b, - 0x3a, 0xa4, 0x85, 0x4a, 0x53, 0x03, 0xed, 0x40, 0xd2, 0xf0, 0xdf, 0x97, 0x61, 0xc8, 0xd0, 0xa4, - 0xb9, 0x66, 0x91, 0xf5, 0x90, 0x99, 0x45, 0xa5, 0x03, 0x98, 0x45, 0x3f, 0x01, 0x75, 0x57, 0x4a, - 0xf9, 0x62, 0x2a, 0xe1, 0x66, 0x75, 0x87, 0x16, 0xf4, 0xaa, 0x09, 0x6b, 0x9e, 0x68, 0x2e, 0x95, - 0x68, 0x23, 0x34, 0x44, 0x85, 0x69, 0x88, 0xbc, 0x4c, 0x18, 0xa1, 0x29, 0xba, 0x9f, 0x61, 0xe5, - 0xa2, 0xda, 0x9e, 0x78, 0x2f, 0x19, 0x91, 0xce, 0xcb, 0x45, 0x2d, 0xcf, 0xcb, 0x66, 0x6c, 0xe2, - 0xd8, 0xdf, 0xb2, 0xd4, 0xc7, 0xbd, 0x0f, 0x85, 0x11, 0x6e, 0xa7, 0x0b, 0x23, 0x5c, 0x2a, 0x64, - 0x98, 0x7b, 0x54, 0x44, 0xb8, 0x0e, 0x83, 0x33, 0x61, 0xab, 0xe5, 0x04, 0x0d, 0xf4, 0xfd, 0x30, - 0xe8, 0xf2, 0x9f, 0xc2, 0xb1, 0xc3, 0x8e, 0x07, 0x05, 0x14, 0x4b, 0x18, 0x7a, 0x1c, 0x2a, 0x4e, - 0xd4, 0x94, 0xce, 0x1c, 0x16, 0x0a, 0x33, 0x15, 0x35, 0x63, 0xcc, 0x5a, 0xed, 0x5f, 0xab, 0x00, - 0x3b, 0x81, 0x76, 0x22, 0xd2, 0x58, 0x0d, 0x59, 0x25, 0xbe, 0x63, 0x3d, 0x54, 0xd3, 0x9b, 0xa5, - 0x87, 0xf9, 0x60, 0xcd, 0x38, 0x5c, 0x29, 0xdf, 0xe7, 0xc3, 0x95, 0x1e, 0xe7, 0x65, 0x95, 0x87, - 0xe8, 0xbc, 0xcc, 0xfe, 0x9c, 0x05, 0x48, 0x85, 0x2d, 0xe8, 0x03, 0xed, 0x49, 0xa8, 0xab, 0x00, - 0x06, 0x61, 0x58, 0x69, 0x11, 0x21, 0x01, 0x58, 0xe3, 0xf4, 0xb1, 0x43, 0x7e, 0x52, 0xca, 0xef, - 0x72, 0x3a, 0x8a, 0x96, 0x49, 0x7d, 0x21, 0xce, 0xed, 0xdf, 0x2e, 0xc1, 0x23, 0x5c, 0x25, 0x2f, - 0x3a, 0x81, 0xd3, 0x24, 0x2d, 0xda, 0xab, 0x7e, 0x43, 0x14, 0x5c, 0xba, 0x35, 0xf3, 0x64, 0x54, - 0xec, 0x51, 0xd7, 0x2e, 0x5f, 0x73, 0x7c, 0x95, 0xcd, 0x07, 0x5e, 0x82, 0x19, 0x71, 0x14, 0x43, - 0x4d, 0x96, 0x7e, 0x17, 0xb2, 0xb8, 0x20, 0x46, 0x4a, 0x2c, 0x09, 0xbd, 0x49, 0xb0, 0x62, 0x44, - 0x0d, 0x57, 0x3f, 0x74, 0x37, 0x31, 0x69, 0x87, 0x4c, 0xee, 0x1a, 0x41, 0x89, 0x0b, 0xa2, 0x1d, - 0x2b, 0x0c, 0xfb, 0xb7, 0x2d, 0xc8, 0x6a, 0x24, 0xa3, 0xe4, 0x99, 0xb5, 0x67, 0xc9, 0xb3, 0x03, - 0xd4, 0x1c, 0xfb, 0x10, 0x0c, 0x39, 0x09, 0x35, 0x22, 0xf8, 0xb6, 0xbb, 0x7c, 0xb8, 0x63, 0x8d, - 0xc5, 0xb0, 0xe1, 0xad, 0x7b, 0x6c, 0xbb, 0x6d, 0x92, 0xb3, 0xff, 0x47, 0x05, 0x4e, 0x75, 0xe5, - 0x6e, 0xa0, 0x17, 0x61, 0xd8, 0x15, 0xd3, 0xa3, 0x2d, 0x1d, 0x5a, 0x75, 0x33, 0x88, 0x4d, 0xc3, - 0x70, 0x0a, 0xb3, 0x8f, 0x09, 0x3a, 0x0f, 0xa7, 0x23, 0xba, 0xd1, 0xef, 0x90, 0xa9, 0xf5, 0x84, - 0x44, 0x2b, 0xc4, 0x0d, 0x83, 0x06, 0x2f, 0xcc, 0x57, 0x9e, 0x7e, 0xf4, 0xee, 0xee, 0xf8, 0x69, - 0xdc, 0x0d, 0xc6, 0x79, 0xcf, 0xa0, 0x36, 0x8c, 0xf8, 0xa6, 0x0d, 0x28, 0x36, 0x00, 0x87, 0x32, - 0x1f, 0x95, 0x8d, 0x90, 0x6a, 0xc6, 0x69, 0x06, 0x69, 0x43, 0xb2, 0xfa, 0x80, 0x0c, 0xc9, 0x4f, - 0x69, 0x43, 0x92, 0x9f, 0xbf, 0x7f, 0xb0, 0xe0, 0xdc, 0x9d, 0xe3, 0xb6, 0x24, 0x5f, 0x86, 0x9a, - 0x8c, 0x4d, 0xea, 0x2b, 0xa6, 0xc7, 0xa4, 0xd3, 0x43, 0xa2, 0xdd, 0x2b, 0x41, 0xce, 0x26, 0x84, - 0xae, 0x33, 0xad, 0xf1, 0x53, 0xeb, 0xec, 0x60, 0x5a, 0x1f, 0x6d, 0xf3, 0xb8, 0x2c, 0xae, 0xdb, - 0x3e, 0x50, 0xf4, 0x26, 0x4a, 0x87, 0x6a, 0xa9, 0x94, 0x06, 0x15, 0xae, 0x75, 0x11, 0x40, 0x1b, - 0x6a, 0x22, 0x60, 0x5d, 0x1d, 0xfb, 0x6a, 0x7b, 0x0e, 0x1b, 0x58, 0x74, 0x4f, 0xed, 0x05, 0x71, - 0xe2, 0xf8, 0xfe, 0x15, 0x2f, 0x48, 0x84, 0x73, 0x50, 0x29, 0xf1, 0x79, 0x0d, 0xc2, 0x26, 0xde, - 0xb9, 0xf7, 0x18, 0xdf, 0xe5, 0x20, 0xdf, 0x73, 0x03, 0x1e, 0x9b, 0xf3, 0x12, 0x95, 0x66, 0xa1, - 0xe6, 0x11, 0xb5, 0xc3, 0x54, 0xda, 0x90, 0xd5, 0x33, 0x6d, 0xc8, 0x48, 0x73, 0x28, 0xa5, 0xb3, - 0x32, 0xb2, 0x69, 0x0e, 0xf6, 0x8b, 0x70, 0x66, 0xce, 0x4b, 0x2e, 0x7b, 0x3e, 0x39, 0x20, 0x13, - 0xfb, 0x9f, 0x0d, 0xc0, 0xb0, 0x99, 0xa8, 0x77, 0x90, 0xcc, 0xa7, 0x2f, 0x50, 0x53, 0x4b, 0xbc, - 0x9d, 0xa7, 0x0e, 0xcd, 0x6e, 0x1d, 0x39, 0x6b, 0x30, 0x7f, 0xc4, 0x0c, 0x6b, 0x4b, 0xf3, 0xc4, - 0x66, 0x07, 0xd0, 0x1d, 0xa8, 0xae, 0xb3, 0x30, 0xfc, 0x72, 0x11, 0x91, 0x05, 0x79, 0x23, 0xaa, - 0x97, 0x19, 0x0f, 0xe4, 0xe7, 0xfc, 0xa8, 0x86, 0x8c, 0xd2, 0xb9, 0x5d, 0x46, 0xe8, 0xa8, 0xc8, - 0xea, 0x52, 0x18, 0xbd, 0x44, 0x7d, 0xf5, 0x10, 0xa2, 0x3e, 0x25, 0x78, 0x07, 0x1e, 0x90, 0xe0, - 0x65, 0x29, 0x15, 0xc9, 0x06, 0xb3, 0xdf, 0x44, 0xac, 0xfb, 0x20, 0x1b, 0x04, 0x23, 0xa5, 0x22, - 0x05, 0xc6, 0x59, 0x7c, 0xf4, 0x71, 0x25, 0xba, 0x6b, 0x45, 0xf8, 0x55, 0xcd, 0x19, 0x7d, 0xdc, - 0x52, 0xfb, 0x73, 0x25, 0x18, 0x9d, 0x0b, 0x3a, 0xcb, 0x73, 0xcb, 0x9d, 0x35, 0xdf, 0x73, 0xaf, - 0x91, 0x1d, 0x2a, 0x9a, 0x37, 0xc9, 0xce, 0xfc, 0xac, 0x58, 0x41, 0x6a, 0xce, 0x5c, 0xa3, 0x8d, - 0x98, 0xc3, 0xa8, 0x30, 0x5a, 0xf7, 0x82, 0x26, 0x89, 0xda, 0x91, 0x27, 0x5c, 0x9e, 0x86, 0x30, - 0xba, 0xac, 0x41, 0xd8, 0xc4, 0xa3, 0xb4, 0xc3, 0x3b, 0x01, 0x89, 0xb2, 0x86, 0xec, 0x12, 0x6d, - 0xc4, 0x1c, 0x46, 0x91, 0x92, 0xa8, 0x13, 0x27, 0x62, 0x32, 0x2a, 0xa4, 0x55, 0xda, 0x88, 0x39, - 0x8c, 0xae, 0xf4, 0xb8, 0xb3, 0xc6, 0x02, 0x37, 0x32, 0x81, 0xf5, 0x2b, 0xbc, 0x19, 0x4b, 0x38, - 0x45, 0xdd, 0x24, 0x3b, 0xb3, 0x74, 0xd7, 0x9b, 0xc9, 0xaf, 0xb9, 0xc6, 0x9b, 0xb1, 0x84, 0xb3, - 0x8a, 0x82, 0xe9, 0xe1, 0xf8, 0xae, 0xab, 0x28, 0x98, 0xee, 0x7e, 0x8f, 0xfd, 0xf3, 0x2f, 0x58, - 0x30, 0x6c, 0x86, 0x5b, 0xa1, 0x66, 0xc6, 0xc6, 0x5d, 0xea, 0x2a, 0x48, 0xfb, 0xa3, 0x79, 0x37, - 0x74, 0x35, 0xbd, 0x24, 0x6c, 0xc7, 0xcf, 0x92, 0xa0, 0xe9, 0x05, 0x84, 0x9d, 0xa2, 0xf3, 0x30, - 0xad, 0x54, 0x2c, 0xd7, 0x4c, 0xd8, 0x20, 0x87, 0x30, 0x92, 0xed, 0x5b, 0x70, 0xaa, 0x2b, 0xa9, - 0xaa, 0x0f, 0xd3, 0x62, 0xdf, 0x94, 0x56, 0x1b, 0xc3, 0x10, 0x25, 0x2c, 0xab, 0xda, 0xcc, 0xc0, - 0x29, 0xbe, 0x90, 0x28, 0xa7, 0x15, 0x77, 0x83, 0xb4, 0x54, 0xa2, 0x1c, 0xf3, 0xaf, 0xdf, 0xcc, - 0x02, 0x71, 0x37, 0xbe, 0xfd, 0x79, 0x0b, 0x46, 0x52, 0x79, 0x6e, 0x05, 0x19, 0x41, 0x6c, 0xa5, - 0x85, 0x2c, 0xfa, 0x8f, 0x85, 0x40, 0x97, 0x99, 0x32, 0xd5, 0x2b, 0x4d, 0x83, 0xb0, 0x89, 0x67, - 0x7f, 0xa9, 0x04, 0x35, 0x19, 0x41, 0xd1, 0x47, 0x57, 0x3e, 0x6b, 0xc1, 0x88, 0x3a, 0xd3, 0x60, - 0xce, 0xb2, 0x52, 0x11, 0x49, 0x09, 0xb4, 0x07, 0x6a, 0xbb, 0x1d, 0xac, 0x87, 0xda, 0x22, 0xc7, - 0x26, 0x33, 0x9c, 0xe6, 0x8d, 0x6e, 0x02, 0xc4, 0x3b, 0x71, 0x42, 0x5a, 0x86, 0xdb, 0xce, 0x36, - 0x56, 0xdc, 0x84, 0x1b, 0x46, 0x84, 0xae, 0xaf, 0xeb, 0x61, 0x83, 0xac, 0x28, 0x4c, 0x6d, 0x42, - 0xe9, 0x36, 0x6c, 0x50, 0xb2, 0xff, 0x7e, 0x09, 0x4e, 0x66, 0xbb, 0x84, 0x3e, 0x08, 0xc3, 0x92, - 0xbb, 0x71, 0xdb, 0x98, 0x0c, 0x1b, 0x19, 0xc6, 0x06, 0xec, 0xde, 0xee, 0xf8, 0x78, 0xf7, 0x6d, - 0x6f, 0x13, 0x26, 0x0a, 0x4e, 0x11, 0xe3, 0x07, 0x4b, 0xe2, 0x04, 0x74, 0x7a, 0x67, 0xaa, 0xdd, - 0x16, 0xa7, 0x43, 0xc6, 0xc1, 0x92, 0x09, 0xc5, 0x19, 0x6c, 0xb4, 0x0c, 0x67, 0x8c, 0x96, 0xeb, - 0xc4, 0x6b, 0x6e, 0xac, 0x85, 0x91, 0xdc, 0x59, 0x3d, 0xae, 0x03, 0xbb, 0xba, 0x71, 0x70, 0xee, - 0x93, 0x54, 0xdb, 0xbb, 0x4e, 0xdb, 0x71, 0xbd, 0x64, 0x47, 0xf8, 0x21, 0x95, 0x6c, 0x9a, 0x11, - 0xed, 0x58, 0x61, 0xd8, 0x8b, 0x50, 0xe9, 0x73, 0x06, 0xf5, 0x65, 0xd1, 0xbf, 0x0c, 0x35, 0x4a, - 0x4e, 0x9a, 0x77, 0x45, 0x90, 0x0c, 0xa1, 0x26, 0x2f, 0x0c, 0x41, 0x36, 0x94, 0x3d, 0x47, 0x9e, - 0xdd, 0xa9, 0xd7, 0x9a, 0x8f, 0xe3, 0x0e, 0xdb, 0x24, 0x53, 0x20, 0x7a, 0x12, 0xca, 0x64, 0xbb, - 0x9d, 0x3d, 0xa4, 0xbb, 0xb4, 0xdd, 0xf6, 0x22, 0x12, 0x53, 0x24, 0xb2, 0xdd, 0x46, 0xe7, 0xa0, - 0xe4, 0x35, 0x84, 0x92, 0x02, 0x81, 0x53, 0x9a, 0x9f, 0xc5, 0x25, 0xaf, 0x61, 0x6f, 0x43, 0x5d, - 0xdd, 0x50, 0x82, 0x36, 0xa5, 0xec, 0xb6, 0x8a, 0x08, 0x79, 0x92, 0x74, 0x7b, 0x48, 0xed, 0x0e, - 0x80, 0x4e, 0xf8, 0x2b, 0x4a, 0xbe, 0x5c, 0x80, 0x8a, 0x1b, 0x8a, 0x64, 0xe4, 0x9a, 0x26, 0xc3, - 0x84, 0x36, 0x83, 0xd8, 0xb7, 0x60, 0xf4, 0x5a, 0x10, 0xde, 0x61, 0xe5, 0xd5, 0x59, 0x35, 0x31, - 0x4a, 0x78, 0x9d, 0xfe, 0xc8, 0x9a, 0x08, 0x0c, 0x8a, 0x39, 0x4c, 0xd5, 0x67, 0x2a, 0xf5, 0xaa, - 0xcf, 0x64, 0x7f, 0xc2, 0x82, 0x61, 0x95, 0x39, 0x34, 0xb7, 0xb5, 0x49, 0xe9, 0x36, 0xa3, 0xb0, - 0xd3, 0xce, 0xd2, 0x65, 0x77, 0x08, 0x61, 0x0e, 0x33, 0x53, 0xea, 0x4a, 0xfb, 0xa4, 0xd4, 0x5d, - 0x80, 0xca, 0xa6, 0x17, 0x34, 0xb2, 0x97, 0x62, 0x5c, 0xf3, 0x82, 0x06, 0x66, 0x10, 0xda, 0x85, - 0x93, 0xaa, 0x0b, 0x52, 0x21, 0xbc, 0x08, 0xc3, 0x6b, 0x1d, 0xcf, 0x6f, 0xc8, 0x32, 0x69, 0x19, - 0x4f, 0xc9, 0xb4, 0x01, 0xc3, 0x29, 0x4c, 0xba, 0xaf, 0x5b, 0xf3, 0x02, 0x27, 0xda, 0x59, 0xd6, - 0x1a, 0x48, 0x09, 0xa5, 0x69, 0x05, 0xc1, 0x06, 0x96, 0xfd, 0x66, 0x19, 0x46, 0xd3, 0xf9, 0x53, - 0x7d, 0x6c, 0xaf, 0x9e, 0x84, 0x2a, 0x4b, 0xa9, 0xca, 0x7e, 0x5a, 0xf6, 0x3c, 0xe6, 0x30, 0x14, - 0xc3, 0x00, 0x2f, 0xc6, 0x50, 0xcc, 0x85, 0x32, 0xaa, 0x93, 0xca, 0xbf, 0xc2, 0xe2, 0xc9, 0x44, - 0xfd, 0x07, 0xc1, 0x0a, 0x7d, 0xda, 0x82, 0xc1, 0xb0, 0x6d, 0xd6, 0xf5, 0xf9, 0x40, 0x91, 0xb9, - 0x65, 0x22, 0x59, 0x46, 0x58, 0xc4, 0xea, 0xd3, 0xcb, 0xcf, 0x21, 0x59, 0x9f, 0xfb, 0x61, 0x18, - 0x36, 0x31, 0xf7, 0x33, 0x8a, 0x6b, 0xa6, 0x51, 0xfc, 0x59, 0x73, 0x52, 0x88, 0xec, 0xb9, 0x3e, - 0x96, 0xdb, 0x0d, 0xa8, 0xba, 0x2a, 0x00, 0xe0, 0x50, 0xc5, 0x35, 0x55, 0x75, 0x04, 0x76, 0x08, - 0xc4, 0xa9, 0xd9, 0xdf, 0xb2, 0x8c, 0xf9, 0x81, 0x49, 0x3c, 0xdf, 0x40, 0x11, 0x94, 0x9b, 0x5b, - 0x9b, 0xc2, 0x14, 0xbd, 0x5a, 0xd0, 0xf0, 0xce, 0x6d, 0x6d, 0xea, 0x39, 0x6e, 0xb6, 0x62, 0xca, - 0xac, 0x0f, 0x27, 0x60, 0x2a, 0xc9, 0xb2, 0xbc, 0x7f, 0x92, 0xa5, 0xfd, 0x56, 0x09, 0x4e, 0x75, - 0x4d, 0x2a, 0xf4, 0x06, 0x54, 0x23, 0xfa, 0x96, 0xe2, 0xf5, 0x16, 0x0a, 0x4b, 0x8b, 0x8c, 0xe7, - 0x1b, 0x5a, 0xef, 0xa6, 0xdb, 0x31, 0x67, 0x89, 0xae, 0x02, 0xd2, 0x61, 0x2a, 0xca, 0x03, 0xc9, - 0x5f, 0xf9, 0x9c, 0x78, 0x14, 0x4d, 0x75, 0x61, 0xe0, 0x9c, 0xa7, 0xd0, 0x4b, 0x59, 0x47, 0x66, - 0x39, 0x7d, 0x6e, 0xb9, 0x97, 0x4f, 0xd2, 0xfe, 0x27, 0x25, 0x18, 0x49, 0x95, 0x59, 0x42, 0x3e, - 0xd4, 0x88, 0xcf, 0x9c, 0xfa, 0x52, 0xd9, 0x1c, 0xb5, 0xf8, 0xb0, 0x52, 0x90, 0x97, 0x04, 0x5d, - 0xac, 0x38, 0x3c, 0x1c, 0x87, 0xeb, 0x2f, 0xc2, 0xb0, 0xec, 0xd0, 0x07, 0x9c, 0x96, 0x2f, 0x06, - 0x50, 0xcd, 0xd1, 0x4b, 0x06, 0x0c, 0xa7, 0x30, 0xed, 0xdf, 0x29, 0xc3, 0x18, 0x3f, 0x05, 0x69, - 0xa8, 0x99, 0xb7, 0x28, 0xf7, 0x5b, 0x7f, 0x59, 0x17, 0x43, 0xe3, 0x03, 0xb9, 0x76, 0xd4, 0x5a, - 0xff, 0xf9, 0x8c, 0xfa, 0x8a, 0xcc, 0xfa, 0xb9, 0x4c, 0x64, 0x16, 0x37, 0xbb, 0x9b, 0xc7, 0xd4, - 0xa3, 0xef, 0xae, 0x50, 0xad, 0x5f, 0x2e, 0xc1, 0x89, 0xcc, 0x45, 0x0a, 0xe8, 0xcd, 0x74, 0xed, - 0x5d, 0xab, 0x08, 0x5f, 0xf9, 0x9e, 0xb5, 0xf5, 0x0f, 0x56, 0x81, 0xf7, 0x01, 0x2d, 0x15, 0xfb, - 0xf7, 0x4b, 0x30, 0x9a, 0xbe, 0x01, 0xe2, 0x21, 0x1c, 0xa9, 0x77, 0x43, 0x9d, 0x15, 0x39, 0x67, - 0x37, 0x5b, 0x72, 0x97, 0x3c, 0xaf, 0x27, 0x2d, 0x1b, 0xb1, 0x86, 0x3f, 0x14, 0x85, 0x8d, 0xed, - 0xbf, 0x6b, 0xc1, 0x59, 0xfe, 0x96, 0xd9, 0x79, 0xf8, 0x57, 0xf2, 0x46, 0xf7, 0xd5, 0x62, 0x3b, - 0x98, 0x29, 0xe2, 0xb7, 0xdf, 0xf8, 0xb2, 0x1b, 0xf5, 0x44, 0x6f, 0xd3, 0x53, 0xe1, 0x21, 0xec, - 0xec, 0x81, 0x26, 0x83, 0xfd, 0xfb, 0x65, 0xd0, 0x97, 0x08, 0x22, 0x4f, 0xe4, 0x38, 0x16, 0x52, - 0xcc, 0x70, 0x65, 0x27, 0x70, 0xf5, 0x75, 0x85, 0xb5, 0x4c, 0x8a, 0xe3, 0xcf, 0x58, 0x30, 0xe4, - 0x05, 0x5e, 0xe2, 0x39, 0x6c, 0x1b, 0x5d, 0xcc, 0x05, 0x67, 0x8a, 0xdd, 0x3c, 0xa7, 0x1c, 0x46, - 0xe6, 0x39, 0x8e, 0x62, 0x86, 0x4d, 0xce, 0xe8, 0x23, 0x22, 0x78, 0xba, 0x5c, 0x58, 0x76, 0x6e, - 0x2d, 0x13, 0x31, 0xdd, 0xa6, 0x86, 0x57, 0x12, 0x15, 0x94, 0xd4, 0x8e, 0x29, 0x29, 0x55, 0x17, - 0x57, 0x5f, 0xe7, 0x4c, 0x9b, 0x31, 0x67, 0x64, 0xc7, 0x80, 0xba, 0xc7, 0xe2, 0x80, 0x81, 0xa9, - 0x93, 0x50, 0x77, 0x3a, 0x49, 0xd8, 0xa2, 0xc3, 0x24, 0x8e, 0x9a, 0x74, 0xe8, 0xad, 0x04, 0x60, - 0x8d, 0x63, 0xbf, 0x59, 0x85, 0x4c, 0xd2, 0x21, 0xda, 0x36, 0x2f, 0xc0, 0xb4, 0x8a, 0xbd, 0x00, - 0x53, 0x75, 0x26, 0xef, 0x12, 0x4c, 0xd4, 0x84, 0x6a, 0x7b, 0xc3, 0x89, 0xa5, 0x59, 0xfd, 0xb2, - 0xda, 0xc7, 0xd1, 0xc6, 0x7b, 0xbb, 0xe3, 0x3f, 0xd6, 0x9f, 0xd7, 0x95, 0xce, 0xd5, 0x49, 0x5e, - 0x6c, 0x44, 0xb3, 0x66, 0x34, 0x30, 0xa7, 0x7f, 0x90, 0x2b, 0xde, 0x3e, 0x29, 0xaa, 0xb9, 0x63, - 0x12, 0x77, 0xfc, 0x44, 0xcc, 0x86, 0x97, 0x0b, 0x5c, 0x65, 0x9c, 0xb0, 0x4e, 0x97, 0xe7, 0xff, - 0xb1, 0xc1, 0x14, 0x7d, 0x10, 0xea, 0x71, 0xe2, 0x44, 0xc9, 0x21, 0x13, 0x5c, 0xd5, 0xa0, 0xaf, - 0x48, 0x22, 0x58, 0xd3, 0x43, 0xaf, 0xb0, 0xda, 0xae, 0x5e, 0xbc, 0x71, 0xc8, 0x9c, 0x07, 0x59, - 0x07, 0x56, 0x50, 0xc0, 0x06, 0x35, 0x74, 0x11, 0x80, 0xcd, 0x6d, 0x1e, 0xe8, 0x57, 0x63, 0x5e, - 0x26, 0x25, 0x0a, 0xb1, 0x82, 0x60, 0x03, 0xcb, 0xfe, 0x41, 0x48, 0xd7, 0x7b, 0x40, 0xe3, 0xb2, - 0xbc, 0x04, 0xf7, 0x42, 0xb3, 0xdc, 0x85, 0x54, 0x25, 0x88, 0xdf, 0xb0, 0xc0, 0x2c, 0x4a, 0x81, - 0x5e, 0xe7, 0xd5, 0x2f, 0xac, 0x22, 0x4e, 0x0e, 0x0d, 0xba, 0x13, 0x8b, 0x4e, 0x3b, 0x73, 0x84, - 0x2d, 0x4b, 0x60, 0x9c, 0x7b, 0x0f, 0xd4, 0x24, 0xf4, 0x40, 0x46, 0xdd, 0xc7, 0xe1, 0x74, 0xf6, - 0x7a, 0x70, 0x71, 0xea, 0xb4, 0xbf, 0xeb, 0x47, 0xfa, 0x73, 0x4a, 0xbd, 0xfc, 0x39, 0x7d, 0x5c, - 0x83, 0xfa, 0x9b, 0x16, 0x5c, 0xd8, 0xef, 0x16, 0x73, 0xf4, 0x38, 0x54, 0xee, 0x38, 0x91, 0x2c, - 0xba, 0xcd, 0x04, 0xe5, 0x2d, 0x27, 0x0a, 0x30, 0x6b, 0x45, 0x3b, 0x30, 0xc0, 0xa3, 0xc1, 0x84, - 0xb5, 0xfe, 0x72, 0xb1, 0x77, 0xaa, 0x5f, 0x23, 0xc6, 0x76, 0x81, 0x47, 0xa2, 0x61, 0xc1, 0xd0, - 0xfe, 0xb6, 0x05, 0x68, 0x69, 0x8b, 0x44, 0x91, 0xd7, 0x30, 0xe2, 0xd7, 0xd8, 0xad, 0x28, 0xc6, - 0xed, 0x27, 0x66, 0x8a, 0x6b, 0xe6, 0x56, 0x14, 0xe3, 0x5f, 0xfe, 0xad, 0x28, 0xa5, 0x83, 0xdd, - 0x8a, 0x82, 0x96, 0xe0, 0x6c, 0x8b, 0x6f, 0x37, 0xf8, 0x4d, 0x03, 0x7c, 0xef, 0xa1, 0x12, 0xca, - 0x1e, 0xbb, 0xbb, 0x3b, 0x7e, 0x76, 0x31, 0x0f, 0x01, 0xe7, 0x3f, 0x67, 0xbf, 0x07, 0x10, 0x0f, - 0x5b, 0x9b, 0xc9, 0x8b, 0x41, 0xea, 0xe9, 0x7e, 0xb1, 0xbf, 0x5a, 0x85, 0x13, 0x99, 0x92, 0xac, - 0x74, 0xab, 0xd7, 0x1d, 0xf4, 0x74, 0x64, 0xfd, 0xdd, 0xdd, 0xbd, 0xbe, 0xc2, 0xa8, 0x02, 0xa8, - 0x7a, 0x41, 0xbb, 0x93, 0x14, 0x93, 0x43, 0xca, 0x3b, 0x31, 0x4f, 0x09, 0x1a, 0xee, 0x62, 0xfa, - 0x17, 0x73, 0x36, 0x45, 0x06, 0x65, 0xa5, 0x8c, 0xf1, 0xca, 0x03, 0x72, 0x07, 0x7c, 0x52, 0x87, - 0x48, 0x55, 0x8b, 0x70, 0x2c, 0x66, 0x26, 0xcb, 0x71, 0x1f, 0xb5, 0xff, 0x6a, 0x09, 0x86, 0x8c, - 0x8f, 0x86, 0x7e, 0x3e, 0x5d, 0xb2, 0xc9, 0x2a, 0xee, 0x95, 0x18, 0xfd, 0x09, 0x5d, 0x94, 0x89, - 0xbf, 0xd2, 0x53, 0xdd, 0xd5, 0x9a, 0xee, 0xed, 0x8e, 0x9f, 0xcc, 0xd4, 0x63, 0x4a, 0x55, 0x70, - 0x3a, 0xf7, 0x31, 0x38, 0x91, 0x21, 0x93, 0xf3, 0xca, 0xab, 0xe9, 0xdb, 0xdf, 0x8f, 0xe8, 0x96, - 0x32, 0x87, 0xec, 0xeb, 0x74, 0xc8, 0x44, 0x1a, 0x5d, 0xe8, 0x93, 0x3e, 0x7c, 0xb0, 0x99, 0x6c, - 0xd9, 0x52, 0x9f, 0xd9, 0xb2, 0x4f, 0x43, 0xad, 0x1d, 0xfa, 0x9e, 0xeb, 0xa9, 0x2a, 0x84, 0x2c, - 0x3f, 0x77, 0x59, 0xb4, 0x61, 0x05, 0x45, 0x77, 0xa0, 0xae, 0x2e, 0xca, 0x17, 0xfe, 0xed, 0xa2, - 0x0e, 0x7d, 0x94, 0xd1, 0xa2, 0x2f, 0xc0, 0xd7, 0xbc, 0x90, 0x0d, 0x03, 0x4c, 0x09, 0xca, 0xd0, - 0x7f, 0xe6, 0x7b, 0x67, 0xda, 0x31, 0xc6, 0x02, 0x62, 0xff, 0x1a, 0xc0, 0x99, 0xbc, 0xba, 0xd8, - 0xe8, 0xa3, 0x30, 0xc0, 0xfb, 0x58, 0xcc, 0xd5, 0x0b, 0x79, 0x3c, 0xe6, 0x18, 0x41, 0xd1, 0x2d, - 0xf6, 0x1b, 0x0b, 0x9e, 0x82, 0xbb, 0xef, 0xac, 0x89, 0x19, 0x72, 0x3c, 0xdc, 0x17, 0x1c, 0xcd, - 0x7d, 0xc1, 0xe1, 0xdc, 0x7d, 0x67, 0x0d, 0x6d, 0x43, 0xb5, 0xe9, 0x25, 0xc4, 0x11, 0x4e, 0x84, - 0x5b, 0xc7, 0xc2, 0x9c, 0x38, 0xdc, 0x4a, 0x63, 0x3f, 0x31, 0x67, 0x88, 0xbe, 0x66, 0xc1, 0x89, - 0xb5, 0x74, 0x6a, 0xbc, 0x10, 0x9e, 0xce, 0x31, 0xd4, 0x3e, 0x4f, 0x33, 0xe2, 0xd7, 0x02, 0x65, - 0x1a, 0x71, 0xb6, 0x3b, 0xe8, 0x53, 0x16, 0x0c, 0xae, 0x7b, 0xbe, 0x51, 0x06, 0xf7, 0x18, 0x3e, - 0xce, 0x65, 0xc6, 0x40, 0xef, 0x38, 0xf8, 0xff, 0x18, 0x4b, 0xce, 0xbd, 0x34, 0xd5, 0xc0, 0x51, - 0x35, 0xd5, 0xe0, 0x03, 0xd2, 0x54, 0x9f, 0xb1, 0xa0, 0xae, 0x46, 0x5a, 0xa4, 0x3b, 0x7f, 0xf0, - 0x18, 0x3f, 0x39, 0xf7, 0x9c, 0xa8, 0xbf, 0x58, 0x33, 0x47, 0x5f, 0xb4, 0x60, 0xc8, 0x79, 0xa3, - 0x13, 0x91, 0x06, 0xd9, 0x0a, 0xdb, 0xb1, 0xb8, 0x53, 0xf0, 0xd5, 0xe2, 0x3b, 0x33, 0x45, 0x99, - 0xcc, 0x92, 0xad, 0xa5, 0x76, 0x2c, 0xd2, 0x92, 0x74, 0x03, 0x36, 0xbb, 0x80, 0x3e, 0x47, 0xf7, - 0x97, 0x6e, 0x4b, 0x18, 0x7c, 0xa2, 0x5a, 0xce, 0x87, 0x8a, 0xef, 0xd1, 0x8a, 0xe2, 0x21, 0xb6, - 0x9a, 0xea, 0x3f, 0x36, 0xf8, 0xdb, 0xbb, 0x25, 0x18, 0xdf, 0xe7, 0x85, 0xd0, 0x8b, 0x30, 0x1c, - 0x46, 0x4d, 0x27, 0xf0, 0xde, 0x30, 0x4b, 0x6f, 0x28, 0xa3, 0x6f, 0xc9, 0x80, 0xe1, 0x14, 0xa6, - 0x99, 0x1f, 0x5e, 0xda, 0x27, 0x3f, 0xfc, 0x02, 0x54, 0x22, 0xd2, 0x0e, 0xb3, 0x7b, 0x17, 0x96, - 0xa1, 0xc0, 0x20, 0xe8, 0x09, 0x28, 0x3b, 0x6d, 0x4f, 0xc4, 0xc5, 0xa9, 0x2d, 0xd9, 0xd4, 0xf2, - 0x3c, 0xa6, 0xed, 0xa9, 0x72, 0x15, 0xd5, 0xfb, 0x52, 0xae, 0x82, 0x6a, 0x25, 0x71, 0x94, 0x32, - 0xa0, 0xb5, 0x52, 0xfa, 0x88, 0xc3, 0x7e, 0xab, 0x0c, 0x4f, 0xec, 0x39, 0x7d, 0x75, 0x58, 0xa0, - 0xb5, 0x47, 0x58, 0xa0, 0x1c, 0x9e, 0xd2, 0x7e, 0xc3, 0x53, 0xee, 0x31, 0x3c, 0x9f, 0xa2, 0xab, - 0x52, 0x96, 0x2c, 0x29, 0xe6, 0x92, 0xba, 0x5e, 0x15, 0x50, 0xc4, 0x82, 0x94, 0x50, 0xac, 0xf9, - 0xd2, 0x2d, 0x49, 0x2a, 0x37, 0xba, 0x5a, 0x84, 0x56, 0xea, 0x59, 0xc2, 0x84, 0x2f, 0xc5, 0x5e, - 0x09, 0xd7, 0xf6, 0x57, 0x4a, 0xf0, 0x64, 0x1f, 0xca, 0xc4, 0x9c, 0xc5, 0x56, 0x9f, 0xb3, 0xf8, - 0xbb, 0xfb, 0x33, 0xd9, 0x7f, 0xd5, 0x82, 0x73, 0xbd, 0x75, 0x19, 0x7a, 0x0e, 0x86, 0xd6, 0x22, - 0x27, 0x70, 0x37, 0xd8, 0xc5, 0x9b, 0x72, 0x50, 0xd8, 0x58, 0xeb, 0x66, 0x6c, 0xe2, 0xd0, 0xdd, - 0x36, 0x0f, 0x91, 0x30, 0x30, 0x64, 0x2e, 0x2b, 0xdd, 0x6d, 0xaf, 0x66, 0x81, 0xb8, 0x1b, 0xdf, - 0xfe, 0xd3, 0x52, 0x7e, 0xb7, 0xb8, 0xcd, 0x73, 0x90, 0xef, 0x24, 0xbe, 0x42, 0xa9, 0x0f, 0x59, - 0x52, 0xbe, 0xdf, 0xb2, 0xa4, 0xd2, 0x4b, 0x96, 0xa0, 0x59, 0x38, 0x69, 0xdc, 0xe8, 0xc2, 0xf3, - 0x93, 0x79, 0xfc, 0xaf, 0x2a, 0xda, 0xb1, 0x9c, 0x81, 0xe3, 0xae, 0x27, 0xd0, 0x33, 0x50, 0xf3, - 0x82, 0x98, 0xb8, 0x9d, 0x88, 0xc7, 0x9d, 0x1b, 0x39, 0x61, 0xf3, 0xa2, 0x1d, 0x2b, 0x0c, 0xfb, - 0x17, 0x4a, 0xf0, 0x58, 0x4f, 0xb3, 0xef, 0x3e, 0xc9, 0x2e, 0xf3, 0x73, 0x54, 0xee, 0xcf, 0xe7, - 0x30, 0x07, 0xa9, 0xba, 0xef, 0x20, 0xfd, 0x41, 0xef, 0x89, 0x49, 0xb7, 0x00, 0xdf, 0xb3, 0xa3, - 0xf4, 0x12, 0x8c, 0x38, 0xed, 0x36, 0xc7, 0x63, 0xe1, 0xa3, 0x99, 0xa2, 0x3d, 0x53, 0x26, 0x10, - 0xa7, 0x71, 0xfb, 0xd2, 0x9e, 0xbf, 0x5e, 0x82, 0xf3, 0x7b, 0x5b, 0x37, 0xe9, 0xc0, 0x1b, 0xab, - 0x8f, 0xea, 0xe6, 0xfb, 0xc7, 0xf2, 0x7c, 0xb7, 0x4f, 0xc7, 0xff, 0x68, 0x41, 0x1d, 0x93, 0x75, - 0x2e, 0x53, 0xd1, 0x6d, 0x31, 0xb1, 0xac, 0x22, 0x8a, 0xa2, 0xd2, 0xe9, 0x18, 0x7b, 0xac, 0x58, - 0x68, 0xde, 0x14, 0xed, 0xbe, 0x2f, 0xa9, 0x74, 0xa0, 0xfb, 0x92, 0xd4, 0x8d, 0x39, 0xe5, 0xde, - 0x37, 0xe6, 0xd8, 0x5f, 0x1f, 0xa4, 0xaf, 0xd7, 0x0e, 0x67, 0x22, 0xd2, 0x88, 0xe9, 0xc7, 0xea, - 0x44, 0xbe, 0xf8, 0xf2, 0xea, 0x63, 0xdd, 0xc0, 0x0b, 0x98, 0xb6, 0xa7, 0xce, 0xd3, 0x4a, 0x07, - 0x2a, 0xf4, 0x52, 0xde, 0xb7, 0xd0, 0xcb, 0x4b, 0x30, 0x12, 0xc7, 0x1b, 0xcb, 0x91, 0xb7, 0xe5, - 0x24, 0xe4, 0x1a, 0xd9, 0x11, 0xb6, 0xa9, 0x2e, 0xce, 0xb0, 0x72, 0x45, 0x03, 0x71, 0x1a, 0x17, - 0xcd, 0xc1, 0x29, 0x5d, 0x6e, 0x85, 0x44, 0x09, 0x4b, 0xd1, 0xe0, 0xeb, 0x47, 0x65, 0x62, 0xeb, - 0x02, 0x2d, 0x02, 0x01, 0x77, 0x3f, 0x43, 0xb5, 0x42, 0xaa, 0x91, 0x76, 0x64, 0x20, 0xad, 0x15, - 0x52, 0x74, 0x68, 0x5f, 0xba, 0x9e, 0x40, 0x8b, 0x70, 0x9a, 0x4f, 0x8c, 0xa9, 0x76, 0xdb, 0x78, - 0xa3, 0xc1, 0x74, 0x31, 0xca, 0xb9, 0x6e, 0x14, 0x9c, 0xf7, 0x1c, 0x7a, 0x01, 0x86, 0x54, 0xf3, - 0xfc, 0xac, 0x38, 0x0a, 0x52, 0xae, 0x28, 0x45, 0x66, 0xbe, 0x81, 0x4d, 0x3c, 0xf4, 0x01, 0x78, - 0x54, 0xff, 0xe5, 0x79, 0x7c, 0xfc, 0x7c, 0x74, 0x56, 0x54, 0xb2, 0x52, 0xf7, 0xb3, 0xcc, 0xe5, - 0xa2, 0x35, 0x70, 0xaf, 0xe7, 0xd1, 0x1a, 0x9c, 0x53, 0xa0, 0x4b, 0x41, 0xc2, 0x92, 0x72, 0x62, - 0x32, 0xed, 0xc4, 0xe4, 0x46, 0xe4, 0xb3, 0x7d, 0x58, 0x5d, 0x5f, 0x9d, 0x39, 0xe7, 0x25, 0x57, - 0xf2, 0x30, 0xf1, 0x02, 0xde, 0x83, 0x0a, 0x95, 0x45, 0x24, 0x70, 0xd6, 0x7c, 0xb2, 0x34, 0x33, - 0xcf, 0x2a, 0x62, 0x19, 0xc7, 0xb1, 0x97, 0x24, 0x00, 0x6b, 0x1c, 0x15, 0x26, 0x3c, 0xdc, 0xf3, - 0x1a, 0xd7, 0x65, 0x38, 0xd3, 0x74, 0xdb, 0xd4, 0x62, 0xf3, 0x5c, 0x32, 0xe5, 0xb2, 0xa8, 0x48, - 0xfa, 0x61, 0x78, 0x95, 0x50, 0x15, 0x03, 0x3f, 0x37, 0xb3, 0xdc, 0x85, 0x83, 0x73, 0x9f, 0x64, - 0xd1, 0xb3, 0x51, 0xb8, 0xbd, 0x33, 0x76, 0x3a, 0x13, 0x3d, 0x4b, 0x1b, 0x31, 0x87, 0xa1, 0xab, - 0x80, 0x58, 0x42, 0xc5, 0x95, 0x24, 0x69, 0x2b, 0x13, 0x71, 0xec, 0x0c, 0x7b, 0x25, 0x15, 0x0b, - 0x78, 0xb9, 0x0b, 0x03, 0xe7, 0x3c, 0x65, 0xff, 0x07, 0x0b, 0x46, 0xd4, 0x7a, 0xbd, 0x0f, 0x29, - 0x45, 0x7e, 0x3a, 0xa5, 0x68, 0xee, 0xe8, 0x12, 0x8f, 0xf5, 0xbc, 0x47, 0x5c, 0xfa, 0x4f, 0x0d, - 0x01, 0x68, 0xa9, 0xa8, 0xd4, 0xb8, 0xd5, 0x53, 0x8d, 0x3f, 0xb4, 0x12, 0x29, 0xaf, 0xfc, 0x4d, - 0xf5, 0xc1, 0x96, 0xbf, 0x59, 0x81, 0xb3, 0x52, 0xab, 0xf1, 0x03, 0xbf, 0x2b, 0x61, 0xac, 0x04, - 0x5c, 0x6d, 0xfa, 0x09, 0x41, 0xe8, 0xec, 0x7c, 0x1e, 0x12, 0xce, 0x7f, 0x36, 0xa5, 0x4c, 0x07, - 0xf7, 0x53, 0xa6, 0x7a, 0x4d, 0x2f, 0xac, 0xcb, 0x8b, 0x58, 0x32, 0x6b, 0x7a, 0xe1, 0xf2, 0x0a, - 0xd6, 0x38, 0xf9, 0x82, 0xbd, 0x5e, 0x90, 0x60, 0x87, 0x03, 0x0b, 0x76, 0x29, 0x62, 0x86, 0x7a, - 0x8a, 0x18, 0x69, 0x10, 0x0d, 0xf7, 0x34, 0x88, 0xde, 0x0b, 0xa3, 0x5e, 0xb0, 0x41, 0x22, 0x2f, - 0x21, 0x0d, 0xb6, 0x16, 0x98, 0xf8, 0xa9, 0x69, 0xb5, 0x3e, 0x9f, 0x82, 0xe2, 0x0c, 0x76, 0x5a, - 0x2e, 0x8e, 0xf6, 0x21, 0x17, 0x7b, 0x68, 0xa3, 0x13, 0xc5, 0x68, 0xa3, 0x93, 0x47, 0xd7, 0x46, - 0xa7, 0x8e, 0x55, 0x1b, 0xa1, 0x42, 0xb4, 0x51, 0x5f, 0x82, 0xde, 0xd8, 0x34, 0x9f, 0xd9, 0x67, - 0xd3, 0xdc, 0x4b, 0x15, 0x9d, 0x3d, 0xb4, 0x2a, 0xca, 0xd7, 0x32, 0x8f, 0x1c, 0x4a, 0xcb, 0x7c, - 0xa6, 0x04, 0x67, 0xb5, 0x1c, 0xa6, 0xb3, 0xdf, 0x5b, 0xa7, 0x92, 0x88, 0xdd, 0xe5, 0xc5, 0x0f, - 0xdf, 0x8c, 0x0c, 0x37, 0x9d, 0x2c, 0xa7, 0x20, 0xd8, 0xc0, 0x62, 0x89, 0x62, 0x24, 0x62, 0xb5, - 0x90, 0xb3, 0x42, 0x7a, 0x46, 0xb4, 0x63, 0x85, 0x41, 0xe7, 0x17, 0xfd, 0x2d, 0x92, 0x6f, 0xb3, - 0x15, 0xff, 0x66, 0x34, 0x08, 0x9b, 0x78, 0xe8, 0x69, 0xce, 0x84, 0x09, 0x08, 0x2a, 0xa8, 0x87, - 0xc5, 0xe5, 0xbe, 0x52, 0x26, 0x28, 0xa8, 0xec, 0x0e, 0xcb, 0x08, 0xac, 0x76, 0x77, 0x87, 0xc5, - 0xb1, 0x29, 0x0c, 0xfb, 0x7f, 0x5a, 0xf0, 0x58, 0xee, 0x50, 0xdc, 0x07, 0xe5, 0xbb, 0x9d, 0x56, - 0xbe, 0x2b, 0x45, 0x6d, 0x37, 0x8c, 0xb7, 0xe8, 0xa1, 0x88, 0xff, 0x9d, 0x05, 0xa3, 0x1a, 0xff, - 0x3e, 0xbc, 0xaa, 0x97, 0x7e, 0xd5, 0xe2, 0x76, 0x56, 0xf5, 0xae, 0x77, 0xfb, 0x9d, 0x12, 0xa8, - 0x2a, 0x9c, 0x53, 0xae, 0xac, 0x71, 0xbc, 0xcf, 0x71, 0xf0, 0x0e, 0x0c, 0xb0, 0xd3, 0xec, 0xb8, - 0x98, 0x48, 0x9d, 0x34, 0x7f, 0x76, 0x32, 0xae, 0x23, 0x05, 0xd8, 0xdf, 0x18, 0x0b, 0x86, 0xac, - 0x52, 0xb7, 0x17, 0x53, 0x69, 0xde, 0x10, 0xb9, 0x75, 0xba, 0x52, 0xb7, 0x68, 0xc7, 0x0a, 0x83, - 0xaa, 0x07, 0xcf, 0x0d, 0x83, 0x19, 0xdf, 0x89, 0xe5, 0x05, 0x96, 0x4a, 0x3d, 0xcc, 0x4b, 0x00, - 0xd6, 0x38, 0xec, 0xa0, 0xdb, 0x8b, 0xdb, 0xbe, 0xb3, 0x63, 0x78, 0x1d, 0x8c, 0x22, 0x13, 0x0a, - 0x84, 0x4d, 0x3c, 0xbb, 0x05, 0x63, 0xe9, 0x97, 0x98, 0x25, 0xeb, 0x2c, 0xca, 0xb4, 0xaf, 0xe1, - 0x9c, 0x84, 0xba, 0xc3, 0x9e, 0x5a, 0xe8, 0x38, 0xd9, 0x7b, 0xe7, 0xa7, 0x24, 0x00, 0x6b, 0x1c, - 0xfb, 0x57, 0x2c, 0x38, 0x9d, 0x33, 0x68, 0x05, 0xe6, 0x2e, 0x26, 0x5a, 0xda, 0xe4, 0x29, 0xf6, - 0x1f, 0x80, 0xc1, 0x06, 0x59, 0x77, 0x64, 0x1c, 0xa3, 0x21, 0xdb, 0x67, 0x79, 0x33, 0x96, 0x70, - 0xfb, 0xbf, 0x5b, 0x70, 0x22, 0xdd, 0xd7, 0x98, 0xe5, 0x03, 0xf1, 0x61, 0xf2, 0x62, 0x37, 0xdc, - 0x22, 0xd1, 0x0e, 0x7d, 0x73, 0x2b, 0x93, 0x0f, 0xd4, 0x85, 0x81, 0x73, 0x9e, 0x62, 0x35, 0x78, - 0x1b, 0x6a, 0xb4, 0xe5, 0x8c, 0xbc, 0x59, 0xe4, 0x8c, 0xd4, 0x1f, 0xd3, 0x8c, 0x79, 0x50, 0x2c, - 0xb1, 0xc9, 0xdf, 0xfe, 0x76, 0x05, 0x54, 0x72, 0x33, 0x0b, 0x22, 0x2b, 0x28, 0x04, 0xef, 0xa0, - 0x69, 0x60, 0x6a, 0x32, 0x54, 0xf6, 0x8a, 0xea, 0xe0, 0x5e, 0x12, 0xd3, 0xe1, 0xab, 0xde, 0x70, - 0x55, 0x83, 0xb0, 0x89, 0x47, 0x7b, 0xe2, 0x7b, 0x5b, 0x84, 0x3f, 0x34, 0x90, 0xee, 0xc9, 0x82, - 0x04, 0x60, 0x8d, 0x43, 0x7b, 0xd2, 0xf0, 0xd6, 0xd7, 0xc5, 0x96, 0x5f, 0xf5, 0x84, 0x8e, 0x0e, - 0x66, 0x10, 0x5e, 0x56, 0x3d, 0xdc, 0x14, 0x56, 0xb0, 0x51, 0x56, 0x3d, 0xdc, 0xc4, 0x0c, 0x42, - 0xed, 0xb6, 0x20, 0x8c, 0x5a, 0x8e, 0xef, 0xbd, 0x41, 0x1a, 0x8a, 0x8b, 0xb0, 0x7e, 0x95, 0xdd, - 0x76, 0xbd, 0x1b, 0x05, 0xe7, 0x3d, 0x47, 0x67, 0x60, 0x3b, 0x22, 0x0d, 0xcf, 0x4d, 0x4c, 0x6a, - 0x90, 0x9e, 0x81, 0xcb, 0x5d, 0x18, 0x38, 0xe7, 0x29, 0x34, 0x05, 0x27, 0x64, 0x72, 0xba, 0x2c, - 0x3d, 0x34, 0x94, 0x2e, 0x75, 0x82, 0xd3, 0x60, 0x9c, 0xc5, 0xa7, 0x52, 0xad, 0x25, 0xaa, 0x8e, - 0x31, 0x63, 0xd9, 0x90, 0x6a, 0xb2, 0x1a, 0x19, 0x56, 0x18, 0xf6, 0x27, 0xcb, 0x54, 0x0b, 0xf7, - 0xa8, 0xb6, 0x77, 0xdf, 0x42, 0x3e, 0xd3, 0x33, 0xb2, 0xd2, 0xc7, 0x8c, 0x7c, 0x1e, 0x86, 0x6f, - 0xc7, 0x61, 0xa0, 0xc2, 0x29, 0xab, 0x3d, 0xc3, 0x29, 0x0d, 0xac, 0xfc, 0x70, 0xca, 0x81, 0xa2, - 0xc2, 0x29, 0x07, 0x0f, 0x19, 0x4e, 0xf9, 0xcd, 0x2a, 0xa8, 0xfb, 0x5d, 0xae, 0x93, 0xe4, 0x4e, - 0x18, 0x6d, 0x7a, 0x41, 0x93, 0x25, 0xf5, 0x7f, 0xcd, 0x82, 0x61, 0xbe, 0x5e, 0x16, 0xcc, 0x74, - 0xb8, 0xf5, 0x82, 0x2e, 0x0e, 0x49, 0x31, 0x9b, 0x58, 0x35, 0x18, 0x65, 0xee, 0x4f, 0x35, 0x41, - 0x38, 0xd5, 0x23, 0xf4, 0x31, 0x00, 0xe9, 0x1f, 0x5d, 0x97, 0x22, 0x73, 0xbe, 0x98, 0xfe, 0x61, - 0xb2, 0xae, 0x6d, 0xe0, 0x55, 0xc5, 0x04, 0x1b, 0x0c, 0xd1, 0x67, 0x74, 0xaa, 0x20, 0xcf, 0xbb, - 0xf8, 0xc8, 0xb1, 0x8c, 0x4d, 0x3f, 0x89, 0x82, 0x18, 0x06, 0xbd, 0xa0, 0x49, 0xe7, 0x89, 0x08, - 0x3b, 0x7b, 0x57, 0x5e, 0x41, 0x8c, 0x85, 0xd0, 0x69, 0x4c, 0x3b, 0xbe, 0x13, 0xb8, 0x24, 0x9a, - 0xe7, 0xe8, 0xe6, 0xad, 0xe1, 0xac, 0x01, 0x4b, 0x42, 0x5d, 0x37, 0xe3, 0x54, 0xfb, 0xb9, 0x19, - 0xe7, 0xdc, 0xfb, 0xe0, 0x54, 0xd7, 0xc7, 0x3c, 0x50, 0x5e, 0xe0, 0xe1, 0x53, 0x0a, 0xed, 0x7f, - 0x3a, 0xa0, 0x95, 0xd6, 0xf5, 0xb0, 0xc1, 0xef, 0x67, 0x89, 0xf4, 0x17, 0x15, 0x36, 0x6e, 0x81, - 0x53, 0xc4, 0xb8, 0x79, 0x5c, 0x35, 0x62, 0x93, 0x25, 0x9d, 0xa3, 0x6d, 0x27, 0x22, 0xc1, 0x71, - 0xcf, 0xd1, 0x65, 0xc5, 0x04, 0x1b, 0x0c, 0xd1, 0x46, 0x2a, 0x31, 0xe8, 0xf2, 0xd1, 0x13, 0x83, - 0x58, 0xa9, 0xb0, 0xbc, 0x2b, 0x15, 0xbe, 0x68, 0xc1, 0x68, 0x90, 0x9a, 0xb9, 0xc5, 0xc4, 0x02, - 0xe7, 0xaf, 0x0a, 0x7e, 0x3d, 0x58, 0xba, 0x0d, 0x67, 0xf8, 0xe7, 0xa9, 0xb4, 0xea, 0x01, 0x55, - 0x9a, 0xbe, 0xe8, 0x69, 0xa0, 0xd7, 0x45, 0x4f, 0x28, 0x50, 0x37, 0xdd, 0x0d, 0x16, 0x7e, 0xd3, - 0x1d, 0xe4, 0xdc, 0x72, 0x77, 0x0b, 0xea, 0x6e, 0x44, 0x9c, 0xe4, 0x90, 0x97, 0x9e, 0xb1, 0xb0, - 0x86, 0x19, 0x49, 0x00, 0x6b, 0x5a, 0xf6, 0xff, 0xa9, 0xc0, 0x49, 0x39, 0x22, 0x32, 0x8f, 0x80, - 0xea, 0x47, 0xce, 0x57, 0x1b, 0xb7, 0x4a, 0x3f, 0x5e, 0x91, 0x00, 0xac, 0x71, 0xa8, 0x3d, 0xd6, - 0x89, 0xc9, 0x52, 0x9b, 0x04, 0x0b, 0xde, 0x5a, 0x2c, 0x8e, 0xe3, 0xd4, 0x42, 0xb9, 0xa1, 0x41, - 0xd8, 0xc4, 0xa3, 0xc6, 0x38, 0xb7, 0x8b, 0xe3, 0x6c, 0x0e, 0x92, 0xb0, 0xb7, 0xb1, 0x84, 0xa3, - 0x9f, 0xcd, 0x2d, 0xff, 0x5b, 0x4c, 0xf6, 0x5d, 0x57, 0xfa, 0xc4, 0x01, 0xef, 0xc9, 0xfc, 0xdb, - 0x16, 0x9c, 0xe5, 0xad, 0x72, 0x24, 0x6f, 0xb4, 0x1b, 0x4e, 0x42, 0xe2, 0x62, 0xca, 0xf1, 0xe7, - 0xf4, 0x4f, 0x3b, 0x79, 0xf3, 0xd8, 0xe2, 0xfc, 0xde, 0xa0, 0x37, 0x2d, 0x38, 0xb1, 0x99, 0x2a, - 0xdc, 0x22, 0x55, 0xc7, 0x51, 0x6b, 0x2a, 0xa4, 0x88, 0xea, 0xa5, 0x96, 0x6e, 0x8f, 0x71, 0x96, - 0xbb, 0xfd, 0xa7, 0x16, 0x98, 0x62, 0xf4, 0xfe, 0xd7, 0x7b, 0x39, 0xb8, 0x29, 0x28, 0xad, 0xcb, - 0xea, 0x5e, 0x47, 0xe5, 0x1d, 0xaf, 0x21, 0xf6, 0x17, 0xfa, 0xf4, 0x75, 0x7e, 0x16, 0xd3, 0x76, - 0xfb, 0x1f, 0x55, 0xb5, 0xdf, 0x42, 0x24, 0xb7, 0x7d, 0x4f, 0xbc, 0xf6, 0xba, 0xaa, 0x18, 0xc7, - 0xdf, 0xfc, 0x7a, 0x57, 0xc5, 0xb8, 0x1f, 0x39, 0x78, 0xee, 0x22, 0x1f, 0xa0, 0x5e, 0x05, 0xe3, - 0x06, 0xf7, 0x49, 0x5c, 0xbc, 0x0d, 0x35, 0xba, 0x05, 0x63, 0x0e, 0xc8, 0x5a, 0xaa, 0x53, 0xb5, - 0x2b, 0xa2, 0xfd, 0xde, 0xee, 0xf8, 0x0f, 0x1f, 0xbc, 0x5b, 0xf2, 0x69, 0xac, 0xe8, 0xa3, 0x18, - 0xea, 0xf4, 0x37, 0xcb, 0xb1, 0x14, 0x9b, 0xbb, 0x1b, 0x4a, 0x66, 0x4a, 0x40, 0x21, 0x09, 0x9c, - 0x9a, 0x0f, 0x0a, 0xa0, 0xce, 0xae, 0x14, 0x66, 0x4c, 0xf9, 0x1e, 0x70, 0x59, 0x65, 0x3a, 0x4a, - 0xc0, 0xbd, 0xdd, 0xf1, 0x97, 0x0e, 0xce, 0x54, 0x3d, 0x8e, 0x35, 0x0b, 0xfb, 0x4b, 0x15, 0x3d, - 0x77, 0x45, 0xa1, 0xc0, 0xef, 0x89, 0xb9, 0xfb, 0x62, 0x66, 0xee, 0x5e, 0xe8, 0x9a, 0xbb, 0xa3, - 0xfa, 0xea, 0xdb, 0xd4, 0x6c, 0xbc, 0xdf, 0x86, 0xc0, 0xfe, 0xfe, 0x06, 0x66, 0x01, 0xbd, 0xde, - 0xf1, 0x22, 0x12, 0x2f, 0x47, 0x9d, 0xc0, 0x0b, 0x9a, 0x6c, 0x3a, 0xd6, 0x4c, 0x0b, 0x28, 0x05, - 0xc6, 0x59, 0x7c, 0xba, 0xa9, 0xa7, 0xdf, 0xfc, 0x96, 0xb3, 0xc5, 0x67, 0x95, 0x51, 0x3b, 0x6d, - 0x45, 0xb4, 0x63, 0x85, 0x61, 0x7f, 0x9d, 0x9d, 0x65, 0x1b, 0xc9, 0xdd, 0x74, 0x4e, 0xf8, 0xec, - 0x0e, 0x67, 0x5e, 0x78, 0x4d, 0xcd, 0x09, 0x7e, 0x71, 0x33, 0x87, 0xa1, 0x3b, 0x30, 0xb8, 0xc6, - 0x2f, 0x31, 0x2c, 0xa6, 0xc8, 0xbc, 0xb8, 0x11, 0x91, 0x5d, 0x55, 0x23, 0xaf, 0x47, 0xbc, 0xa7, - 0x7f, 0x62, 0xc9, 0xcd, 0xfe, 0x46, 0x05, 0x4e, 0x64, 0x6e, 0xf9, 0x4d, 0x95, 0xbc, 0x2d, 0xed, - 0x5b, 0xf2, 0xf6, 0xc3, 0x00, 0x0d, 0xd2, 0xf6, 0xc3, 0x1d, 0x66, 0x8e, 0x55, 0x0e, 0x6c, 0x8e, - 0x29, 0x0b, 0x7e, 0x56, 0x51, 0xc1, 0x06, 0x45, 0x51, 0x6d, 0x8e, 0x57, 0xd0, 0xcd, 0x54, 0x9b, - 0x33, 0xae, 0xa2, 0x18, 0xb8, 0xbf, 0x57, 0x51, 0x78, 0x70, 0x82, 0x77, 0x51, 0xa5, 0x50, 0x1f, - 0x22, 0x53, 0x9a, 0x25, 0xa1, 0xcc, 0xa6, 0xc9, 0xe0, 0x2c, 0xdd, 0x07, 0x79, 0x89, 0x37, 0x7a, - 0x37, 0xd4, 0xe5, 0x77, 0x8e, 0xc7, 0xea, 0xba, 0x0c, 0x85, 0x9c, 0x06, 0xec, 0x72, 0x6d, 0xf1, - 0xd3, 0xfe, 0x42, 0x89, 0x5a, 0xcf, 0xfc, 0x9f, 0x2a, 0x27, 0xf4, 0x14, 0x0c, 0x38, 0x9d, 0x64, - 0x23, 0xec, 0xba, 0x08, 0x71, 0x8a, 0xb5, 0x62, 0x01, 0x45, 0x0b, 0x50, 0x69, 0xe8, 0x12, 0x31, - 0x07, 0x19, 0x45, 0xed, 0x88, 0x74, 0x12, 0x82, 0x19, 0x15, 0xf4, 0x38, 0x54, 0x12, 0xa7, 0x29, - 0xb3, 0xd5, 0x58, 0x86, 0xf2, 0xaa, 0xd3, 0x8c, 0x31, 0x6b, 0x35, 0x95, 0x66, 0x65, 0x1f, 0xa5, - 0xf9, 0x12, 0x8c, 0xc4, 0x5e, 0x33, 0x70, 0x92, 0x4e, 0x44, 0x8c, 0xc3, 0x35, 0x1d, 0x2f, 0x61, - 0x02, 0x71, 0x1a, 0xd7, 0xfe, 0x37, 0x23, 0x70, 0x66, 0x65, 0x66, 0x51, 0x16, 0x3e, 0x3f, 0xb6, - 0x84, 0xb3, 0x3c, 0x1e, 0xf7, 0x2f, 0xe1, 0xac, 0x07, 0x77, 0xdf, 0x48, 0x38, 0xf3, 0x8d, 0x84, - 0xb3, 0x74, 0xf6, 0x4f, 0xb9, 0x88, 0xec, 0x9f, 0xbc, 0x1e, 0xf4, 0x93, 0xfd, 0x73, 0x6c, 0x19, - 0x68, 0x7b, 0x76, 0xe8, 0x40, 0x19, 0x68, 0x2a, 0x3d, 0xaf, 0x90, 0x44, 0x88, 0x1e, 0x9f, 0x2a, - 0x37, 0x3d, 0x4f, 0xa5, 0x46, 0xf1, 0x24, 0x1f, 0x21, 0x60, 0x5f, 0x2d, 0xbe, 0x03, 0x7d, 0xa4, - 0x46, 0x89, 0x3c, 0x23, 0x33, 0x1d, 0x6f, 0xb0, 0x88, 0x74, 0xbc, 0xbc, 0xee, 0xec, 0x9b, 0x8e, - 0xf7, 0x12, 0x8c, 0xb8, 0x7e, 0x18, 0x90, 0xe5, 0x28, 0x4c, 0x42, 0x37, 0xf4, 0x85, 0x31, 0xad, - 0x44, 0xc2, 0x8c, 0x09, 0xc4, 0x69, 0xdc, 0x5e, 0xb9, 0x7c, 0xf5, 0xa3, 0xe6, 0xf2, 0xc1, 0x03, - 0xca, 0xe5, 0xfb, 0x69, 0x9d, 0x75, 0x3e, 0xc4, 0xbe, 0xc8, 0x87, 0x8b, 0xff, 0x22, 0xfd, 0xa4, - 0x9e, 0xa3, 0xb7, 0xf8, 0x4d, 0x84, 0xd4, 0x1c, 0x9d, 0x09, 0x5b, 0xd4, 0xdc, 0x1a, 0x66, 0x43, - 0xf2, 0xda, 0x31, 0x4c, 0xd8, 0x5b, 0x2b, 0x9a, 0x8d, 0xba, 0x9d, 0x50, 0x37, 0xe1, 0x74, 0x47, - 0xb2, 0x19, 0x7d, 0x23, 0x45, 0x64, 0xf4, 0xe5, 0xf5, 0xab, 0xbf, 0x8c, 0xbe, 0xa3, 0x24, 0xe9, - 0x7f, 0xb5, 0x04, 0xdf, 0xb7, 0xef, 0x88, 0xa0, 0x3b, 0x00, 0x89, 0xd3, 0x14, 0xeb, 0x46, 0x9c, - 0x9a, 0x1c, 0x31, 0xc6, 0x72, 0x55, 0xd2, 0xe3, 0x6f, 0xa6, 0xfe, 0xb2, 0xf3, 0x08, 0xf9, 0x9b, - 0x85, 0x56, 0x86, 0x7e, 0x57, 0xe0, 0x3e, 0x0e, 0x7d, 0x82, 0x19, 0x84, 0x5a, 0x23, 0x11, 0x69, - 0xea, 0x9b, 0xbc, 0xd5, 0x6c, 0xc2, 0xac, 0x15, 0x0b, 0x28, 0x7a, 0x01, 0x86, 0x1c, 0xdf, 0xe7, - 0xa9, 0x45, 0x24, 0x16, 0x37, 0x23, 0xe9, 0x6a, 0x80, 0x1a, 0x84, 0x4d, 0x3c, 0xfb, 0x4f, 0x4a, - 0x30, 0xbe, 0x8f, 0x88, 0xeb, 0x4a, 0x96, 0xac, 0xf6, 0x9d, 0x2c, 0x29, 0xb2, 0x0e, 0x06, 0x7a, - 0x64, 0x1d, 0xbc, 0x00, 0x43, 0x09, 0x71, 0x5a, 0x22, 0x2a, 0x4b, 0xb8, 0x03, 0xf4, 0x31, 0xb0, - 0x06, 0x61, 0x13, 0x8f, 0x0a, 0xd5, 0x51, 0xc7, 0x75, 0x49, 0x1c, 0xcb, 0xb4, 0x02, 0xe1, 0x52, - 0x2d, 0x2c, 0x67, 0x81, 0x79, 0xaa, 0xa7, 0x52, 0x2c, 0x70, 0x86, 0x65, 0x76, 0xc0, 0xeb, 0x7d, - 0x0e, 0xf8, 0x2f, 0x96, 0xe0, 0x89, 0x3d, 0x95, 0x6d, 0xdf, 0xa9, 0x35, 0x9d, 0x98, 0x44, 0xd9, - 0x89, 0x73, 0x23, 0x26, 0x11, 0x66, 0x10, 0x3e, 0x4a, 0xed, 0xb6, 0x71, 0x53, 0x7a, 0xd1, 0x79, - 0x5f, 0x7c, 0x94, 0x52, 0x2c, 0x70, 0x86, 0xe5, 0x61, 0xa7, 0xe5, 0xdf, 0x2b, 0xc1, 0x93, 0x7d, - 0x98, 0x24, 0x05, 0xe6, 0xc7, 0xa5, 0xb3, 0x14, 0xcb, 0x0f, 0x28, 0x99, 0xf4, 0x90, 0xc3, 0xf5, - 0xf5, 0x12, 0x9c, 0xeb, 0x6d, 0x19, 0xa0, 0x1f, 0x85, 0x13, 0x91, 0x0a, 0xc5, 0x32, 0x13, 0x1c, - 0x4f, 0x73, 0x77, 0x42, 0x0a, 0x84, 0xb3, 0xb8, 0x68, 0x02, 0xa0, 0xed, 0x24, 0x1b, 0xf1, 0xa5, - 0x6d, 0x2f, 0x4e, 0x44, 0x3d, 0xa1, 0x51, 0x7e, 0x80, 0x25, 0x5b, 0xb1, 0x81, 0x41, 0xd9, 0xb1, - 0x7f, 0xb3, 0xe1, 0xf5, 0x30, 0xe1, 0x0f, 0xf1, 0x5d, 0xcd, 0x69, 0x79, 0xfb, 0x8a, 0x01, 0xc2, - 0x59, 0x5c, 0xca, 0x8e, 0x1d, 0x91, 0xf2, 0x8e, 0xf2, 0xed, 0x0e, 0x63, 0xb7, 0xa0, 0x5a, 0xb1, - 0x81, 0x91, 0x4d, 0xdd, 0xac, 0xee, 0x9f, 0xba, 0x69, 0xff, 0xc3, 0x12, 0x3c, 0xd6, 0xd3, 0xb2, - 0xec, 0x6f, 0x01, 0x3e, 0x7c, 0xe9, 0x96, 0x87, 0x9b, 0x3b, 0x07, 0xcd, 0xc3, 0xea, 0x31, 0xd3, - 0x44, 0x5a, 0xe0, 0xe1, 0xf3, 0xea, 0x1f, 0xbe, 0xf1, 0xec, 0xca, 0x04, 0xac, 0x1c, 0x20, 0x13, - 0x30, 0xf3, 0x31, 0xaa, 0x7d, 0x2e, 0xe4, 0x3f, 0x2b, 0xf7, 0x1c, 0x5e, 0xba, 0x13, 0xed, 0xcb, - 0x59, 0x3b, 0x0b, 0x27, 0xbd, 0x80, 0xdd, 0xc4, 0xb5, 0xd2, 0x59, 0x13, 0x25, 0x66, 0x4a, 0xe9, - 0x7b, 0xf0, 0xe7, 0x33, 0x70, 0xdc, 0xf5, 0xc4, 0x43, 0x98, 0x30, 0x78, 0xb8, 0x21, 0x3d, 0x58, - 0x6e, 0x30, 0x5a, 0x82, 0xb3, 0x72, 0x28, 0x36, 0x9c, 0x88, 0x34, 0x84, 0x1a, 0x89, 0x45, 0x56, - 0xc5, 0x63, 0x3c, 0x33, 0x23, 0x07, 0x01, 0xe7, 0x3f, 0xc7, 0x2e, 0x3f, 0x0a, 0xdb, 0x9e, 0x2b, - 0xf6, 0x5c, 0xfa, 0xf2, 0x23, 0xda, 0x88, 0x39, 0xcc, 0xfe, 0x4a, 0x09, 0xce, 0xef, 0x6d, 0x1f, - 0xcb, 0xef, 0x61, 0xf5, 0xf1, 0x3d, 0x4a, 0x0f, 0xe4, 0x7b, 0x94, 0x0f, 0xf1, 0x3d, 0x2a, 0xfb, - 0xca, 0x9b, 0x0f, 0x43, 0x5d, 0xf5, 0x84, 0x47, 0xbd, 0xab, 0xe5, 0xd8, 0x15, 0xf5, 0xae, 0xd6, - 0xa2, 0x81, 0x45, 0xc7, 0x8d, 0x6e, 0x16, 0x32, 0x72, 0xe5, 0x1a, 0xd9, 0x61, 0x3b, 0x07, 0xfb, - 0x87, 0x60, 0x58, 0x39, 0xc4, 0xfa, 0xbd, 0x2c, 0xcb, 0xfe, 0xd2, 0x00, 0x8c, 0xa4, 0x0a, 0x60, - 0xa6, 0xfc, 0xcf, 0xd6, 0xbe, 0xfe, 0x67, 0x96, 0xc5, 0xd0, 0x09, 0xe4, 0x4d, 0x7a, 0x46, 0x16, - 0x43, 0x27, 0x20, 0x98, 0xc3, 0xa8, 0xe1, 0xdf, 0x88, 0x76, 0x70, 0x27, 0x10, 0x23, 0xab, 0x0c, - 0xff, 0x59, 0xd6, 0x8a, 0x05, 0x14, 0x7d, 0xc2, 0x82, 0xe1, 0x98, 0x1d, 0x6e, 0x70, 0xef, 0xbd, - 0x58, 0x8e, 0x57, 0x8f, 0x5e, 0xdf, 0x53, 0x15, 0x7b, 0x65, 0x01, 0x44, 0x66, 0x0b, 0x4e, 0x71, - 0x44, 0x9f, 0xb6, 0xa0, 0xae, 0x2e, 0xfc, 0x11, 0xd7, 0x5d, 0xae, 0x14, 0x5b, 0x5f, 0x94, 0xbb, - 0x7d, 0xd5, 0x39, 0x91, 0x2a, 0xf4, 0x88, 0x35, 0x63, 0x14, 0x2b, 0xd7, 0xfa, 0xe0, 0xf1, 0xb8, - 0xd6, 0x21, 0xc7, 0xad, 0xfe, 0x6e, 0xa8, 0xb7, 0x9c, 0xc0, 0x5b, 0x27, 0x71, 0xc2, 0xbd, 0xdd, - 0xb2, 0xec, 0xb1, 0x6c, 0xc4, 0x1a, 0x4e, 0x4d, 0x95, 0x98, 0xbd, 0x58, 0x62, 0xb8, 0xa7, 0x99, - 0xa9, 0xb2, 0xa2, 0x9b, 0xb1, 0x89, 0x63, 0xfa, 0xd2, 0xe1, 0x81, 0xfa, 0xd2, 0x87, 0xf6, 0xf1, - 0xa5, 0xff, 0x03, 0x0b, 0xce, 0xe6, 0x7e, 0xb5, 0x87, 0x37, 0x2e, 0xd4, 0xfe, 0x72, 0x15, 0x4e, - 0xe7, 0x54, 0xb2, 0x45, 0x3b, 0xe6, 0x7c, 0xb6, 0x8a, 0x08, 0xb1, 0x48, 0x47, 0x0c, 0xc8, 0x61, - 0xcc, 0x99, 0xc4, 0x07, 0x3b, 0xc9, 0xd2, 0xa7, 0x49, 0xe5, 0xfb, 0x7b, 0x9a, 0x64, 0x4c, 0xcb, - 0xca, 0x03, 0x9d, 0x96, 0xd5, 0xbd, 0xa7, 0x25, 0xfa, 0x55, 0x0b, 0xc6, 0x5a, 0x3d, 0xae, 0x4f, - 0x10, 0x1e, 0xe2, 0x9b, 0xc7, 0x73, 0x39, 0xc3, 0xf4, 0xe3, 0x77, 0x77, 0xc7, 0x7b, 0xde, 0x5a, - 0x81, 0x7b, 0xf6, 0xca, 0xfe, 0x76, 0x19, 0x58, 0x19, 0x65, 0x56, 0xad, 0x70, 0x07, 0x7d, 0xdc, - 0x2c, 0x88, 0x6d, 0x15, 0x55, 0xbc, 0x99, 0x13, 0x57, 0x05, 0xb5, 0xf9, 0x08, 0xe6, 0xd5, 0xd7, - 0xce, 0x0a, 0xad, 0x52, 0x1f, 0x42, 0xcb, 0x97, 0x95, 0xc7, 0xcb, 0xc5, 0x57, 0x1e, 0xaf, 0x67, - 0xab, 0x8e, 0xef, 0xfd, 0x89, 0x2b, 0x0f, 0xe5, 0x27, 0xfe, 0x9b, 0x16, 0x17, 0x3c, 0x99, 0xaf, - 0xa0, 0x2d, 0x03, 0x6b, 0x0f, 0xcb, 0xe0, 0x19, 0xa8, 0xc5, 0xc4, 0x5f, 0xbf, 0x42, 0x1c, 0x5f, - 0x58, 0x10, 0xfa, 0x78, 0x5f, 0xb4, 0x63, 0x85, 0xc1, 0xae, 0x26, 0xf6, 0xfd, 0xf0, 0xce, 0xa5, - 0x56, 0x3b, 0xd9, 0x11, 0xb6, 0x84, 0xbe, 0x9a, 0x58, 0x41, 0xb0, 0x81, 0x65, 0xff, 0xad, 0x12, - 0x9f, 0x81, 0x22, 0x46, 0xe4, 0xc5, 0xcc, 0x65, 0x92, 0xfd, 0x87, 0x57, 0x7c, 0x14, 0xc0, 0x0d, - 0x5b, 0x6d, 0x6a, 0x11, 0xaf, 0x86, 0xc2, 0x30, 0xbd, 0x72, 0xe4, 0x3b, 0xe2, 0x05, 0x3d, 0xfd, - 0x1a, 0xba, 0x0d, 0x1b, 0xfc, 0x52, 0xb2, 0xb4, 0xbc, 0xaf, 0x2c, 0x4d, 0x89, 0x95, 0xca, 0x3e, - 0xda, 0xee, 0x4f, 0x2c, 0x48, 0x59, 0x44, 0xa8, 0x0d, 0x55, 0xda, 0xdd, 0x1d, 0xb1, 0x42, 0x97, - 0x8a, 0x33, 0xbf, 0xa8, 0x68, 0x14, 0xd3, 0x9e, 0xfd, 0xc4, 0x9c, 0x11, 0xf2, 0x45, 0x28, 0x09, - 0x1f, 0xd5, 0xeb, 0xc5, 0x31, 0xbc, 0x12, 0x86, 0x9b, 0xfc, 0x04, 0x5a, 0x87, 0xa5, 0xd8, 0x2f, - 0xc2, 0xa9, 0xae, 0x4e, 0xb1, 0x7b, 0xe3, 0xc2, 0xc8, 0xed, 0x9a, 0xae, 0x2c, 0xbf, 0x15, 0x73, - 0x98, 0xfd, 0x75, 0x0b, 0x4e, 0x66, 0xc9, 0xa3, 0xb7, 0x2c, 0x38, 0x15, 0x67, 0xe9, 0x1d, 0xd7, - 0xd8, 0xa9, 0x70, 0xd0, 0x2e, 0x10, 0xee, 0xee, 0x84, 0xfd, 0x7f, 0xc5, 0xe4, 0xbf, 0xe5, 0x05, - 0x8d, 0xf0, 0x8e, 0x32, 0x4c, 0xac, 0x9e, 0x86, 0x09, 0x5d, 0x8f, 0xee, 0x06, 0x69, 0x74, 0xfc, - 0xae, 0xc4, 0xda, 0x15, 0xd1, 0x8e, 0x15, 0x06, 0xcb, 0x23, 0xec, 0x88, 0xab, 0x09, 0x32, 0x93, - 0x72, 0x56, 0xb4, 0x63, 0x85, 0x81, 0x9e, 0x87, 0x61, 0xe3, 0x25, 0xe5, 0xbc, 0x64, 0x06, 0xb9, - 0xa1, 0x32, 0x63, 0x9c, 0xc2, 0x42, 0x13, 0x00, 0xca, 0xc8, 0x91, 0x2a, 0x92, 0xb9, 0xd0, 0x94, - 0x24, 0x8a, 0xb1, 0x81, 0xc1, 0xb2, 0x76, 0xfd, 0x4e, 0xcc, 0x4e, 0x3f, 0x06, 0x74, 0xb9, 0xdc, - 0x19, 0xd1, 0x86, 0x15, 0x94, 0x4a, 0x93, 0x96, 0x13, 0x74, 0x1c, 0x9f, 0x8e, 0x90, 0xd8, 0x14, - 0xab, 0x65, 0xb8, 0xa8, 0x20, 0xd8, 0xc0, 0xa2, 0x6f, 0x9c, 0x78, 0x2d, 0xf2, 0x4a, 0x18, 0xc8, - 0x30, 0x3e, 0x7d, 0x3e, 0x27, 0xda, 0xb1, 0xc2, 0xb0, 0xff, 0xab, 0x05, 0x27, 0x74, 0x0d, 0x00, - 0x7e, 0x43, 0xbc, 0xb9, 0x67, 0xb4, 0xf6, 0xdd, 0xc3, 0xa7, 0x93, 0xa3, 0x4b, 0x7d, 0x25, 0x47, - 0x9b, 0x79, 0xcb, 0xe5, 0x3d, 0xf3, 0x96, 0xbf, 0x5f, 0xdf, 0x3e, 0xcc, 0x13, 0x9c, 0x87, 0xf2, - 0x6e, 0x1e, 0x46, 0x36, 0x0c, 0xb8, 0x8e, 0x2a, 0x80, 0x33, 0xcc, 0xf7, 0x0e, 0x33, 0x53, 0x0c, - 0x49, 0x40, 0xec, 0x25, 0xa8, 0xab, 0x73, 0x21, 0xb9, 0x51, 0xb5, 0xf2, 0x37, 0xaa, 0x7d, 0xe5, - 0x4f, 0x4e, 0xaf, 0x7d, 0xe3, 0x3b, 0xe7, 0xdf, 0xf1, 0x7b, 0xdf, 0x39, 0xff, 0x8e, 0x3f, 0xfc, - 0xce, 0xf9, 0x77, 0x7c, 0xe2, 0xee, 0x79, 0xeb, 0x1b, 0x77, 0xcf, 0x5b, 0xbf, 0x77, 0xf7, 0xbc, - 0xf5, 0x87, 0x77, 0xcf, 0x5b, 0xdf, 0xbe, 0x7b, 0xde, 0xfa, 0xe2, 0x7f, 0x3e, 0xff, 0x8e, 0x57, - 0x72, 0xe3, 0x38, 0xe9, 0x8f, 0x67, 0xdd, 0xc6, 0xe4, 0xd6, 0x45, 0x16, 0x4a, 0x48, 0x97, 0xd7, - 0xa4, 0x31, 0xa7, 0x26, 0xe5, 0xf2, 0xfa, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x8d, 0x56, 0x5a, - 0xe3, 0xda, 0xe3, 0x00, 0x00, + 0x75, 0x98, 0x66, 0x17, 0x0b, 0xec, 0x3e, 0x7c, 0x90, 0x6c, 0x92, 0x77, 0x20, 0x75, 0x77, 0xa0, + 0xe7, 0xe2, 0xd3, 0x39, 0xba, 0x03, 0x7c, 0xf4, 0x9d, 0x7c, 0xf1, 0xd9, 0x92, 0xb1, 0x00, 0x09, + 0x82, 0x04, 0x08, 0x5c, 0x03, 0x24, 0xa5, 0x93, 0x4f, 0xa7, 0xc1, 0x6e, 0x63, 0x31, 0xc4, 0xec, + 0xcc, 0xdc, 0xcc, 0x2c, 0x08, 0x9c, 0x25, 0x59, 0xb2, 0x64, 0x5b, 0x89, 0x3e, 0x4e, 0x91, 0x92, + 0xf2, 0x39, 0xb1, 0x14, 0xd9, 0x72, 0x52, 0x71, 0x25, 0xaa, 0x38, 0xc9, 0x8f, 0x38, 0x71, 0x52, + 0x2e, 0xdb, 0xa9, 0x94, 0x52, 0x4a, 0xca, 0x2e, 0x97, 0xcb, 0x72, 0x12, 0x1b, 0x91, 0x98, 0x4a, + 0x25, 0x95, 0xaa, 0xb8, 0xca, 0x89, 0x7f, 0x24, 0x4c, 0x7e, 0xa4, 0xfa, 0xbb, 0x67, 0x76, 0x16, + 0x58, 0x00, 0x03, 0x92, 0x52, 0xee, 0xdf, 0x6e, 0xbf, 0x37, 0xef, 0xf5, 0xf4, 0x74, 0xbf, 0xf7, + 0xfa, 0xf5, 0x7b, 0xaf, 0x61, 0xa1, 0xe5, 0x26, 0x1b, 0x9d, 0xb5, 0xc9, 0x46, 0xd0, 0x9e, 0x72, + 0xa2, 0x56, 0x10, 0x46, 0xc1, 0x6d, 0xf6, 0xe3, 0xd9, 0x46, 0x73, 0x6a, 0xeb, 0xe2, 0x54, 0xb8, + 0xd9, 0x9a, 0x72, 0x42, 0x37, 0x9e, 0x72, 0xc2, 0xd0, 0x73, 0x1b, 0x4e, 0xe2, 0x06, 0xfe, 0xd4, + 0xd6, 0x73, 0x8e, 0x17, 0x6e, 0x38, 0xcf, 0x4d, 0xb5, 0x88, 0x4f, 0x22, 0x27, 0x21, 0xcd, 0xc9, + 0x30, 0x0a, 0x92, 0x00, 0xfd, 0xa8, 0xa6, 0x36, 0x29, 0xa9, 0xb1, 0x1f, 0xaf, 0x35, 0x9a, 0x93, + 0x5b, 0x17, 0x27, 0xc3, 0xcd, 0xd6, 0x24, 0xa5, 0x36, 0x69, 0x50, 0x9b, 0x94, 0xd4, 0xce, 0x3f, + 0x6b, 0xf4, 0xa5, 0x15, 0xb4, 0x82, 0x29, 0x46, 0x74, 0xad, 0xb3, 0xce, 0xfe, 0xb1, 0x3f, 0xec, + 0x17, 0x67, 0x76, 0xde, 0xde, 0x7c, 0x31, 0x9e, 0x74, 0x03, 0xda, 0xbd, 0xa9, 0x46, 0x10, 0x91, + 0xa9, 0xad, 0xae, 0x0e, 0x9d, 0xbf, 0xa2, 0x71, 0xc8, 0x76, 0x42, 0xfc, 0xd8, 0x0d, 0xfc, 0xf8, + 0x59, 0xda, 0x05, 0x12, 0x6d, 0x91, 0xc8, 0x7c, 0x3d, 0x03, 0x21, 0x8f, 0xd2, 0xf3, 0x9a, 0x52, + 0xdb, 0x69, 0x6c, 0xb8, 0x3e, 0x89, 0x76, 0xf4, 0xe3, 0x6d, 0x92, 0x38, 0x79, 0x4f, 0x4d, 0xf5, + 0x7a, 0x2a, 0xea, 0xf8, 0x89, 0xdb, 0x26, 0x5d, 0x0f, 0xbc, 0x67, 0xbf, 0x07, 0xe2, 0xc6, 0x06, + 0x69, 0x3b, 0x5d, 0xcf, 0xfd, 0x50, 0xaf, 0xe7, 0x3a, 0x89, 0xeb, 0x4d, 0xb9, 0x7e, 0x12, 0x27, + 0x51, 0xf6, 0x21, 0xfb, 0x17, 0x2d, 0x18, 0x9d, 0xbe, 0xb5, 0x32, 0xdd, 0x49, 0x36, 0x66, 0x02, + 0x7f, 0xdd, 0x6d, 0xa1, 0x17, 0x60, 0xb8, 0xe1, 0x75, 0xe2, 0x84, 0x44, 0xd7, 0x9d, 0x36, 0x19, + 0xb7, 0x2e, 0x58, 0x4f, 0xd7, 0xea, 0xa7, 0xbf, 0xb1, 0x3b, 0xf1, 0x8e, 0xbb, 0xbb, 0x13, 0xc3, + 0x33, 0x1a, 0x84, 0x4d, 0x3c, 0xf4, 0x03, 0x30, 0x14, 0x05, 0x1e, 0x99, 0xc6, 0xd7, 0xc7, 0x4b, + 0xec, 0x91, 0x13, 0xe2, 0x91, 0x21, 0xcc, 0x9b, 0xb1, 0x84, 0x53, 0xd4, 0x30, 0x0a, 0xd6, 0x5d, + 0x8f, 0x8c, 0x97, 0xd3, 0xa8, 0xcb, 0xbc, 0x19, 0x4b, 0xb8, 0xfd, 0x87, 0x25, 0x80, 0xe9, 0x30, + 0x5c, 0x8e, 0x82, 0xdb, 0xa4, 0x91, 0xa0, 0x0f, 0x43, 0x95, 0x0e, 0x73, 0xd3, 0x49, 0x1c, 0xd6, + 0xb1, 0xe1, 0x8b, 0x3f, 0x38, 0xc9, 0xdf, 0x7a, 0xd2, 0x7c, 0x6b, 0x3d, 0xc9, 0x28, 0xf6, 0xe4, + 0xd6, 0x73, 0x93, 0x4b, 0x6b, 0xf4, 0xf9, 0x45, 0x92, 0x38, 0x75, 0x24, 0x98, 0x81, 0x6e, 0xc3, + 0x8a, 0x2a, 0xf2, 0x61, 0x20, 0x0e, 0x49, 0x83, 0xbd, 0xc3, 0xf0, 0xc5, 0x85, 0xc9, 0xa3, 0xcc, + 0xe6, 0x49, 0xdd, 0xf3, 0x95, 0x90, 0x34, 0xea, 0x23, 0x82, 0xf3, 0x00, 0xfd, 0x87, 0x19, 0x1f, + 0xb4, 0x05, 0x83, 0x71, 0xe2, 0x24, 0x9d, 0x98, 0x0d, 0xc5, 0xf0, 0xc5, 0xeb, 0x85, 0x71, 0x64, + 0x54, 0xeb, 0x63, 0x82, 0xe7, 0x20, 0xff, 0x8f, 0x05, 0x37, 0xfb, 0x4f, 0x2c, 0x18, 0xd3, 0xc8, + 0x0b, 0x6e, 0x9c, 0xa0, 0x9f, 0xe8, 0x1a, 0xdc, 0xc9, 0xfe, 0x06, 0x97, 0x3e, 0xcd, 0x86, 0xf6, + 0xa4, 0x60, 0x56, 0x95, 0x2d, 0xc6, 0xc0, 0xb6, 0xa1, 0xe2, 0x26, 0xa4, 0x1d, 0x8f, 0x97, 0x2e, + 0x94, 0x9f, 0x1e, 0xbe, 0x78, 0xa5, 0xa8, 0xf7, 0xac, 0x8f, 0x0a, 0xa6, 0x95, 0x79, 0x4a, 0x1e, + 0x73, 0x2e, 0xf6, 0xaf, 0x8e, 0x98, 0xef, 0x47, 0x07, 0x1c, 0x3d, 0x07, 0xc3, 0x71, 0xd0, 0x89, + 0x1a, 0x04, 0x93, 0x30, 0x88, 0xc7, 0xad, 0x0b, 0x65, 0x3a, 0xf5, 0xe8, 0xa4, 0x5e, 0xd1, 0xcd, + 0xd8, 0xc4, 0x41, 0x9f, 0xb7, 0x60, 0xa4, 0x49, 0xe2, 0xc4, 0xf5, 0x19, 0x7f, 0xd9, 0xf9, 0xd5, + 0x23, 0x77, 0x5e, 0x36, 0xce, 0x6a, 0xe2, 0xf5, 0x33, 0xe2, 0x45, 0x46, 0x8c, 0xc6, 0x18, 0xa7, + 0xf8, 0xd3, 0xc5, 0xd9, 0x24, 0x71, 0x23, 0x72, 0x43, 0xfa, 0x5f, 0x2c, 0x1f, 0xb5, 0x38, 0x67, + 0x35, 0x08, 0x9b, 0x78, 0xc8, 0x87, 0x0a, 0x5d, 0x7c, 0xf1, 0xf8, 0x00, 0xeb, 0xff, 0xfc, 0xd1, + 0xfa, 0x2f, 0x06, 0x95, 0xae, 0x6b, 0x3d, 0xfa, 0xf4, 0x5f, 0x8c, 0x39, 0x1b, 0xf4, 0x39, 0x0b, + 0xc6, 0x85, 0x70, 0xc0, 0x84, 0x0f, 0xe8, 0xad, 0x0d, 0x37, 0x21, 0x9e, 0x1b, 0x27, 0xe3, 0x15, + 0xd6, 0x87, 0xa9, 0xfe, 0xe6, 0xd6, 0x5c, 0x14, 0x74, 0xc2, 0x6b, 0xae, 0xdf, 0xac, 0x5f, 0x10, + 0x9c, 0xc6, 0x67, 0x7a, 0x10, 0xc6, 0x3d, 0x59, 0xa2, 0x2f, 0x59, 0x70, 0xde, 0x77, 0xda, 0x24, + 0x0e, 0x1d, 0xfa, 0x69, 0x39, 0xb8, 0xee, 0x39, 0x8d, 0x4d, 0xd6, 0xa3, 0xc1, 0xc3, 0xf5, 0xc8, + 0x16, 0x3d, 0x3a, 0x7f, 0xbd, 0x27, 0x69, 0xbc, 0x07, 0x5b, 0xf4, 0x35, 0x0b, 0x4e, 0x05, 0x51, + 0xb8, 0xe1, 0xf8, 0xa4, 0x29, 0xa1, 0xf1, 0xf8, 0x10, 0x5b, 0x7a, 0x1f, 0x3a, 0xda, 0x27, 0x5a, + 0xca, 0x92, 0x5d, 0x0c, 0x7c, 0x37, 0x09, 0xa2, 0x15, 0x92, 0x24, 0xae, 0xdf, 0x8a, 0xeb, 0x67, + 0xef, 0xee, 0x4e, 0x9c, 0xea, 0xc2, 0xc2, 0xdd, 0xfd, 0x41, 0x3f, 0x09, 0xc3, 0xf1, 0x8e, 0xdf, + 0xb8, 0xe5, 0xfa, 0xcd, 0xe0, 0x4e, 0x3c, 0x5e, 0x2d, 0x62, 0xf9, 0xae, 0x28, 0x82, 0x62, 0x01, + 0x6a, 0x06, 0xd8, 0xe4, 0x96, 0xff, 0xe1, 0xf4, 0x54, 0xaa, 0x15, 0xfd, 0xe1, 0xf4, 0x64, 0xda, + 0x83, 0x2d, 0xfa, 0x39, 0x0b, 0x46, 0x63, 0xb7, 0xe5, 0x3b, 0x49, 0x27, 0x22, 0xd7, 0xc8, 0x4e, + 0x3c, 0x0e, 0xac, 0x23, 0x57, 0x8f, 0x38, 0x2a, 0x06, 0xc9, 0xfa, 0x59, 0xd1, 0xc7, 0x51, 0xb3, + 0x35, 0xc6, 0x69, 0xbe, 0x79, 0x0b, 0x4d, 0x4f, 0xeb, 0xe1, 0x62, 0x17, 0x9a, 0x9e, 0xd4, 0x3d, + 0x59, 0xa2, 0x1f, 0x87, 0x93, 0xbc, 0x49, 0x8d, 0x6c, 0x3c, 0x3e, 0xc2, 0x04, 0xed, 0x99, 0xbb, + 0xbb, 0x13, 0x27, 0x57, 0x32, 0x30, 0xdc, 0x85, 0x8d, 0x5e, 0x87, 0x89, 0x90, 0x44, 0x6d, 0x37, + 0x59, 0xf2, 0xbd, 0x1d, 0x29, 0xbe, 0x1b, 0x41, 0x48, 0x9a, 0xa2, 0x3b, 0xf1, 0xf8, 0xe8, 0x05, + 0xeb, 0xe9, 0x6a, 0xfd, 0x5d, 0xa2, 0x9b, 0x13, 0xcb, 0x7b, 0xa3, 0xe3, 0xfd, 0xe8, 0xd9, 0xff, + 0xba, 0x04, 0x27, 0xb3, 0x8a, 0x13, 0xfd, 0x1d, 0x0b, 0x4e, 0xdc, 0xbe, 0x93, 0xac, 0x06, 0x9b, + 0xc4, 0x8f, 0xeb, 0x3b, 0x54, 0xbc, 0x31, 0x95, 0x31, 0x7c, 0xb1, 0x51, 0xac, 0x8a, 0x9e, 0xbc, + 0x9a, 0xe6, 0x72, 0xc9, 0x4f, 0xa2, 0x9d, 0xfa, 0xa3, 0xe2, 0xed, 0x4e, 0x5c, 0xbd, 0xb5, 0x6a, + 0x42, 0x71, 0xb6, 0x53, 0xe7, 0x3f, 0x63, 0xc1, 0x99, 0x3c, 0x12, 0xe8, 0x24, 0x94, 0x37, 0xc9, + 0x0e, 0x37, 0xe0, 0x30, 0xfd, 0x89, 0x5e, 0x85, 0xca, 0x96, 0xe3, 0x75, 0x88, 0xb0, 0x6e, 0xe6, + 0x8e, 0xf6, 0x22, 0xaa, 0x67, 0x98, 0x53, 0xfd, 0x91, 0xd2, 0x8b, 0x96, 0xfd, 0xbb, 0x65, 0x18, + 0x36, 0xf4, 0xdb, 0x7d, 0xb0, 0xd8, 0x82, 0x94, 0xc5, 0xb6, 0x58, 0x98, 0x6a, 0xee, 0x69, 0xb2, + 0xdd, 0xc9, 0x98, 0x6c, 0x4b, 0xc5, 0xb1, 0xdc, 0xd3, 0x66, 0x43, 0x09, 0xd4, 0x82, 0x90, 0x5a, + 0xef, 0x54, 0xf5, 0x0f, 0x14, 0xf1, 0x09, 0x97, 0x24, 0xb9, 0xfa, 0xe8, 0xdd, 0xdd, 0x89, 0x9a, + 0xfa, 0x8b, 0x35, 0x23, 0xfb, 0x5b, 0x16, 0x9c, 0x31, 0xfa, 0x38, 0x13, 0xf8, 0x4d, 0x97, 0x7d, + 0xda, 0x0b, 0x30, 0x90, 0xec, 0x84, 0x72, 0x87, 0xa0, 0x46, 0x6a, 0x75, 0x27, 0x24, 0x98, 0x41, + 0xa8, 0xa1, 0xdf, 0x26, 0x71, 0xec, 0xb4, 0x48, 0x76, 0x4f, 0xb0, 0xc8, 0x9b, 0xb1, 0x84, 0xa3, + 0x08, 0x90, 0xe7, 0xc4, 0xc9, 0x6a, 0xe4, 0xf8, 0x31, 0x23, 0xbf, 0xea, 0xb6, 0x89, 0x18, 0xe0, + 0xbf, 0xd8, 0xdf, 0x8c, 0xa1, 0x4f, 0xd4, 0x1f, 0xb9, 0xbb, 0x3b, 0x81, 0x16, 0xba, 0x28, 0xe1, + 0x1c, 0xea, 0xf6, 0x97, 0x2c, 0x78, 0x24, 0xdf, 0x16, 0x43, 0x4f, 0xc1, 0x20, 0xdf, 0x1e, 0x8a, + 0xb7, 0xd3, 0x9f, 0x84, 0xb5, 0x62, 0x01, 0x45, 0x53, 0x50, 0x53, 0x7a, 0x42, 0xbc, 0xe3, 0x29, + 0x81, 0x5a, 0xd3, 0xca, 0x45, 0xe3, 0xd0, 0x41, 0xa3, 0x7f, 0x84, 0xe5, 0xa6, 0x06, 0x8d, 0xed, + 0xa7, 0x18, 0xc4, 0xfe, 0x8f, 0x16, 0x9c, 0x30, 0x7a, 0x75, 0x1f, 0x4c, 0x73, 0x3f, 0x6d, 0x9a, + 0xcf, 0x17, 0x36, 0x9f, 0x7b, 0xd8, 0xe6, 0x9f, 0xb3, 0xe0, 0xbc, 0x81, 0xb5, 0xe8, 0x24, 0x8d, + 0x8d, 0x4b, 0xdb, 0x61, 0x44, 0x62, 0xba, 0xf5, 0x46, 0x8f, 0x1b, 0x72, 0xab, 0x3e, 0x2c, 0x28, + 0x94, 0xaf, 0x91, 0x1d, 0x2e, 0xc4, 0x9e, 0x81, 0x2a, 0x9f, 0x9c, 0x41, 0x24, 0x46, 0x5c, 0xbd, + 0xdb, 0x92, 0x68, 0xc7, 0x0a, 0x03, 0xd9, 0x30, 0xc8, 0x84, 0x13, 0x5d, 0xac, 0x54, 0x0d, 0x01, + 0xfd, 0x88, 0x37, 0x59, 0x0b, 0x16, 0x10, 0x3b, 0x4e, 0x75, 0x67, 0x39, 0x22, 0xec, 0xe3, 0x36, + 0x2f, 0xbb, 0xc4, 0x6b, 0xc6, 0x74, 0xdb, 0xe0, 0xf8, 0x7e, 0x90, 0x88, 0x1d, 0x80, 0xb1, 0x6d, + 0x98, 0xd6, 0xcd, 0xd8, 0xc4, 0xa1, 0x4c, 0x3d, 0x67, 0x8d, 0x78, 0x7c, 0x44, 0x05, 0xd3, 0x05, + 0xd6, 0x82, 0x05, 0xc4, 0xbe, 0x5b, 0x62, 0x1b, 0x14, 0xb5, 0xf4, 0xc9, 0xfd, 0xd8, 0xdd, 0x46, + 0x29, 0x59, 0xb9, 0x5c, 0x9c, 0xe0, 0x22, 0xbd, 0x77, 0xb8, 0x6f, 0x64, 0xc4, 0x25, 0x2e, 0x94, + 0xeb, 0xde, 0xbb, 0xdc, 0xdf, 0x2a, 0xc1, 0x44, 0xfa, 0x81, 0x2e, 0x69, 0x4b, 0xb7, 0x54, 0x06, + 0xa3, 0xac, 0xbf, 0xc3, 0xc0, 0xc7, 0x26, 0x5e, 0x0f, 0x81, 0x55, 0x3a, 0x4e, 0x81, 0x65, 0xca, + 0xd3, 0xf2, 0x3e, 0xf2, 0xf4, 0x29, 0x35, 0xea, 0x03, 0x19, 0x01, 0x96, 0xd6, 0x29, 0x17, 0x60, + 0x20, 0x4e, 0x48, 0x38, 0x5e, 0x49, 0xcb, 0xa3, 0x95, 0x84, 0x84, 0x98, 0x41, 0xec, 0xff, 0x56, + 0x82, 0x47, 0xd3, 0x63, 0xa8, 0x55, 0xc0, 0xfb, 0x52, 0x2a, 0xe0, 0xdd, 0xa6, 0x0a, 0xb8, 0xb7, + 0x3b, 0xf1, 0xce, 0x1e, 0x8f, 0x7d, 0xd7, 0x68, 0x08, 0x34, 0x97, 0x19, 0xc5, 0xa9, 0xf4, 0x28, + 0xde, 0xdb, 0x9d, 0x78, 0xbc, 0xc7, 0x3b, 0x66, 0x86, 0xf9, 0x29, 0x18, 0x8c, 0x88, 0x13, 0x07, + 0xbe, 0x18, 0x68, 0xf5, 0x39, 0x30, 0x6b, 0xc5, 0x02, 0x6a, 0xff, 0x7e, 0x2d, 0x3b, 0xd8, 0x73, + 0xdc, 0x61, 0x17, 0x44, 0xc8, 0x85, 0x01, 0x66, 0xd6, 0x73, 0xd1, 0x70, 0xed, 0x68, 0xcb, 0x88, + 0xaa, 0x01, 0x45, 0xba, 0x5e, 0xa5, 0x5f, 0x8d, 0x36, 0x61, 0xc6, 0x02, 0x6d, 0x43, 0xb5, 0x21, + 0xad, 0xed, 0x52, 0x11, 0x7e, 0x29, 0x61, 0x6b, 0x6b, 0x8e, 0x23, 0x54, 0x5e, 0x2b, 0x13, 0x5d, + 0x71, 0x43, 0x04, 0xca, 0x2d, 0x37, 0x11, 0x9f, 0xf5, 0x88, 0xfb, 0xa9, 0x39, 0xd7, 0x78, 0xc5, + 0x21, 0xaa, 0x44, 0xe6, 0xdc, 0x04, 0x53, 0xfa, 0xe8, 0x67, 0x2c, 0x18, 0x8e, 0x1b, 0xed, 0xe5, + 0x28, 0xd8, 0x72, 0x9b, 0x24, 0x12, 0xd6, 0xd4, 0x11, 0x45, 0xd3, 0xca, 0xcc, 0xa2, 0x24, 0xa8, + 0xf9, 0xf2, 0xfd, 0xad, 0x86, 0x60, 0x93, 0x2f, 0xdd, 0x65, 0x3c, 0x2a, 0xde, 0x7d, 0x96, 0x34, + 0x5c, 0xaa, 0xff, 0xe4, 0xa6, 0x8a, 0xcd, 0x94, 0x23, 0x5b, 0x97, 0xb3, 0x9d, 0xc6, 0x26, 0x5d, + 0x6f, 0xba, 0x43, 0xef, 0xbc, 0xbb, 0x3b, 0xf1, 0xe8, 0x4c, 0x3e, 0x4f, 0xdc, 0xab, 0x33, 0x6c, + 0xc0, 0xc2, 0x8e, 0xe7, 0x61, 0xf2, 0x7a, 0x87, 0x30, 0x97, 0x49, 0x01, 0x03, 0xb6, 0xac, 0x09, + 0x66, 0x06, 0xcc, 0x80, 0x60, 0x93, 0x2f, 0x7a, 0x1d, 0x06, 0xdb, 0x4e, 0x12, 0xb9, 0xdb, 0xc2, + 0x4f, 0x72, 0x44, 0x7b, 0x7f, 0x91, 0xd1, 0xd2, 0xcc, 0x99, 0xa6, 0xe6, 0x8d, 0x58, 0x30, 0x42, + 0x6d, 0xa8, 0xb4, 0x49, 0xd4, 0x22, 0xe3, 0xd5, 0x22, 0x7c, 0xc2, 0x8b, 0x94, 0x94, 0x66, 0x58, + 0xa3, 0xd6, 0x11, 0x6b, 0xc3, 0x9c, 0x0b, 0x7a, 0x15, 0xaa, 0x31, 0xf1, 0x48, 0x83, 0xda, 0x37, + 0x35, 0xc6, 0xf1, 0x87, 0xfa, 0xb4, 0xf5, 0xa8, 0x61, 0xb1, 0x22, 0x1e, 0xe5, 0x0b, 0x4c, 0xfe, + 0xc3, 0x8a, 0x24, 0x1d, 0xc0, 0xd0, 0xeb, 0xb4, 0x5c, 0x7f, 0x1c, 0x8a, 0x18, 0xc0, 0x65, 0x46, + 0x2b, 0x33, 0x80, 0xbc, 0x11, 0x0b, 0x46, 0xf6, 0x7f, 0xb6, 0x00, 0xa5, 0x85, 0xda, 0x7d, 0x30, + 0x6a, 0x5f, 0x4f, 0x1b, 0xb5, 0x0b, 0x45, 0x5a, 0x1d, 0x3d, 0xec, 0xda, 0xdf, 0xa8, 0x41, 0x46, + 0x1d, 0x5c, 0x27, 0x71, 0x42, 0x9a, 0x6f, 0x8b, 0xf0, 0xb7, 0x45, 0xf8, 0xdb, 0x22, 0x5c, 0x89, + 0xf0, 0xb5, 0x8c, 0x08, 0x7f, 0xaf, 0xb1, 0xea, 0xf5, 0x01, 0xec, 0x6b, 0xea, 0x84, 0xd6, 0xec, + 0x81, 0x81, 0x40, 0x25, 0xc1, 0xd5, 0x95, 0xa5, 0xeb, 0xb9, 0x32, 0xfb, 0xb5, 0xb4, 0xcc, 0x3e, + 0x2a, 0x8b, 0xff, 0x1f, 0xa4, 0xf4, 0xbf, 0xb2, 0xe0, 0x5d, 0x69, 0xe9, 0x25, 0x67, 0xce, 0x7c, + 0xcb, 0x0f, 0x22, 0x32, 0xeb, 0xae, 0xaf, 0x93, 0x88, 0xf8, 0x0d, 0x12, 0x2b, 0x2f, 0x86, 0xd5, + 0xcb, 0x8b, 0x81, 0x9e, 0x87, 0x91, 0xdb, 0x71, 0xe0, 0x2f, 0x07, 0xae, 0x2f, 0x44, 0x10, 0xdd, + 0x08, 0x9f, 0xbc, 0xbb, 0x3b, 0x31, 0x42, 0x47, 0x54, 0xb6, 0xe3, 0x14, 0x16, 0x9a, 0x81, 0x53, + 0xb7, 0x5f, 0x5f, 0x76, 0x12, 0xc3, 0x1d, 0x20, 0x37, 0xee, 0xec, 0xc0, 0xe2, 0xea, 0xcb, 0x19, + 0x20, 0xee, 0xc6, 0xb7, 0xff, 0x66, 0x09, 0xce, 0x65, 0x5e, 0x24, 0xf0, 0xbc, 0xa0, 0x93, 0xd0, + 0x4d, 0x0d, 0xfa, 0x8a, 0x05, 0x27, 0xdb, 0x69, 0x8f, 0x43, 0x2c, 0x1c, 0xbb, 0xef, 0x2f, 0x4c, + 0x47, 0x64, 0x5c, 0x1a, 0xf5, 0x71, 0x31, 0x42, 0x27, 0x33, 0x80, 0x18, 0x77, 0xf5, 0x05, 0xbd, + 0x0a, 0xb5, 0xb6, 0xb3, 0x7d, 0x23, 0x6c, 0x3a, 0x89, 0xdc, 0x4f, 0xf6, 0x76, 0x03, 0x74, 0x12, + 0xd7, 0x9b, 0xe4, 0x47, 0xfb, 0x93, 0xf3, 0x7e, 0xb2, 0x14, 0xad, 0x24, 0x91, 0xeb, 0xb7, 0xb8, + 0x3b, 0x6f, 0x51, 0x92, 0xc1, 0x9a, 0xa2, 0xfd, 0x65, 0x2b, 0xab, 0xa4, 0xd4, 0xe8, 0x44, 0x4e, + 0x42, 0x5a, 0x3b, 0xe8, 0x23, 0x50, 0xa1, 0x1b, 0x3f, 0x39, 0x2a, 0xb7, 0x8a, 0xd4, 0x9c, 0xc6, + 0x97, 0xd0, 0x4a, 0x94, 0xfe, 0x8b, 0x31, 0x67, 0x6a, 0x7f, 0xa5, 0x96, 0x35, 0x16, 0xd8, 0xe1, + 0xed, 0x45, 0x80, 0x56, 0xb0, 0x4a, 0xda, 0xa1, 0x47, 0x87, 0xc5, 0x62, 0x27, 0x00, 0xca, 0xd7, + 0x31, 0xa7, 0x20, 0xd8, 0xc0, 0x42, 0x7f, 0xd9, 0x02, 0x68, 0xc9, 0x39, 0x2f, 0x0d, 0x81, 0x1b, + 0x45, 0xbe, 0x8e, 0x5e, 0x51, 0xba, 0x2f, 0x8a, 0x21, 0x36, 0x98, 0xa3, 0x9f, 0xb6, 0xa0, 0x9a, + 0xc8, 0xee, 0x73, 0xd5, 0xb8, 0x5a, 0x64, 0x4f, 0xe4, 0x4b, 0x6b, 0x9b, 0x48, 0x0d, 0x89, 0xe2, + 0x8b, 0x7e, 0xd6, 0x02, 0x88, 0x77, 0xfc, 0xc6, 0x72, 0xe0, 0xb9, 0x8d, 0x1d, 0xa1, 0x31, 0x6f, + 0x16, 0xea, 0x8f, 0x51, 0xd4, 0xeb, 0x63, 0x74, 0x34, 0xf4, 0x7f, 0x6c, 0x70, 0x46, 0x1f, 0x83, + 0x6a, 0x2c, 0xa6, 0x9b, 0xd0, 0x91, 0xab, 0xc5, 0x7a, 0x85, 0x38, 0x6d, 0x21, 0x5e, 0xc5, 0x3f, + 0xac, 0x78, 0xa2, 0x9f, 0xb7, 0xe0, 0x44, 0x98, 0xf6, 0xf3, 0x09, 0x75, 0x58, 0x9c, 0x0c, 0xc8, + 0xf8, 0x11, 0xeb, 0xa7, 0xef, 0xee, 0x4e, 0x9c, 0xc8, 0x34, 0xe2, 0x6c, 0x2f, 0xa8, 0x04, 0xd4, + 0x33, 0x78, 0x29, 0xe4, 0x3e, 0xc7, 0x21, 0x2d, 0x01, 0xe7, 0xb2, 0x40, 0xdc, 0x8d, 0x8f, 0x96, + 0xe1, 0x0c, 0xed, 0xdd, 0x0e, 0x37, 0x3f, 0xa5, 0x7a, 0x89, 0x99, 0x32, 0xac, 0xd6, 0x1f, 0x13, + 0x33, 0x84, 0x79, 0xf5, 0xb3, 0x38, 0x38, 0xf7, 0x49, 0xf4, 0xbb, 0x16, 0x3c, 0xe6, 0x32, 0x35, + 0x60, 0x3a, 0xcc, 0xb5, 0x46, 0x10, 0x27, 0xb1, 0xa4, 0x50, 0x59, 0xd1, 0x4b, 0xfd, 0xd4, 0xff, + 0x82, 0x78, 0x83, 0xc7, 0xe6, 0xf7, 0xe8, 0x12, 0xde, 0xb3, 0xc3, 0xe8, 0x87, 0x61, 0x54, 0xae, + 0x8b, 0x65, 0x2a, 0x82, 0x99, 0xa2, 0xad, 0xd5, 0x4f, 0xdd, 0xdd, 0x9d, 0x18, 0x5d, 0x35, 0x01, + 0x38, 0x8d, 0x67, 0x7f, 0xb3, 0x94, 0x3a, 0x0f, 0x51, 0x4e, 0x48, 0x26, 0x6e, 0x1a, 0xd2, 0xff, + 0x23, 0xa5, 0x67, 0xa1, 0xe2, 0x46, 0x79, 0x97, 0xb4, 0xb8, 0x51, 0x4d, 0x31, 0x36, 0x98, 0x53, + 0xa3, 0xf4, 0x94, 0x93, 0x75, 0x75, 0x0a, 0x09, 0xf8, 0x6a, 0x91, 0x5d, 0xea, 0x3e, 0xbd, 0x3a, + 0x27, 0xba, 0x76, 0xaa, 0x0b, 0x84, 0xbb, 0xbb, 0x64, 0x7f, 0x33, 0x7d, 0x06, 0x63, 0x2c, 0xde, + 0x3e, 0xce, 0x97, 0x3e, 0x6f, 0xc1, 0x70, 0x14, 0x78, 0x9e, 0xeb, 0xb7, 0xa8, 0xa0, 0x11, 0xda, + 0xf2, 0x83, 0xc7, 0xa2, 0xb0, 0x84, 0x44, 0x61, 0xa6, 0x2d, 0xd6, 0x3c, 0xb1, 0xd9, 0x01, 0xfb, + 0x4f, 0x2c, 0x18, 0xef, 0x25, 0x10, 0x11, 0x81, 0x77, 0xca, 0xd5, 0xae, 0xa2, 0x2b, 0x96, 0xfc, + 0x59, 0xe2, 0x11, 0xe5, 0x78, 0xae, 0xd6, 0x9f, 0x14, 0xaf, 0xf9, 0xce, 0xe5, 0xde, 0xa8, 0x78, + 0x2f, 0x3a, 0xe8, 0x15, 0x38, 0x69, 0xbc, 0x57, 0xac, 0x06, 0xa6, 0x56, 0x9f, 0xa4, 0x16, 0xc8, + 0x74, 0x06, 0x76, 0x6f, 0x77, 0xe2, 0x91, 0x6c, 0x9b, 0x90, 0xd8, 0x5d, 0x74, 0xec, 0x5f, 0x29, + 0x65, 0xbf, 0x96, 0x52, 0xb6, 0x6f, 0x59, 0x5d, 0xdb, 0xf9, 0xf7, 0x1f, 0x87, 0x82, 0x63, 0x1b, + 0x7f, 0x15, 0xc0, 0xd1, 0x1b, 0xe7, 0x01, 0x9e, 0x10, 0xdb, 0xff, 0x66, 0x00, 0xf6, 0xe8, 0x59, + 0x1f, 0xd6, 0xf3, 0x81, 0x8f, 0x15, 0x3f, 0x6b, 0xa9, 0x23, 0xa7, 0x32, 0x5b, 0xe4, 0xcd, 0xe3, + 0x1a, 0x7b, 0xbe, 0x81, 0x89, 0x79, 0x94, 0x82, 0x72, 0x63, 0xa7, 0x0f, 0xb7, 0xd0, 0x57, 0xad, + 0xf4, 0xa1, 0x19, 0x0f, 0x3b, 0x73, 0x8f, 0xad, 0x4f, 0xc6, 0x49, 0x1c, 0xef, 0x98, 0x3e, 0xbf, + 0xe9, 0x75, 0x46, 0x37, 0x09, 0xb0, 0xee, 0xfa, 0x8e, 0xe7, 0xbe, 0x41, 0xb7, 0x27, 0x15, 0xa6, + 0x61, 0x99, 0xc9, 0x72, 0x59, 0xb5, 0x62, 0x03, 0xe3, 0xfc, 0x5f, 0x82, 0x61, 0xe3, 0xcd, 0x73, + 0x82, 0x2b, 0xce, 0x98, 0xc1, 0x15, 0x35, 0x23, 0x26, 0xe2, 0xfc, 0x7b, 0xe1, 0x64, 0xb6, 0x83, + 0x07, 0x79, 0xde, 0xfe, 0x5f, 0x43, 0xd9, 0x53, 0xac, 0x55, 0x12, 0xb5, 0x69, 0xd7, 0xde, 0xf6, + 0x2c, 0xbd, 0xed, 0x59, 0x7a, 0xdb, 0xb3, 0x64, 0x1e, 0x0e, 0x08, 0xaf, 0xc9, 0xd0, 0x7d, 0xf2, + 0x9a, 0xa4, 0xfc, 0x40, 0xd5, 0xc2, 0xfd, 0x40, 0xf6, 0xdd, 0x0a, 0xa4, 0xec, 0x28, 0x3e, 0xde, + 0x3f, 0x00, 0x43, 0x11, 0x09, 0x83, 0x1b, 0x78, 0x41, 0xe8, 0x10, 0x1d, 0x6b, 0xcf, 0x9b, 0xb1, + 0x84, 0x53, 0x5d, 0x13, 0x3a, 0xc9, 0x86, 0x50, 0x22, 0x4a, 0xd7, 0x2c, 0x3b, 0xc9, 0x06, 0x66, + 0x10, 0xf4, 0x5e, 0x18, 0x4b, 0x9c, 0xa8, 0x45, 0xed, 0xed, 0x2d, 0xf6, 0x59, 0xc5, 0x59, 0xe7, + 0x23, 0x02, 0x77, 0x6c, 0x35, 0x05, 0xc5, 0x19, 0x6c, 0xf4, 0x3a, 0x0c, 0x6c, 0x10, 0xaf, 0x2d, + 0x86, 0x7c, 0xa5, 0x38, 0x19, 0xcf, 0xde, 0xf5, 0x0a, 0xf1, 0xda, 0x5c, 0x02, 0xd1, 0x5f, 0x98, + 0xb1, 0xa2, 0xf3, 0xad, 0xb6, 0xd9, 0x89, 0x93, 0xa0, 0xed, 0xbe, 0x21, 0x5d, 0x7c, 0xef, 0x2f, + 0x98, 0xf1, 0x35, 0x49, 0x9f, 0xfb, 0x52, 0xd4, 0x5f, 0xac, 0x39, 0xb3, 0x7e, 0x34, 0xdd, 0x88, + 0x7d, 0xaa, 0x1d, 0xe1, 0xa9, 0x2b, 0xba, 0x1f, 0xb3, 0x92, 0x3e, 0xef, 0x87, 0xfa, 0x8b, 0x35, + 0x67, 0xb4, 0xa3, 0xe6, 0xfd, 0x30, 0xeb, 0xc3, 0x8d, 0x82, 0xfb, 0xc0, 0xe7, 0x7c, 0xee, 0xfc, + 0x7f, 0x12, 0x2a, 0x8d, 0x0d, 0x27, 0x4a, 0xc6, 0x47, 0xd8, 0xa4, 0x51, 0x3e, 0x9d, 0x19, 0xda, + 0x88, 0x39, 0x0c, 0x3d, 0x0e, 0xe5, 0x88, 0xac, 0xb3, 0xb8, 0x4d, 0x23, 0xa2, 0x07, 0x93, 0x75, + 0x4c, 0xdb, 0xed, 0x5f, 0x2a, 0xa5, 0xcd, 0xa5, 0xf4, 0x7b, 0xf3, 0xd9, 0xde, 0xe8, 0x44, 0xb1, + 0xf4, 0xfb, 0x18, 0xb3, 0x9d, 0x35, 0x63, 0x09, 0x47, 0x9f, 0xb0, 0x60, 0xe8, 0x76, 0x1c, 0xf8, + 0x3e, 0x49, 0x84, 0x6a, 0xba, 0x59, 0xf0, 0x50, 0x5c, 0xe5, 0xd4, 0x75, 0x1f, 0x44, 0x03, 0x96, + 0x7c, 0x69, 0x77, 0xc9, 0x76, 0xc3, 0xeb, 0x34, 0xbb, 0x82, 0x34, 0x2e, 0xf1, 0x66, 0x2c, 0xe1, + 0x14, 0xd5, 0xf5, 0x39, 0xea, 0x40, 0x1a, 0x75, 0xde, 0x17, 0xa8, 0x02, 0x6e, 0xff, 0xf5, 0x41, + 0x38, 0x9b, 0xbb, 0x38, 0xa8, 0x21, 0xc3, 0x4c, 0x85, 0xcb, 0xae, 0x47, 0x64, 0x78, 0x12, 0x33, + 0x64, 0x6e, 0xaa, 0x56, 0x6c, 0x60, 0xa0, 0x9f, 0x02, 0x08, 0x9d, 0xc8, 0x69, 0x13, 0xe5, 0x97, + 0x3d, 0xb2, 0xbd, 0x40, 0xfb, 0xb1, 0x2c, 0x69, 0xea, 0xbd, 0xa9, 0x6a, 0x8a, 0xb1, 0xc1, 0x12, + 0xbd, 0x00, 0xc3, 0x11, 0xf1, 0x88, 0x13, 0xb3, 0xb0, 0xdf, 0x6c, 0x0e, 0x03, 0xd6, 0x20, 0x6c, + 0xe2, 0xa1, 0xa7, 0x54, 0x24, 0x57, 0x26, 0xa2, 0x25, 0x1d, 0xcd, 0x85, 0xde, 0xb4, 0x60, 0x6c, + 0xdd, 0xf5, 0x88, 0xe6, 0x2e, 0x32, 0x0e, 0x96, 0x8e, 0xfe, 0x92, 0x97, 0x4d, 0xba, 0x5a, 0x42, + 0xa6, 0x9a, 0x63, 0x9c, 0x61, 0x4f, 0x3f, 0xf3, 0x16, 0x89, 0x98, 0x68, 0x1d, 0x4c, 0x7f, 0xe6, + 0x9b, 0xbc, 0x19, 0x4b, 0x38, 0x9a, 0x86, 0x13, 0xa1, 0x13, 0xc7, 0x33, 0x11, 0x69, 0x12, 0x3f, + 0x71, 0x1d, 0x8f, 0xe7, 0x03, 0x54, 0x75, 0x3c, 0xf0, 0x72, 0x1a, 0x8c, 0xb3, 0xf8, 0xe8, 0x03, + 0xf0, 0x28, 0x77, 0x7c, 0x2c, 0xba, 0x71, 0xec, 0xfa, 0x2d, 0x3d, 0x0d, 0x84, 0xff, 0x67, 0x42, + 0x90, 0x7a, 0x74, 0x3e, 0x1f, 0x0d, 0xf7, 0x7a, 0x1e, 0x3d, 0x03, 0xd5, 0x78, 0xd3, 0x0d, 0x67, + 0xa2, 0x66, 0xcc, 0x0e, 0x3d, 0xaa, 0xda, 0xdb, 0xb8, 0x22, 0xda, 0xb1, 0xc2, 0x40, 0x0d, 0x18, + 0xe1, 0x9f, 0x84, 0x87, 0xa2, 0x09, 0xf9, 0xf8, 0x6c, 0x4f, 0xf5, 0x28, 0xd2, 0xdb, 0x26, 0xb1, + 0x73, 0xe7, 0x92, 0x3c, 0x82, 0xe1, 0x27, 0x06, 0x37, 0x0d, 0x32, 0x38, 0x45, 0xd4, 0xfe, 0x85, + 0x52, 0x7a, 0xc7, 0x6d, 0x2e, 0x52, 0x14, 0xd3, 0xa5, 0x98, 0xdc, 0x74, 0x22, 0xe9, 0x8d, 0x39, + 0x62, 0xda, 0x82, 0xa0, 0x7b, 0xd3, 0x89, 0xcc, 0x45, 0xcd, 0x18, 0x60, 0xc9, 0x09, 0xdd, 0x86, + 0x81, 0xc4, 0x73, 0x0a, 0xca, 0x73, 0x32, 0x38, 0x6a, 0x07, 0xc8, 0xc2, 0x74, 0x8c, 0x19, 0x0f, + 0xf4, 0x18, 0xb5, 0xfa, 0xd7, 0xe4, 0x11, 0x89, 0x30, 0xd4, 0xd7, 0x62, 0xcc, 0x5a, 0xed, 0x7b, + 0x90, 0x23, 0x57, 0x95, 0x22, 0x43, 0x17, 0x01, 0xe8, 0x06, 0x72, 0x39, 0x22, 0xeb, 0xee, 0xb6, + 0x30, 0x24, 0xd4, 0xda, 0xbd, 0xae, 0x20, 0xd8, 0xc0, 0x92, 0xcf, 0xac, 0x74, 0xd6, 0xe9, 0x33, + 0xa5, 0xee, 0x67, 0x38, 0x04, 0x1b, 0x58, 0xe8, 0x79, 0x18, 0x74, 0xdb, 0x4e, 0x4b, 0x85, 0x60, + 0x3e, 0x46, 0x17, 0xed, 0x3c, 0x6b, 0xb9, 0xb7, 0x3b, 0x31, 0xa6, 0x3a, 0xc4, 0x9a, 0xb0, 0xc0, + 0x45, 0xbf, 0x62, 0xc1, 0x48, 0x23, 0x68, 0xb7, 0x03, 0x9f, 0x6f, 0xbb, 0xc4, 0x1e, 0xf2, 0xf6, + 0x71, 0xa9, 0xf9, 0xc9, 0x19, 0x83, 0x19, 0xdf, 0x44, 0xaa, 0x84, 0x2c, 0x13, 0x84, 0x53, 0xbd, + 0x32, 0xd7, 0x76, 0x65, 0x9f, 0xb5, 0xfd, 0xeb, 0x16, 0x9c, 0xe2, 0xcf, 0x1a, 0xbb, 0x41, 0x91, + 0x7b, 0x14, 0x1c, 0xf3, 0x6b, 0x75, 0x6d, 0x90, 0x95, 0x97, 0xae, 0x0b, 0x8e, 0xbb, 0x3b, 0x89, + 0xe6, 0xe0, 0xd4, 0x7a, 0x10, 0x35, 0x88, 0x39, 0x10, 0x42, 0x30, 0x29, 0x42, 0x97, 0xb3, 0x08, + 0xb8, 0xfb, 0x19, 0x74, 0x13, 0x1e, 0x31, 0x1a, 0xcd, 0x71, 0xe0, 0xb2, 0xe9, 0x09, 0x41, 0xed, + 0x91, 0xcb, 0xb9, 0x58, 0xb8, 0xc7, 0xd3, 0x69, 0x87, 0x49, 0xad, 0x0f, 0x87, 0xc9, 0x6b, 0x70, + 0xae, 0xd1, 0x3d, 0x32, 0x5b, 0x71, 0x67, 0x2d, 0xe6, 0x92, 0xaa, 0x5a, 0xff, 0x3e, 0x41, 0xe0, + 0xdc, 0x4c, 0x2f, 0x44, 0xdc, 0x9b, 0x06, 0xfa, 0x08, 0x54, 0x23, 0xc2, 0xbe, 0x4a, 0x2c, 0x12, + 0x71, 0x8e, 0xb8, 0x4b, 0xd6, 0x16, 0x28, 0x27, 0xab, 0x65, 0xaf, 0x68, 0x88, 0xb1, 0xe2, 0x88, + 0xee, 0xc0, 0x50, 0xe8, 0x24, 0x8d, 0x0d, 0x91, 0x7e, 0x73, 0xe4, 0xf8, 0x17, 0xc5, 0x9c, 0xf9, + 0xc0, 0x8d, 0x84, 0x5d, 0xce, 0x04, 0x4b, 0x6e, 0xd4, 0x1a, 0x69, 0x04, 0xed, 0x30, 0xf0, 0x89, + 0x9f, 0xc4, 0xe3, 0xa3, 0xda, 0x1a, 0x99, 0x51, 0xad, 0xd8, 0xc0, 0x40, 0xcb, 0x70, 0x86, 0xf9, + 0x8c, 0x6e, 0xb9, 0xc9, 0x46, 0xd0, 0x49, 0xe4, 0x16, 0x68, 0x7c, 0x2c, 0x7d, 0x54, 0xb1, 0x90, + 0x83, 0x83, 0x73, 0x9f, 0x3c, 0xff, 0x3e, 0x38, 0xd5, 0xb5, 0x94, 0x0f, 0xe4, 0xae, 0x99, 0x85, + 0x47, 0xf2, 0x17, 0xcd, 0x81, 0x9c, 0x36, 0xff, 0x38, 0x13, 0x36, 0x6b, 0x18, 0xd2, 0x7d, 0x38, + 0x00, 0x1d, 0x28, 0x13, 0x7f, 0x4b, 0xe8, 0x90, 0xcb, 0x47, 0xfb, 0x76, 0x97, 0xfc, 0x2d, 0xbe, + 0xe6, 0x99, 0x97, 0xe3, 0x92, 0xbf, 0x85, 0x29, 0x6d, 0xf4, 0x45, 0x2b, 0x65, 0x08, 0x72, 0xb7, + 0xe1, 0x87, 0x8e, 0x65, 0xe7, 0xd0, 0xb7, 0x6d, 0x68, 0xff, 0xdb, 0x12, 0x5c, 0xd8, 0x8f, 0x48, + 0x1f, 0xc3, 0xf7, 0x24, 0x0c, 0xc6, 0xec, 0x20, 0x5c, 0x08, 0xe5, 0x61, 0x3a, 0x57, 0xf9, 0xd1, + 0xf8, 0x6b, 0x58, 0x80, 0x90, 0x07, 0xe5, 0xb6, 0x13, 0x0a, 0x6f, 0xd2, 0xfc, 0x51, 0x13, 0x69, + 0xe8, 0x7f, 0xc7, 0x5b, 0x74, 0x42, 0xee, 0xa3, 0x30, 0x1a, 0x30, 0x65, 0x83, 0x12, 0xa8, 0x38, + 0x51, 0xe4, 0xc8, 0x53, 0xd7, 0x6b, 0xc5, 0xf0, 0x9b, 0xa6, 0x24, 0xf9, 0xa1, 0x55, 0xaa, 0x09, + 0x73, 0x66, 0xf6, 0x67, 0x87, 0x52, 0xc9, 0x24, 0xec, 0x28, 0x3d, 0x86, 0x41, 0xe1, 0x44, 0xb2, + 0x8a, 0xce, 0x5f, 0xe2, 0xd9, 0x80, 0x6c, 0x9f, 0x28, 0x72, 0xaa, 0x05, 0x2b, 0xf4, 0x19, 0x8b, + 0x65, 0x2e, 0xcb, 0x04, 0x1b, 0xb1, 0x3b, 0x3b, 0x9e, 0x44, 0x6a, 0x33, 0x1f, 0x5a, 0x36, 0x62, + 0x93, 0xbb, 0xa8, 0x40, 0xc0, 0xac, 0xd2, 0xee, 0x0a, 0x04, 0xcc, 0xca, 0x94, 0x70, 0xb4, 0x9d, + 0x73, 0x64, 0x5e, 0x40, 0xf6, 0x6b, 0x1f, 0x87, 0xe4, 0x5f, 0xb5, 0xe0, 0x94, 0x9b, 0x3d, 0xfb, + 0x14, 0x7b, 0x99, 0x23, 0x06, 0x65, 0xf4, 0x3e, 0x5a, 0x55, 0xea, 0xbc, 0x0b, 0x84, 0xbb, 0x3b, + 0x83, 0x9a, 0x30, 0xe0, 0xfa, 0xeb, 0x81, 0x30, 0x62, 0xea, 0x47, 0xeb, 0xd4, 0xbc, 0xbf, 0x1e, + 0xe8, 0xd5, 0x4c, 0xff, 0x61, 0x46, 0x1d, 0x2d, 0xc0, 0x99, 0x48, 0x78, 0x9b, 0xae, 0xb8, 0x71, + 0x12, 0x44, 0x3b, 0x0b, 0x6e, 0xdb, 0x4d, 0x98, 0x01, 0x52, 0xae, 0x8f, 0x53, 0xfd, 0x80, 0x73, + 0xe0, 0x38, 0xf7, 0x29, 0xf4, 0x06, 0x0c, 0xc9, 0x54, 0xeb, 0x6a, 0x11, 0xfb, 0xc2, 0xee, 0xf9, + 0xaf, 0x26, 0xd3, 0x8a, 0xc8, 0xaa, 0x96, 0x0c, 0xed, 0x37, 0x87, 0xa1, 0xfb, 0x58, 0x14, 0x7d, + 0x14, 0x6a, 0x91, 0x4a, 0xff, 0xb6, 0x8a, 0x50, 0xd7, 0xf2, 0xfb, 0x8a, 0x23, 0x59, 0x65, 0x0a, + 0xe9, 0x44, 0x6f, 0xcd, 0x91, 0x6e, 0x58, 0x62, 0x7d, 0x7a, 0x5a, 0xc0, 0xdc, 0x16, 0x5c, 0xf5, + 0xc9, 0xd8, 0x8e, 0xdf, 0xc0, 0x8c, 0x07, 0x8a, 0x60, 0x70, 0x83, 0x38, 0x5e, 0xb2, 0x51, 0x8c, + 0x13, 0xff, 0x0a, 0xa3, 0x95, 0x4d, 0x02, 0xe2, 0xad, 0x58, 0x70, 0x42, 0xdb, 0x30, 0xb4, 0xc1, + 0x27, 0x80, 0xd8, 0x43, 0x2c, 0x1e, 0x75, 0x70, 0x53, 0xb3, 0x4a, 0x7f, 0x6e, 0xd1, 0x80, 0x25, + 0x3b, 0x16, 0x6f, 0x63, 0x44, 0x04, 0xf0, 0xa5, 0x5b, 0x5c, 0xfe, 0x53, 0xff, 0xe1, 0x00, 0x1f, + 0x86, 0x91, 0x88, 0x34, 0x02, 0xbf, 0xe1, 0x7a, 0xa4, 0x39, 0x2d, 0x1d, 0xf4, 0x07, 0xc9, 0x9a, + 0x61, 0xfb, 0x70, 0x6c, 0xd0, 0xc0, 0x29, 0x8a, 0xe8, 0xd3, 0x16, 0x8c, 0xa9, 0x9c, 0x51, 0xfa, + 0x41, 0x88, 0x70, 0x08, 0x2f, 0x14, 0x94, 0xa1, 0xca, 0x68, 0xd6, 0xd1, 0xdd, 0xdd, 0x89, 0xb1, + 0x74, 0x1b, 0xce, 0xf0, 0x45, 0xaf, 0x00, 0x04, 0x6b, 0x3c, 0xa8, 0x66, 0x3a, 0x11, 0xde, 0xe1, + 0x83, 0xbc, 0xea, 0x18, 0x4f, 0x9f, 0x93, 0x14, 0xb0, 0x41, 0x0d, 0x5d, 0x03, 0xe0, 0xcb, 0x66, + 0x75, 0x27, 0x94, 0x1b, 0x0d, 0x99, 0xf6, 0x04, 0x2b, 0x0a, 0x72, 0x6f, 0x77, 0xa2, 0xdb, 0x5b, + 0xc7, 0x02, 0x17, 0x8c, 0xc7, 0xd1, 0x4f, 0xc2, 0x50, 0xdc, 0x69, 0xb7, 0x1d, 0xe5, 0x3b, 0x2e, + 0x30, 0x21, 0x8f, 0xd3, 0x35, 0x44, 0x11, 0x6f, 0xc0, 0x92, 0x23, 0xba, 0x4d, 0x85, 0x6a, 0x2c, + 0xdc, 0x88, 0x6c, 0x15, 0x71, 0x9b, 0x60, 0x98, 0xbd, 0xd3, 0x7b, 0xa4, 0xe1, 0x8d, 0x73, 0x70, + 0xee, 0xed, 0x4e, 0x3c, 0x92, 0x6e, 0x5f, 0x08, 0x44, 0x8a, 0x5c, 0x2e, 0x4d, 0x74, 0x55, 0x56, + 0x5e, 0xa1, 0xaf, 0x2d, 0x0b, 0x02, 0x3c, 0xad, 0x2b, 0xaf, 0xb0, 0xe6, 0xde, 0x63, 0x66, 0x3e, + 0x8c, 0x16, 0xe1, 0x74, 0x23, 0xf0, 0x93, 0x28, 0xf0, 0x3c, 0x5e, 0x79, 0x88, 0xef, 0xf9, 0xb8, + 0x6f, 0xf9, 0x9d, 0xa2, 0xdb, 0xa7, 0x67, 0xba, 0x51, 0x70, 0xde, 0x73, 0xb6, 0x9f, 0x8e, 0x36, + 0x14, 0x83, 0xf3, 0x3c, 0x8c, 0x90, 0xed, 0x84, 0x44, 0xbe, 0xe3, 0xdd, 0xc0, 0x0b, 0xd2, 0xab, + 0xca, 0xd6, 0xc0, 0x25, 0xa3, 0x1d, 0xa7, 0xb0, 0x90, 0xad, 0x1c, 0x1d, 0x46, 0xda, 0x27, 0x77, + 0x74, 0x48, 0xb7, 0x86, 0xfd, 0xbf, 0x4b, 0x29, 0x83, 0x6c, 0x35, 0x22, 0x04, 0x05, 0x50, 0xf1, + 0x83, 0xa6, 0x92, 0xfd, 0x57, 0x8b, 0x91, 0xfd, 0xd7, 0x83, 0xa6, 0x51, 0x9e, 0x85, 0xfe, 0x8b, + 0x31, 0xe7, 0xc3, 0xea, 0x57, 0xc8, 0x42, 0x1f, 0x0c, 0x20, 0x36, 0x1a, 0x45, 0x72, 0x56, 0xf5, + 0x2b, 0x96, 0x4c, 0x46, 0x38, 0xcd, 0x17, 0x6d, 0x42, 0x65, 0x23, 0x88, 0x13, 0xb9, 0xfd, 0x38, + 0xe2, 0x4e, 0xe7, 0x4a, 0x10, 0x27, 0xcc, 0x8a, 0x50, 0xaf, 0x4d, 0x5b, 0x62, 0xcc, 0x79, 0xd8, + 0xff, 0xc5, 0x4a, 0xf9, 0xd0, 0x6f, 0xb1, 0xc8, 0xdb, 0x2d, 0xe2, 0xd3, 0x65, 0x6d, 0x86, 0x1a, + 0xfd, 0x70, 0x26, 0x8f, 0xf1, 0x5d, 0xbd, 0x0a, 0x6b, 0xdd, 0xa1, 0x14, 0x26, 0x19, 0x09, 0x23, + 0x2a, 0xe9, 0xe3, 0x56, 0x3a, 0xa3, 0xb4, 0x54, 0xc4, 0x06, 0xc3, 0xcc, 0xaa, 0xde, 0x37, 0x39, + 0xd5, 0xfe, 0xa2, 0x05, 0x43, 0x75, 0xa7, 0xb1, 0x19, 0xac, 0xaf, 0xa3, 0x67, 0xa0, 0xda, 0xec, + 0x44, 0x66, 0x72, 0xab, 0x72, 0x1c, 0xcc, 0x8a, 0x76, 0xac, 0x30, 0xe8, 0x1c, 0x5e, 0x77, 0x1a, + 0x32, 0xb7, 0xba, 0xcc, 0xe7, 0xf0, 0x65, 0xd6, 0x82, 0x05, 0x04, 0xbd, 0x00, 0xc3, 0x6d, 0x67, + 0x5b, 0x3e, 0x9c, 0x75, 0xe0, 0x2f, 0x6a, 0x10, 0x36, 0xf1, 0xec, 0x7f, 0x69, 0xc1, 0x78, 0xdd, + 0x89, 0xdd, 0xc6, 0x74, 0x27, 0xd9, 0xa8, 0xbb, 0xc9, 0x5a, 0xa7, 0xb1, 0x49, 0x12, 0x9e, 0x50, + 0x4f, 0x7b, 0xd9, 0x89, 0xe9, 0x52, 0x52, 0xfb, 0x3a, 0xd5, 0xcb, 0x1b, 0xa2, 0x1d, 0x2b, 0x0c, + 0xf4, 0x06, 0x0c, 0x87, 0x4e, 0x1c, 0xdf, 0x09, 0xa2, 0x26, 0x26, 0xeb, 0xc5, 0x94, 0xb3, 0x58, + 0x21, 0x8d, 0x88, 0x24, 0x98, 0xac, 0x8b, 0x43, 0x66, 0x4d, 0x1f, 0x9b, 0xcc, 0xec, 0xcf, 0x5b, + 0x70, 0xae, 0x4e, 0x9c, 0x88, 0x44, 0xac, 0xfa, 0x85, 0x7a, 0x91, 0x19, 0x2f, 0xe8, 0x34, 0xd1, + 0xeb, 0x50, 0x4d, 0x68, 0x33, 0xed, 0x96, 0x55, 0x6c, 0xb7, 0xd8, 0x19, 0xf1, 0xaa, 0x20, 0x8e, + 0x15, 0x1b, 0xfb, 0x6f, 0x58, 0x30, 0xc2, 0x8e, 0xdb, 0x66, 0x49, 0xe2, 0xb8, 0x5e, 0x57, 0x91, + 0x28, 0xab, 0xcf, 0x22, 0x51, 0x17, 0x60, 0x60, 0x23, 0x68, 0x93, 0xec, 0x51, 0xf1, 0x95, 0x80, + 0x6e, 0xab, 0x29, 0x04, 0x3d, 0x47, 0x3f, 0xbc, 0xeb, 0x27, 0x0e, 0x5d, 0x02, 0xd2, 0x9d, 0x7b, + 0x82, 0x7f, 0x74, 0xd5, 0x8c, 0x4d, 0x1c, 0xfb, 0xb7, 0x6a, 0x30, 0x24, 0xe2, 0x09, 0xfa, 0x2e, + 0xaa, 0x20, 0xf7, 0xf7, 0xa5, 0x9e, 0xfb, 0xfb, 0x18, 0x06, 0x1b, 0xac, 0x5a, 0x9d, 0x30, 0x23, + 0xaf, 0x15, 0x12, 0x80, 0xc2, 0x0b, 0xe0, 0xe9, 0x6e, 0xf1, 0xff, 0x58, 0xb0, 0x42, 0x5f, 0xb0, + 0xe0, 0x44, 0x23, 0xf0, 0x7d, 0xd2, 0xd0, 0x36, 0xce, 0x40, 0x11, 0x71, 0x06, 0x33, 0x69, 0xa2, + 0xfa, 0xac, 0x27, 0x03, 0xc0, 0x59, 0xf6, 0xe8, 0x25, 0x18, 0xe5, 0x63, 0x76, 0x33, 0xe5, 0x83, + 0xd6, 0xb5, 0x83, 0x4c, 0x20, 0x4e, 0xe3, 0xa2, 0x49, 0xee, 0xcb, 0x17, 0x55, 0x7a, 0x06, 0xb5, + 0xab, 0xce, 0xa8, 0xcf, 0x63, 0x60, 0xa0, 0x08, 0x50, 0x44, 0xd6, 0x23, 0x12, 0x6f, 0x88, 0x78, + 0x0b, 0x66, 0x5f, 0x0d, 0x1d, 0x2e, 0x01, 0x1b, 0x77, 0x51, 0xc2, 0x39, 0xd4, 0xd1, 0xa6, 0xd8, + 0x60, 0x56, 0x8b, 0x90, 0xa1, 0xe2, 0x33, 0xf7, 0xdc, 0x67, 0x4e, 0x40, 0x25, 0xde, 0x70, 0xa2, + 0x26, 0xb3, 0xeb, 0xca, 0x3c, 0xe9, 0x67, 0x85, 0x36, 0x60, 0xde, 0x8e, 0x66, 0xe1, 0x64, 0xa6, + 0xf2, 0x51, 0x2c, 0x7c, 0xc5, 0x2a, 0xc1, 0x23, 0x53, 0x33, 0x29, 0xc6, 0x5d, 0x4f, 0x98, 0xce, + 0x87, 0xe1, 0x7d, 0x9c, 0x0f, 0x3b, 0x2a, 0xaa, 0x8f, 0x7b, 0x71, 0x5f, 0x2e, 0x64, 0x00, 0xfa, + 0x0a, 0xe1, 0xfb, 0x5c, 0x26, 0x84, 0x6f, 0x94, 0x75, 0xe0, 0x66, 0x31, 0x1d, 0x38, 0x78, 0xbc, + 0xde, 0x83, 0x8c, 0xbf, 0xfb, 0x73, 0x0b, 0xe4, 0x77, 0x9d, 0x71, 0x1a, 0x1b, 0x84, 0x4e, 0x19, + 0xf4, 0x5e, 0x18, 0x53, 0x5b, 0xe8, 0x99, 0xa0, 0xe3, 0xf3, 0xd0, 0xbb, 0xb2, 0x3e, 0x14, 0xc6, + 0x29, 0x28, 0xce, 0x60, 0xa3, 0x29, 0xa8, 0xd1, 0x71, 0xe2, 0x8f, 0x72, 0x5d, 0xab, 0xb6, 0xe9, + 0xd3, 0xcb, 0xf3, 0xe2, 0x29, 0x8d, 0x83, 0x02, 0x38, 0xe5, 0x39, 0x71, 0xc2, 0x7a, 0x40, 0x77, + 0xd4, 0x87, 0x2c, 0x7f, 0xc0, 0xb2, 0x08, 0x16, 0xb2, 0x84, 0x70, 0x37, 0x6d, 0xfb, 0x5b, 0x03, + 0x30, 0x9a, 0x92, 0x8c, 0x07, 0x54, 0xd2, 0xcf, 0x40, 0x55, 0xea, 0xcd, 0x6c, 0xa1, 0x16, 0xa5, + 0x5c, 0x15, 0x06, 0x55, 0x5a, 0x6b, 0x5a, 0xab, 0x66, 0x8d, 0x0a, 0x43, 0xe1, 0x62, 0x13, 0x8f, + 0x09, 0xe5, 0xc4, 0x8b, 0x67, 0x3c, 0x97, 0xf8, 0x09, 0xef, 0x66, 0x31, 0x42, 0x79, 0x75, 0x61, + 0xc5, 0x24, 0xaa, 0x85, 0x72, 0x06, 0x80, 0xb3, 0xec, 0xd1, 0xa7, 0x2c, 0x18, 0x75, 0xee, 0xc4, + 0xba, 0xa4, 0xaa, 0x08, 0xd6, 0x3b, 0xa2, 0x92, 0x4a, 0x55, 0x69, 0xe5, 0x2e, 0xdf, 0x54, 0x13, + 0x4e, 0x33, 0x45, 0x6f, 0x59, 0x80, 0xc8, 0x36, 0x69, 0xc8, 0x70, 0x42, 0xd1, 0x97, 0xc1, 0x22, + 0x76, 0x9a, 0x97, 0xba, 0xe8, 0x72, 0xa9, 0xde, 0xdd, 0x8e, 0x73, 0xfa, 0x60, 0xff, 0xb3, 0xb2, + 0x5a, 0x50, 0x3a, 0x82, 0xd5, 0x31, 0x22, 0xe9, 0xac, 0xc3, 0x47, 0xd2, 0xe9, 0x88, 0x84, 0xee, + 0xac, 0xca, 0x54, 0x12, 0x56, 0xe9, 0x01, 0x25, 0x61, 0xfd, 0xb4, 0x95, 0x2a, 0x49, 0x34, 0x7c, + 0xf1, 0x95, 0x62, 0xa3, 0x67, 0x27, 0x79, 0xb4, 0x44, 0x46, 0xba, 0xa7, 0x83, 0x64, 0xa8, 0x34, + 0x35, 0xd0, 0x0e, 0x24, 0x0d, 0xff, 0x7d, 0x19, 0x86, 0x0d, 0x4d, 0x9a, 0x6b, 0x16, 0x59, 0x0f, + 0x99, 0x59, 0x54, 0x3a, 0x80, 0x59, 0xf4, 0x53, 0x50, 0x6b, 0x48, 0x29, 0x5f, 0x4c, 0x51, 0xde, + 0xac, 0xee, 0xd0, 0x82, 0x5e, 0x35, 0x61, 0xcd, 0x13, 0xcd, 0xa5, 0x52, 0x77, 0x84, 0x86, 0x18, + 0x60, 0x1a, 0x22, 0x2f, 0xb7, 0x46, 0x68, 0x8a, 0xee, 0x67, 0x58, 0xe5, 0xaa, 0xd0, 0x15, 0xef, + 0x25, 0x63, 0xdc, 0x79, 0xe5, 0xaa, 0xe5, 0x79, 0xd9, 0x8c, 0x4d, 0x1c, 0xfb, 0x5b, 0x96, 0xfa, + 0xb8, 0xf7, 0xa1, 0x46, 0xc3, 0xed, 0x74, 0x8d, 0x86, 0x4b, 0x85, 0x0c, 0x73, 0x8f, 0xe2, 0x0c, + 0xd7, 0x61, 0x68, 0x26, 0x68, 0xb7, 0x1d, 0xbf, 0x89, 0xbe, 0x1f, 0x86, 0x1a, 0xfc, 0xa7, 0x70, + 0xec, 0xb0, 0xe3, 0x41, 0x01, 0xc5, 0x12, 0x86, 0x1e, 0x83, 0x01, 0x27, 0x6a, 0x49, 0x67, 0x0e, + 0x0b, 0xae, 0x99, 0x8e, 0x5a, 0x31, 0x66, 0xad, 0xf6, 0x3f, 0x1a, 0x00, 0x76, 0xa6, 0xed, 0x44, + 0xa4, 0xb9, 0x1a, 0xb0, 0xa2, 0x80, 0xc7, 0x7a, 0xa8, 0xa6, 0x37, 0x4b, 0x0f, 0xf3, 0xc1, 0x9a, + 0x71, 0xb8, 0x52, 0xbe, 0xcf, 0x87, 0x2b, 0x3d, 0xce, 0xcb, 0x06, 0x1e, 0xa2, 0xf3, 0x32, 0xfb, + 0xb3, 0x16, 0x20, 0x15, 0x08, 0xa1, 0x0f, 0xb4, 0xa7, 0xa0, 0xa6, 0x42, 0x22, 0x84, 0x61, 0xa5, + 0x45, 0x84, 0x04, 0x60, 0x8d, 0xd3, 0xc7, 0x0e, 0xf9, 0x49, 0x29, 0xbf, 0xcb, 0xe9, 0xb8, 0x5c, + 0x26, 0xf5, 0x85, 0x38, 0xb7, 0x7f, 0xbb, 0x04, 0x8f, 0x70, 0x95, 0xbc, 0xe8, 0xf8, 0x4e, 0x8b, + 0xb4, 0x69, 0xaf, 0xfa, 0x0d, 0x51, 0x68, 0xd0, 0xad, 0x99, 0x2b, 0xe3, 0x6c, 0x8f, 0xba, 0x76, + 0xf9, 0x9a, 0xe3, 0xab, 0x6c, 0xde, 0x77, 0x13, 0xcc, 0x88, 0xa3, 0x18, 0xaa, 0xb2, 0x62, 0xbd, + 0x90, 0xc5, 0x05, 0x31, 0x52, 0x62, 0x49, 0xe8, 0x4d, 0x82, 0x15, 0x23, 0x6a, 0xb8, 0x7a, 0x41, + 0x63, 0x13, 0x93, 0x30, 0x60, 0x72, 0xd7, 0x08, 0x73, 0x5c, 0x10, 0xed, 0x58, 0x61, 0xd8, 0xbf, + 0x6d, 0x41, 0x56, 0x23, 0x19, 0xd5, 0xd7, 0xac, 0x3d, 0xab, 0xaf, 0x1d, 0xa0, 0xfc, 0xd9, 0x4f, + 0xc0, 0xb0, 0x93, 0x50, 0x23, 0x82, 0x6f, 0xbb, 0xcb, 0x87, 0x3b, 0xd6, 0x58, 0x0c, 0x9a, 0xee, + 0xba, 0xcb, 0xb6, 0xdb, 0x26, 0x39, 0xfb, 0x7f, 0x0c, 0xc0, 0xa9, 0xae, 0x6c, 0x10, 0xf4, 0x22, + 0x8c, 0x34, 0xc4, 0xf4, 0x08, 0xa5, 0x43, 0xab, 0x66, 0x86, 0xc5, 0x69, 0x18, 0x4e, 0x61, 0xf6, + 0x31, 0x41, 0xe7, 0xe1, 0x74, 0x44, 0x37, 0xfa, 0x1d, 0x32, 0xbd, 0x9e, 0x90, 0x68, 0x85, 0x34, + 0x02, 0xbf, 0xc9, 0x6b, 0x04, 0x96, 0xeb, 0x8f, 0xde, 0xdd, 0x9d, 0x38, 0x8d, 0xbb, 0xc1, 0x38, + 0xef, 0x19, 0x14, 0xc2, 0xa8, 0x67, 0xda, 0x80, 0x62, 0x03, 0x70, 0x28, 0xf3, 0x51, 0xd9, 0x08, + 0xa9, 0x66, 0x9c, 0x66, 0x90, 0x36, 0x24, 0x2b, 0x0f, 0xc8, 0x90, 0xfc, 0xa4, 0x36, 0x24, 0xf9, + 0xf9, 0xfb, 0x07, 0x0b, 0xce, 0x06, 0x3a, 0x6e, 0x4b, 0xf2, 0x65, 0xa8, 0xca, 0xd8, 0xa4, 0xbe, + 0x62, 0x7a, 0x4c, 0x3a, 0x3d, 0x24, 0xda, 0xbd, 0x12, 0xe4, 0x6c, 0x42, 0xe8, 0x3a, 0xd3, 0x1a, + 0x3f, 0xb5, 0xce, 0x0e, 0xa6, 0xf5, 0xd1, 0x36, 0x8f, 0xcb, 0xe2, 0xba, 0xed, 0x03, 0x45, 0x6f, + 0xa2, 0x74, 0xa8, 0x96, 0x4a, 0x92, 0x50, 0xe1, 0x5a, 0x17, 0x01, 0xb4, 0xa1, 0x26, 0x42, 0xe0, + 0xd5, 0xb1, 0xaf, 0xb6, 0xe7, 0xb0, 0x81, 0x45, 0xf7, 0xd4, 0xae, 0x1f, 0x27, 0x8e, 0xe7, 0x5d, + 0x71, 0xfd, 0x44, 0x38, 0x07, 0x95, 0x12, 0x9f, 0xd7, 0x20, 0x6c, 0xe2, 0x9d, 0x7f, 0x8f, 0xf1, + 0x5d, 0x0e, 0xf2, 0x3d, 0x37, 0xe0, 0xdc, 0x9c, 0x9b, 0xa8, 0xc4, 0x0d, 0x35, 0x8f, 0xa8, 0x1d, + 0xa6, 0x12, 0x91, 0xac, 0x9e, 0x89, 0x48, 0x46, 0xe2, 0x44, 0x29, 0x9d, 0xe7, 0x91, 0x4d, 0x9c, + 0xb0, 0x5f, 0x84, 0x33, 0x73, 0x6e, 0x72, 0xd9, 0xf5, 0xc8, 0x01, 0x99, 0xd8, 0xbf, 0x39, 0x08, + 0x23, 0x66, 0xea, 0xdf, 0x41, 0x72, 0xa9, 0x3e, 0x4f, 0x4d, 0x2d, 0xf1, 0x76, 0xae, 0x3a, 0x34, + 0xbb, 0x75, 0xe4, 0x3c, 0xc4, 0xfc, 0x11, 0x33, 0xac, 0x2d, 0xcd, 0x13, 0x9b, 0x1d, 0x40, 0x77, + 0xa0, 0xb2, 0xce, 0x02, 0xfb, 0xcb, 0x45, 0x44, 0x16, 0xe4, 0x8d, 0xa8, 0x5e, 0x66, 0x3c, 0x35, + 0x80, 0xf3, 0xa3, 0x1a, 0x32, 0x4a, 0x67, 0x8b, 0x19, 0xc1, 0xa8, 0x22, 0x4f, 0x4c, 0x61, 0xf4, + 0x12, 0xf5, 0x95, 0x43, 0x88, 0xfa, 0x94, 0xe0, 0x1d, 0x7c, 0x40, 0x82, 0x97, 0x25, 0x69, 0x24, + 0x1b, 0xcc, 0x7e, 0x13, 0xd1, 0xf3, 0x43, 0x6c, 0x10, 0x8c, 0x24, 0x8d, 0x14, 0x18, 0x67, 0xf1, + 0xd1, 0xc7, 0x94, 0xe8, 0xae, 0x16, 0xe1, 0x57, 0x35, 0x67, 0xf4, 0x71, 0x4b, 0xed, 0xcf, 0x96, + 0x60, 0x6c, 0xce, 0xef, 0x2c, 0xcf, 0x2d, 0x77, 0xd6, 0x3c, 0xb7, 0x71, 0x8d, 0xec, 0x50, 0xd1, + 0xbc, 0x49, 0x76, 0xe6, 0x67, 0xc5, 0x0a, 0x52, 0x73, 0xe6, 0x1a, 0x6d, 0xc4, 0x1c, 0x46, 0x85, + 0xd1, 0xba, 0xeb, 0xb7, 0x48, 0x14, 0x46, 0xae, 0x70, 0x79, 0x1a, 0xc2, 0xe8, 0xb2, 0x06, 0x61, + 0x13, 0x8f, 0xd2, 0x0e, 0xee, 0xf8, 0x24, 0xca, 0x1a, 0xb2, 0x4b, 0xb4, 0x11, 0x73, 0x18, 0x45, + 0x4a, 0xa2, 0x4e, 0x9c, 0x88, 0xc9, 0xa8, 0x90, 0x56, 0x69, 0x23, 0xe6, 0x30, 0xba, 0xd2, 0xe3, + 0xce, 0x1a, 0x0b, 0xdc, 0xc8, 0x84, 0xea, 0xaf, 0xf0, 0x66, 0x2c, 0xe1, 0x14, 0x75, 0x93, 0xec, + 0xcc, 0xd2, 0x5d, 0x6f, 0x26, 0x63, 0xe7, 0x1a, 0x6f, 0xc6, 0x12, 0xce, 0x8a, 0x1b, 0xa6, 0x87, + 0xe3, 0xbb, 0xae, 0xb8, 0x61, 0xba, 0xfb, 0x3d, 0xf6, 0xcf, 0xbf, 0x6c, 0xc1, 0x88, 0x19, 0x6e, + 0x85, 0x5a, 0x19, 0x1b, 0x77, 0xa9, 0xab, 0x36, 0xee, 0x8f, 0xe5, 0x5d, 0x2c, 0xd6, 0x72, 0x93, + 0x20, 0x8c, 0x9f, 0x25, 0x7e, 0xcb, 0xf5, 0x09, 0x3b, 0x45, 0xe7, 0x61, 0x5a, 0xa9, 0x58, 0xae, + 0x99, 0xa0, 0x49, 0x0e, 0x61, 0x24, 0xdb, 0xb7, 0xe0, 0x54, 0x57, 0x9a, 0x56, 0x1f, 0xa6, 0xc5, + 0xbe, 0x49, 0xb2, 0x36, 0x86, 0x61, 0x4a, 0x58, 0x16, 0xd8, 0x99, 0x81, 0x53, 0x7c, 0x21, 0x51, + 0x4e, 0x2b, 0x8d, 0x0d, 0xd2, 0x56, 0xa9, 0x77, 0xcc, 0xbf, 0x7e, 0x33, 0x0b, 0xc4, 0xdd, 0xf8, + 0xf6, 0xe7, 0x2c, 0x18, 0x4d, 0x65, 0xce, 0x15, 0x64, 0x04, 0xb1, 0x95, 0x16, 0xb0, 0xe8, 0x3f, + 0x16, 0x02, 0x5d, 0x66, 0xca, 0x54, 0xaf, 0x34, 0x0d, 0xc2, 0x26, 0x9e, 0xfd, 0xc5, 0x12, 0x54, + 0x65, 0x04, 0x45, 0x1f, 0x5d, 0xf9, 0x8c, 0x05, 0xa3, 0xea, 0x4c, 0x83, 0x39, 0xcb, 0x4a, 0x45, + 0xa4, 0x39, 0xd0, 0x1e, 0xa8, 0xed, 0xb6, 0xbf, 0x1e, 0x68, 0x8b, 0x1c, 0x9b, 0xcc, 0x70, 0x9a, + 0x37, 0xba, 0x09, 0x10, 0xef, 0xc4, 0x09, 0x69, 0x1b, 0x6e, 0x3b, 0xdb, 0x58, 0x71, 0x93, 0x8d, + 0x20, 0x22, 0x74, 0x7d, 0x5d, 0x0f, 0x9a, 0x64, 0x45, 0x61, 0x6a, 0x13, 0x4a, 0xb7, 0x61, 0x83, + 0x92, 0xfd, 0x0f, 0x4a, 0x70, 0x32, 0xdb, 0x25, 0xf4, 0x41, 0x18, 0x91, 0xdc, 0x8d, 0x3b, 0xd2, + 0x64, 0xd8, 0xc8, 0x08, 0x36, 0x60, 0xf7, 0x76, 0x27, 0x26, 0xba, 0x2f, 0xa9, 0x9b, 0x34, 0x51, + 0x70, 0x8a, 0x18, 0x3f, 0x58, 0x12, 0x27, 0xa0, 0xf5, 0x9d, 0xe9, 0x30, 0x14, 0xa7, 0x43, 0xc6, + 0xc1, 0x92, 0x09, 0xc5, 0x19, 0x6c, 0xb4, 0x0c, 0x67, 0x8c, 0x96, 0xeb, 0xc4, 0x6d, 0x6d, 0xac, + 0x05, 0x91, 0xdc, 0x59, 0x3d, 0xa6, 0x03, 0xbb, 0xba, 0x71, 0x70, 0xee, 0x93, 0x54, 0xdb, 0x37, + 0x9c, 0xd0, 0x69, 0xb8, 0xc9, 0x8e, 0xf0, 0x43, 0x2a, 0xd9, 0x34, 0x23, 0xda, 0xb1, 0xc2, 0xb0, + 0x17, 0x61, 0xa0, 0xcf, 0x19, 0xd4, 0x97, 0x45, 0xff, 0x32, 0x54, 0x29, 0x39, 0x69, 0xde, 0x15, + 0x41, 0x32, 0x80, 0xaa, 0xbc, 0xbb, 0x04, 0xd9, 0x50, 0x76, 0x1d, 0x79, 0x76, 0xa7, 0x5e, 0x6b, + 0x3e, 0x8e, 0x3b, 0x6c, 0x93, 0x4c, 0x81, 0xe8, 0x49, 0x28, 0x93, 0xed, 0x30, 0x7b, 0x48, 0x77, + 0x69, 0x3b, 0x74, 0x23, 0x12, 0x53, 0x24, 0xb2, 0x1d, 0xa2, 0xf3, 0x50, 0x72, 0x9b, 0x42, 0x49, + 0x81, 0xc0, 0x29, 0xcd, 0xcf, 0xe2, 0x92, 0xdb, 0xb4, 0xb7, 0xa1, 0xa6, 0x2e, 0x4b, 0x41, 0x9b, + 0x52, 0x76, 0x5b, 0x45, 0x84, 0x3c, 0x49, 0xba, 0x3d, 0xa4, 0x76, 0x07, 0x40, 0xa7, 0x10, 0x16, + 0x25, 0x5f, 0x2e, 0xc0, 0x40, 0x23, 0x10, 0xe9, 0xcd, 0x55, 0x4d, 0x86, 0x09, 0x6d, 0x06, 0xb1, + 0x6f, 0xc1, 0xd8, 0x35, 0x3f, 0xb8, 0xc3, 0x2a, 0xbd, 0xb3, 0xc2, 0x66, 0x94, 0xf0, 0x3a, 0xfd, + 0x91, 0x35, 0x11, 0x18, 0x14, 0x73, 0x98, 0xaa, 0xf8, 0x54, 0xea, 0x55, 0xf1, 0xc9, 0xfe, 0xb8, + 0x05, 0x23, 0x2a, 0x17, 0x69, 0x6e, 0x6b, 0x93, 0xd2, 0x6d, 0x45, 0x41, 0x27, 0xcc, 0xd2, 0x65, + 0xd7, 0x19, 0x61, 0x0e, 0x33, 0x93, 0xf4, 0x4a, 0xfb, 0x24, 0xe9, 0x5d, 0x80, 0x81, 0x4d, 0xd7, + 0x6f, 0x66, 0xef, 0xe7, 0xb8, 0xe6, 0xfa, 0x4d, 0xcc, 0x20, 0xb4, 0x0b, 0x27, 0x55, 0x17, 0xa4, + 0x42, 0x78, 0x11, 0x46, 0xd6, 0x3a, 0xae, 0xd7, 0x94, 0x15, 0xdb, 0x32, 0x9e, 0x92, 0xba, 0x01, + 0xc3, 0x29, 0x4c, 0xba, 0xaf, 0x5b, 0x73, 0x7d, 0x27, 0xda, 0x59, 0xd6, 0x1a, 0x48, 0x09, 0xa5, + 0xba, 0x82, 0x60, 0x03, 0xcb, 0x7e, 0xb3, 0x0c, 0x63, 0xe9, 0x8c, 0xac, 0x3e, 0xb6, 0x57, 0x4f, + 0x42, 0x85, 0x25, 0x69, 0x65, 0x3f, 0x2d, 0x2f, 0x72, 0xc6, 0x61, 0x28, 0x86, 0x41, 0x5e, 0xde, + 0xa1, 0x98, 0xbb, 0x6d, 0x54, 0x27, 0x95, 0x7f, 0x85, 0xc5, 0x93, 0x89, 0x8a, 0x12, 0x82, 0x15, + 0xfa, 0x94, 0x05, 0x43, 0x41, 0x68, 0x56, 0x0a, 0xfa, 0x40, 0x91, 0xd9, 0x6a, 0x22, 0x59, 0x46, + 0x58, 0xc4, 0xea, 0xd3, 0xcb, 0xcf, 0x21, 0x59, 0x9f, 0xff, 0x11, 0x18, 0x31, 0x31, 0xf7, 0x33, + 0x8a, 0xab, 0xa6, 0x51, 0xfc, 0x19, 0x73, 0x52, 0x88, 0x7c, 0xbc, 0x3e, 0x96, 0xdb, 0x0d, 0xa8, + 0x34, 0x54, 0x00, 0xc0, 0xa1, 0xea, 0x7c, 0xaa, 0x7a, 0x0b, 0xec, 0x10, 0x88, 0x53, 0xb3, 0xbf, + 0x65, 0x19, 0xf3, 0x03, 0x93, 0x78, 0xbe, 0x89, 0x22, 0x28, 0xb7, 0xb6, 0x36, 0x85, 0x29, 0x7a, + 0xb5, 0xa0, 0xe1, 0x9d, 0xdb, 0xda, 0xd4, 0x73, 0xdc, 0x6c, 0xc5, 0x94, 0x59, 0x1f, 0x4e, 0xc0, + 0x54, 0xda, 0x66, 0x79, 0xff, 0xb4, 0x4d, 0xfb, 0xad, 0x12, 0x9c, 0xea, 0x9a, 0x54, 0xe8, 0x0d, + 0xa8, 0x44, 0xf4, 0x2d, 0xc5, 0xeb, 0x2d, 0x14, 0x96, 0x68, 0x19, 0xcf, 0x37, 0xb5, 0xde, 0x4d, + 0xb7, 0x63, 0xce, 0x12, 0x5d, 0x05, 0xa4, 0xc3, 0x54, 0x94, 0x07, 0x92, 0xbf, 0xf2, 0x79, 0xf1, + 0x28, 0x9a, 0xee, 0xc2, 0xc0, 0x39, 0x4f, 0xa1, 0x97, 0xb2, 0x8e, 0xcc, 0x72, 0xfa, 0xdc, 0x72, + 0x2f, 0x9f, 0xa4, 0xfd, 0xcf, 0x4b, 0x30, 0x9a, 0x2a, 0xdc, 0x84, 0x3c, 0xa8, 0x12, 0x8f, 0x39, + 0xf5, 0xa5, 0xb2, 0x39, 0x6a, 0x1d, 0x64, 0xa5, 0x20, 0x2f, 0x09, 0xba, 0x58, 0x71, 0x78, 0x38, + 0x0e, 0xd7, 0x5f, 0x84, 0x11, 0xd9, 0xa1, 0x0f, 0x38, 0x6d, 0x4f, 0x0c, 0xa0, 0x9a, 0xa3, 0x97, + 0x0c, 0x18, 0x4e, 0x61, 0xda, 0xbf, 0x53, 0x86, 0x71, 0x7e, 0x0a, 0xd2, 0x54, 0x33, 0x6f, 0x51, + 0xee, 0xb7, 0xfe, 0x8a, 0x2e, 0xaf, 0xc6, 0x07, 0x72, 0xed, 0xa8, 0xd7, 0x0e, 0xe4, 0x33, 0xea, + 0x2b, 0x32, 0xeb, 0x2b, 0x99, 0xc8, 0x2c, 0x6e, 0x76, 0xb7, 0x8e, 0xa9, 0x47, 0xdf, 0x5d, 0xa1, + 0x5a, 0x7f, 0xb7, 0x04, 0x27, 0x32, 0x77, 0x3a, 0xa0, 0x37, 0xd3, 0x65, 0x80, 0xad, 0x22, 0x7c, + 0xe5, 0x7b, 0x96, 0xf9, 0x3f, 0x58, 0x31, 0xe0, 0x07, 0xb4, 0x54, 0xec, 0x3f, 0x28, 0xc1, 0x58, + 0xfa, 0x32, 0x8a, 0x87, 0x70, 0xa4, 0xde, 0x0d, 0x35, 0x56, 0x6f, 0x9d, 0x5d, 0xb2, 0xc9, 0x5d, + 0xf2, 0xbc, 0xb4, 0xb5, 0x6c, 0xc4, 0x1a, 0xfe, 0x50, 0xd4, 0x58, 0xb6, 0xff, 0x9e, 0x05, 0x67, + 0xf9, 0x5b, 0x66, 0xe7, 0xe1, 0x5f, 0xcd, 0x1b, 0xdd, 0x57, 0x8b, 0xed, 0x60, 0xa6, 0x2c, 0xe0, + 0x7e, 0xe3, 0xcb, 0x2e, 0xf7, 0x13, 0xbd, 0x4d, 0x4f, 0x85, 0x87, 0xb0, 0xb3, 0x07, 0x9a, 0x0c, + 0xf6, 0x1f, 0x94, 0x41, 0xdf, 0x67, 0x88, 0x5c, 0x91, 0xe3, 0x58, 0x48, 0x79, 0xc4, 0x95, 0x1d, + 0xbf, 0xa1, 0x6f, 0x4e, 0xac, 0x66, 0x52, 0x1c, 0x7f, 0xce, 0x82, 0x61, 0xd7, 0x77, 0x13, 0xd7, + 0x61, 0xdb, 0xe8, 0x62, 0xee, 0x5a, 0x53, 0xec, 0xe6, 0x39, 0xe5, 0x20, 0x32, 0xcf, 0x71, 0x14, + 0x33, 0x6c, 0x72, 0x46, 0x1f, 0x16, 0xc1, 0xd3, 0xe5, 0xc2, 0xb2, 0x73, 0xab, 0x99, 0x88, 0xe9, + 0x90, 0x1a, 0x5e, 0x49, 0x54, 0x50, 0x52, 0x3b, 0xa6, 0xa4, 0x54, 0xa5, 0x5d, 0x7d, 0xb3, 0x34, + 0x6d, 0xc6, 0x9c, 0x91, 0x1d, 0x03, 0xea, 0x1e, 0x8b, 0x03, 0x06, 0xa6, 0x4e, 0x41, 0xcd, 0xe9, + 0x24, 0x41, 0x9b, 0x0e, 0x93, 0x38, 0x6a, 0xd2, 0xa1, 0xb7, 0x12, 0x80, 0x35, 0x8e, 0xfd, 0x66, + 0x05, 0x32, 0x49, 0x87, 0x68, 0xdb, 0xbc, 0x8b, 0xd3, 0x2a, 0xf6, 0x2e, 0x4e, 0xd5, 0x99, 0xbc, + 0xfb, 0x38, 0x51, 0x0b, 0x2a, 0xe1, 0x86, 0x13, 0x4b, 0xb3, 0xfa, 0x65, 0xb5, 0x8f, 0xa3, 0x8d, + 0xf7, 0x76, 0x27, 0x7e, 0xbc, 0x3f, 0xaf, 0x2b, 0x9d, 0xab, 0x53, 0xbc, 0x7c, 0x89, 0x66, 0xcd, + 0x68, 0x60, 0x4e, 0xff, 0x20, 0xb7, 0xcd, 0x7d, 0x42, 0x14, 0x96, 0xc7, 0x24, 0xee, 0x78, 0x89, + 0x98, 0x0d, 0x2f, 0x17, 0xb8, 0xca, 0x38, 0x61, 0x9d, 0x2e, 0xcf, 0xff, 0x63, 0x83, 0x29, 0xfa, + 0x20, 0xd4, 0xe2, 0xc4, 0x89, 0x92, 0x43, 0x26, 0xb8, 0xaa, 0x41, 0x5f, 0x91, 0x44, 0xb0, 0xa6, + 0x87, 0x5e, 0x61, 0xd5, 0x62, 0xdd, 0x78, 0xe3, 0x90, 0x39, 0x0f, 0xb2, 0xb2, 0xac, 0xa0, 0x80, + 0x0d, 0x6a, 0xe8, 0x22, 0x00, 0x9b, 0xdb, 0x3c, 0xd0, 0xaf, 0xca, 0xbc, 0x4c, 0x4a, 0x14, 0x62, + 0x05, 0xc1, 0x06, 0x96, 0xfd, 0x83, 0x90, 0xae, 0xf7, 0x80, 0x26, 0x64, 0x79, 0x09, 0xee, 0x85, + 0x66, 0xb9, 0x0b, 0xa9, 0x4a, 0x10, 0xbf, 0x6e, 0x81, 0x59, 0x94, 0x02, 0xbd, 0xce, 0xab, 0x5f, + 0x58, 0x45, 0x9c, 0x1c, 0x1a, 0x74, 0x27, 0x17, 0x9d, 0x30, 0x73, 0x84, 0x2d, 0x4b, 0x60, 0x9c, + 0x7f, 0x0f, 0x54, 0x25, 0xf4, 0x40, 0x46, 0xdd, 0xc7, 0xe0, 0x74, 0xf6, 0xa6, 0x72, 0x71, 0xea, + 0xb4, 0xbf, 0xeb, 0x47, 0xfa, 0x73, 0x4a, 0xbd, 0xfc, 0x39, 0x7d, 0xdc, 0xc8, 0xfa, 0x1b, 0x16, + 0x5c, 0xd8, 0xef, 0x42, 0x75, 0xf4, 0x18, 0x0c, 0xdc, 0x71, 0x22, 0x59, 0xc6, 0x9b, 0x09, 0xca, + 0x5b, 0x4e, 0xe4, 0x63, 0xd6, 0x8a, 0x76, 0x60, 0x90, 0x47, 0x83, 0x09, 0x6b, 0xfd, 0xe5, 0x62, + 0xaf, 0x77, 0xbf, 0x46, 0x8c, 0xed, 0x02, 0x8f, 0x44, 0xc3, 0x82, 0xa1, 0xfd, 0x6d, 0x0b, 0xd0, + 0xd2, 0x16, 0x89, 0x22, 0xb7, 0x69, 0xc4, 0xaf, 0xb1, 0x0b, 0x5a, 0x8c, 0x8b, 0x58, 0xcc, 0x14, + 0xd7, 0xcc, 0x05, 0x2d, 0xc6, 0xbf, 0xfc, 0x0b, 0x5a, 0x4a, 0x07, 0xbb, 0xa0, 0x05, 0x2d, 0xc1, + 0xd9, 0x36, 0xdf, 0x6e, 0xf0, 0x4b, 0x0f, 0xf8, 0xde, 0x43, 0x25, 0x94, 0x9d, 0xbb, 0xbb, 0x3b, + 0x71, 0x76, 0x31, 0x0f, 0x01, 0xe7, 0x3f, 0x67, 0xbf, 0x07, 0x10, 0x0f, 0x5b, 0x9b, 0xc9, 0x8b, + 0x41, 0xea, 0xe9, 0x7e, 0xb1, 0xbf, 0x5c, 0x81, 0x13, 0x99, 0x22, 0xaf, 0x74, 0xab, 0xd7, 0x1d, + 0xf4, 0x74, 0x64, 0xfd, 0xdd, 0xdd, 0xbd, 0xbe, 0xc2, 0xa8, 0x7c, 0xa8, 0xb8, 0x7e, 0xd8, 0x49, + 0x8a, 0xc9, 0x21, 0xe5, 0x9d, 0x98, 0xa7, 0x04, 0x0d, 0x77, 0x31, 0xfd, 0x8b, 0x39, 0x9b, 0x22, + 0x83, 0xb2, 0x52, 0xc6, 0xf8, 0xc0, 0x03, 0x72, 0x07, 0x7c, 0x42, 0x87, 0x48, 0x55, 0x8a, 0x70, + 0x2c, 0x66, 0x26, 0xcb, 0x71, 0x1f, 0xb5, 0xff, 0x5a, 0x09, 0x86, 0x8d, 0x8f, 0x86, 0x7e, 0x29, + 0x5d, 0xb2, 0xc9, 0x2a, 0xee, 0x95, 0x18, 0xfd, 0x49, 0x5d, 0x94, 0x89, 0xbf, 0xd2, 0x53, 0xdd, + 0xd5, 0x9a, 0xee, 0xed, 0x4e, 0x9c, 0xcc, 0xd4, 0x63, 0x4a, 0x55, 0x70, 0x3a, 0xff, 0x51, 0x38, + 0x91, 0x21, 0x93, 0xf3, 0xca, 0xab, 0xe9, 0x8b, 0xe8, 0x8f, 0xe8, 0x96, 0x32, 0x87, 0xec, 0xeb, + 0x74, 0xc8, 0x44, 0x1a, 0x5d, 0xe0, 0x91, 0x3e, 0x7c, 0xb0, 0x99, 0x6c, 0xd9, 0x52, 0x9f, 0xd9, + 0xb2, 0x4f, 0x43, 0x35, 0x0c, 0x3c, 0xb7, 0xe1, 0xaa, 0xba, 0x86, 0x2c, 0x3f, 0x77, 0x59, 0xb4, + 0x61, 0x05, 0x45, 0x77, 0xa0, 0xa6, 0xee, 0xec, 0x17, 0xfe, 0xed, 0xa2, 0x0e, 0x7d, 0x94, 0xd1, + 0xa2, 0xef, 0xe2, 0xd7, 0xbc, 0x90, 0x0d, 0x83, 0x4c, 0x09, 0xca, 0xd0, 0x7f, 0xe6, 0x7b, 0x67, + 0xda, 0x31, 0xc6, 0x02, 0x62, 0x7f, 0xad, 0x06, 0x67, 0xf2, 0x2a, 0x6d, 0xa3, 0x8f, 0xc0, 0x20, + 0xef, 0x63, 0x31, 0x97, 0x39, 0xe4, 0xf1, 0x98, 0x63, 0x04, 0x45, 0xb7, 0xd8, 0x6f, 0x2c, 0x78, + 0x0a, 0xee, 0x9e, 0xb3, 0x26, 0x66, 0xc8, 0xf1, 0x70, 0x5f, 0x70, 0x34, 0xf7, 0x05, 0x87, 0x73, + 0xf7, 0x9c, 0x35, 0xb4, 0x0d, 0x95, 0x96, 0x9b, 0x10, 0x47, 0x38, 0x11, 0x6e, 0x1d, 0x0b, 0x73, + 0xe2, 0x70, 0x2b, 0x8d, 0xfd, 0xc4, 0x9c, 0x21, 0xfa, 0xaa, 0x05, 0x27, 0xd6, 0xd2, 0xa9, 0xf1, + 0x42, 0x78, 0x3a, 0xc7, 0x50, 0x4d, 0x3d, 0xcd, 0x88, 0xdf, 0x50, 0x94, 0x69, 0xc4, 0xd9, 0xee, + 0xa0, 0x4f, 0x5a, 0x30, 0xb4, 0xee, 0x7a, 0x46, 0x61, 0xdd, 0x63, 0xf8, 0x38, 0x97, 0x19, 0x03, + 0xbd, 0xe3, 0xe0, 0xff, 0x63, 0x2c, 0x39, 0xf7, 0xd2, 0x54, 0x83, 0x47, 0xd5, 0x54, 0x43, 0x0f, + 0x48, 0x53, 0x7d, 0xda, 0x82, 0x9a, 0x1a, 0x69, 0x91, 0xee, 0xfc, 0xc1, 0x63, 0xfc, 0xe4, 0xdc, + 0x73, 0xa2, 0xfe, 0x62, 0xcd, 0x1c, 0x7d, 0xc1, 0x82, 0x61, 0xe7, 0x8d, 0x4e, 0x44, 0x9a, 0x64, + 0x2b, 0x08, 0x63, 0x71, 0xbd, 0xe1, 0xab, 0xc5, 0x77, 0x66, 0x9a, 0x32, 0x99, 0x25, 0x5b, 0x4b, + 0x61, 0x2c, 0xd2, 0x92, 0x74, 0x03, 0x36, 0xbb, 0x60, 0xef, 0x96, 0x60, 0x62, 0x1f, 0x0a, 0xe8, + 0x45, 0x18, 0x09, 0xa2, 0x96, 0xe3, 0xbb, 0x6f, 0x98, 0xb5, 0x2e, 0x94, 0x95, 0xb5, 0x64, 0xc0, + 0x70, 0x0a, 0xd3, 0x4c, 0xc8, 0x2e, 0xed, 0x93, 0x90, 0x7d, 0x01, 0x06, 0x22, 0x12, 0x06, 0xd9, + 0xcd, 0x02, 0x4b, 0x09, 0x60, 0x10, 0xf4, 0x38, 0x94, 0x9d, 0xd0, 0x15, 0x81, 0x68, 0x6a, 0x0f, + 0x34, 0xbd, 0x3c, 0x8f, 0x69, 0x7b, 0xaa, 0x3e, 0x44, 0xe5, 0xbe, 0xd4, 0x87, 0xa0, 0x6a, 0x40, + 0x9c, 0x5d, 0x0c, 0x6a, 0x35, 0x90, 0x3e, 0x53, 0xb0, 0xdf, 0x2a, 0xc3, 0xe3, 0x7b, 0xce, 0x17, + 0x1d, 0x87, 0x67, 0xed, 0x11, 0x87, 0x27, 0x87, 0xa7, 0xb4, 0xdf, 0xf0, 0x94, 0x7b, 0x0c, 0xcf, + 0x27, 0xe9, 0x32, 0x90, 0x35, 0x42, 0x8a, 0xb9, 0xa0, 0xae, 0x57, 0xc9, 0x11, 0xb1, 0x02, 0x24, + 0x14, 0x6b, 0xbe, 0x74, 0x0f, 0x90, 0x4a, 0x46, 0xae, 0x14, 0xa1, 0x06, 0x7a, 0xd6, 0x0c, 0xe1, + 0x73, 0xbf, 0x57, 0x86, 0xb3, 0xfd, 0xf3, 0x25, 0x78, 0xb2, 0x0f, 0xe9, 0x6d, 0xce, 0x62, 0xab, + 0xcf, 0x59, 0xfc, 0xdd, 0xfd, 0x99, 0xec, 0xbf, 0x66, 0xc1, 0xf9, 0xde, 0xca, 0x03, 0x3d, 0x07, + 0xc3, 0x6b, 0x91, 0xe3, 0x37, 0x36, 0xd8, 0xa5, 0x9b, 0x72, 0x50, 0xd8, 0x58, 0xeb, 0x66, 0x6c, + 0xe2, 0xd0, 0xed, 0x2d, 0x8f, 0x49, 0x30, 0x30, 0x64, 0xf2, 0x28, 0xdd, 0xde, 0xae, 0x66, 0x81, + 0xb8, 0x1b, 0xdf, 0xfe, 0xb3, 0x52, 0x7e, 0xb7, 0xb8, 0x91, 0x71, 0x90, 0xef, 0x24, 0xbe, 0x42, + 0xa9, 0x0f, 0x59, 0x52, 0xbe, 0xdf, 0xb2, 0x64, 0xa0, 0x97, 0x2c, 0x41, 0xb3, 0x70, 0xd2, 0xb8, + 0x94, 0x85, 0x27, 0x04, 0xf3, 0x80, 0x5b, 0x55, 0x25, 0x63, 0x39, 0x03, 0xc7, 0x5d, 0x4f, 0xa0, + 0x67, 0xa0, 0xea, 0xfa, 0x31, 0x69, 0x74, 0x22, 0x1e, 0xe8, 0x6d, 0x24, 0x61, 0xcd, 0x8b, 0x76, + 0xac, 0x30, 0xec, 0x5f, 0x2e, 0xc1, 0xb9, 0x9e, 0x76, 0xd6, 0x7d, 0x92, 0x5d, 0xe6, 0xe7, 0x18, + 0xb8, 0x3f, 0x9f, 0xc3, 0x1c, 0xa4, 0xca, 0xbe, 0x83, 0xf4, 0x87, 0xbd, 0x27, 0x26, 0xb5, 0xb9, + 0xbf, 0x67, 0x47, 0xe9, 0x25, 0x18, 0x75, 0xc2, 0x90, 0xe3, 0xb1, 0x78, 0xcd, 0x4c, 0x95, 0x9c, + 0x69, 0x13, 0x88, 0xd3, 0xb8, 0x7d, 0x69, 0xcf, 0x3f, 0xb6, 0xa0, 0x86, 0xc9, 0x3a, 0x97, 0x0e, + 0xe8, 0xb6, 0x18, 0x22, 0xab, 0x88, 0x7a, 0x9a, 0x74, 0x60, 0x63, 0x97, 0xd5, 0x99, 0xcc, 0x1b, + 0xec, 0xee, 0xcb, 0x7b, 0x4a, 0x07, 0xba, 0xbc, 0x47, 0x5d, 0xdf, 0x52, 0xee, 0x7d, 0x7d, 0x8b, + 0xfd, 0xf5, 0x21, 0xfa, 0x7a, 0x61, 0x30, 0x13, 0x91, 0x66, 0x4c, 0xbf, 0x6f, 0x27, 0xf2, 0xc4, + 0x24, 0x51, 0xdf, 0xf7, 0x06, 0x5e, 0xc0, 0xb4, 0x3d, 0x75, 0x14, 0x53, 0x3a, 0x50, 0x8d, 0x90, + 0xf2, 0xbe, 0x35, 0x42, 0x5e, 0x82, 0xd1, 0x38, 0xde, 0x58, 0x8e, 0xdc, 0x2d, 0x27, 0x21, 0xd7, + 0xc8, 0x8e, 0xb0, 0xb2, 0x74, 0x5e, 0xff, 0xca, 0x15, 0x0d, 0xc4, 0x69, 0x5c, 0x34, 0x07, 0xa7, + 0x74, 0xa5, 0x0e, 0x12, 0x25, 0x2c, 0xba, 0x9f, 0xcf, 0x04, 0x95, 0xc4, 0xab, 0x6b, 0x7b, 0x08, + 0x04, 0xdc, 0xfd, 0x0c, 0x95, 0x6f, 0xa9, 0x46, 0xda, 0x91, 0xc1, 0xb4, 0x7c, 0x4b, 0xd1, 0xa1, + 0x7d, 0xe9, 0x7a, 0x02, 0x2d, 0xc2, 0x69, 0x3e, 0x31, 0xa6, 0xc3, 0xd0, 0x78, 0xa3, 0xa1, 0x74, + 0x1d, 0xc3, 0xb9, 0x6e, 0x14, 0x9c, 0xf7, 0x1c, 0x7a, 0x01, 0x86, 0x55, 0xf3, 0xfc, 0xac, 0x38, + 0x45, 0x50, 0x5e, 0x0c, 0x45, 0x66, 0xbe, 0x89, 0x4d, 0x3c, 0xf4, 0x01, 0x78, 0x54, 0xff, 0xe5, + 0x29, 0x60, 0xfc, 0x68, 0x6d, 0x56, 0x14, 0x41, 0x52, 0x97, 0x85, 0xcc, 0xe5, 0xa2, 0x35, 0x71, + 0xaf, 0xe7, 0xd1, 0x1a, 0x9c, 0x57, 0xa0, 0x4b, 0x7e, 0xc2, 0xf2, 0x39, 0x62, 0x52, 0x77, 0x62, + 0x72, 0x23, 0xf2, 0xc4, 0x6d, 0xab, 0xea, 0x1e, 0xc7, 0x39, 0x37, 0xb9, 0x92, 0x87, 0x89, 0x17, + 0xf0, 0x1e, 0x54, 0xd0, 0x14, 0xd4, 0x88, 0xef, 0xac, 0x79, 0x64, 0x69, 0x66, 0x9e, 0x15, 0x53, + 0x32, 0x4e, 0xf2, 0x2e, 0x49, 0x00, 0xd6, 0x38, 0x2a, 0xc2, 0x74, 0xa4, 0xe7, 0x9d, 0xa2, 0xcb, + 0x70, 0xa6, 0xd5, 0x08, 0xa9, 0xed, 0xe1, 0x36, 0xc8, 0x74, 0x83, 0x05, 0xd4, 0xd1, 0x0f, 0xc3, + 0x0b, 0x4c, 0xaa, 0xf0, 0xe9, 0xb9, 0x99, 0xe5, 0x2e, 0x1c, 0x9c, 0xfb, 0x24, 0x0b, 0xbc, 0x8c, + 0x82, 0xed, 0x9d, 0xf1, 0xd3, 0x99, 0xc0, 0x4b, 0xda, 0x88, 0x39, 0x0c, 0x5d, 0x05, 0xc4, 0x62, + 0xf1, 0xaf, 0x24, 0x49, 0xa8, 0x8c, 0x9d, 0xf1, 0x33, 0xec, 0x95, 0x54, 0x18, 0xd9, 0xe5, 0x2e, + 0x0c, 0x9c, 0xf3, 0x94, 0xfd, 0x1f, 0x2c, 0x18, 0x55, 0xeb, 0xf5, 0x3e, 0x64, 0xa3, 0x78, 0xe9, + 0x6c, 0x94, 0xb9, 0xa3, 0x4b, 0x3c, 0xd6, 0xf3, 0x1e, 0x21, 0xcd, 0x3f, 0x33, 0x0c, 0xa0, 0xa5, + 0xa2, 0x52, 0x48, 0x56, 0x4f, 0x85, 0xf4, 0xd0, 0x4a, 0xa4, 0xbc, 0xca, 0x29, 0x95, 0x07, 0x5b, + 0x39, 0x65, 0x05, 0xce, 0x4a, 0x73, 0x81, 0x9f, 0x15, 0x5d, 0x09, 0x62, 0x25, 0xe0, 0xaa, 0xf5, + 0xc7, 0x05, 0xa1, 0xb3, 0xf3, 0x79, 0x48, 0x38, 0xff, 0xd9, 0x94, 0x95, 0x32, 0xb4, 0x9f, 0x95, + 0xa2, 0xd7, 0xf4, 0xc2, 0xba, 0xbc, 0x15, 0x24, 0xb3, 0xa6, 0x17, 0x2e, 0xaf, 0x60, 0x8d, 0x93, + 0x2f, 0xd8, 0x6b, 0x05, 0x09, 0x76, 0x38, 0xb0, 0x60, 0x97, 0x22, 0x66, 0xb8, 0xa7, 0x88, 0x91, + 0x3e, 0xe9, 0x91, 0x9e, 0x3e, 0xe9, 0xf7, 0xc2, 0x98, 0xeb, 0x6f, 0x90, 0xc8, 0x4d, 0x48, 0x93, + 0xad, 0x05, 0x26, 0x7e, 0xaa, 0x5a, 0xad, 0xcf, 0xa7, 0xa0, 0x38, 0x83, 0x9d, 0x96, 0x8b, 0x63, + 0x7d, 0xc8, 0xc5, 0x1e, 0xda, 0xe8, 0x44, 0x31, 0xda, 0xe8, 0xe4, 0xd1, 0xb5, 0xd1, 0xa9, 0x63, + 0xd5, 0x46, 0xa8, 0x10, 0x6d, 0xd4, 0x97, 0xa0, 0x37, 0xb6, 0x7f, 0x67, 0xf6, 0xd9, 0xfe, 0xf5, + 0x52, 0x45, 0x67, 0x0f, 0xad, 0x8a, 0xf2, 0xb5, 0xcc, 0x23, 0x87, 0xd2, 0x32, 0x9f, 0x2e, 0xc1, + 0x59, 0x2d, 0x87, 0xe9, 0xec, 0x77, 0xd7, 0xa9, 0x24, 0x62, 0x17, 0x4b, 0xf1, 0x73, 0x1b, 0x23, + 0x39, 0x4a, 0xe7, 0x59, 0x29, 0x08, 0x36, 0xb0, 0x58, 0x8e, 0x11, 0x89, 0x58, 0x19, 0xdd, 0xac, + 0x90, 0x9e, 0x11, 0xed, 0x58, 0x61, 0xd0, 0xf9, 0x45, 0x7f, 0x8b, 0xbc, 0xcd, 0x6c, 0xb1, 0xb8, + 0x19, 0x0d, 0xc2, 0x26, 0x1e, 0x7a, 0x9a, 0x33, 0x61, 0x02, 0x82, 0x0a, 0xea, 0x11, 0x71, 0xd3, + 0xac, 0x94, 0x09, 0x0a, 0x2a, 0xbb, 0xc3, 0x92, 0xc9, 0x2a, 0xdd, 0xdd, 0x61, 0x21, 0x50, 0x0a, + 0xc3, 0xfe, 0x9f, 0x16, 0x9c, 0xcb, 0x1d, 0x8a, 0xfb, 0xa0, 0x7c, 0xb7, 0xd3, 0xca, 0x77, 0xa5, + 0xa8, 0xed, 0x86, 0xf1, 0x16, 0x3d, 0x14, 0xf1, 0xbf, 0xb3, 0x60, 0x4c, 0xe3, 0xdf, 0x87, 0x57, + 0x75, 0xd3, 0xaf, 0x5a, 0xdc, 0xce, 0xaa, 0xd6, 0xf5, 0x6e, 0xbf, 0x53, 0x02, 0x55, 0xc0, 0x71, + 0xba, 0x21, 0xcb, 0xe3, 0xee, 0x73, 0x92, 0xb8, 0x03, 0x83, 0xec, 0x20, 0x34, 0x2e, 0x26, 0xc8, + 0x23, 0xcd, 0x9f, 0x1d, 0xaa, 0xea, 0x43, 0x66, 0xf6, 0x37, 0xc6, 0x82, 0x21, 0x2b, 0xf2, 0xec, + 0xc6, 0x54, 0x9a, 0x37, 0x45, 0x5a, 0x96, 0x2e, 0xf2, 0x2c, 0xda, 0xb1, 0xc2, 0xa0, 0xea, 0xc1, + 0x6d, 0x04, 0xfe, 0x8c, 0xe7, 0xc4, 0xf2, 0x36, 0x45, 0xa5, 0x1e, 0xe6, 0x25, 0x00, 0x6b, 0x1c, + 0x76, 0x46, 0xea, 0xc6, 0xa1, 0xe7, 0xec, 0x18, 0xfb, 0x67, 0xa3, 0x3e, 0x81, 0x02, 0x61, 0x13, + 0xcf, 0x6e, 0xc3, 0x78, 0xfa, 0x25, 0x66, 0xc9, 0x3a, 0x0b, 0x50, 0xec, 0x6b, 0x38, 0xa7, 0xa0, + 0xe6, 0xb0, 0xa7, 0x16, 0x3a, 0x4e, 0xf6, 0x12, 0xf4, 0x69, 0x09, 0xc0, 0x1a, 0xc7, 0xfe, 0x55, + 0x0b, 0x4e, 0xe7, 0x0c, 0x5a, 0x81, 0x69, 0x6f, 0x89, 0x96, 0x36, 0x79, 0x8a, 0xfd, 0x07, 0x60, + 0xa8, 0x49, 0xd6, 0x1d, 0x19, 0x02, 0x67, 0xc8, 0xf6, 0x59, 0xde, 0x8c, 0x25, 0xdc, 0xfe, 0xef, + 0x16, 0x9c, 0x48, 0xf7, 0x35, 0x66, 0xa9, 0x24, 0x7c, 0x98, 0xdc, 0xb8, 0x11, 0x6c, 0x91, 0x68, + 0x87, 0xbe, 0xb9, 0x95, 0x49, 0x25, 0xe9, 0xc2, 0xc0, 0x39, 0x4f, 0xb1, 0xf2, 0xad, 0x4d, 0x35, + 0xda, 0x72, 0x46, 0xde, 0x2c, 0x72, 0x46, 0xea, 0x8f, 0x69, 0x1e, 0x97, 0x2b, 0x96, 0xd8, 0xe4, + 0x6f, 0x7f, 0x7b, 0x00, 0x54, 0x5e, 0x2c, 0x8b, 0x3f, 0x2a, 0x28, 0x7a, 0xeb, 0xa0, 0x19, 0x44, + 0x6a, 0x32, 0x0c, 0xec, 0x15, 0x10, 0xc0, 0xbd, 0x24, 0xa6, 0xeb, 0x52, 0xbd, 0xe1, 0xaa, 0x06, + 0x61, 0x13, 0x8f, 0xf6, 0xc4, 0x73, 0xb7, 0x08, 0x7f, 0x68, 0x30, 0xdd, 0x93, 0x05, 0x09, 0xc0, + 0x1a, 0x87, 0xf6, 0xa4, 0xe9, 0xae, 0xaf, 0x8b, 0x2d, 0xbf, 0xea, 0x09, 0x1d, 0x1d, 0xcc, 0x20, + 0xbc, 0x22, 0x77, 0xb0, 0x29, 0xac, 0x60, 0xa3, 0x22, 0x77, 0xb0, 0x89, 0x19, 0x84, 0xda, 0x6d, + 0x7e, 0x10, 0xb5, 0xd9, 0x25, 0xf5, 0x4d, 0xc5, 0x45, 0x58, 0xbf, 0xca, 0x6e, 0xbb, 0xde, 0x8d, + 0x82, 0xf3, 0x9e, 0xa3, 0x33, 0x30, 0x8c, 0x48, 0xd3, 0x6d, 0x24, 0x26, 0x35, 0x48, 0xcf, 0xc0, + 0xe5, 0x2e, 0x0c, 0x9c, 0xf3, 0x14, 0x9a, 0x86, 0x13, 0x32, 0xaf, 0x59, 0x56, 0xad, 0x19, 0x4e, + 0x57, 0xc9, 0xc0, 0x69, 0x30, 0xce, 0xe2, 0x53, 0xa9, 0xd6, 0x16, 0x05, 0xab, 0x98, 0xb1, 0x6c, + 0x48, 0x35, 0x59, 0xc8, 0x0a, 0x2b, 0x0c, 0xfb, 0x13, 0x65, 0xaa, 0x85, 0x7b, 0x14, 0x6a, 0xbb, + 0x6f, 0xd1, 0x82, 0xe9, 0x19, 0x39, 0xd0, 0xc7, 0x8c, 0x7c, 0x1e, 0x46, 0x6e, 0xc7, 0x81, 0xaf, + 0x22, 0xf1, 0x2a, 0x3d, 0x23, 0xf1, 0x0c, 0xac, 0xfc, 0x48, 0xbc, 0xc1, 0xa2, 0x22, 0xf1, 0x86, + 0x0e, 0x19, 0x89, 0xf7, 0xcd, 0x0a, 0xa8, 0xab, 0x41, 0xae, 0x93, 0xe4, 0x4e, 0x10, 0x6d, 0xba, + 0x7e, 0x8b, 0xe5, 0x83, 0x7f, 0xd5, 0x82, 0x11, 0xbe, 0x5e, 0x16, 0xcc, 0x4c, 0xaa, 0xf5, 0x82, + 0xee, 0x9c, 0x48, 0x31, 0x9b, 0x5c, 0x35, 0x18, 0x65, 0x2e, 0xf3, 0x34, 0x41, 0x38, 0xd5, 0x23, + 0xf4, 0x51, 0x00, 0xe9, 0x1f, 0x5d, 0x97, 0x22, 0x73, 0xbe, 0x98, 0xfe, 0x61, 0xb2, 0xae, 0x6d, + 0xe0, 0x55, 0xc5, 0x04, 0x1b, 0x0c, 0xd1, 0xa7, 0x75, 0x96, 0x19, 0x0f, 0xd9, 0xff, 0xf0, 0xb1, + 0x8c, 0x4d, 0x3f, 0x39, 0x66, 0x18, 0x86, 0x5c, 0xbf, 0x45, 0xe7, 0x89, 0x88, 0x58, 0x7a, 0x57, + 0x5e, 0x2d, 0x85, 0x85, 0xc0, 0x69, 0xd6, 0x1d, 0xcf, 0xf1, 0x1b, 0x24, 0x9a, 0xe7, 0xe8, 0xe6, + 0x15, 0xd6, 0xac, 0x01, 0x4b, 0x42, 0x5d, 0x97, 0xaa, 0x54, 0xfa, 0xb9, 0x54, 0xe5, 0xfc, 0xfb, + 0xe0, 0x54, 0xd7, 0xc7, 0x3c, 0x50, 0x4a, 0xd9, 0xe1, 0xb3, 0xd1, 0xec, 0x7f, 0x31, 0xa8, 0x95, + 0xd6, 0xf5, 0xa0, 0xc9, 0xaf, 0xf6, 0x88, 0xf4, 0x17, 0x15, 0x36, 0x6e, 0x81, 0x53, 0xc4, 0xb8, + 0x06, 0x5b, 0x35, 0x62, 0x93, 0x25, 0x9d, 0xa3, 0xa1, 0x13, 0x11, 0xff, 0xb8, 0xe7, 0xe8, 0xb2, + 0x62, 0x82, 0x0d, 0x86, 0x68, 0x23, 0x95, 0x53, 0x72, 0xf9, 0xe8, 0x39, 0x25, 0xac, 0xca, 0x54, + 0x5e, 0x35, 0xfe, 0x2f, 0x58, 0x30, 0xe6, 0xa7, 0x66, 0x6e, 0x31, 0x61, 0xa4, 0xf9, 0xab, 0x82, + 0xdf, 0x2c, 0x95, 0x6e, 0xc3, 0x19, 0xfe, 0x79, 0x2a, 0xad, 0x72, 0x40, 0x95, 0xa6, 0xef, 0x08, + 0x1a, 0xec, 0x75, 0x47, 0x10, 0xf2, 0xd5, 0x25, 0x69, 0x43, 0x85, 0x5f, 0x92, 0x06, 0x39, 0x17, + 0xa4, 0xdd, 0x82, 0x5a, 0x23, 0x22, 0x4e, 0x72, 0xc8, 0xfb, 0xb2, 0xd8, 0x01, 0xfd, 0x8c, 0x24, + 0x80, 0x35, 0x2d, 0xfb, 0xff, 0x0c, 0xc0, 0x49, 0x39, 0x22, 0x32, 0x04, 0x9d, 0xea, 0x47, 0xce, + 0x57, 0x1b, 0xb7, 0x4a, 0x3f, 0x5e, 0x91, 0x00, 0xac, 0x71, 0xa8, 0x3d, 0xd6, 0x89, 0xc9, 0x52, + 0x48, 0xfc, 0x05, 0x77, 0x2d, 0x16, 0xe7, 0x9c, 0x6a, 0xa1, 0xdc, 0xd0, 0x20, 0x6c, 0xe2, 0x51, + 0x63, 0x9c, 0xdb, 0xc5, 0x71, 0x36, 0x7d, 0x45, 0xd8, 0xdb, 0x58, 0xc2, 0xd1, 0x2f, 0xe4, 0x56, + 0x8e, 0x2d, 0x26, 0x71, 0xab, 0x2b, 0xf2, 0xfe, 0x80, 0x57, 0x2c, 0xfe, 0x6d, 0x0b, 0xce, 0xf2, + 0x56, 0x39, 0x92, 0x37, 0xc2, 0xa6, 0x93, 0x90, 0xb8, 0x98, 0x4a, 0xee, 0x39, 0xfd, 0xd3, 0x4e, + 0xde, 0x3c, 0xb6, 0x38, 0xbf, 0x37, 0xe8, 0x4d, 0x0b, 0x4e, 0x6c, 0xa6, 0x6a, 0x7e, 0x48, 0xd5, + 0x71, 0xd4, 0x74, 0xfc, 0x14, 0x51, 0xbd, 0xd4, 0xd2, 0xed, 0x31, 0xce, 0x72, 0xb7, 0xff, 0xcc, + 0x02, 0x53, 0x8c, 0xde, 0xff, 0x52, 0x21, 0x07, 0x37, 0x05, 0xa5, 0x75, 0x59, 0xe9, 0x69, 0x5d, + 0x3e, 0x0e, 0xe5, 0x8e, 0xdb, 0x14, 0xfb, 0x0b, 0x7d, 0xfa, 0x3a, 0x3f, 0x8b, 0x69, 0xbb, 0xfd, + 0x4f, 0x2b, 0xda, 0x6f, 0x21, 0xf2, 0xa2, 0xbe, 0x27, 0x5e, 0x7b, 0x5d, 0x15, 0x1b, 0xe3, 0x6f, + 0x7e, 0xbd, 0xab, 0xd8, 0xd8, 0x8f, 0x1e, 0x3c, 0xed, 0x8d, 0x0f, 0x50, 0xaf, 0x5a, 0x63, 0x43, + 0xfb, 0xe4, 0xbc, 0xdd, 0x86, 0x2a, 0xdd, 0x82, 0x31, 0x07, 0x64, 0x35, 0xd5, 0xa9, 0xea, 0x15, + 0xd1, 0x7e, 0x6f, 0x77, 0xe2, 0x47, 0x0e, 0xde, 0x2d, 0xf9, 0x34, 0x56, 0xf4, 0x51, 0x0c, 0x35, + 0xfa, 0x9b, 0xa5, 0xe7, 0x89, 0xcd, 0xdd, 0x0d, 0x25, 0x33, 0x25, 0xa0, 0x90, 0xdc, 0x3f, 0xcd, + 0x07, 0xf9, 0x50, 0x63, 0xb7, 0xd1, 0x32, 0xa6, 0x7c, 0x0f, 0xb8, 0xac, 0x92, 0xe4, 0x24, 0xe0, + 0xde, 0xee, 0xc4, 0x4b, 0x07, 0x67, 0xaa, 0x1e, 0xc7, 0x9a, 0x85, 0xfd, 0xc5, 0x01, 0x3d, 0x77, + 0x45, 0x8d, 0xb9, 0xef, 0x89, 0xb9, 0xfb, 0x62, 0x66, 0xee, 0x5e, 0xe8, 0x9a, 0xbb, 0x63, 0xfa, + 0xd6, 0xd4, 0xd4, 0x6c, 0xbc, 0xdf, 0x86, 0xc0, 0xfe, 0xfe, 0x06, 0x66, 0x01, 0xbd, 0xde, 0x71, + 0x23, 0x12, 0x2f, 0x47, 0x1d, 0xdf, 0xf5, 0x5b, 0x6c, 0x3a, 0x56, 0x4d, 0x0b, 0x28, 0x05, 0xc6, + 0x59, 0x7c, 0xba, 0xa9, 0xa7, 0xdf, 0xfc, 0x96, 0xb3, 0xc5, 0x67, 0x95, 0x51, 0x76, 0x6b, 0x45, + 0xb4, 0x63, 0x85, 0x61, 0x7f, 0x9d, 0x9d, 0x65, 0x1b, 0x79, 0xc1, 0x74, 0x4e, 0x78, 0xec, 0xfa, + 0x5f, 0x5e, 0xb3, 0x4b, 0xcd, 0x09, 0x7e, 0xe7, 0x2f, 0x87, 0xa1, 0x3b, 0x30, 0xb4, 0xc6, 0xef, + 0xbf, 0x2b, 0xa6, 0x3e, 0xb9, 0xb8, 0x4c, 0x8f, 0xdd, 0x72, 0x22, 0x6f, 0xd6, 0xbb, 0xa7, 0x7f, + 0x62, 0xc9, 0xcd, 0xfe, 0xfd, 0x0a, 0x9c, 0xc8, 0x5c, 0x10, 0x9b, 0xaa, 0x96, 0x5a, 0xda, 0xb7, + 0x5a, 0xea, 0x87, 0x00, 0x9a, 0x24, 0xf4, 0x82, 0x1d, 0x66, 0x8e, 0x0d, 0x1c, 0xd8, 0x1c, 0x53, + 0x16, 0xfc, 0xac, 0xa2, 0x82, 0x0d, 0x8a, 0xa2, 0x50, 0x19, 0x2f, 0xbe, 0x9a, 0x29, 0x54, 0x66, + 0xdc, 0x62, 0x30, 0x78, 0x7f, 0x6f, 0x31, 0x70, 0xe1, 0x04, 0xef, 0xa2, 0xca, 0xbe, 0x3d, 0x44, + 0x92, 0x2d, 0xcb, 0x5f, 0x98, 0x4d, 0x93, 0xc1, 0x59, 0xba, 0x0f, 0xf2, 0xfe, 0x67, 0xf4, 0x6e, + 0xa8, 0xc9, 0xef, 0x1c, 0x8f, 0xd7, 0x74, 0x05, 0x03, 0x39, 0x0d, 0xd8, 0xbd, 0xcc, 0xe2, 0x67, + 0x57, 0x21, 0x01, 0x78, 0x50, 0x85, 0x04, 0xec, 0xcf, 0x97, 0xa8, 0x1d, 0xcf, 0xfb, 0xa5, 0x6a, + 0xe2, 0x3c, 0x05, 0x83, 0x4e, 0x27, 0xd9, 0x08, 0xba, 0x6e, 0xf3, 0x9b, 0x66, 0xad, 0x58, 0x40, + 0xd1, 0x02, 0x0c, 0x34, 0x75, 0x9d, 0x93, 0x83, 0x7c, 0x4f, 0xed, 0x12, 0x75, 0x12, 0x82, 0x19, + 0x15, 0xf4, 0x18, 0x0c, 0x24, 0x4e, 0x4b, 0xa6, 0x5c, 0xb1, 0x34, 0xdb, 0x55, 0xa7, 0x15, 0x63, + 0xd6, 0x6a, 0xaa, 0xef, 0x81, 0x7d, 0xd4, 0xf7, 0x4b, 0x30, 0x1a, 0xbb, 0x2d, 0xdf, 0x49, 0x3a, + 0x11, 0x31, 0x8e, 0xf9, 0x74, 0xe4, 0x86, 0x09, 0xc4, 0x69, 0x5c, 0xfb, 0x37, 0x47, 0xe0, 0xcc, + 0xca, 0xcc, 0xa2, 0xac, 0xde, 0x7d, 0x6c, 0x59, 0x53, 0x79, 0x3c, 0xee, 0x5f, 0xd6, 0x54, 0x0f, + 0xee, 0x9e, 0x91, 0x35, 0xe5, 0x19, 0x59, 0x53, 0xe9, 0x14, 0x96, 0x72, 0x11, 0x29, 0x2c, 0x79, + 0x3d, 0xe8, 0x27, 0x85, 0xe5, 0xd8, 0xd2, 0xa8, 0xf6, 0xec, 0xd0, 0x81, 0xd2, 0xa8, 0x54, 0x8e, + 0x59, 0x21, 0xc9, 0x05, 0x3d, 0x3e, 0x55, 0x6e, 0x8e, 0x99, 0xca, 0xef, 0xe1, 0x89, 0x33, 0x42, + 0xd4, 0xbf, 0x5a, 0x7c, 0x07, 0xfa, 0xc8, 0xef, 0x11, 0xb9, 0x3b, 0x66, 0x4e, 0xd9, 0x50, 0x11, + 0x39, 0x65, 0x79, 0xdd, 0xd9, 0x37, 0xa7, 0xec, 0x25, 0x18, 0x6d, 0x78, 0x81, 0x4f, 0x96, 0xa3, + 0x20, 0x09, 0x1a, 0x81, 0x27, 0xcc, 0x7a, 0x25, 0x12, 0x66, 0x4c, 0x20, 0x4e, 0xe3, 0xf6, 0x4a, + 0x48, 0xab, 0x1d, 0x35, 0x21, 0x0d, 0x1e, 0x50, 0x42, 0xda, 0xcf, 0xea, 0xd4, 0xe9, 0x61, 0xf6, + 0x45, 0x3e, 0x54, 0xfc, 0x17, 0xe9, 0x27, 0x7f, 0x1a, 0xbd, 0xc5, 0xaf, 0xd3, 0xa3, 0x86, 0xf1, + 0x4c, 0xd0, 0xa6, 0x86, 0xdf, 0x08, 0x1b, 0x92, 0xd7, 0x8e, 0x61, 0xc2, 0xde, 0x5a, 0xd1, 0x6c, + 0xd4, 0x15, 0x7b, 0xba, 0x09, 0xa7, 0x3b, 0x72, 0x94, 0xd4, 0xee, 0x2f, 0x97, 0xe0, 0xfb, 0xf6, + 0xed, 0x02, 0xba, 0x03, 0x90, 0x38, 0x2d, 0x31, 0x51, 0xc5, 0x81, 0xc9, 0x11, 0xc3, 0x2b, 0x57, + 0x25, 0x3d, 0x5e, 0x93, 0x44, 0xfd, 0x65, 0x47, 0x11, 0xf2, 0x37, 0x8b, 0xaa, 0x0c, 0xbc, 0xae, + 0xd2, 0x8d, 0x38, 0xf0, 0x08, 0x66, 0x10, 0xaa, 0xfe, 0x23, 0xd2, 0xd2, 0xf7, 0x3f, 0xab, 0xcf, + 0x87, 0x59, 0x2b, 0x16, 0x50, 0xf4, 0x02, 0x0c, 0x3b, 0x9e, 0xc7, 0xf3, 0x63, 0x48, 0x2c, 0xee, + 0xd3, 0xd1, 0x35, 0xe4, 0x34, 0x08, 0x9b, 0x78, 0xf6, 0x9f, 0x96, 0x60, 0x62, 0x1f, 0x99, 0xd2, + 0x95, 0xf1, 0x57, 0xe9, 0x3b, 0xe3, 0x4f, 0xe4, 0x28, 0x0c, 0xf6, 0xc8, 0x51, 0x78, 0x01, 0x86, + 0x13, 0xe2, 0xb4, 0x45, 0x40, 0x96, 0xf0, 0x04, 0xe8, 0x13, 0x60, 0x0d, 0xc2, 0x26, 0x1e, 0x95, + 0x62, 0x63, 0x4e, 0xa3, 0x41, 0xe2, 0x58, 0x26, 0x21, 0x08, 0x6f, 0x6a, 0x61, 0x19, 0x0e, 0xcc, + 0x49, 0x3d, 0x9d, 0x62, 0x81, 0x33, 0x2c, 0xb3, 0x03, 0x5e, 0xeb, 0x73, 0xc0, 0xbf, 0x56, 0x82, + 0xc7, 0xf7, 0xd4, 0x6e, 0x7d, 0xe7, 0x87, 0x74, 0x62, 0x12, 0x65, 0x27, 0xce, 0x8d, 0x98, 0x44, + 0x98, 0x41, 0xf8, 0x28, 0x85, 0xa1, 0x71, 0xbf, 0x76, 0xd1, 0xc9, 0x4b, 0x7c, 0x94, 0x52, 0x2c, + 0x70, 0x86, 0xe5, 0x61, 0xa7, 0xe5, 0xdf, 0x2f, 0xc1, 0x93, 0x7d, 0xd8, 0x00, 0x05, 0x26, 0x79, + 0xa5, 0x53, 0xed, 0xca, 0x0f, 0x28, 0x23, 0xf2, 0x90, 0xc3, 0xf5, 0xf5, 0x12, 0x9c, 0xef, 0xad, + 0x8a, 0xd1, 0x8f, 0xc1, 0x89, 0x48, 0x45, 0x61, 0x99, 0x59, 0x7a, 0xa7, 0xb9, 0x27, 0x21, 0x05, + 0xc2, 0x59, 0x5c, 0x34, 0x09, 0x10, 0x3a, 0xc9, 0x46, 0x7c, 0x69, 0xdb, 0x8d, 0x13, 0x51, 0x85, + 0x66, 0x8c, 0x9f, 0x5d, 0xc9, 0x56, 0x6c, 0x60, 0x50, 0x76, 0xec, 0xdf, 0x6c, 0x70, 0x3d, 0x48, + 0xf8, 0x43, 0x7c, 0x1b, 0x71, 0x5a, 0xde, 0xd9, 0x61, 0x80, 0x70, 0x16, 0x97, 0xb2, 0x63, 0xa7, + 0xa3, 0xbc, 0xa3, 0x7c, 0x7f, 0xc1, 0xd8, 0x2d, 0xa8, 0x56, 0x6c, 0x60, 0x64, 0xf3, 0x0f, 0x2b, + 0xfb, 0xe7, 0x1f, 0xda, 0xff, 0xa4, 0x04, 0xe7, 0x7a, 0x9a, 0x72, 0xfd, 0x2d, 0xc0, 0x87, 0x2f, + 0x67, 0xf0, 0x70, 0x73, 0xe7, 0x80, 0xb9, 0x6d, 0x7f, 0xdc, 0x63, 0xa6, 0x89, 0xdc, 0xb6, 0xc3, + 0x27, 0x87, 0x3f, 0x7c, 0xe3, 0xd9, 0x95, 0xce, 0x36, 0x70, 0x80, 0x74, 0xb6, 0xcc, 0xc7, 0xa8, + 0xf4, 0xb9, 0x90, 0xff, 0xbc, 0xdc, 0x73, 0x78, 0xe9, 0xd6, 0xaf, 0x2f, 0x3f, 0xed, 0x2c, 0x9c, + 0x74, 0x7d, 0x76, 0x7f, 0xd3, 0x4a, 0x67, 0x4d, 0x14, 0x26, 0x29, 0xa5, 0x6f, 0x4f, 0x9f, 0xcf, + 0xc0, 0x71, 0xd7, 0x13, 0x0f, 0x61, 0x7a, 0xe1, 0xe1, 0x86, 0xf4, 0x60, 0x09, 0xae, 0x68, 0x09, + 0xce, 0xca, 0xa1, 0xd8, 0x70, 0x22, 0xd2, 0x14, 0x6a, 0x24, 0x16, 0x09, 0x15, 0xe7, 0x78, 0x52, + 0x46, 0x0e, 0x02, 0xce, 0x7f, 0x8e, 0x5d, 0x99, 0x13, 0x84, 0x6e, 0x43, 0x6c, 0x72, 0xf4, 0x95, + 0x39, 0xb4, 0x11, 0x73, 0x98, 0xfd, 0x21, 0xa8, 0xa9, 0xf7, 0xe7, 0x61, 0xdd, 0x6a, 0xd2, 0x75, + 0x85, 0x75, 0xab, 0x19, 0x67, 0x60, 0xd1, 0xaf, 0x45, 0x4d, 0xe2, 0xcc, 0xea, 0xb9, 0x46, 0x76, + 0x98, 0x7d, 0x6c, 0xff, 0x10, 0x8c, 0x28, 0x3f, 0x4b, 0xbf, 0x17, 0x09, 0xd9, 0x5f, 0x1c, 0x84, + 0xd1, 0x54, 0x71, 0xc0, 0x94, 0x83, 0xd5, 0xda, 0xd7, 0xc1, 0xca, 0xc2, 0xf4, 0x3b, 0xbe, 0xbc, + 0x65, 0xcc, 0x08, 0xd3, 0xef, 0xf8, 0x04, 0x73, 0x18, 0x35, 0x6f, 0x9b, 0xd1, 0x0e, 0xee, 0xf8, + 0x22, 0x9c, 0x56, 0x99, 0xb7, 0xb3, 0xac, 0x15, 0x0b, 0x28, 0xfa, 0xb8, 0x05, 0x23, 0x31, 0xf3, + 0xde, 0x73, 0xf7, 0xb4, 0x98, 0x74, 0x57, 0x8f, 0x5e, 0xfb, 0x50, 0x15, 0xc2, 0x64, 0x11, 0x32, + 0x66, 0x0b, 0x4e, 0x71, 0x44, 0x9f, 0xb2, 0xa0, 0xa6, 0x2e, 0x43, 0x11, 0x57, 0x01, 0xae, 0x14, + 0x5b, 0x7b, 0x91, 0xfb, 0x35, 0xd5, 0x41, 0x88, 0x2a, 0x82, 0x87, 0x35, 0x63, 0x14, 0x2b, 0xdf, + 0xf1, 0xd0, 0xf1, 0xf8, 0x8e, 0x21, 0xc7, 0x6f, 0xfc, 0x6e, 0xa8, 0xb5, 0x1d, 0xdf, 0x5d, 0x27, + 0x71, 0xc2, 0xdd, 0xb9, 0xb2, 0x24, 0xac, 0x6c, 0xc4, 0x1a, 0x4e, 0x15, 0x72, 0xcc, 0x5e, 0x2c, + 0x31, 0xfc, 0xaf, 0x4c, 0x21, 0xaf, 0xe8, 0x66, 0x6c, 0xe2, 0x98, 0xce, 0x62, 0x78, 0xa0, 0xce, + 0xe2, 0xe1, 0xbd, 0x9d, 0xc5, 0xf6, 0x3f, 0xb4, 0xe0, 0x6c, 0xee, 0x57, 0x7b, 0x78, 0x03, 0x1f, + 0xed, 0x2f, 0x55, 0xe0, 0x74, 0x4e, 0x95, 0x4f, 0xb4, 0x63, 0xce, 0x67, 0xab, 0x88, 0x18, 0x82, + 0xf4, 0x91, 0xb8, 0x1c, 0xc6, 0x9c, 0x49, 0x7c, 0xb0, 0xa3, 0x1a, 0x7d, 0x5c, 0x52, 0xbe, 0xbf, + 0xc7, 0x25, 0xc6, 0xb4, 0x1c, 0x78, 0xa0, 0xd3, 0xb2, 0xb2, 0xcf, 0x19, 0xc6, 0xaf, 0x59, 0x30, + 0xde, 0xee, 0x51, 0x5a, 0x5e, 0x38, 0x1e, 0x6f, 0x1e, 0x4f, 0xe1, 0xfa, 0xfa, 0x63, 0x77, 0x77, + 0x27, 0x7a, 0x56, 0xf4, 0xc7, 0x3d, 0x7b, 0x65, 0x7f, 0xbb, 0x0c, 0xac, 0xc4, 0x2c, 0xab, 0xe4, + 0xb6, 0x83, 0x3e, 0x66, 0x16, 0x0b, 0xb6, 0x8a, 0x2a, 0x6c, 0xcb, 0x89, 0xab, 0x62, 0xc3, 0x7c, + 0x04, 0xf3, 0x6a, 0x0f, 0x67, 0x85, 0x56, 0xa9, 0x0f, 0xa1, 0xe5, 0xc9, 0xaa, 0xcc, 0xe5, 0xe2, + 0xab, 0x32, 0xd7, 0xb2, 0x15, 0x99, 0xf7, 0xfe, 0xc4, 0x03, 0x0f, 0xe5, 0x27, 0xfe, 0x45, 0x8b, + 0x0b, 0x9e, 0xcc, 0x57, 0xd0, 0x96, 0x81, 0xb5, 0x87, 0x65, 0xf0, 0x0c, 0x54, 0x63, 0xe2, 0xad, + 0x5f, 0x21, 0x8e, 0x27, 0x2c, 0x08, 0x7d, 0x7e, 0x2d, 0xda, 0xb1, 0xc2, 0x60, 0xd7, 0xb6, 0x7a, + 0x5e, 0x70, 0xe7, 0x52, 0x3b, 0x4c, 0x76, 0x84, 0x2d, 0xa1, 0xaf, 0x6d, 0x55, 0x10, 0x6c, 0x60, + 0xd9, 0x7f, 0xab, 0xc4, 0x67, 0xa0, 0x08, 0x82, 0x78, 0x31, 0x73, 0xd1, 0x5e, 0xff, 0xf1, 0x03, + 0x1f, 0x01, 0x68, 0xa8, 0x2b, 0xea, 0xc5, 0x99, 0xd0, 0x95, 0x23, 0xdf, 0x9f, 0x2d, 0xe8, 0xe9, + 0xd7, 0xd0, 0x6d, 0xd8, 0xe0, 0x97, 0x92, 0xa5, 0xe5, 0x7d, 0x65, 0x69, 0x4a, 0xac, 0x0c, 0xec, + 0xa3, 0xed, 0xfe, 0xd4, 0x82, 0x94, 0x45, 0x84, 0x42, 0xa8, 0xd0, 0xee, 0xee, 0x14, 0x73, 0xfb, + 0xbe, 0x49, 0x9a, 0x8a, 0x46, 0x31, 0xed, 0xd9, 0x4f, 0xcc, 0x19, 0x21, 0x4f, 0xc4, 0x4a, 0xf0, + 0x51, 0xbd, 0x5e, 0x1c, 0xc3, 0x2b, 0x41, 0xb0, 0xc9, 0x0f, 0x36, 0x75, 0xdc, 0x85, 0xfd, 0x22, + 0x9c, 0xea, 0xea, 0x14, 0xbb, 0x53, 0x2b, 0xa0, 0xda, 0x27, 0x33, 0x5d, 0x59, 0x02, 0x27, 0xe6, + 0x30, 0xfb, 0xeb, 0x16, 0x9c, 0xcc, 0x92, 0x47, 0x6f, 0x59, 0x70, 0x2a, 0xce, 0xd2, 0x3b, 0xae, + 0xb1, 0x53, 0xf1, 0x8e, 0x5d, 0x20, 0xdc, 0xdd, 0x09, 0xfb, 0xff, 0x8a, 0xc9, 0x7f, 0xcb, 0xf5, + 0x9b, 0xc1, 0x1d, 0x65, 0x98, 0x58, 0x3d, 0x0d, 0x13, 0xba, 0x1e, 0x1b, 0x1b, 0xa4, 0xd9, 0xf1, + 0xba, 0x32, 0x47, 0x57, 0x44, 0x3b, 0x56, 0x18, 0x2c, 0x51, 0xae, 0x23, 0xca, 0xb6, 0x67, 0x26, + 0xe5, 0xac, 0x68, 0xc7, 0x0a, 0x03, 0x3d, 0x0f, 0x23, 0xc6, 0x4b, 0xca, 0x79, 0xc9, 0x0c, 0x72, + 0x43, 0x65, 0xc6, 0x38, 0x85, 0x85, 0x26, 0x01, 0x94, 0x91, 0x23, 0x55, 0x24, 0x73, 0x14, 0x29, + 0x49, 0x14, 0x63, 0x03, 0x83, 0xa5, 0xa5, 0x7a, 0x9d, 0x98, 0xf9, 0xf8, 0x07, 0x75, 0x29, 0xd1, + 0x19, 0xd1, 0x86, 0x15, 0x94, 0x4a, 0x93, 0xb6, 0xe3, 0x77, 0x1c, 0x8f, 0x8e, 0x90, 0xd8, 0xfa, + 0xa9, 0x65, 0xb8, 0xa8, 0x20, 0xd8, 0xc0, 0xa2, 0x6f, 0x9c, 0xb8, 0x6d, 0xf2, 0x4a, 0xe0, 0xcb, + 0x38, 0x35, 0x7d, 0xec, 0x23, 0xda, 0xb1, 0xc2, 0xb0, 0xff, 0xab, 0x05, 0x27, 0x74, 0x92, 0x3b, + 0xbf, 0x3d, 0xdb, 0xdc, 0xa9, 0x5a, 0xfb, 0xee, 0x54, 0xd3, 0xd9, 0xbf, 0xa5, 0xbe, 0xb2, 0x7f, + 0xcd, 0xc4, 0xdc, 0xf2, 0x9e, 0x89, 0xb9, 0xdf, 0xaf, 0x6f, 0x66, 0xe5, 0x19, 0xbc, 0xc3, 0x79, + 0xb7, 0xb2, 0x22, 0x1b, 0x06, 0x1b, 0x8e, 0xaa, 0xf0, 0x32, 0xc2, 0xf7, 0x0e, 0x33, 0xd3, 0x0c, + 0x49, 0x40, 0xec, 0x25, 0xa8, 0xa9, 0xd3, 0x0f, 0xb9, 0x51, 0xb5, 0xf2, 0x37, 0xaa, 0x7d, 0x25, + 0x08, 0xd6, 0xd7, 0xbe, 0xf1, 0x9d, 0x27, 0xde, 0xf1, 0x7b, 0xdf, 0x79, 0xe2, 0x1d, 0x7f, 0xf4, + 0x9d, 0x27, 0xde, 0xf1, 0xf1, 0xbb, 0x4f, 0x58, 0xdf, 0xb8, 0xfb, 0x84, 0xf5, 0x7b, 0x77, 0x9f, + 0xb0, 0xfe, 0xe8, 0xee, 0x13, 0xd6, 0xb7, 0xef, 0x3e, 0x61, 0x7d, 0xe1, 0x3f, 0x3d, 0xf1, 0x8e, + 0x57, 0x72, 0x03, 0x15, 0xe9, 0x8f, 0x67, 0x1b, 0xcd, 0xa9, 0xad, 0x8b, 0x2c, 0x56, 0x8e, 0x2e, + 0xaf, 0x29, 0x63, 0x4e, 0x4d, 0xc9, 0xe5, 0xf5, 0xff, 0x02, 0x00, 0x00, 0xff, 0xff, 0x87, 0xd4, + 0x96, 0xc0, 0xad, 0xe1, 0x00, 0x00, } func (m *AWSAuthConfig) Marshal() (dAtA []byte, err error) { @@ -5220,6 +5161,11 @@ func (m *AWSAuthConfig) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + i -= len(m.Profile) + copy(dAtA[i:], m.Profile) + i = encodeVarintGenerated(dAtA, i, uint64(len(m.Profile))) + i-- + dAtA[i] = 0x1a i -= len(m.RoleARN) copy(dAtA[i:], m.RoleARN) i = encodeVarintGenerated(dAtA, i, uint64(len(m.RoleARN))) @@ -6489,6 +6435,13 @@ func (m *ApplicationSetSpec) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if m.TemplatePatch != nil { + i -= len(*m.TemplatePatch) + copy(dAtA[i:], *m.TemplatePatch) + i = encodeVarintGenerated(dAtA, i, uint64(len(*m.TemplatePatch))) + i-- + dAtA[i] = 0x52 + } if len(m.IgnoreApplicationDifferences) > 0 { for iNdEx := len(m.IgnoreApplicationDifferences) - 1; iNdEx >= 0; iNdEx-- { { @@ -7310,6 +7263,14 @@ func (m *ApplicationSourceKustomize) MarshalToSizedBuffer(dAtA []byte) (int, err _ = i var l int _ = l + i-- + if m.LabelWithoutSelector { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x70 if len(m.Components) > 0 { for iNdEx := len(m.Components) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.Components[iNdEx]) @@ -10940,18 +10901,6 @@ func (m *PullRequestGenerator) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if m.ScmManager != nil { - { - size, err := m.ScmManager.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintGenerated(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x52 - } if m.AzureDevOps != nil { { size, err := m.AzureDevOps.MarshalToSizedBuffer(dAtA[:i]) @@ -11458,64 +11407,6 @@ func (m *PullRequestGeneratorGithub) MarshalToSizedBuffer(dAtA []byte) (int, err return len(dAtA) - i, nil } -func (m *PullRequestGeneratorScmManager) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *PullRequestGeneratorScmManager) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *PullRequestGeneratorScmManager) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - i-- - if m.Insecure { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x28 - if m.TokenRef != nil { - { - size, err := m.TokenRef.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintGenerated(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x22 - } - i -= len(m.API) - copy(dAtA[i:], m.API) - i = encodeVarintGenerated(dAtA, i, uint64(len(m.API))) - i-- - dAtA[i] = 0x1a - i -= len(m.Name) - copy(dAtA[i:], m.Name) - i = encodeVarintGenerated(dAtA, i, uint64(len(m.Name))) - i-- - dAtA[i] = 0x12 - i -= len(m.Namespace) - copy(dAtA[i:], m.Namespace) - i = encodeVarintGenerated(dAtA, i, uint64(len(m.Namespace))) - i-- - dAtA[i] = 0xa - return len(dAtA) - i, nil -} - func (m *RefTarget) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -12919,6 +12810,16 @@ func (m *RevisionHistory) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size, err := m.InitiatedBy.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintGenerated(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x52 if len(m.Revisions) > 0 { for iNdEx := len(m.Revisions) - 1; iNdEx >= 0; iNdEx-- { i -= len(m.Revisions[iNdEx]) @@ -13062,18 +12963,6 @@ func (m *SCMProviderGenerator) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l - if m.ScmManager != nil { - { - size, err := m.ScmManager.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintGenerated(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x6a - } if m.AWSCodeCommit != nil { { size, err := m.AWSCodeCommit.MarshalToSizedBuffer(dAtA[:i]) @@ -13703,62 +13592,6 @@ func (m *SCMProviderGeneratorGitlab) MarshalToSizedBuffer(dAtA []byte) (int, err return len(dAtA) - i, nil } -func (m *SCMProviderGeneratorScmManager) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *SCMProviderGeneratorScmManager) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *SCMProviderGeneratorScmManager) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - i-- - if m.Insecure { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x20 - i-- - if m.AllBranches { - dAtA[i] = 1 - } else { - dAtA[i] = 0 - } - i-- - dAtA[i] = 0x18 - if m.TokenRef != nil { - { - size, err := m.TokenRef.MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintGenerated(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x12 - } - i -= len(m.API) - copy(dAtA[i:], m.API) - i = encodeVarintGenerated(dAtA, i, uint64(len(m.API))) - i-- - dAtA[i] = 0xa - return len(dAtA) - i, nil -} - func (m *SecretRef) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -14540,6 +14373,8 @@ func (m *AWSAuthConfig) Size() (n int) { n += 1 + l + sovGenerated(uint64(l)) l = len(m.RoleARN) n += 1 + l + sovGenerated(uint64(l)) + l = len(m.Profile) + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -15046,6 +14881,10 @@ func (m *ApplicationSetSpec) Size() (n int) { n += 1 + l + sovGenerated(uint64(l)) } } + if m.TemplatePatch != nil { + l = len(*m.TemplatePatch) + n += 1 + l + sovGenerated(uint64(l)) + } return n } @@ -15363,6 +15202,7 @@ func (m *ApplicationSourceKustomize) Size() (n int) { n += 1 + l + sovGenerated(uint64(l)) } } + n += 2 return n } @@ -16720,10 +16560,6 @@ func (m *PullRequestGenerator) Size() (n int) { l = m.AzureDevOps.Size() n += 1 + l + sovGenerated(uint64(l)) } - if m.ScmManager != nil { - l = m.ScmManager.Size() - n += 1 + l + sovGenerated(uint64(l)) - } return n } @@ -16886,26 +16722,6 @@ func (m *PullRequestGeneratorGithub) Size() (n int) { return n } -func (m *PullRequestGeneratorScmManager) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.Namespace) - n += 1 + l + sovGenerated(uint64(l)) - l = len(m.Name) - n += 1 + l + sovGenerated(uint64(l)) - l = len(m.API) - n += 1 + l + sovGenerated(uint64(l)) - if m.TokenRef != nil { - l = m.TokenRef.Size() - n += 1 + l + sovGenerated(uint64(l)) - } - n += 2 - return n -} - func (m *RefTarget) Size() (n int) { if m == nil { return 0 @@ -17439,6 +17255,8 @@ func (m *RevisionHistory) Size() (n int) { n += 1 + l + sovGenerated(uint64(l)) } } + l = m.InitiatedBy.Size() + n += 1 + l + sovGenerated(uint64(l)) return n } @@ -17520,10 +17338,6 @@ func (m *SCMProviderGenerator) Size() (n int) { l = m.AWSCodeCommit.Size() n += 1 + l + sovGenerated(uint64(l)) } - if m.ScmManager != nil { - l = m.ScmManager.Size() - n += 1 + l + sovGenerated(uint64(l)) - } return n } @@ -17700,23 +17514,6 @@ func (m *SCMProviderGeneratorGitlab) Size() (n int) { return n } -func (m *SCMProviderGeneratorScmManager) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - l = len(m.API) - n += 1 + l + sovGenerated(uint64(l)) - if m.TokenRef != nil { - l = m.TokenRef.Size() - n += 1 + l + sovGenerated(uint64(l)) - } - n += 2 - n += 2 - return n -} - func (m *SecretRef) Size() (n int) { if m == nil { return 0 @@ -18028,6 +17825,7 @@ func (this *AWSAuthConfig) String() string { s := strings.Join([]string{`&AWSAuthConfig{`, `ClusterName:` + fmt.Sprintf("%v", this.ClusterName) + `,`, `RoleARN:` + fmt.Sprintf("%v", this.RoleARN) + `,`, + `Profile:` + fmt.Sprintf("%v", this.Profile) + `,`, `}`, }, "") return s @@ -18379,6 +18177,7 @@ func (this *ApplicationSetSpec) String() string { `GoTemplateOptions:` + fmt.Sprintf("%v", this.GoTemplateOptions) + `,`, `ApplyNestedSelectors:` + fmt.Sprintf("%v", this.ApplyNestedSelectors) + `,`, `IgnoreApplicationDifferences:` + repeatedStringForIgnoreApplicationDifferences + `,`, + `TemplatePatch:` + valueToStringGenerated(this.TemplatePatch) + `,`, `}`, }, "") return s @@ -18618,6 +18417,7 @@ func (this *ApplicationSourceKustomize) String() string { `Replicas:` + repeatedStringForReplicas + `,`, `Patches:` + repeatedStringForPatches + `,`, `Components:` + fmt.Sprintf("%v", this.Components) + `,`, + `LabelWithoutSelector:` + fmt.Sprintf("%v", this.LabelWithoutSelector) + `,`, `}`, }, "") return s @@ -19707,7 +19507,6 @@ func (this *PullRequestGenerator) String() string { `Template:` + strings.Replace(strings.Replace(this.Template.String(), "ApplicationSetTemplate", "ApplicationSetTemplate", 1), `&`, ``, 1) + `,`, `Bitbucket:` + strings.Replace(this.Bitbucket.String(), "PullRequestGeneratorBitbucket", "PullRequestGeneratorBitbucket", 1) + `,`, `AzureDevOps:` + strings.Replace(this.AzureDevOps.String(), "PullRequestGeneratorAzureDevOps", "PullRequestGeneratorAzureDevOps", 1) + `,`, - `ScmManager:` + strings.Replace(this.ScmManager.String(), "PullRequestGeneratorScmManager", "PullRequestGeneratorScmManager", 1) + `,`, `}`, }, "") return s @@ -19809,20 +19608,6 @@ func (this *PullRequestGeneratorGithub) String() string { }, "") return s } -func (this *PullRequestGeneratorScmManager) String() string { - if this == nil { - return "nil" - } - s := strings.Join([]string{`&PullRequestGeneratorScmManager{`, - `Namespace:` + fmt.Sprintf("%v", this.Namespace) + `,`, - `Name:` + fmt.Sprintf("%v", this.Name) + `,`, - `API:` + fmt.Sprintf("%v", this.API) + `,`, - `TokenRef:` + strings.Replace(this.TokenRef.String(), "SecretRef", "SecretRef", 1) + `,`, - `Insecure:` + fmt.Sprintf("%v", this.Insecure) + `,`, - `}`, - }, "") - return s -} func (this *RefTarget) String() string { if this == nil { return "nil" @@ -20220,6 +20005,7 @@ func (this *RevisionHistory) String() string { `DeployStartedAt:` + strings.Replace(fmt.Sprintf("%v", this.DeployStartedAt), "Time", "v1.Time", 1) + `,`, `Sources:` + repeatedStringForSources + `,`, `Revisions:` + fmt.Sprintf("%v", this.Revisions) + `,`, + `InitiatedBy:` + strings.Replace(strings.Replace(this.InitiatedBy.String(), "OperationInitiator", "OperationInitiator", 1), `&`, ``, 1) + `,`, `}`, }, "") return s @@ -20270,7 +20056,6 @@ func (this *SCMProviderGenerator) String() string { `Template:` + strings.Replace(strings.Replace(this.Template.String(), "ApplicationSetTemplate", "ApplicationSetTemplate", 1), `&`, ``, 1) + `,`, `Values:` + mapStringForValues + `,`, `AWSCodeCommit:` + strings.Replace(this.AWSCodeCommit.String(), "SCMProviderGeneratorAWSCodeCommit", "SCMProviderGeneratorAWSCodeCommit", 1) + `,`, - `ScmManager:` + strings.Replace(this.ScmManager.String(), "SCMProviderGeneratorScmManager", "SCMProviderGeneratorScmManager", 1) + `,`, `}`, }, "") return s @@ -20392,19 +20177,6 @@ func (this *SCMProviderGeneratorGitlab) String() string { }, "") return s } -func (this *SCMProviderGeneratorScmManager) String() string { - if this == nil { - return "nil" - } - s := strings.Join([]string{`&SCMProviderGeneratorScmManager{`, - `API:` + fmt.Sprintf("%v", this.API) + `,`, - `TokenRef:` + strings.Replace(this.TokenRef.String(), "SecretRef", "SecretRef", 1) + `,`, - `AllBranches:` + fmt.Sprintf("%v", this.AllBranches) + `,`, - `Insecure:` + fmt.Sprintf("%v", this.Insecure) + `,`, - `}`, - }, "") - return s -} func (this *SecretRef) String() string { if this == nil { return "nil" @@ -20705,6 +20477,38 @@ func (m *AWSAuthConfig) Unmarshal(dAtA []byte) error { } m.RoleARN = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Profile", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Profile = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -24677,6 +24481,39 @@ func (m *ApplicationSetSpec) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 10: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field TemplatePatch", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + s := string(dAtA[iNdEx:postIndex]) + m.TemplatePatch = &s + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -27507,6 +27344,26 @@ func (m *ApplicationSourceKustomize) Unmarshal(dAtA []byte) error { } m.Components = append(m.Components, string(dAtA[iNdEx:postIndex])) iNdEx = postIndex + case 14: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field LabelWithoutSelector", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.LabelWithoutSelector = bool(v != 0) default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -39128,42 +38985,6 @@ func (m *PullRequestGenerator) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 10: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ScmManager", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.ScmManager == nil { - m.ScmManager = &PullRequestGeneratorScmManager{} - } - if err := m.ScmManager.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -40040,273 +39861,7 @@ func (m *PullRequestGeneratorGitLab) Unmarshal(dAtA []byte) error { } m.API = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field TokenRef", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.TokenRef == nil { - m.TokenRef = &SecretRef{} - } - if err := m.TokenRef.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Labels", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Labels = append(m.Labels, string(dAtA[iNdEx:postIndex])) - iNdEx = postIndex - case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field PullRequestState", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.PullRequestState = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 6: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Insecure", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Insecure = bool(v != 0) - default: - iNdEx = preIndex - skippy, err := skipGenerated(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthGenerated - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *PullRequestGeneratorGitea) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: PullRequestGeneratorGitea: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: PullRequestGeneratorGitea: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Owner", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Owner = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Repo", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Repo = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field API", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.API = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 4: + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TokenRef", wireType) } @@ -40342,7 +39897,71 @@ func (m *PullRequestGeneratorGitea) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Labels", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Labels = append(m.Labels, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field PullRequestState", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.PullRequestState = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 6: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Insecure", wireType) } @@ -40383,7 +40002,7 @@ func (m *PullRequestGeneratorGitea) Unmarshal(dAtA []byte) error { } return nil } -func (m *PullRequestGeneratorGithub) Unmarshal(dAtA []byte) error { +func (m *PullRequestGeneratorGitea) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -40406,10 +40025,10 @@ func (m *PullRequestGeneratorGithub) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: PullRequestGeneratorGithub: wiretype end group for non-group") + return fmt.Errorf("proto: PullRequestGeneratorGitea: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: PullRequestGeneratorGithub: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: PullRequestGeneratorGitea: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -40545,42 +40164,10 @@ func (m *PullRequestGeneratorGithub) Unmarshal(dAtA []byte) error { } iNdEx = postIndex case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field AppSecretName", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.AppSecretName = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 6: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Labels", wireType) + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Insecure", wireType) } - var stringLen uint64 + var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenerated @@ -40590,24 +40177,12 @@ func (m *PullRequestGeneratorGithub) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= uint64(b&0x7F) << shift + v |= int(b&0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Labels = append(m.Labels, string(dAtA[iNdEx:postIndex])) - iNdEx = postIndex + m.Insecure = bool(v != 0) default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -40629,7 +40204,7 @@ func (m *PullRequestGeneratorGithub) Unmarshal(dAtA []byte) error { } return nil } -func (m *PullRequestGeneratorScmManager) Unmarshal(dAtA []byte) error { +func (m *PullRequestGeneratorGithub) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -40652,15 +40227,15 @@ func (m *PullRequestGeneratorScmManager) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: PullRequestGeneratorScmManager: wiretype end group for non-group") + return fmt.Errorf("proto: PullRequestGeneratorGithub: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: PullRequestGeneratorScmManager: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: PullRequestGeneratorGithub: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Namespace", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Owner", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -40688,11 +40263,11 @@ func (m *PullRequestGeneratorScmManager) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Namespace = string(dAtA[iNdEx:postIndex]) + m.Owner = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Name", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Repo", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -40720,7 +40295,7 @@ func (m *PullRequestGeneratorScmManager) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Name = string(dAtA[iNdEx:postIndex]) + m.Repo = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 3: if wireType != 2 { @@ -40791,10 +40366,10 @@ func (m *PullRequestGeneratorScmManager) Unmarshal(dAtA []byte) error { } iNdEx = postIndex case 5: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Insecure", wireType) + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AppSecretName", wireType) } - var v int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenerated @@ -40804,12 +40379,56 @@ func (m *PullRequestGeneratorScmManager) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - v |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - m.Insecure = bool(v != 0) + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AppSecretName = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Labels", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Labels = append(m.Labels, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -46196,43 +45815,76 @@ func (m *RevisionHistory) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Sources = append(m.Sources, ApplicationSource{}) - if err := m.Sources[len(m.Sources)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.Sources = append(m.Sources, ApplicationSource{}) + if err := m.Sources[len(m.Sources)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 9: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Revisions", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Revisions = append(m.Revisions, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 10: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field InitiatedBy", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.InitiatedBy.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex - case 9: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Revisions", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Revisions = append(m.Revisions, string(dAtA[iNdEx:postIndex])) - iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -46992,42 +46644,6 @@ func (m *SCMProviderGenerator) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 13: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ScmManager", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.ScmManager == nil { - m.ScmManager = &SCMProviderGeneratorScmManager{} - } - if err := m.ScmManager.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -47995,205 +47611,15 @@ func (m *SCMProviderGeneratorGitea) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: SCMProviderGeneratorGitea: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: SCMProviderGeneratorGitea: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Owner", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Owner = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field API", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.API = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field TokenRef", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - if m.TokenRef == nil { - m.TokenRef = &SecretRef{} - } - if err := m.TokenRef.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex - case 4: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field AllBranches", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.AllBranches = bool(v != 0) - case 5: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Insecure", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.Insecure = bool(v != 0) - default: - iNdEx = preIndex - skippy, err := skipGenerated(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthGenerated - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *SCMProviderGeneratorGithub) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: SCMProviderGeneratorGithub: wiretype end group for non-group") + return fmt.Errorf("proto: SCMProviderGeneratorGitea: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: SCMProviderGeneratorGithub: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: SCMProviderGeneratorGitea: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Organization", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Owner", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -48221,7 +47647,7 @@ func (m *SCMProviderGeneratorGithub) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Organization = string(dAtA[iNdEx:postIndex]) + m.Owner = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: if wireType != 2 { @@ -48292,10 +47718,10 @@ func (m *SCMProviderGeneratorGithub) Unmarshal(dAtA []byte) error { } iNdEx = postIndex case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field AppSecretName", wireType) + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field AllBranches", wireType) } - var stringLen uint64 + var v int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenerated @@ -48305,27 +47731,15 @@ func (m *SCMProviderGeneratorGithub) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= uint64(b&0x7F) << shift + v |= int(b&0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.AppSecretName = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex + m.AllBranches = bool(v != 0) case 5: if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field AllBranches", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Insecure", wireType) } var v int for shift := uint(0); ; shift += 7 { @@ -48342,7 +47756,7 @@ func (m *SCMProviderGeneratorGithub) Unmarshal(dAtA []byte) error { break } } - m.AllBranches = bool(v != 0) + m.Insecure = bool(v != 0) default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -48364,7 +47778,7 @@ func (m *SCMProviderGeneratorGithub) Unmarshal(dAtA []byte) error { } return nil } -func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { +func (m *SCMProviderGeneratorGithub) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -48387,15 +47801,15 @@ func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: SCMProviderGeneratorGitlab: wiretype end group for non-group") + return fmt.Errorf("proto: SCMProviderGeneratorGithub: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: SCMProviderGeneratorGitlab: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: SCMProviderGeneratorGithub: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Group", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Organization", wireType) } var stringLen uint64 for shift := uint(0); ; shift += 7 { @@ -48423,29 +47837,9 @@ func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Group = string(dAtA[iNdEx:postIndex]) + m.Organization = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field IncludeSubgroups", wireType) - } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - m.IncludeSubgroups = bool(v != 0) - case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field API", wireType) } @@ -48477,7 +47871,7 @@ func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { } m.API = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 4: + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TokenRef", wireType) } @@ -48513,11 +47907,11 @@ func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 5: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field AllBranches", wireType) + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AppSecretName", wireType) } - var v int + var stringLen uint64 for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflowGenerated @@ -48527,35 +47921,27 @@ func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - v |= int(b&0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } - m.AllBranches = bool(v != 0) - case 6: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Insecure", wireType) + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated } - var v int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - v |= int(b&0x7F) << shift - if b < 0x80 { - break - } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated } - m.Insecure = bool(v != 0) - case 7: + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.AppSecretName = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field IncludeSharedProjects", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field AllBranches", wireType) } var v int for shift := uint(0); ; shift += 7 { @@ -48572,40 +47958,7 @@ func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { break } } - b := bool(v != 0) - m.IncludeSharedProjects = &b - case 8: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Topic", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowGenerated - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthGenerated - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthGenerated - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Topic = string(dAtA[iNdEx:postIndex]) - iNdEx = postIndex + m.AllBranches = bool(v != 0) default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) @@ -48627,7 +47980,7 @@ func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { } return nil } -func (m *SCMProviderGeneratorScmManager) Unmarshal(dAtA []byte) error { +func (m *SCMProviderGeneratorGitlab) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -48650,13 +48003,65 @@ func (m *SCMProviderGeneratorScmManager) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: SCMProviderGeneratorScmManager: wiretype end group for non-group") + return fmt.Errorf("proto: SCMProviderGeneratorGitlab: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: SCMProviderGeneratorScmManager: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: SCMProviderGeneratorGitlab: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Group", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Group = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field IncludeSubgroups", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.IncludeSubgroups = bool(v != 0) + case 3: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field API", wireType) } @@ -48688,7 +48093,7 @@ func (m *SCMProviderGeneratorScmManager) Unmarshal(dAtA []byte) error { } m.API = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex - case 2: + case 4: if wireType != 2 { return fmt.Errorf("proto: wrong wireType = %d for field TokenRef", wireType) } @@ -48724,7 +48129,7 @@ func (m *SCMProviderGeneratorScmManager) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex - case 3: + case 5: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field AllBranches", wireType) } @@ -48744,7 +48149,7 @@ func (m *SCMProviderGeneratorScmManager) Unmarshal(dAtA []byte) error { } } m.AllBranches = bool(v != 0) - case 4: + case 6: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Insecure", wireType) } @@ -48764,6 +48169,59 @@ func (m *SCMProviderGeneratorScmManager) Unmarshal(dAtA []byte) error { } } m.Insecure = bool(v != 0) + case 7: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field IncludeSharedProjects", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + b := bool(v != 0) + m.IncludeSharedProjects = &b + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Topic", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowGenerated + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthGenerated + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthGenerated + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Topic = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipGenerated(dAtA[iNdEx:]) diff --git a/pkg/apis/application/v1alpha1/generated.proto b/pkg/apis/application/v1alpha1/generated.proto index 2cbda55eb80adf..ae2f0a89105ad0 100644 --- a/pkg/apis/application/v1alpha1/generated.proto +++ b/pkg/apis/application/v1alpha1/generated.proto @@ -22,6 +22,9 @@ message AWSAuthConfig { // RoleARN contains optional role ARN. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain. optional string roleARN = 2; + + // Profile contains optional role ARN. If set then AWS IAM Authenticator uses the profile to perform cluster operations instead of the default AWS credential provider chain. + optional string profile = 3; } // AppProject provides a logical grouping of applications, providing controls for: @@ -316,6 +319,8 @@ message ApplicationSetSpec { optional bool applyNestedSelectors = 8; repeated ApplicationSetResourceIgnoreDifferences ignoreApplicationDifferences = 9; + + optional string templatePatch = 10; } // ApplicationSetStatus defines the observed state of ApplicationSet @@ -524,6 +529,9 @@ message ApplicationSourceKustomize { // Components specifies a list of kustomize components to add to the kustomization before building repeated string components = 13; + + // LabelWithoutSelector specifies whether to apply common labels to resource selectors or not + optional bool labelWithoutSelector = 14; } // ApplicationSourcePlugin holds options specific to config management plugins @@ -1913,6 +1921,9 @@ message RevisionHistory { // Revisions holds the revision of each source in sources field the sync was performed against repeated string revisions = 9; + + // InitiatedBy contains information about who initiated the operations + optional OperationInitiator initiatedBy = 10; } // RevisionMetadata contains metadata for a specific revision in a Git repository diff --git a/pkg/apis/application/v1alpha1/openapi_generated.go b/pkg/apis/application/v1alpha1/openapi_generated.go index 959c3412f981d7..637e4300f5d149 100644 --- a/pkg/apis/application/v1alpha1/openapi_generated.go +++ b/pkg/apis/application/v1alpha1/openapi_generated.go @@ -193,6 +193,13 @@ func schema_pkg_apis_application_v1alpha1_AWSAuthConfig(ref common.ReferenceCall Format: "", }, }, + "profile": { + SchemaProps: spec.SchemaProps{ + Description: "Profile contains optional role ARN. If set then AWS IAM Authenticator uses the profile to perform cluster operations instead of the default AWS credential provider chain.", + Type: []string{"string"}, + Format: "", + }, + }, }, }, }, @@ -1283,6 +1290,12 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSetSpec(ref common.Referenc }, }, }, + "templatePatch": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, }, Required: []string{"generators", "template"}, }, @@ -1974,6 +1987,13 @@ func schema_pkg_apis_application_v1alpha1_ApplicationSourceKustomize(ref common. }, }, }, + "labelWithoutSelector": { + SchemaProps: spec.SchemaProps{ + Description: "LabelWithoutSelector specifies whether to apply common labels to resource selectors or not", + Type: []string{"boolean"}, + Format: "", + }, + }, }, }, }, @@ -6716,12 +6736,19 @@ func schema_pkg_apis_application_v1alpha1_RevisionHistory(ref common.ReferenceCa }, }, }, + "initiatedBy": { + SchemaProps: spec.SchemaProps{ + Description: "InitiatedBy contains information about who initiated the operations", + Default: map[string]interface{}{}, + Ref: ref("github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OperationInitiator"), + }, + }, }, Required: []string{"deployedAt", "id"}, }, }, Dependencies: []string{ - "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ApplicationSource", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.ApplicationSource", "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1.OperationInitiator", "k8s.io/apimachinery/pkg/apis/meta/v1.Time"}, } } diff --git a/pkg/apis/application/v1alpha1/repository_types.go b/pkg/apis/application/v1alpha1/repository_types.go index 31e8c47971414d..3a557813d87c69 100644 --- a/pkg/apis/application/v1alpha1/repository_types.go +++ b/pkg/apis/application/v1alpha1/repository_types.go @@ -196,7 +196,7 @@ func (repo *Repository) GetGitCreds(store git.CredsStore) git.Creds { return git.NewHTTPSCreds(repo.Username, repo.Password, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store, repo.ForceHttpBasicAuth) } if repo.SSHPrivateKey != "" { - return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure(), store) + return git.NewSSHCreds(repo.SSHPrivateKey, getCAPath(repo.Repo), repo.IsInsecure(), store, repo.Proxy) } if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 { return git.NewGitHubAppCreds(repo.GithubAppId, repo.GithubAppInstallationId, repo.GithubAppPrivateKey, repo.GitHubAppEnterpriseBaseURL, repo.Repo, repo.TLSClientCertData, repo.TLSClientCertKey, repo.IsInsecure(), repo.Proxy, store) diff --git a/pkg/apis/application/v1alpha1/types.go b/pkg/apis/application/v1alpha1/types.go index 1957ec59a341e7..fc2908c4643dc5 100644 --- a/pkg/apis/application/v1alpha1/types.go +++ b/pkg/apis/application/v1alpha1/types.go @@ -469,6 +469,8 @@ type ApplicationSourceKustomize struct { Patches KustomizePatches `json:"patches,omitempty" protobuf:"bytes,12,opt,name=patches"` // Components specifies a list of kustomize components to add to the kustomization before building Components []string `json:"components,omitempty" protobuf:"bytes,13,rep,name=components"` + //LabelWithoutSelector specifies whether to apply common labels to resource selectors or not + LabelWithoutSelector bool `json:"labelWithoutSelector,omitempty" protobuf:"bytes,14,opt,name=labelWithoutSelector"` } type KustomizeReplica struct { @@ -955,6 +957,35 @@ type ApplicationStatus struct { ControllerNamespace string `json:"controllerNamespace,omitempty" protobuf:"bytes,13,opt,name=controllerNamespace"` } +// GetRevisions will return the current revision associated with the Application. +// If app has multisources, it will return all corresponding revisions preserving +// order from the app.spec.sources. If app has only one source, it will return a +// single revision in the list. +func (a *ApplicationStatus) GetRevisions() []string { + revisions := []string{} + if len(a.Sync.Revisions) > 0 { + revisions = a.Sync.Revisions + } else if a.Sync.Revision != "" { + revisions = append(revisions, a.Sync.Revision) + } + return revisions +} + +// BuildComparedToStatus will build a ComparedTo object based on the current +// Application state. +func (app *Application) BuildComparedToStatus() ComparedTo { + ct := ComparedTo{ + Destination: app.Spec.Destination, + IgnoreDifferences: app.Spec.IgnoreDifferences, + } + if app.Spec.HasMultipleSources() { + ct.Sources = app.Spec.Sources + } else { + ct.Source = app.Spec.GetSource() + } + return ct +} + // JWTTokens represents a list of JWT tokens type JWTTokens struct { Items []JWTToken `json:"items,omitempty" protobuf:"bytes,1,opt,name=items"` @@ -1372,6 +1403,8 @@ type RevisionHistory struct { Sources ApplicationSources `json:"sources,omitempty" protobuf:"bytes,8,opt,name=sources"` // Revisions holds the revision of each source in sources field the sync was performed against Revisions []string `json:"revisions,omitempty" protobuf:"bytes,9,opt,name=revisions"` + // InitiatedBy contains information about who initiated the operations + InitiatedBy OperationInitiator `json:"initiatedBy,omitempty" protobuf:"bytes,10,opt,name=initiatedBy"` } // ApplicationWatchEvent contains information about application change. @@ -1825,6 +1858,9 @@ type AWSAuthConfig struct { // RoleARN contains optional role ARN. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain. RoleARN string `json:"roleARN,omitempty" protobuf:"bytes,2,opt,name=roleARN"` + + // Profile contains optional role ARN. If set then AWS IAM Authenticator uses the profile to perform cluster operations instead of the default AWS credential provider chain. + Profile string `json:"profile,omitempty" protobuf:"bytes,3,opt,name=profile"` } // ExecProviderConfig is config used to call an external command to perform cluster authentication @@ -2622,6 +2658,18 @@ func (app *Application) IsRefreshRequested() (RefreshType, bool) { return refreshType, true } +func (app *Application) HasPostDeleteFinalizer(stage ...string) bool { + return getFinalizerIndex(app.ObjectMeta, strings.Join(append([]string{PostDeleteFinalizerName}, stage...), "/")) > -1 +} + +func (app *Application) SetPostDeleteFinalizer(stage ...string) { + setFinalizer(&app.ObjectMeta, strings.Join(append([]string{PostDeleteFinalizerName}, stage...), "/"), true) +} + +func (app *Application) UnSetPostDeleteFinalizer(stage ...string) { + setFinalizer(&app.ObjectMeta, strings.Join(append([]string{PostDeleteFinalizerName}, stage...), "/"), false) +} + // SetCascadedDeletion will enable cascaded deletion by setting the propagation policy finalizer func (app *Application) SetCascadedDeletion(finalizer string) { setFinalizer(&app.ObjectMeta, finalizer, true) @@ -2944,6 +2992,9 @@ func (c *Cluster) RawRestConfig() *rest.Config { if c.Config.AWSAuthConfig.RoleARN != "" { args = append(args, "--role-arn", c.Config.AWSAuthConfig.RoleARN) } + if c.Config.AWSAuthConfig.Profile != "" { + args = append(args, "--profile", c.Config.AWSAuthConfig.Profile) + } config = &rest.Config{ Host: c.Server, TLSClientConfig: tlsClientConfig, diff --git a/pkg/apis/application/v1alpha1/types_test.go b/pkg/apis/application/v1alpha1/types_test.go index 668cf97b07e5a6..2374f5fb503e66 100644 --- a/pkg/apis/application/v1alpha1/types_test.go +++ b/pkg/apis/application/v1alpha1/types_test.go @@ -370,7 +370,7 @@ func TestAppProject_IsDestinationPermitted_PermitOnlyProjectScopedClusters(t *te projDest: []ApplicationDestination{{ Server: "https://my-cluster.123.com", Namespace: "default", }}, - appDest: ApplicationDestination{Server: "https://some-other-cluster.com", Namespace: "default"}, + appDest: ApplicationDestination{Server: "https://some-other-cluster.example.com", Namespace: "default"}, clusters: []*Cluster{{ Server: "https://my-cluster.123.com", }}, diff --git a/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go index d362ff27de133a..e3b45da925be62 100644 --- a/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/application/v1alpha1/zz_generated.deepcopy.go @@ -733,6 +733,11 @@ func (in *ApplicationSetSpec) DeepCopyInto(out *ApplicationSetSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.TemplatePatch != nil { + in, out := &in.TemplatePatch, &out.TemplatePatch + *out = new(string) + **out = **in + } return } @@ -3710,6 +3715,7 @@ func (in *RevisionHistory) DeepCopyInto(out *RevisionHistory) { *out = make([]string, len(*in)) copy(*out, *in) } + out.InitiatedBy = in.InitiatedBy return } diff --git a/pkg/ratelimiter/ratelimiter.go b/pkg/ratelimiter/ratelimiter.go index 32507d883e8ae3..1c491a584873e2 100644 --- a/pkg/ratelimiter/ratelimiter.go +++ b/pkg/ratelimiter/ratelimiter.go @@ -11,7 +11,7 @@ import ( type AppControllerRateLimiterConfig struct { BucketSize int64 - BucketQPS int64 + BucketQPS float64 FailureCoolDown time.Duration BaseDelay time.Duration MaxDelay time.Duration @@ -22,7 +22,8 @@ func GetDefaultAppRateLimiterConfig() *AppControllerRateLimiterConfig { return &AppControllerRateLimiterConfig{ // global queue rate limit config 500, - 50, + // when WORKQUEUE_BUCKET_QPS is MaxFloat64 global bucket limiting is disabled(default) + math.MaxFloat64, // individual item rate limit config // when WORKQUEUE_FAILURE_COOLDOWN is 0 per item rate limiting is disabled(default) 0, diff --git a/reposerver/apiclient/repository.pb.go b/reposerver/apiclient/repository.pb.go index 4c05248b87e163..914a967db3dfc0 100644 --- a/reposerver/apiclient/repository.pb.go +++ b/reposerver/apiclient/repository.pb.go @@ -1910,6 +1910,7 @@ type GitFilesRequest struct { Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"` Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"` NewGitFileGlobbingEnabled bool `protobuf:"varint,5,opt,name=NewGitFileGlobbingEnabled,proto3" json:"NewGitFileGlobbingEnabled,omitempty"` + NoRevisionCache bool `protobuf:"varint,6,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -1983,6 +1984,13 @@ func (m *GitFilesRequest) GetNewGitFileGlobbingEnabled() bool { return false } +func (m *GitFilesRequest) GetNoRevisionCache() bool { + if m != nil { + return m.NoRevisionCache + } + return false +} + type GitFilesResponse struct { // Map consisting of path of the path to its contents in bytes Map map[string][]byte `protobuf:"bytes,1,rep,name=map,proto3" json:"map,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` @@ -2035,6 +2043,7 @@ type GitDirectoriesRequest struct { Repo *v1alpha1.Repository `protobuf:"bytes,1,opt,name=repo,proto3" json:"repo,omitempty"` SubmoduleEnabled bool `protobuf:"varint,2,opt,name=submoduleEnabled,proto3" json:"submoduleEnabled,omitempty"` Revision string `protobuf:"bytes,3,opt,name=revision,proto3" json:"revision,omitempty"` + NoRevisionCache bool `protobuf:"varint,4,opt,name=noRevisionCache,proto3" json:"noRevisionCache,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -2094,6 +2103,13 @@ func (m *GitDirectoriesRequest) GetRevision() string { return "" } +func (m *GitDirectoriesRequest) GetNoRevisionCache() bool { + if m != nil { + return m.NoRevisionCache + } + return false +} + type GitDirectoriesResponse struct { // A set of directory paths Paths []string `protobuf:"bytes,1,rep,name=paths,proto3" json:"paths,omitempty"` @@ -2189,140 +2205,140 @@ func init() { } var fileDescriptor_dd8723cfcc820480 = []byte{ - // 2114 bytes of a gzipped FileDescriptorProto + // 2127 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x5a, 0x5b, 0x6f, 0x1b, 0xc7, - 0x15, 0xe6, 0x92, 0xba, 0x90, 0x47, 0xb2, 0x44, 0x8d, 0x75, 0x59, 0x31, 0x8e, 0xa0, 0x6c, 0x6b, - 0x43, 0xb5, 0x13, 0x12, 0x92, 0x91, 0xb8, 0x70, 0xd2, 0x14, 0x8a, 0x62, 0x4b, 0x8e, 0x2d, 0x5b, - 0x5d, 0xbb, 0x2d, 0xd2, 0xba, 0x2d, 0x86, 0xcb, 0x21, 0xb9, 0xe1, 0x5e, 0xc6, 0xbb, 0xb3, 0x0a, - 0x64, 0xa0, 0x0f, 0x45, 0x8b, 0x02, 0xfd, 0x03, 0x7d, 0xe8, 0xff, 0x28, 0xfa, 0x54, 0xf4, 0xa9, - 0x97, 0xc7, 0xa0, 0x7f, 0xa0, 0x85, 0x1f, 0xfb, 0x2b, 0x8a, 0xb9, 0xec, 0x95, 0x2b, 0xd9, 0x29, - 0x65, 0x19, 0xcd, 0x8b, 0xbd, 0x73, 0xe6, 0xcc, 0x39, 0x67, 0xce, 0x9c, 0xcb, 0x37, 0x43, 0xc1, - 0xb5, 0x80, 0x50, 0x3f, 0x24, 0xc1, 0x31, 0x09, 0x3a, 0xe2, 0xd3, 0x66, 0x7e, 0x70, 0x92, 0xf9, - 0x6c, 0xd3, 0xc0, 0x67, 0x3e, 0x82, 0x94, 0xd2, 0x7a, 0x30, 0xb0, 0xd9, 0x30, 0xea, 0xb6, 0x2d, - 0xdf, 0xed, 0xe0, 0x60, 0xe0, 0xd3, 0xc0, 0xff, 0x42, 0x7c, 0xbc, 0x67, 0xf5, 0x3a, 0xc7, 0x3b, - 0x1d, 0x3a, 0x1a, 0x74, 0x30, 0xb5, 0xc3, 0x0e, 0xa6, 0xd4, 0xb1, 0x2d, 0xcc, 0x6c, 0xdf, 0xeb, - 0x1c, 0x6f, 0x63, 0x87, 0x0e, 0xf1, 0x76, 0x67, 0x40, 0x3c, 0x12, 0x60, 0x46, 0x7a, 0x52, 0x72, - 0xeb, 0xad, 0x81, 0xef, 0x0f, 0x1c, 0xd2, 0x11, 0xa3, 0x6e, 0xd4, 0xef, 0x10, 0x97, 0x32, 0xa5, - 0xd6, 0xf8, 0xcf, 0x3c, 0x2c, 0x1e, 0x62, 0xcf, 0xee, 0x93, 0x90, 0x99, 0xe4, 0x59, 0x44, 0x42, - 0x86, 0x9e, 0xc2, 0x14, 0x37, 0x46, 0xd7, 0x36, 0xb5, 0xad, 0xb9, 0x9d, 0x83, 0x76, 0x6a, 0x4d, - 0x3b, 0xb6, 0x46, 0x7c, 0xfc, 0xc2, 0xea, 0xb5, 0x8f, 0x77, 0xda, 0x74, 0x34, 0x68, 0x73, 0x6b, - 0xda, 0x19, 0x6b, 0xda, 0xb1, 0x35, 0x6d, 0x33, 0xd9, 0x96, 0x29, 0xa4, 0xa2, 0x16, 0xd4, 0x03, - 0x72, 0x6c, 0x87, 0xb6, 0xef, 0xe9, 0xd5, 0x4d, 0x6d, 0xab, 0x61, 0x26, 0x63, 0xa4, 0xc3, 0xac, - 0xe7, 0xef, 0x61, 0x6b, 0x48, 0xf4, 0xda, 0xa6, 0xb6, 0x55, 0x37, 0xe3, 0x21, 0xda, 0x84, 0x39, - 0x4c, 0xe9, 0x03, 0xdc, 0x25, 0xce, 0x7d, 0x72, 0xa2, 0x4f, 0x89, 0x85, 0x59, 0x12, 0x5f, 0x8b, - 0x29, 0x7d, 0x88, 0x5d, 0xa2, 0x4f, 0x8b, 0xd9, 0x78, 0x88, 0xae, 0x40, 0xc3, 0xc3, 0x2e, 0x09, - 0x29, 0xb6, 0x88, 0x5e, 0x17, 0x73, 0x29, 0x01, 0xfd, 0x12, 0x96, 0x32, 0x86, 0x3f, 0xf6, 0xa3, - 0xc0, 0x22, 0x3a, 0x88, 0xad, 0x3f, 0x9a, 0x6c, 0xeb, 0xbb, 0x45, 0xb1, 0xe6, 0xb8, 0x26, 0xf4, - 0x73, 0x98, 0x16, 0x27, 0xaf, 0xcf, 0x6d, 0xd6, 0xce, 0xd5, 0xdb, 0x52, 0x2c, 0xf2, 0x60, 0x96, - 0x3a, 0xd1, 0xc0, 0xf6, 0x42, 0x7d, 0x5e, 0x68, 0x78, 0x32, 0x99, 0x86, 0x3d, 0xdf, 0xeb, 0xdb, - 0x83, 0x43, 0xec, 0xe1, 0x01, 0x71, 0x89, 0xc7, 0x8e, 0x84, 0x70, 0x33, 0x56, 0x82, 0x9e, 0x43, - 0x73, 0x14, 0x85, 0xcc, 0x77, 0xed, 0xe7, 0xe4, 0x11, 0xe5, 0x6b, 0x43, 0xfd, 0x92, 0xf0, 0xe6, - 0xc3, 0xc9, 0x14, 0xdf, 0x2f, 0x48, 0x35, 0xc7, 0xf4, 0xf0, 0x20, 0x19, 0x45, 0x5d, 0xf2, 0x23, - 0x12, 0x88, 0xe8, 0x5a, 0x90, 0x41, 0x92, 0x21, 0xc9, 0x30, 0xb2, 0xd5, 0x28, 0xd4, 0x17, 0x37, - 0x6b, 0x32, 0x8c, 0x12, 0x12, 0xda, 0x82, 0xc5, 0x63, 0x12, 0xd8, 0xfd, 0x93, 0xc7, 0xf6, 0xc0, - 0xc3, 0x2c, 0x0a, 0x88, 0xde, 0x14, 0xa1, 0x58, 0x24, 0x23, 0x17, 0x2e, 0x0d, 0x89, 0xe3, 0x72, - 0x97, 0xef, 0x05, 0xa4, 0x17, 0xea, 0x4b, 0xc2, 0xbf, 0xfb, 0x93, 0x9f, 0xa0, 0x10, 0x67, 0xe6, - 0xa5, 0x73, 0xc3, 0x3c, 0xdf, 0x54, 0x99, 0x22, 0x73, 0x04, 0x49, 0xc3, 0x0a, 0x64, 0x74, 0x0d, - 0x16, 0x58, 0x80, 0xad, 0x91, 0xed, 0x0d, 0x0e, 0x09, 0x1b, 0xfa, 0x3d, 0xfd, 0xb2, 0xf0, 0x44, - 0x81, 0x8a, 0x2c, 0x40, 0xc4, 0xc3, 0x5d, 0x87, 0xf4, 0x64, 0x2c, 0x3e, 0x39, 0xa1, 0x24, 0xd4, - 0x97, 0xc5, 0x2e, 0x6e, 0xb6, 0x33, 0x15, 0xaa, 0x50, 0x20, 0xda, 0x77, 0xc6, 0x56, 0xdd, 0xf1, - 0x58, 0x70, 0x62, 0x96, 0x88, 0x43, 0x23, 0x98, 0xe3, 0xfb, 0x88, 0x43, 0x61, 0x45, 0x84, 0xc2, - 0xbd, 0xc9, 0x7c, 0x74, 0x90, 0x0a, 0x34, 0xb3, 0xd2, 0x51, 0x1b, 0xd0, 0x10, 0x87, 0x87, 0x91, - 0xc3, 0x6c, 0xea, 0x10, 0x69, 0x46, 0xa8, 0xaf, 0x0a, 0x37, 0x95, 0xcc, 0xa0, 0xfb, 0x00, 0x01, - 0xe9, 0xc7, 0x7c, 0x6b, 0x62, 0xe7, 0x37, 0xce, 0xda, 0xb9, 0x99, 0x70, 0xcb, 0x1d, 0x67, 0x96, - 0x73, 0xe5, 0x7c, 0x1b, 0xc4, 0x62, 0x2a, 0xdb, 0x45, 0x5a, 0xeb, 0x22, 0xc4, 0x4a, 0x66, 0x78, - 0x2c, 0x2a, 0xaa, 0x28, 0x5a, 0xeb, 0x32, 0x5a, 0x33, 0xa4, 0xd6, 0x1d, 0x58, 0x3b, 0xc5, 0xd5, - 0xa8, 0x09, 0xb5, 0x11, 0x39, 0x11, 0x25, 0xba, 0x61, 0xf2, 0x4f, 0xb4, 0x0c, 0xd3, 0xc7, 0xd8, - 0x89, 0x88, 0x28, 0xaa, 0x75, 0x53, 0x0e, 0x6e, 0x57, 0xbf, 0xab, 0xb5, 0x7e, 0xab, 0xc1, 0x62, - 0xc1, 0xf0, 0x92, 0xf5, 0x3f, 0xcb, 0xae, 0x3f, 0x87, 0x30, 0xee, 0x3f, 0xc1, 0xc1, 0x80, 0xb0, - 0x8c, 0x21, 0xc6, 0x3f, 0x35, 0xd0, 0x0b, 0x1e, 0xfd, 0xb1, 0xcd, 0x86, 0x77, 0x6d, 0x87, 0x84, - 0xe8, 0x16, 0xcc, 0x06, 0x92, 0xa6, 0x1a, 0xcf, 0x5b, 0x67, 0x1c, 0xc4, 0x41, 0xc5, 0x8c, 0xb9, - 0xd1, 0xc7, 0x50, 0x77, 0x09, 0xc3, 0x3d, 0xcc, 0xb0, 0xb2, 0x7d, 0xb3, 0x6c, 0x25, 0xd7, 0x72, - 0xa8, 0xf8, 0x0e, 0x2a, 0x66, 0xb2, 0x06, 0xbd, 0x0f, 0xd3, 0xd6, 0x30, 0xf2, 0x46, 0xa2, 0xe5, - 0xcc, 0xed, 0xbc, 0x7d, 0xda, 0xe2, 0x3d, 0xce, 0x74, 0x50, 0x31, 0x25, 0xf7, 0x27, 0x33, 0x30, - 0x45, 0x71, 0xc0, 0x8c, 0xbb, 0xb0, 0x5c, 0xa6, 0x82, 0xf7, 0x39, 0x6b, 0x48, 0xac, 0x51, 0x18, - 0xb9, 0xca, 0xcd, 0xc9, 0x18, 0x21, 0x98, 0x0a, 0xed, 0xe7, 0xd2, 0xd5, 0x35, 0x53, 0x7c, 0x1b, - 0xdf, 0x81, 0xa5, 0x31, 0x6d, 0xfc, 0x50, 0xa5, 0x6d, 0x5c, 0xc2, 0xbc, 0x52, 0x6d, 0x44, 0xb0, - 0xf2, 0x44, 0xf8, 0x22, 0x29, 0xf6, 0x17, 0xd1, 0xb9, 0x8d, 0x03, 0x58, 0x2d, 0xaa, 0x0d, 0xa9, - 0xef, 0x85, 0x84, 0x87, 0xbe, 0xa8, 0x8e, 0x36, 0xe9, 0xa5, 0xb3, 0xc2, 0x8a, 0xba, 0x59, 0x32, - 0x63, 0xfc, 0xaa, 0x0a, 0xab, 0x26, 0x09, 0x7d, 0xe7, 0x98, 0xc4, 0xa5, 0xeb, 0x62, 0xc0, 0xc7, - 0x4f, 0xa1, 0x86, 0x29, 0x55, 0x61, 0x72, 0xef, 0xdc, 0xda, 0xbb, 0xc9, 0xa5, 0xa2, 0x77, 0x61, - 0x09, 0xbb, 0x5d, 0x7b, 0x10, 0xf9, 0x51, 0x18, 0x6f, 0x4b, 0x04, 0x55, 0xc3, 0x1c, 0x9f, 0x30, - 0x2c, 0x58, 0x1b, 0x73, 0x81, 0x72, 0x67, 0x16, 0x22, 0x69, 0x05, 0x88, 0x54, 0xaa, 0xa4, 0x7a, - 0x9a, 0x92, 0xbf, 0x69, 0xd0, 0x4c, 0x53, 0x47, 0x89, 0xbf, 0x02, 0x0d, 0x57, 0xd1, 0x42, 0x5d, - 0x13, 0xf5, 0x29, 0x25, 0xe4, 0xd1, 0x52, 0xb5, 0x88, 0x96, 0x56, 0x61, 0x46, 0x82, 0x59, 0xb5, - 0x31, 0x35, 0xca, 0x99, 0x3c, 0x55, 0x30, 0x79, 0x03, 0x20, 0x4c, 0xea, 0x97, 0x3e, 0x23, 0x66, - 0x33, 0x14, 0x64, 0xc0, 0xbc, 0xec, 0xad, 0x26, 0x09, 0x23, 0x87, 0xe9, 0xb3, 0x82, 0x23, 0x47, - 0x33, 0x7c, 0x58, 0x7c, 0x60, 0xf3, 0x3d, 0xf4, 0xc3, 0x8b, 0x09, 0xf6, 0x0f, 0x60, 0x8a, 0x2b, - 0xe3, 0x1b, 0xeb, 0x06, 0xd8, 0xb3, 0x86, 0x24, 0xf6, 0x55, 0x32, 0xe6, 0x69, 0xcc, 0xf0, 0x20, - 0xd4, 0xab, 0x82, 0x2e, 0xbe, 0x8d, 0x3f, 0x55, 0xa5, 0xa5, 0xbb, 0x94, 0x86, 0x6f, 0x1e, 0x50, - 0x97, 0xb7, 0xf8, 0xda, 0x78, 0x8b, 0x2f, 0x98, 0xfc, 0x75, 0x5a, 0xfc, 0x39, 0xb5, 0x29, 0x23, - 0x82, 0xd9, 0x5d, 0x4a, 0xb9, 0x21, 0x68, 0x1b, 0xa6, 0x30, 0xa5, 0xd2, 0xe1, 0x85, 0x8a, 0xac, - 0x58, 0xf8, 0xff, 0xca, 0x24, 0xc1, 0xda, 0xba, 0x05, 0x8d, 0x84, 0xf4, 0x32, 0xb5, 0x8d, 0xac, - 0xda, 0x4d, 0x00, 0x89, 0x61, 0xef, 0x79, 0x7d, 0x9f, 0x1f, 0x29, 0x0f, 0x76, 0xb5, 0x54, 0x7c, - 0x1b, 0xb7, 0x63, 0x0e, 0x61, 0xdb, 0xbb, 0x30, 0x6d, 0x33, 0xe2, 0xc6, 0xc6, 0xad, 0x66, 0x8d, - 0x4b, 0x05, 0x99, 0x92, 0xc9, 0xf8, 0x7b, 0x1d, 0xd6, 0xf9, 0x89, 0x3d, 0x16, 0x69, 0xb2, 0x4b, - 0xe9, 0xa7, 0x84, 0x61, 0xdb, 0x09, 0x7f, 0x10, 0x91, 0xe0, 0xe4, 0x35, 0x07, 0xc6, 0x00, 0x66, - 0x64, 0x96, 0xa9, 0x7a, 0x77, 0xee, 0xd7, 0x19, 0x25, 0x3e, 0xbd, 0xc3, 0xd4, 0x5e, 0xcf, 0x1d, - 0xa6, 0xec, 0x4e, 0x31, 0x75, 0x41, 0x77, 0x8a, 0xd3, 0xaf, 0x95, 0x99, 0xcb, 0xea, 0x4c, 0xfe, - 0xb2, 0x5a, 0x02, 0xd5, 0x67, 0x5f, 0x15, 0xaa, 0xd7, 0x4b, 0xa1, 0xba, 0x5b, 0x9a, 0xc7, 0x0d, - 0xe1, 0xee, 0xef, 0x65, 0x23, 0xf0, 0xd4, 0x58, 0x9b, 0x04, 0xb4, 0xc3, 0x6b, 0x05, 0xed, 0x3f, - 0xcc, 0x81, 0x70, 0x79, 0x0d, 0x7e, 0xff, 0xd5, 0xf6, 0x74, 0x06, 0x1c, 0xff, 0xc6, 0x81, 0xe7, - 0xdf, 0x08, 0xcc, 0x44, 0xfd, 0xd4, 0x07, 0x49, 0x43, 0xe7, 0x7d, 0x88, 0xb7, 0x56, 0x55, 0xb4, - 0xf8, 0x37, 0xba, 0x01, 0x53, 0xdc, 0xc9, 0x0a, 0xd4, 0xae, 0x65, 0xfd, 0xc9, 0x4f, 0x62, 0x97, - 0xd2, 0xc7, 0x94, 0x58, 0xa6, 0x60, 0x42, 0xb7, 0xa1, 0x91, 0x04, 0xbe, 0xca, 0xac, 0x2b, 0xd9, - 0x15, 0x49, 0x9e, 0xc4, 0xcb, 0x52, 0x76, 0xbe, 0xb6, 0x67, 0x07, 0xc4, 0x12, 0x90, 0x6f, 0x7a, - 0x7c, 0xed, 0xa7, 0xf1, 0x64, 0xb2, 0x36, 0x61, 0x47, 0xdb, 0x30, 0x23, 0xdf, 0x0d, 0x44, 0x06, - 0xcd, 0xed, 0xac, 0x8f, 0x17, 0xd3, 0x78, 0x95, 0x62, 0x34, 0xfe, 0xaa, 0xc1, 0x3b, 0x69, 0x40, - 0xc4, 0xd9, 0x14, 0xa3, 0xee, 0x37, 0xdf, 0x71, 0xaf, 0xc1, 0x82, 0x80, 0xf9, 0xe9, 0xf3, 0x81, - 0x7c, 0xc9, 0x2a, 0x50, 0x8d, 0x3f, 0x6a, 0x70, 0x75, 0x7c, 0x1f, 0x7b, 0x43, 0x1c, 0xb0, 0xe4, - 0x78, 0x2f, 0x62, 0x2f, 0x71, 0xc3, 0xab, 0xa6, 0x0d, 0x2f, 0xb7, 0xbf, 0x5a, 0x7e, 0x7f, 0xc6, - 0x5f, 0xaa, 0x30, 0x97, 0x09, 0xa0, 0xb2, 0x86, 0xc9, 0x01, 0x9f, 0x88, 0x5b, 0x71, 0xb1, 0x13, - 0x4d, 0xa1, 0x61, 0x66, 0x28, 0x68, 0x04, 0x40, 0x71, 0x80, 0x5d, 0xc2, 0x48, 0xc0, 0x2b, 0x39, - 0xcf, 0xf8, 0xfb, 0x93, 0x57, 0x97, 0xa3, 0x58, 0xa6, 0x99, 0x11, 0xcf, 0x11, 0xab, 0x50, 0x1d, - 0xaa, 0xfa, 0xad, 0x46, 0xe8, 0x4b, 0x58, 0xe8, 0xdb, 0x0e, 0x39, 0x4a, 0x0d, 0x99, 0x11, 0x86, - 0x3c, 0x9a, 0xdc, 0x90, 0xbb, 0x59, 0xb9, 0x66, 0x41, 0x8d, 0x71, 0x1d, 0x9a, 0xc5, 0x7c, 0xe2, - 0x46, 0xda, 0x2e, 0x1e, 0x24, 0xde, 0x52, 0x23, 0x03, 0x41, 0xb3, 0x98, 0x3f, 0xc6, 0xbf, 0xaa, - 0xb0, 0x92, 0x88, 0xdb, 0xf5, 0x3c, 0x3f, 0xf2, 0x2c, 0xf1, 0x14, 0x57, 0x7a, 0x16, 0xcb, 0x30, - 0xcd, 0x6c, 0xe6, 0x24, 0xc0, 0x47, 0x0c, 0x78, 0xef, 0x62, 0xbe, 0xef, 0x30, 0x9b, 0xaa, 0x03, - 0x8e, 0x87, 0xf2, 0xec, 0x9f, 0x45, 0x76, 0x40, 0x7a, 0xa2, 0x12, 0xd4, 0xcd, 0x64, 0xcc, 0xe7, - 0x38, 0xaa, 0x11, 0x30, 0x5e, 0x3a, 0x33, 0x19, 0x8b, 0xb8, 0xf7, 0x1d, 0x87, 0x58, 0xdc, 0x1d, - 0x19, 0xa0, 0x5f, 0xa0, 0x8a, 0x0b, 0x04, 0x0b, 0x6c, 0x6f, 0xa0, 0x60, 0xbe, 0x1a, 0x71, 0x3b, - 0x71, 0x10, 0xe0, 0x13, 0xbd, 0x2e, 0x1c, 0x20, 0x07, 0xe8, 0x23, 0xa8, 0xb9, 0x98, 0xaa, 0x46, - 0x77, 0x3d, 0x57, 0x1d, 0xca, 0x3c, 0xd0, 0x3e, 0xc4, 0x54, 0x76, 0x02, 0xbe, 0xac, 0xf5, 0x01, - 0xd4, 0x63, 0xc2, 0xd7, 0x82, 0x84, 0x5f, 0xc0, 0xa5, 0x5c, 0xf1, 0x41, 0x9f, 0xc3, 0x6a, 0x1a, - 0x51, 0x59, 0x85, 0x0a, 0x04, 0xbe, 0xf3, 0x52, 0xcb, 0xcc, 0x53, 0x04, 0x18, 0xcf, 0x60, 0x89, - 0x87, 0x8c, 0x48, 0xfc, 0x0b, 0xba, 0xda, 0x7c, 0x08, 0x8d, 0x44, 0x65, 0x69, 0xcc, 0xb4, 0xa0, - 0x7e, 0x1c, 0x3f, 0x91, 0xca, 0xbb, 0x4d, 0x32, 0x36, 0x76, 0x01, 0x65, 0xed, 0x55, 0x1d, 0xe8, - 0x46, 0x1e, 0x14, 0xaf, 0x14, 0xdb, 0x8d, 0x60, 0x8f, 0x31, 0xf1, 0xef, 0xaa, 0xb0, 0xb8, 0x6f, - 0x8b, 0x57, 0x8e, 0x0b, 0x2a, 0x72, 0xd7, 0xa1, 0x19, 0x46, 0x5d, 0xd7, 0xef, 0x45, 0x0e, 0x51, - 0xa0, 0x40, 0x75, 0xfa, 0x31, 0xfa, 0x59, 0xc5, 0x8f, 0x3b, 0x8b, 0x62, 0x36, 0x54, 0x37, 0x5c, - 0xf1, 0x8d, 0x3e, 0x82, 0xf5, 0x87, 0xe4, 0x4b, 0xb5, 0x9f, 0x7d, 0xc7, 0xef, 0x76, 0x6d, 0x6f, - 0x10, 0x2b, 0x99, 0x16, 0x4a, 0x4e, 0x67, 0x30, 0x7e, 0xad, 0x41, 0x33, 0xf5, 0x85, 0xf2, 0xe6, - 0x2d, 0x19, 0xf5, 0xd2, 0x97, 0x57, 0xb3, 0xbe, 0x2c, 0xb2, 0xfe, 0xef, 0x01, 0x3f, 0x9f, 0x0d, - 0xf8, 0x3f, 0x6b, 0xb0, 0xb2, 0x6f, 0xb3, 0xb8, 0xd4, 0xd8, 0xff, 0x67, 0xe7, 0x62, 0xb4, 0x61, - 0xb5, 0x68, 0xbe, 0x72, 0xe5, 0x32, 0x4c, 0xf3, 0x53, 0x8a, 0xef, 0xee, 0x72, 0xb0, 0xf3, 0x55, - 0x03, 0x96, 0xd2, 0xe6, 0xcb, 0xff, 0xb5, 0x2d, 0x82, 0x1e, 0x41, 0x73, 0x5f, 0xfd, 0x76, 0x16, - 0xbf, 0x99, 0xa0, 0xb3, 0x1e, 0x21, 0x5b, 0x57, 0xca, 0x27, 0xa5, 0x6a, 0xa3, 0x82, 0x2c, 0x58, - 0x2f, 0x0a, 0x4c, 0xdf, 0x3b, 0xbf, 0x7d, 0x86, 0xe4, 0x84, 0xeb, 0x65, 0x2a, 0xb6, 0x34, 0xf4, - 0x39, 0x2c, 0xe4, 0x5f, 0xe5, 0x50, 0xae, 0x1a, 0x95, 0x3e, 0x14, 0xb6, 0x8c, 0xb3, 0x58, 0x12, - 0xfb, 0x9f, 0x72, 0xe8, 0x9b, 0x7b, 0xa2, 0x42, 0x46, 0x1e, 0x98, 0x97, 0x3d, 0xe1, 0xb5, 0xbe, - 0x75, 0x26, 0x4f, 0x22, 0xfd, 0x43, 0xa8, 0xc7, 0x4f, 0x3a, 0x79, 0x37, 0x17, 0x1e, 0x7a, 0x5a, - 0xcd, 0xbc, 0xbc, 0x7e, 0x68, 0x54, 0xd0, 0xc7, 0x72, 0x31, 0xbf, 0xf2, 0x8f, 0x2f, 0xce, 0x3c, - 0x64, 0xb4, 0x2e, 0x97, 0x3c, 0x1e, 0x18, 0x15, 0xf4, 0x7d, 0x98, 0xe3, 0x5f, 0x47, 0xea, 0x57, - 0xab, 0xd5, 0xb6, 0xfc, 0x91, 0xb4, 0x1d, 0xff, 0x48, 0xda, 0xbe, 0xe3, 0x52, 0x76, 0xd2, 0x2a, - 0xb9, 0xdd, 0x2b, 0x01, 0x4f, 0xe1, 0xd2, 0x3e, 0x61, 0x29, 0x18, 0x47, 0x57, 0x5f, 0xe9, 0xca, - 0xd2, 0x32, 0x8a, 0x6c, 0xe3, 0x78, 0xde, 0xa8, 0xa0, 0xdf, 0x6b, 0x70, 0x79, 0x9f, 0xb0, 0x22, - 0xbc, 0x45, 0xef, 0x95, 0x2b, 0x39, 0x05, 0x06, 0xb7, 0x1e, 0x4e, 0x9a, 0xaf, 0x79, 0xb1, 0x46, - 0x05, 0xfd, 0x41, 0x83, 0xb5, 0x8c, 0x61, 0x59, 0xbc, 0x8a, 0xb6, 0xcf, 0x36, 0xae, 0x04, 0xdb, - 0xb6, 0x3e, 0x9b, 0xf0, 0xc7, 0xc8, 0x8c, 0x48, 0xa3, 0x82, 0x8e, 0xc4, 0x99, 0xa4, 0xed, 0x09, - 0xbd, 0x5d, 0xda, 0x87, 0x12, 0xed, 0x1b, 0xa7, 0x4d, 0x27, 0xe7, 0xf0, 0x19, 0xcc, 0xed, 0x13, - 0x16, 0x57, 0xdd, 0x7c, 0xa4, 0x15, 0x5a, 0x58, 0x3e, 0x55, 0x8b, 0x85, 0x5a, 0x44, 0xcc, 0x92, - 0x94, 0x95, 0xa9, 0x53, 0xf9, 0x5c, 0x2d, 0x2d, 0xc1, 0xf9, 0x88, 0x29, 0x2f, 0x73, 0x46, 0xe5, - 0x93, 0xdd, 0x7f, 0xbc, 0xd8, 0xd0, 0xbe, 0x7a, 0xb1, 0xa1, 0xfd, 0xfb, 0xc5, 0x86, 0xf6, 0x93, - 0x9b, 0x2f, 0xf9, 0x0b, 0x82, 0xcc, 0x1f, 0x25, 0x60, 0x6a, 0x5b, 0x8e, 0x4d, 0x3c, 0xd6, 0x9d, - 0x11, 0xc1, 0x7f, 0xf3, 0xbf, 0x01, 0x00, 0x00, 0xff, 0xff, 0xf8, 0x86, 0xe4, 0x0d, 0xb3, 0x20, - 0x00, 0x00, + 0xf5, 0xe7, 0x92, 0x94, 0x44, 0x1e, 0xd9, 0x12, 0x35, 0xd6, 0x65, 0xc5, 0x38, 0x82, 0xb2, 0xff, + 0xbf, 0x0d, 0xd5, 0x4e, 0x48, 0x48, 0x46, 0xe2, 0xc2, 0x49, 0x53, 0x28, 0x8a, 0x2d, 0x39, 0xb6, + 0x6c, 0x75, 0xed, 0xb6, 0x48, 0xeb, 0xb6, 0x18, 0x2e, 0x87, 0xe4, 0x86, 0x7b, 0x19, 0xef, 0xce, + 0x2a, 0x90, 0x81, 0x3e, 0x14, 0x2d, 0xfa, 0x11, 0xfa, 0xd0, 0xaf, 0x51, 0x14, 0x7d, 0xec, 0x53, + 0x2f, 0x8f, 0x41, 0xbf, 0x40, 0x0b, 0xbf, 0x14, 0xe8, 0xa7, 0x28, 0xe6, 0xb2, 0x57, 0xae, 0x64, + 0xa7, 0x94, 0x15, 0xb4, 0x2f, 0xf6, 0xce, 0x99, 0x33, 0xe7, 0x9c, 0x39, 0x73, 0x2e, 0xbf, 0x19, + 0x0a, 0xae, 0x07, 0x84, 0xfa, 0x21, 0x09, 0x8e, 0x49, 0xd0, 0x15, 0x9f, 0x36, 0xf3, 0x83, 0x93, + 0xcc, 0x67, 0x87, 0x06, 0x3e, 0xf3, 0x11, 0xa4, 0x94, 0xf6, 0xc3, 0xa1, 0xcd, 0x46, 0x51, 0xaf, + 0x63, 0xf9, 0x6e, 0x17, 0x07, 0x43, 0x9f, 0x06, 0xfe, 0x17, 0xe2, 0xe3, 0x3d, 0xab, 0xdf, 0x3d, + 0xde, 0xe9, 0xd2, 0xf1, 0xb0, 0x8b, 0xa9, 0x1d, 0x76, 0x31, 0xa5, 0x8e, 0x6d, 0x61, 0x66, 0xfb, + 0x5e, 0xf7, 0x78, 0x1b, 0x3b, 0x74, 0x84, 0xb7, 0xbb, 0x43, 0xe2, 0x91, 0x00, 0x33, 0xd2, 0x97, + 0x92, 0xdb, 0x6f, 0x0d, 0x7d, 0x7f, 0xe8, 0x90, 0xae, 0x18, 0xf5, 0xa2, 0x41, 0x97, 0xb8, 0x94, + 0x29, 0xb5, 0xc6, 0xbf, 0x2e, 0xc1, 0xe2, 0x21, 0xf6, 0xec, 0x01, 0x09, 0x99, 0x49, 0x9e, 0x47, + 0x24, 0x64, 0xe8, 0x19, 0xd4, 0xb9, 0x31, 0xba, 0xb6, 0xa9, 0x6d, 0xcd, 0xef, 0x1c, 0x74, 0x52, + 0x6b, 0x3a, 0xb1, 0x35, 0xe2, 0xe3, 0x67, 0x56, 0xbf, 0x73, 0xbc, 0xd3, 0xa1, 0xe3, 0x61, 0x87, + 0x5b, 0xd3, 0xc9, 0x58, 0xd3, 0x89, 0xad, 0xe9, 0x98, 0xc9, 0xb6, 0x4c, 0x21, 0x15, 0xb5, 0xa1, + 0x11, 0x90, 0x63, 0x3b, 0xb4, 0x7d, 0x4f, 0xaf, 0x6e, 0x6a, 0x5b, 0x4d, 0x33, 0x19, 0x23, 0x1d, + 0xe6, 0x3c, 0x7f, 0x0f, 0x5b, 0x23, 0xa2, 0xd7, 0x36, 0xb5, 0xad, 0x86, 0x19, 0x0f, 0xd1, 0x26, + 0xcc, 0x63, 0x4a, 0x1f, 0xe2, 0x1e, 0x71, 0x1e, 0x90, 0x13, 0xbd, 0x2e, 0x16, 0x66, 0x49, 0x7c, + 0x2d, 0xa6, 0xf4, 0x11, 0x76, 0x89, 0x3e, 0x23, 0x66, 0xe3, 0x21, 0xba, 0x0a, 0x4d, 0x0f, 0xbb, + 0x24, 0xa4, 0xd8, 0x22, 0x7a, 0x43, 0xcc, 0xa5, 0x04, 0xf4, 0x73, 0x58, 0xca, 0x18, 0xfe, 0xc4, + 0x8f, 0x02, 0x8b, 0xe8, 0x20, 0xb6, 0xfe, 0x78, 0xba, 0xad, 0xef, 0x16, 0xc5, 0x9a, 0x93, 0x9a, + 0xd0, 0x4f, 0x61, 0x46, 0x9c, 0xbc, 0x3e, 0xbf, 0x59, 0x3b, 0x57, 0x6f, 0x4b, 0xb1, 0xc8, 0x83, + 0x39, 0xea, 0x44, 0x43, 0xdb, 0x0b, 0xf5, 0x4b, 0x42, 0xc3, 0xd3, 0xe9, 0x34, 0xec, 0xf9, 0xde, + 0xc0, 0x1e, 0x1e, 0x62, 0x0f, 0x0f, 0x89, 0x4b, 0x3c, 0x76, 0x24, 0x84, 0x9b, 0xb1, 0x12, 0xf4, + 0x02, 0x5a, 0xe3, 0x28, 0x64, 0xbe, 0x6b, 0xbf, 0x20, 0x8f, 0x29, 0x5f, 0x1b, 0xea, 0x97, 0x85, + 0x37, 0x1f, 0x4d, 0xa7, 0xf8, 0x41, 0x41, 0xaa, 0x39, 0xa1, 0x87, 0x07, 0xc9, 0x38, 0xea, 0x91, + 0x1f, 0x90, 0x40, 0x44, 0xd7, 0x82, 0x0c, 0x92, 0x0c, 0x49, 0x86, 0x91, 0xad, 0x46, 0xa1, 0xbe, + 0xb8, 0x59, 0x93, 0x61, 0x94, 0x90, 0xd0, 0x16, 0x2c, 0x1e, 0x93, 0xc0, 0x1e, 0x9c, 0x3c, 0xb1, + 0x87, 0x1e, 0x66, 0x51, 0x40, 0xf4, 0x96, 0x08, 0xc5, 0x22, 0x19, 0xb9, 0x70, 0x79, 0x44, 0x1c, + 0x97, 0xbb, 0x7c, 0x2f, 0x20, 0xfd, 0x50, 0x5f, 0x12, 0xfe, 0xdd, 0x9f, 0xfe, 0x04, 0x85, 0x38, + 0x33, 0x2f, 0x9d, 0x1b, 0xe6, 0xf9, 0xa6, 0xca, 0x14, 0x99, 0x23, 0x48, 0x1a, 0x56, 0x20, 0xa3, + 0xeb, 0xb0, 0xc0, 0x02, 0x6c, 0x8d, 0x6d, 0x6f, 0x78, 0x48, 0xd8, 0xc8, 0xef, 0xeb, 0x57, 0x84, + 0x27, 0x0a, 0x54, 0x64, 0x01, 0x22, 0x1e, 0xee, 0x39, 0xa4, 0x2f, 0x63, 0xf1, 0xe9, 0x09, 0x25, + 0xa1, 0xbe, 0x2c, 0x76, 0x71, 0xab, 0x93, 0xa9, 0x50, 0x85, 0x02, 0xd1, 0xb9, 0x3b, 0xb1, 0xea, + 0xae, 0xc7, 0x82, 0x13, 0xb3, 0x44, 0x1c, 0x1a, 0xc3, 0x3c, 0xdf, 0x47, 0x1c, 0x0a, 0x2b, 0x22, + 0x14, 0xee, 0x4f, 0xe7, 0xa3, 0x83, 0x54, 0xa0, 0x99, 0x95, 0x8e, 0x3a, 0x80, 0x46, 0x38, 0x3c, + 0x8c, 0x1c, 0x66, 0x53, 0x87, 0x48, 0x33, 0x42, 0x7d, 0x55, 0xb8, 0xa9, 0x64, 0x06, 0x3d, 0x00, + 0x08, 0xc8, 0x20, 0xe6, 0x5b, 0x13, 0x3b, 0xbf, 0x79, 0xd6, 0xce, 0xcd, 0x84, 0x5b, 0xee, 0x38, + 0xb3, 0x9c, 0x2b, 0xe7, 0xdb, 0x20, 0x16, 0x53, 0xd9, 0x2e, 0xd2, 0x5a, 0x17, 0x21, 0x56, 0x32, + 0xc3, 0x63, 0x51, 0x51, 0x45, 0xd1, 0x5a, 0x97, 0xd1, 0x9a, 0x21, 0xb5, 0xef, 0xc2, 0xda, 0x29, + 0xae, 0x46, 0x2d, 0xa8, 0x8d, 0xc9, 0x89, 0x28, 0xd1, 0x4d, 0x93, 0x7f, 0xa2, 0x65, 0x98, 0x39, + 0xc6, 0x4e, 0x44, 0x44, 0x51, 0x6d, 0x98, 0x72, 0x70, 0xa7, 0xfa, 0x6d, 0xad, 0xfd, 0x6b, 0x0d, + 0x16, 0x0b, 0x86, 0x97, 0xac, 0xff, 0x49, 0x76, 0xfd, 0x39, 0x84, 0xf1, 0xe0, 0x29, 0x0e, 0x86, + 0x84, 0x65, 0x0c, 0x31, 0xfe, 0xa6, 0x81, 0x5e, 0xf0, 0xe8, 0x0f, 0x6d, 0x36, 0xba, 0x67, 0x3b, + 0x24, 0x44, 0xb7, 0x61, 0x2e, 0x90, 0x34, 0xd5, 0x78, 0xde, 0x3a, 0xe3, 0x20, 0x0e, 0x2a, 0x66, + 0xcc, 0x8d, 0x3e, 0x86, 0x86, 0x4b, 0x18, 0xee, 0x63, 0x86, 0x95, 0xed, 0x9b, 0x65, 0x2b, 0xb9, + 0x96, 0x43, 0xc5, 0x77, 0x50, 0x31, 0x93, 0x35, 0xe8, 0x7d, 0x98, 0xb1, 0x46, 0x91, 0x37, 0x16, + 0x2d, 0x67, 0x7e, 0xe7, 0xed, 0xd3, 0x16, 0xef, 0x71, 0xa6, 0x83, 0x8a, 0x29, 0xb9, 0x3f, 0x99, + 0x85, 0x3a, 0xc5, 0x01, 0x33, 0xee, 0xc1, 0x72, 0x99, 0x0a, 0xde, 0xe7, 0xac, 0x11, 0xb1, 0xc6, + 0x61, 0xe4, 0x2a, 0x37, 0x27, 0x63, 0x84, 0xa0, 0x1e, 0xda, 0x2f, 0xa4, 0xab, 0x6b, 0xa6, 0xf8, + 0x36, 0xbe, 0x05, 0x4b, 0x13, 0xda, 0xf8, 0xa1, 0x4a, 0xdb, 0xb8, 0x84, 0x4b, 0x4a, 0xb5, 0x11, + 0xc1, 0xca, 0x53, 0xe1, 0x8b, 0xa4, 0xd8, 0x5f, 0x44, 0xe7, 0x36, 0x0e, 0x60, 0xb5, 0xa8, 0x36, + 0xa4, 0xbe, 0x17, 0x12, 0x1e, 0xfa, 0xa2, 0x3a, 0xda, 0xa4, 0x9f, 0xce, 0x0a, 0x2b, 0x1a, 0x66, + 0xc9, 0x8c, 0xf1, 0x8b, 0x2a, 0xac, 0x9a, 0x24, 0xf4, 0x9d, 0x63, 0x12, 0x97, 0xae, 0x8b, 0x01, + 0x1f, 0x3f, 0x86, 0x1a, 0xa6, 0x54, 0x85, 0xc9, 0xfd, 0x73, 0x6b, 0xef, 0x26, 0x97, 0x8a, 0xde, + 0x85, 0x25, 0xec, 0xf6, 0xec, 0x61, 0xe4, 0x47, 0x61, 0xbc, 0x2d, 0x11, 0x54, 0x4d, 0x73, 0x72, + 0xc2, 0xb0, 0x60, 0x6d, 0xc2, 0x05, 0xca, 0x9d, 0x59, 0x88, 0xa4, 0x15, 0x20, 0x52, 0xa9, 0x92, + 0xea, 0x69, 0x4a, 0xfe, 0xac, 0x41, 0x2b, 0x4d, 0x1d, 0x25, 0xfe, 0x2a, 0x34, 0x5d, 0x45, 0x0b, + 0x75, 0x4d, 0xd4, 0xa7, 0x94, 0x90, 0x47, 0x4b, 0xd5, 0x22, 0x5a, 0x5a, 0x85, 0x59, 0x09, 0x66, + 0xd5, 0xc6, 0xd4, 0x28, 0x67, 0x72, 0xbd, 0x60, 0xf2, 0x06, 0x40, 0x98, 0xd4, 0x2f, 0x7d, 0x56, + 0xcc, 0x66, 0x28, 0xc8, 0x80, 0x4b, 0xb2, 0xb7, 0x9a, 0x24, 0x8c, 0x1c, 0xa6, 0xcf, 0x09, 0x8e, + 0x1c, 0xcd, 0xf0, 0x61, 0xf1, 0xa1, 0xcd, 0xf7, 0x30, 0x08, 0x2f, 0x26, 0xd8, 0x3f, 0x80, 0x3a, + 0x57, 0xc6, 0x37, 0xd6, 0x0b, 0xb0, 0x67, 0x8d, 0x48, 0xec, 0xab, 0x64, 0xcc, 0xd3, 0x98, 0xe1, + 0x61, 0xa8, 0x57, 0x05, 0x5d, 0x7c, 0x1b, 0x7f, 0xa8, 0x4a, 0x4b, 0x77, 0x29, 0x0d, 0xbf, 0x79, + 0x40, 0x5d, 0xde, 0xe2, 0x6b, 0x93, 0x2d, 0xbe, 0x60, 0xf2, 0xd7, 0x69, 0xf1, 0xe7, 0xd4, 0xa6, + 0x8c, 0x08, 0xe6, 0x76, 0x29, 0xe5, 0x86, 0xa0, 0x6d, 0xa8, 0x63, 0x4a, 0xa5, 0xc3, 0x0b, 0x15, + 0x59, 0xb1, 0xf0, 0xff, 0x95, 0x49, 0x82, 0xb5, 0x7d, 0x1b, 0x9a, 0x09, 0xe9, 0x55, 0x6a, 0x9b, + 0x59, 0xb5, 0x9b, 0x00, 0x12, 0xc3, 0xde, 0xf7, 0x06, 0x3e, 0x3f, 0x52, 0x1e, 0xec, 0x6a, 0xa9, + 0xf8, 0x36, 0xee, 0xc4, 0x1c, 0xc2, 0xb6, 0x77, 0x61, 0xc6, 0x66, 0xc4, 0x8d, 0x8d, 0x5b, 0xcd, + 0x1a, 0x97, 0x0a, 0x32, 0x25, 0x93, 0xf1, 0x97, 0x06, 0xac, 0xf3, 0x13, 0x7b, 0x22, 0xd2, 0x64, + 0x97, 0xd2, 0x4f, 0x09, 0xc3, 0xb6, 0x13, 0x7e, 0x2f, 0x22, 0xc1, 0xc9, 0x1b, 0x0e, 0x8c, 0x21, + 0xcc, 0xca, 0x2c, 0x53, 0xf5, 0xee, 0xdc, 0xaf, 0x33, 0x4a, 0x7c, 0x7a, 0x87, 0xa9, 0xbd, 0x99, + 0x3b, 0x4c, 0xd9, 0x9d, 0xa2, 0x7e, 0x41, 0x77, 0x8a, 0xd3, 0xaf, 0x95, 0x99, 0xcb, 0xea, 0x6c, + 0xfe, 0xb2, 0x5a, 0x02, 0xd5, 0xe7, 0x5e, 0x17, 0xaa, 0x37, 0x4a, 0xa1, 0xba, 0x5b, 0x9a, 0xc7, + 0x4d, 0xe1, 0xee, 0xef, 0x64, 0x23, 0xf0, 0xd4, 0x58, 0x9b, 0x06, 0xb4, 0xc3, 0x1b, 0x05, 0xed, + 0xdf, 0xcf, 0x81, 0x70, 0x79, 0x0d, 0x7e, 0xff, 0xf5, 0xf6, 0x74, 0x06, 0x1c, 0xff, 0x9f, 0x03, + 0xcf, 0xbf, 0x12, 0x98, 0x89, 0xfa, 0xa9, 0x0f, 0x92, 0x86, 0xce, 0xfb, 0x10, 0x6f, 0xad, 0xaa, + 0x68, 0xf1, 0x6f, 0x74, 0x13, 0xea, 0xdc, 0xc9, 0x0a, 0xd4, 0xae, 0x65, 0xfd, 0xc9, 0x4f, 0x62, + 0x97, 0xd2, 0x27, 0x94, 0x58, 0xa6, 0x60, 0x42, 0x77, 0xa0, 0x99, 0x04, 0xbe, 0xca, 0xac, 0xab, + 0xd9, 0x15, 0x49, 0x9e, 0xc4, 0xcb, 0x52, 0x76, 0xbe, 0xb6, 0x6f, 0x07, 0xc4, 0x12, 0x90, 0x6f, + 0x66, 0x72, 0xed, 0xa7, 0xf1, 0x64, 0xb2, 0x36, 0x61, 0x47, 0xdb, 0x30, 0x2b, 0xdf, 0x0d, 0x44, + 0x06, 0xcd, 0xef, 0xac, 0x4f, 0x16, 0xd3, 0x78, 0x95, 0x62, 0x34, 0xfe, 0xa4, 0xc1, 0x3b, 0x69, + 0x40, 0xc4, 0xd9, 0x14, 0xa3, 0xee, 0x6f, 0xbe, 0xe3, 0x5e, 0x87, 0x05, 0x01, 0xf3, 0xd3, 0xe7, + 0x03, 0xf9, 0x92, 0x55, 0xa0, 0x1a, 0xbf, 0xd7, 0xe0, 0xda, 0xe4, 0x3e, 0xf6, 0x46, 0x38, 0x60, + 0xc9, 0xf1, 0x5e, 0xc4, 0x5e, 0xe2, 0x86, 0x57, 0x4d, 0x1b, 0x5e, 0x6e, 0x7f, 0xb5, 0xfc, 0xfe, + 0x8c, 0x3f, 0x56, 0x61, 0x3e, 0x13, 0x40, 0x65, 0x0d, 0x93, 0x03, 0x3e, 0x11, 0xb7, 0xe2, 0x62, + 0x27, 0x9a, 0x42, 0xd3, 0xcc, 0x50, 0xd0, 0x18, 0x80, 0xe2, 0x00, 0xbb, 0x84, 0x91, 0x80, 0x57, + 0x72, 0x9e, 0xf1, 0x0f, 0xa6, 0xaf, 0x2e, 0x47, 0xb1, 0x4c, 0x33, 0x23, 0x9e, 0x23, 0x56, 0xa1, + 0x3a, 0x54, 0xf5, 0x5b, 0x8d, 0xd0, 0x97, 0xb0, 0x30, 0xb0, 0x1d, 0x72, 0x94, 0x1a, 0x32, 0x2b, + 0x0c, 0x79, 0x3c, 0xbd, 0x21, 0xf7, 0xb2, 0x72, 0xcd, 0x82, 0x1a, 0xe3, 0x06, 0xb4, 0x8a, 0xf9, + 0xc4, 0x8d, 0xb4, 0x5d, 0x3c, 0x4c, 0xbc, 0xa5, 0x46, 0x06, 0x82, 0x56, 0x31, 0x7f, 0x8c, 0xbf, + 0x57, 0x61, 0x25, 0x11, 0xb7, 0xeb, 0x79, 0x7e, 0xe4, 0x59, 0xe2, 0x29, 0xae, 0xf4, 0x2c, 0x96, + 0x61, 0x86, 0xd9, 0xcc, 0x49, 0x80, 0x8f, 0x18, 0xf0, 0xde, 0xc5, 0x7c, 0xdf, 0x61, 0x36, 0x55, + 0x07, 0x1c, 0x0f, 0xe5, 0xd9, 0x3f, 0x8f, 0xec, 0x80, 0xf4, 0x45, 0x25, 0x68, 0x98, 0xc9, 0x98, + 0xcf, 0x71, 0x54, 0x23, 0x60, 0xbc, 0x74, 0x66, 0x32, 0x16, 0x71, 0xef, 0x3b, 0x0e, 0xb1, 0xb8, + 0x3b, 0x32, 0x40, 0xbf, 0x40, 0x15, 0x17, 0x08, 0x16, 0xd8, 0xde, 0x50, 0xc1, 0x7c, 0x35, 0xe2, + 0x76, 0xe2, 0x20, 0xc0, 0x27, 0x7a, 0x43, 0x38, 0x40, 0x0e, 0xd0, 0x47, 0x50, 0x73, 0x31, 0x55, + 0x8d, 0xee, 0x46, 0xae, 0x3a, 0x94, 0x79, 0xa0, 0x73, 0x88, 0xa9, 0xec, 0x04, 0x7c, 0x59, 0xfb, + 0x03, 0x68, 0xc4, 0x84, 0xaf, 0x05, 0x09, 0xbf, 0x80, 0xcb, 0xb9, 0xe2, 0x83, 0x3e, 0x87, 0xd5, + 0x34, 0xa2, 0xb2, 0x0a, 0x15, 0x08, 0x7c, 0xe7, 0x95, 0x96, 0x99, 0xa7, 0x08, 0x30, 0x9e, 0xc3, + 0x12, 0x0f, 0x19, 0x91, 0xf8, 0x17, 0x74, 0xb5, 0xf9, 0x10, 0x9a, 0x89, 0xca, 0xd2, 0x98, 0x69, + 0x43, 0xe3, 0x38, 0x7e, 0x22, 0x95, 0x77, 0x9b, 0x64, 0x6c, 0xec, 0x02, 0xca, 0xda, 0xab, 0x3a, + 0xd0, 0xcd, 0x3c, 0x28, 0x5e, 0x29, 0xb6, 0x1b, 0xc1, 0x1e, 0x63, 0xe2, 0xdf, 0x55, 0x61, 0x71, + 0xdf, 0x16, 0xaf, 0x1c, 0x17, 0x54, 0xe4, 0x6e, 0x40, 0x2b, 0x8c, 0x7a, 0xae, 0xdf, 0x8f, 0x1c, + 0xa2, 0x40, 0x81, 0xea, 0xf4, 0x13, 0xf4, 0xb3, 0x8a, 0x1f, 0x77, 0x16, 0xc5, 0x6c, 0xa4, 0x6e, + 0xb8, 0xe2, 0x1b, 0x7d, 0x04, 0xeb, 0x8f, 0xc8, 0x97, 0x6a, 0x3f, 0xfb, 0x8e, 0xdf, 0xeb, 0xd9, + 0xde, 0x30, 0x56, 0x32, 0x23, 0x94, 0x9c, 0xce, 0x50, 0x06, 0x15, 0x67, 0x4b, 0xa1, 0xa2, 0xf1, + 0x4b, 0x0d, 0x5a, 0xa9, 0xd7, 0x94, 0xdf, 0x6f, 0xcb, 0xfc, 0x90, 0x5e, 0xbf, 0x96, 0xf5, 0x7a, + 0x91, 0xf5, 0x3f, 0x4f, 0x8d, 0x4b, 0xd9, 0xd4, 0xf8, 0xa7, 0x06, 0x2b, 0xfb, 0x36, 0x8b, 0x8b, + 0x92, 0xfd, 0xdf, 0x76, 0x82, 0x25, 0xfe, 0xae, 0x97, 0xfb, 0xbb, 0x03, 0xab, 0xc5, 0x8d, 0x2a, + 0xa7, 0x2f, 0xc3, 0x0c, 0x3f, 0xf9, 0xf8, 0x3d, 0x40, 0x0e, 0x76, 0xbe, 0x6a, 0xc2, 0x52, 0xda, + 0xd0, 0xf9, 0xbf, 0xb6, 0x45, 0xd0, 0x63, 0x68, 0xed, 0xab, 0xdf, 0xe3, 0xe2, 0x77, 0x18, 0x74, + 0xd6, 0xc3, 0x66, 0xfb, 0x6a, 0xf9, 0xa4, 0x54, 0x6d, 0x54, 0x90, 0x05, 0xeb, 0x45, 0x81, 0xe9, + 0x1b, 0xea, 0xff, 0x9f, 0x21, 0x39, 0xe1, 0x7a, 0x95, 0x8a, 0x2d, 0x0d, 0x7d, 0x0e, 0x0b, 0xf9, + 0x97, 0x3e, 0x94, 0xab, 0x70, 0xa5, 0x8f, 0x8f, 0x6d, 0xe3, 0x2c, 0x96, 0xc4, 0xfe, 0x67, 0x1c, + 0x4e, 0xe7, 0x9e, 0xbd, 0x90, 0x91, 0x07, 0xfb, 0x65, 0xcf, 0x82, 0xed, 0xff, 0x3b, 0x93, 0x27, + 0x91, 0xfe, 0x21, 0x34, 0xe2, 0x67, 0xa2, 0xbc, 0x9b, 0x0b, 0x8f, 0x47, 0xed, 0x56, 0x5e, 0xde, + 0x20, 0x34, 0x2a, 0xe8, 0x63, 0xb9, 0x78, 0x97, 0xd2, 0x92, 0xc5, 0x99, 0xc7, 0x91, 0xf6, 0x95, + 0x92, 0x07, 0x09, 0xa3, 0x82, 0xbe, 0x0b, 0xf3, 0xfc, 0xeb, 0x48, 0xfd, 0x12, 0xb6, 0xda, 0x91, + 0x3f, 0xbc, 0x76, 0xe2, 0x1f, 0x5e, 0x3b, 0x77, 0x5d, 0xca, 0x4e, 0xda, 0x25, 0x2f, 0x06, 0x4a, + 0xc0, 0x33, 0xb8, 0xbc, 0x4f, 0x58, 0x0a, 0xf0, 0xd1, 0xb5, 0xd7, 0xba, 0x06, 0xb5, 0x8d, 0x22, + 0xdb, 0xe4, 0x1d, 0xc1, 0xa8, 0xa0, 0xdf, 0x68, 0x70, 0x65, 0x9f, 0xb0, 0x22, 0x64, 0x46, 0xef, + 0x95, 0x2b, 0x39, 0x05, 0x5a, 0xb7, 0x1f, 0x4d, 0x9b, 0xd9, 0x79, 0xb1, 0x46, 0x05, 0xfd, 0x56, + 0x83, 0xb5, 0x8c, 0x61, 0x59, 0x0c, 0x8c, 0xb6, 0xcf, 0x36, 0xae, 0x04, 0x2f, 0xb7, 0x3f, 0x9b, + 0xf2, 0x07, 0xce, 0x8c, 0x48, 0xa3, 0x82, 0x8e, 0xc4, 0x99, 0xa4, 0x2d, 0x0f, 0xbd, 0x5d, 0xda, + 0xdb, 0x12, 0xed, 0x1b, 0xa7, 0x4d, 0x27, 0xe7, 0xf0, 0x19, 0xcc, 0xef, 0x13, 0x16, 0xd7, 0xe7, + 0x7c, 0xa4, 0x15, 0xda, 0x62, 0x3e, 0x55, 0x8b, 0x25, 0x5d, 0x44, 0xcc, 0x92, 0x94, 0x95, 0xa9, + 0x53, 0xf9, 0x5c, 0x2d, 0x2d, 0xd6, 0xf9, 0x88, 0x29, 0x2f, 0x73, 0x46, 0xe5, 0x93, 0xdd, 0xbf, + 0xbe, 0xdc, 0xd0, 0xbe, 0x7a, 0xb9, 0xa1, 0xfd, 0xe3, 0xe5, 0x86, 0xf6, 0xa3, 0x5b, 0xaf, 0xf8, + 0xab, 0x84, 0xcc, 0x1f, 0x3a, 0x60, 0x6a, 0x5b, 0x8e, 0x4d, 0x3c, 0xd6, 0x9b, 0x15, 0xc1, 0x7f, + 0xeb, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0xf2, 0x91, 0xe2, 0xd9, 0x07, 0x21, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -4679,6 +4695,16 @@ func (m *GitFilesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if m.NoRevisionCache { + i-- + if m.NoRevisionCache { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x30 + } if m.NewGitFileGlobbingEnabled { i-- if m.NewGitFileGlobbingEnabled { @@ -4800,6 +4826,16 @@ func (m *GitDirectoriesRequest) MarshalToSizedBuffer(dAtA []byte) (int, error) { i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if m.NoRevisionCache { + i-- + if m.NoRevisionCache { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x20 + } if len(m.Revision) > 0 { i -= len(m.Revision) copy(dAtA[i:], m.Revision) @@ -5686,6 +5722,9 @@ func (m *GitFilesRequest) Size() (n int) { if m.NewGitFileGlobbingEnabled { n += 2 } + if m.NoRevisionCache { + n += 2 + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -5733,6 +5772,9 @@ func (m *GitDirectoriesRequest) Size() (n int) { if l > 0 { n += 1 + l + sovRepository(uint64(l)) } + if m.NoRevisionCache { + n += 2 + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -10874,6 +10916,26 @@ func (m *GitFilesRequest) Unmarshal(dAtA []byte) error { } } m.NewGitFileGlobbingEnabled = bool(v != 0) + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NoRevisionCache", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRepository + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.NoRevisionCache = bool(v != 0) default: iNdEx = preIndex skippy, err := skipRepository(dAtA[iNdEx:]) @@ -11192,6 +11254,26 @@ func (m *GitDirectoriesRequest) Unmarshal(dAtA []byte) error { } m.Revision = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field NoRevisionCache", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowRepository + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.NoRevisionCache = bool(v != 0) default: iNdEx = preIndex skippy, err := skipRepository(dAtA[iNdEx:]) diff --git a/reposerver/cache/cache.go b/reposerver/cache/cache.go index 79d3a02b62750c..4437bd3ac0dd7f 100644 --- a/reposerver/cache/cache.go +++ b/reposerver/cache/cache.go @@ -12,7 +12,6 @@ import ( "github.com/argoproj/gitops-engine/pkg/utils/text" "github.com/go-git/go-git/v5/plumbing" - "github.com/redis/go-redis/v9" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -44,7 +43,7 @@ func NewCache(cache *cacheutil.Cache, repoCacheExpiration time.Duration, revisio return &Cache{cache, repoCacheExpiration, revisionCacheExpiration} } -func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) func() (*Cache, error) { +func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...cacheutil.Options) func() (*Cache, error) { var repoCacheExpiration time.Duration var revisionCacheExpiration time.Duration @@ -225,6 +224,12 @@ func LogDebugManifestCacheKeyFields(message string, reason string, revision stri } } +func (c *Cache) SetNewRevisionManifests(newRevision string, revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, clusterInfo ClusterRuntimeInfo, namespace string, trackingMethod string, appLabelKey string, appName string, refSourceCommitSHAs ResolvedRevisions) error { + oldKey := manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs) + newKey := manifestCacheKey(newRevision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs) + return c.cache.RenameItem(oldKey, newKey, c.repoCacheExpiration) +} + func (c *Cache) GetManifests(revision string, appSrc *appv1.ApplicationSource, srcRefs appv1.RefTargetRevisionMapping, clusterInfo ClusterRuntimeInfo, namespace string, trackingMethod string, appLabelKey string, appName string, res *CachedManifestResponse, refSourceCommitSHAs ResolvedRevisions) error { err := c.cache.GetItem(manifestCacheKey(revision, appSrc, srcRefs, namespace, trackingMethod, appLabelKey, appName, clusterInfo, refSourceCommitSHAs), res) diff --git a/reposerver/cache/mocks/reposervercache.go b/reposerver/cache/mocks/reposervercache.go new file mode 100644 index 00000000000000..0e49b5816178eb --- /dev/null +++ b/reposerver/cache/mocks/reposervercache.go @@ -0,0 +1,71 @@ +package mocks + +import ( + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + cacheutil "github.com/argoproj/argo-cd/v2/util/cache" + cacheutilmocks "github.com/argoproj/argo-cd/v2/util/cache/mocks" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/mock" +) + +type MockCacheType int + +const ( + MockCacheTypeRedis MockCacheType = iota + MockCacheTypeInMem +) + +type MockRepoCache struct { + mock.Mock + RedisClient *cacheutilmocks.MockCacheClient + StopRedisCallback func() +} + +type MockCacheOptions struct { + RepoCacheExpiration time.Duration + RevisionCacheExpiration time.Duration + ReadDelay time.Duration + WriteDelay time.Duration +} + +type CacheCallCounts struct { + ExternalSets int + ExternalGets int + ExternalDeletes int +} + +// Checks that the cache was called the expected number of times +func (mockCache *MockRepoCache) AssertCacheCalledTimes(t *testing.T, calls *CacheCallCounts) { + mockCache.RedisClient.AssertNumberOfCalls(t, "Get", calls.ExternalGets) + mockCache.RedisClient.AssertNumberOfCalls(t, "Set", calls.ExternalSets) + mockCache.RedisClient.AssertNumberOfCalls(t, "Delete", calls.ExternalDeletes) +} + +func (mockCache *MockRepoCache) ConfigureDefaultCallbacks() { + mockCache.RedisClient.On("Get", mock.Anything, mock.Anything).Return(nil) + mockCache.RedisClient.On("Set", mock.Anything).Return(nil) + mockCache.RedisClient.On("Delete", mock.Anything).Return(nil) +} + +func NewInMemoryRedis() (*redis.Client, func()) { + cacheutil.NewInMemoryCache(5 * time.Second) + mr, err := miniredis.Run() + if err != nil { + panic(err) + } + return redis.NewClient(&redis.Options{Addr: mr.Addr()}), mr.Close +} + +func NewMockRepoCache(cacheOpts *MockCacheOptions) *MockRepoCache { + redisClient, stopRedis := NewInMemoryRedis() + redisCacheClient := &cacheutilmocks.MockCacheClient{ + ReadDelay: cacheOpts.ReadDelay, + WriteDelay: cacheOpts.WriteDelay, + BaseCache: cacheutil.NewRedisCache(redisClient, cacheOpts.RepoCacheExpiration, cacheutil.RedisCompressionNone)} + newMockCache := &MockRepoCache{RedisClient: redisCacheClient, StopRedisCallback: stopRedis} + newMockCache.ConfigureDefaultCallbacks() + return newMockCache +} diff --git a/reposerver/gpgwatcher.go b/reposerver/gpgwatcher.go index 9c2c9be790813d..5b43d6a24ac76a 100644 --- a/reposerver/gpgwatcher.go +++ b/reposerver/gpgwatcher.go @@ -19,7 +19,7 @@ func StartGPGWatcher(sourcePath string) error { forceSync := false watcher, err := fsnotify.NewWatcher() if err != nil { - return err + return fmt.Errorf("failed to create fsnotify Watcher: %w", err) } defer func(watcher *fsnotify.Watcher) { if err = watcher.Close(); err != nil { @@ -83,7 +83,7 @@ func StartGPGWatcher(sourcePath string) error { err = watcher.Add(sourcePath) if err != nil { - return err + return fmt.Errorf("failed to add a new source to the watcher: %w", err) } <-done return fmt.Errorf("Abnormal termination of GPG watcher, refusing to continue.") diff --git a/reposerver/metrics/githandlers_test.go b/reposerver/metrics/githandlers_test.go new file mode 100644 index 00000000000000..6eaeeca82cc369 --- /dev/null +++ b/reposerver/metrics/githandlers_test.go @@ -0,0 +1,122 @@ +package metrics + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/sync/semaphore" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + +func TestEdgeCasesAndErrorHandling(t *testing.T) { + tests := []struct { + name string + setup func() + teardown func() + testFunc func(t *testing.T) + }{ + { + name: "lsRemoteParallelismLimitSemaphore is nil", + testFunc: func(t *testing.T) { + lsRemoteParallelismLimitSemaphore = nil + assert.NotPanics(t, func() { + NewGitClientEventHandlers(&MetricsServer{}) + }) + }, + }, + { + name: "lsRemoteParallelismLimitSemaphore is not nil", + setup: func() { + lsRemoteParallelismLimitSemaphore = semaphore.NewWeighted(1) + }, + teardown: func() { + lsRemoteParallelismLimitSemaphore = nil + }, + testFunc: func(t *testing.T) { + assert.NotPanics(t, func() { + NewGitClientEventHandlers(&MetricsServer{}) + }) + }, + }, + { + name: "lsRemoteParallelismLimitSemaphore is not nil and Acquire returns error", + setup: func() { + lsRemoteParallelismLimitSemaphore = semaphore.NewWeighted(1) + }, + teardown: func() { + lsRemoteParallelismLimitSemaphore = nil + }, + testFunc: func(t *testing.T) { + assert.NotPanics(t, func() { + NewGitClientEventHandlers(&MetricsServer{}) + }) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + if tt.teardown != nil { + defer tt.teardown() + } + tt.testFunc(t) + }) + } +} + +func TestSemaphoreFunctionality(t *testing.T) { + os.Setenv("ARGOCD_GIT_LSREMOTE_PARALLELISM_LIMIT", "1") + + tests := []struct { + name string + setup func() + teardown func() + testFunc func(t *testing.T) + }{ + { + name: "lsRemoteParallelismLimitSemaphore is not nil", + setup: func() { + lsRemoteParallelismLimitSemaphore = semaphore.NewWeighted(1) + }, + teardown: func() { + lsRemoteParallelismLimitSemaphore = nil + }, + testFunc: func(t *testing.T) { + assert.NotPanics(t, func() { + NewGitClientEventHandlers(&MetricsServer{}) + }) + }, + }, + { + name: "lsRemoteParallelismLimitSemaphore is not nil and Acquire returns error", + setup: func() { + lsRemoteParallelismLimitSemaphore = semaphore.NewWeighted(1) + }, + teardown: func() { + lsRemoteParallelismLimitSemaphore = nil + }, + testFunc: func(t *testing.T) { + assert.NotPanics(t, func() { + NewGitClientEventHandlers(&MetricsServer{}) + }) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setup != nil { + tt.setup() + } + if tt.teardown != nil { + defer tt.teardown() + } + tt.testFunc(t) + }) + } +} diff --git a/reposerver/metrics/metrics.go b/reposerver/metrics/metrics.go index e629b75e63d3ce..44f3dbd01e1bb4 100644 --- a/reposerver/metrics/metrics.go +++ b/reposerver/metrics/metrics.go @@ -12,6 +12,7 @@ import ( type MetricsServer struct { handler http.Handler + gitFetchFailCounter *prometheus.CounterVec gitRequestCounter *prometheus.CounterVec gitRequestHistogram *prometheus.HistogramVec repoPendingRequestsGauge *prometheus.GaugeVec @@ -32,6 +33,15 @@ func NewMetricsServer() *MetricsServer { registry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) registry.MustRegister(collectors.NewGoCollector()) + gitFetchFailCounter := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_git_fetch_fail_total", + Help: "Number of git fetch requests failures by repo server", + }, + []string{"repo", "revision"}, + ) + registry.MustRegister(gitFetchFailCounter) + gitRequestCounter := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "argocd_git_request_total", @@ -81,6 +91,7 @@ func NewMetricsServer() *MetricsServer { return &MetricsServer{ handler: promhttp.HandlerFor(registry, promhttp.HandlerOpts{}), + gitFetchFailCounter: gitFetchFailCounter, gitRequestCounter: gitRequestCounter, gitRequestHistogram: gitRequestHistogram, repoPendingRequestsGauge: repoPendingRequestsGauge, @@ -93,6 +104,10 @@ func (m *MetricsServer) GetHandler() http.Handler { return m.handler } +func (m *MetricsServer) IncGitFetchFail(repo string, revision string) { + m.gitFetchFailCounter.WithLabelValues(repo, revision).Inc() +} + // IncGitRequest increments the git requests counter func (m *MetricsServer) IncGitRequest(repo string, requestType GitRequestType) { m.gitRequestCounter.WithLabelValues(repo, string(requestType)).Inc() diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go index f66bc71ac46210..629fdbe60ded43 100644 --- a/reposerver/repository/repository.go +++ b/reposerver/repository/repository.go @@ -300,6 +300,7 @@ func (s *Service) runRepoOperation( var gitClient git.Client var helmClient helm.Client var err error + gitClientOpts := git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache) revision = textutils.FirstNonEmpty(revision, source.TargetRevision) unresolvedRevision := revision if source.IsHelm() { @@ -308,13 +309,13 @@ func (s *Service) runRepoOperation( return err } } else { - gitClient, revision, err = s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache)) + gitClient, revision, err = s.newClientResolveRevision(repo, revision, gitClientOpts) if err != nil { return err } } - repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision) + repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision, gitClientOpts) if err != nil { return err } @@ -463,7 +464,7 @@ type gitClientGetter func(repo *v1alpha1.Repository, revision string, opts ...gi // // Much of this logic is duplicated in runManifestGenAsync. If making changes here, check whether runManifestGenAsync // should be updated. -func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter) (map[string]string, error) { +func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter, gitClientOpts git.ClientOpts) (map[string]string, error) { repoRefs := make(map[string]string) if !hasMultipleSources || source == nil { return repoRefs, nil @@ -490,7 +491,7 @@ func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.Applicat normalizedRepoURL := git.NormalizeGitURL(refSourceMapping.Repo.Repo) _, ok = repoRefs[normalizedRepoURL] if !ok { - _, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision) + _, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, gitClientOpts) if err != nil { log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err) return nil, fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo) @@ -506,6 +507,17 @@ func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.Applicat func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestRequest) (*apiclient.ManifestResponse, error) { var res *apiclient.ManifestResponse var err error + + // Skip this path for ref only sources + if q.HasMultipleSources && q.ApplicationSource.Path == "" && q.ApplicationSource.Chart == "" && q.ApplicationSource.Ref != "" { + log.Debugf("Skipping manifest generation for ref only source for application: %s and ref %s", q.AppName, q.ApplicationSource.Ref) + _, revision, err := s.newClientResolveRevision(q.Repo, q.Revision, git.WithCache(s.cache, !q.NoRevisionCache && !q.NoCache)) + res = &apiclient.ManifestResponse{ + Revision: revision, + } + return res, err + } + cacheFn := func(cacheKey string, refSourceCommitSHAs cache.ResolvedRevisions, firstInvocation bool) (bool, error) { ok, resp, err := s.getManifestCacheEntry(cacheKey, q, refSourceCommitSHAs, firstInvocation) res = resp @@ -728,7 +740,7 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA, return } } else { - gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision) + gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, git.WithCache(s.cache, !q.NoRevisionCache && !q.NoCache)) if err != nil { log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err) ch.errCh <- fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo) @@ -1021,6 +1033,10 @@ func getHelmDependencyRepos(appPath string) ([]*v1alpha1.Repository, error) { repos = append(repos, &v1alpha1.Repository{ Name: r.Repository[1:], }) + } else if strings.HasPrefix(r.Repository, "alias:") { + repos = append(repos, &v1alpha1.Repository{ + Name: strings.TrimPrefix(r.Repository, "alias:"), + }) } else if u, err := url.Parse(r.Repository); err == nil && (u.Scheme == "https" || u.Scheme == "oci") { repo := &v1alpha1.Repository{ // trimming oci:// prefix since it is currently not supported by Argo CD (OCI repos just have no scheme) @@ -1373,7 +1389,7 @@ func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, if q.KustomizeOptions != nil { kustomizeBinary = q.KustomizeOptions.BinaryPath } - k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(gitCredsStore), repoURL, kustomizeBinary) + k := kustomize.NewKustomizeApp(repoRoot, appPath, q.Repo.GetGitCreds(gitCredsStore), repoURL, kustomizeBinary) targetObjs, _, err = k.Build(q.ApplicationSource.Kustomize, q.KustomizeOptions, env) case v1alpha1.ApplicationSourceTypePlugin: pluginName := "" @@ -1960,7 +1976,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD return err } case v1alpha1.ApplicationSourceTypeKustomize: - if err := populateKustomizeAppDetails(res, q, opContext.appPath, commitSHA, s.gitCredsStore); err != nil { + if err := populateKustomizeAppDetails(res, q, repoRoot, opContext.appPath, commitSHA, s.gitCredsStore); err != nil { return err } case v1alpha1.ApplicationSourceTypePlugin: @@ -2101,13 +2117,13 @@ func walkHelmValueFilesInPath(root string, valueFiles *[]string) filepath.WalkFu } } -func populateKustomizeAppDetails(res *apiclient.RepoAppDetailsResponse, q *apiclient.RepoServerAppDetailsQuery, appPath string, reversion string, credsStore git.CredsStore) error { +func populateKustomizeAppDetails(res *apiclient.RepoAppDetailsResponse, q *apiclient.RepoServerAppDetailsQuery, repoRoot string, appPath string, reversion string, credsStore git.CredsStore) error { res.Kustomize = &apiclient.KustomizeAppSpec{} kustomizeBinary := "" if q.KustomizeOptions != nil { kustomizeBinary = q.KustomizeOptions.BinaryPath } - k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(credsStore), q.Repo.Repo, kustomizeBinary) + k := kustomize.NewKustomizeApp(repoRoot, appPath, q.Repo.GetGitCreds(credsStore), q.Repo.Repo, kustomizeBinary) fakeManifestRequest := apiclient.ManifestRequest{ AppName: q.AppName, Namespace: "", // FIXME: omit it for now @@ -2381,7 +2397,11 @@ func directoryPermissionInitializer(rootPath string) goio.Closer { // nolint:unparam func (s *Service) checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) (goio.Closer, error) { closer := s.gitRepoInitializer(gitClient.Root()) - return closer, checkoutRevision(gitClient, revision, submoduleEnabled) + err := checkoutRevision(gitClient, revision, submoduleEnabled) + if err != nil { + s.metricsServer.IncGitFetchFail(gitClient.Root(), revision) + } + return closer, err } func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) error { @@ -2505,6 +2525,7 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ repo := request.GetRepo() revision := request.GetRevision() gitPath := request.GetPath() + noRevisionCache := request.GetNoRevisionCache() enableNewGitFileGlobbing := request.GetNewGitFileGlobbingEnabled() if gitPath == "" { gitPath = "." @@ -2514,7 +2535,7 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ return nil, status.Error(codes.InvalidArgument, "must pass a valid repo") } - gitClient, revision, err := s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, true)) + gitClient, revision, err := s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !noRevisionCache)) if err != nil { return nil, status.Errorf(codes.Internal, "unable to resolve git revision %s: %v", revision, err) } @@ -2567,12 +2588,12 @@ func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequ func (s *Service) GetGitDirectories(_ context.Context, request *apiclient.GitDirectoriesRequest) (*apiclient.GitDirectoriesResponse, error) { repo := request.GetRepo() revision := request.GetRevision() - + noRevisionCache := request.GetNoRevisionCache() if repo == nil { return nil, status.Error(codes.InvalidArgument, "must pass a valid repo") } - gitClient, revision, err := s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, true)) + gitClient, revision, err := s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !noRevisionCache)) if err != nil { return nil, status.Errorf(codes.Internal, "unable to resolve git revision %s: %v", revision, err) } diff --git a/reposerver/repository/repository.proto b/reposerver/repository/repository.proto index 8e4b69000f7e12..de061122e2586e 100644 --- a/reposerver/repository/repository.proto +++ b/reposerver/repository/repository.proto @@ -236,6 +236,7 @@ message GitFilesRequest { string revision = 3; string path = 4; bool NewGitFileGlobbingEnabled = 5; + bool noRevisionCache = 6; } message GitFilesResponse { @@ -247,6 +248,7 @@ message GitDirectoriesRequest { github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.Repository repo = 1; bool submoduleEnabled = 2; string revision = 3; + bool noRevisionCache = 4; } message GitDirectoriesResponse { diff --git a/reposerver/repository/repository_test.go b/reposerver/repository/repository_test.go index a24eb5008c47bd..3f2f74c4e5ae02 100644 --- a/reposerver/repository/repository_test.go +++ b/reposerver/repository/repository_test.go @@ -1,6 +1,7 @@ package repository import ( + "bytes" "context" "encoding/json" "errors" @@ -17,6 +18,7 @@ import ( "testing" "time" + cacheutil "github.com/argoproj/argo-cd/v2/util/cache" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/api/resource" @@ -28,13 +30,14 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/yaml" + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" argoappv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" "github.com/argoproj/argo-cd/v2/reposerver/cache" + repositorymocks "github.com/argoproj/argo-cd/v2/reposerver/cache/mocks" "github.com/argoproj/argo-cd/v2/reposerver/metrics" fileutil "github.com/argoproj/argo-cd/v2/test/fixture/path" "github.com/argoproj/argo-cd/v2/util/argo" - cacheutil "github.com/argoproj/argo-cd/v2/util/cache" dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks" "github.com/argoproj/argo-cd/v2/util/git" gitmocks "github.com/argoproj/argo-cd/v2/util/git/mocks" @@ -51,12 +54,49 @@ gpg: Good signature from "GitHub (web-flow commit signing) " type clientFunc func(*gitmocks.Client, *helmmocks.Client, *iomocks.TempPaths) -func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) { +type repoCacheMocks struct { + mock.Mock + cacheutilCache *cacheutil.Cache + cache *cache.Cache + mockCache *repositorymocks.MockRepoCache +} + +type newGitRepoHelmChartOptions struct { + chartName string + chartVersion string + // valuesFiles is a map of the values file name to the key/value pairs to be written to the file + valuesFiles map[string]map[string]string +} + +type newGitRepoOptions struct { + path string + createPath bool + remote string + addEmptyCommit bool + helmChartOptions newGitRepoHelmChartOptions +} + +func newCacheMocks() *repoCacheMocks { + mockRepoCache := repositorymocks.NewMockRepoCache(&repositorymocks.MockCacheOptions{ + RepoCacheExpiration: 1 * time.Minute, + RevisionCacheExpiration: 1 * time.Minute, + ReadDelay: 0, + WriteDelay: 0, + }) + cacheutilCache := cacheutil.NewCache(mockRepoCache.RedisClient) + return &repoCacheMocks{ + cacheutilCache: cacheutilCache, + cache: cache.NewCache(cacheutilCache, 1*time.Minute, 1*time.Minute), + mockCache: mockRepoCache, + } +} + +func newServiceWithMocks(t *testing.T, root string, signed bool) (*Service, *gitmocks.Client, *repoCacheMocks) { root, err := filepath.Abs(root) if err != nil { panic(err) } - return newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { + return newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { gitClient.On("Init").Return(nil) gitClient.On("Fetch", mock.Anything).Return(nil) gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil) @@ -73,7 +113,7 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) chart := "my-chart" oobChart := "out-of-bounds-chart" version := "1.1.0" - helmClient.On("GetIndex", true).Return(&helm.Index{Entries: map[string]helm.Entries{ + helmClient.On("GetIndex", mock.AnythingOfType("bool")).Return(&helm.Index{Entries: map[string]helm.Entries{ chart: {{Version: "1.0.0"}, {Version: version}}, oobChart: {{Version: "1.0.0"}, {Version: version}}, }}, nil) @@ -89,18 +129,16 @@ func newServiceWithMocks(root string, signed bool) (*Service, *gitmocks.Client) }, root) } -func newServiceWithOpt(cf clientFunc, root string) (*Service, *gitmocks.Client) { +func newServiceWithOpt(t *testing.T, cf clientFunc, root string) (*Service, *gitmocks.Client, *repoCacheMocks) { helmClient := &helmmocks.Client{} gitClient := &gitmocks.Client{} paths := &iomocks.TempPaths{} cf(gitClient, helmClient, paths) - service := NewService(metrics.NewMetricsServer(), cache.NewCache( - cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)), - 1*time.Minute, - 1*time.Minute, - ), RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root) + cacheMocks := newCacheMocks() + t.Cleanup(cacheMocks.mockCache.StopRedisCallback) + service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, root) - service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, prosy string, opts ...git.ClientOpts) (client git.Client, e error) { + service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) { return gitClient, nil } service.newHelmClient = func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client { @@ -110,20 +148,20 @@ func newServiceWithOpt(cf clientFunc, root string) (*Service, *gitmocks.Client) return io.NopCloser } service.gitRepoPaths = paths - return service, gitClient + return service, gitClient, cacheMocks } -func newService(root string) *Service { - service, _ := newServiceWithMocks(root, false) +func newService(t *testing.T, root string) *Service { + service, _, _ := newServiceWithMocks(t, root, false) return service } -func newServiceWithSignature(root string) *Service { - service, _ := newServiceWithMocks(root, true) +func newServiceWithSignature(t *testing.T, root string) *Service { + service, _, _ := newServiceWithMocks(t, root, true) return service } -func newServiceWithCommitSHA(root, revision string) *Service { +func newServiceWithCommitSHA(t *testing.T, root, revision string) *Service { var revisionErr error commitSHARegex := regexp.MustCompile("^[0-9A-Fa-f]{40}$") @@ -131,7 +169,7 @@ func newServiceWithCommitSHA(root, revision string) *Service { revisionErr = errors.New("not a commit SHA") } - service, gitClient := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { + service, gitClient, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { gitClient.On("Init").Return(nil) gitClient.On("Fetch", mock.Anything).Return(nil) gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil) @@ -150,7 +188,7 @@ func newServiceWithCommitSHA(root, revision string) *Service { } func TestGenerateYamlManifestInDir(t *testing.T) { - service := newService("../../manifests/base") + service := newService(t, "../../manifests/base") src := argoappv1.ApplicationSource{Path: "."} q := apiclient.ManifestRequest{ @@ -247,7 +285,7 @@ func TestGenerateManifests_MissingSymlinkDestination(t *testing.T) { } func TestGenerateManifests_K8SAPIResetCache(t *testing.T) { - service := newService("../../manifests/base") + service := newService(t, "../../manifests/base") src := argoappv1.ApplicationSource{Path: "."} q := apiclient.ManifestRequest{ @@ -275,7 +313,7 @@ func TestGenerateManifests_K8SAPIResetCache(t *testing.T) { } func TestGenerateManifests_EmptyCache(t *testing.T) { - service := newService("../../manifests/base") + service, gitMocks, mockCache := newServiceWithMocks(t, "../../manifests/base", false) src := argoappv1.ApplicationSource{Path: "."} q := apiclient.ManifestRequest{ @@ -291,11 +329,141 @@ func TestGenerateManifests_EmptyCache(t *testing.T) { res, err := service.GenerateManifest(context.Background(), &q) assert.NoError(t, err) assert.True(t, len(res.Manifests) > 0) + mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ + ExternalSets: 2, + ExternalGets: 2, + ExternalDeletes: 1}) + gitMocks.AssertCalled(t, "LsRemote", mock.Anything) + gitMocks.AssertCalled(t, "Fetch", mock.Anything) +} + +// Test that when Generate manifest is called with a source that is ref only it does not try to generate manifests or hit the manifest cache +// but it does resolve and cache the revision +func TestGenerateManifest_RefOnlyShortCircuit(t *testing.T) { + lsremoteCalled := false + dir := t.TempDir() + repopath := fmt.Sprintf("%s/tmprepo", dir) + repoRemote := fmt.Sprintf("file://%s", repopath) + cacheMocks := newCacheMocks() + t.Cleanup(cacheMocks.mockCache.StopRedisCallback) + service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath) + service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) { + opts = append(opts, git.WithEventHandlers(git.EventHandlers{ + // Primary check, we want to make sure ls-remote is not called when the item is in cache + OnLsRemote: func(repo string) func() { + return func() { + lsremoteCalled = true + } + }, + OnFetch: func(repo string) func() { + return func() { + assert.Fail(t, "Fetch should not be called from GenerateManifest when the source is ref only") + } + }, + })) + gitClient, err := git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...) + return gitClient, err + } + revision := initGitRepo(t, newGitRepoOptions{ + path: repopath, + createPath: true, + remote: repoRemote, + addEmptyCommit: true, + }) + src := argoappv1.ApplicationSource{RepoURL: repoRemote, TargetRevision: "HEAD", Ref: "test-ref"} + repo := &argoappv1.Repository{ + Repo: repoRemote, + } + q := apiclient.ManifestRequest{ + Repo: repo, + Revision: "HEAD", + HasMultipleSources: true, + ApplicationSource: &src, + ProjectName: "default", + ProjectSourceRepos: []string{"*"}, + } + _, err := service.GenerateManifest(context.Background(), &q) + assert.NoError(t, err) + cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ + ExternalSets: 1, + ExternalGets: 1}) + assert.True(t, lsremoteCalled, "ls-remote should be called when the source is ref only") + var revisions [][2]string + assert.NoError(t, cacheMocks.cacheutilCache.GetItem(fmt.Sprintf("git-refs|%s", repoRemote), &revisions)) + assert.ElementsMatch(t, [][2]string{{"refs/heads/main", revision}, {"HEAD", "ref: refs/heads/main"}}, revisions) +} + +// Test that calling manifest generation on source helm reference helm files that when the revision is cached it does not call ls-remote +func TestGenerateManifestsHelmWithRefs_CachedNoLsRemote(t *testing.T) { + dir := t.TempDir() + repopath := fmt.Sprintf("%s/tmprepo", dir) + cacheMocks := newCacheMocks() + t.Cleanup(func() { + cacheMocks.mockCache.StopRedisCallback() + err := filepath.WalkDir(dir, + func(path string, di fs.DirEntry, err error) error { + if err == nil { + return os.Chmod(path, 0777) + } + return err + }) + if err != nil { + t.Fatal(err) + } + }) + service := NewService(metrics.NewMetricsServer(), cacheMocks.cache, RepoServerInitConstants{ParallelismLimit: 1}, argo.NewResourceTracking(), &git.NoopCredsStore{}, repopath) + var gitClient git.Client + var err error + service.newGitClient = func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (client git.Client, e error) { + opts = append(opts, git.WithEventHandlers(git.EventHandlers{ + // Primary check, we want to make sure ls-remote is not called when the item is in cache + OnLsRemote: func(repo string) func() { + return func() { + assert.Fail(t, "LsRemote should not be called when the item is in cache") + } + }, + })) + gitClient, err = git.NewClientExt(rawRepoURL, root, creds, insecure, enableLfs, proxy, opts...) + return gitClient, err + } + repoRemote := fmt.Sprintf("file://%s", repopath) + revision := initGitRepo(t, newGitRepoOptions{ + path: repopath, + createPath: true, + remote: repoRemote, + helmChartOptions: newGitRepoHelmChartOptions{ + chartName: "my-chart", + chartVersion: "v1.0.0", + valuesFiles: map[string]map[string]string{"test.yaml": {"testval": "test"}}}, + }) + src := argoappv1.ApplicationSource{RepoURL: repoRemote, Path: ".", TargetRevision: "HEAD", Helm: &argoappv1.ApplicationSourceHelm{ + ValueFiles: []string{"$ref/test.yaml"}, + }} + repo := &argoappv1.Repository{ + Repo: repoRemote, + } + q := apiclient.ManifestRequest{ + Repo: repo, + Revision: "HEAD", + HasMultipleSources: true, + ApplicationSource: &src, + ProjectName: "default", + ProjectSourceRepos: []string{"*"}, + RefSources: map[string]*argoappv1.RefTarget{"$ref": {TargetRevision: "HEAD", Repo: *repo}}, + } + err = cacheMocks.cacheutilCache.SetItem(fmt.Sprintf("git-refs|%s", repoRemote), [][2]string{{"HEAD", revision}}, 30*time.Second, false) + assert.NoError(t, err) + _, err = service.GenerateManifest(context.Background(), &q) + assert.NoError(t, err) + cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ + ExternalSets: 2, + ExternalGets: 5}) } // ensure we can use a semver constraint range (>= 1.0.0) and get back the correct chart (1.0.0) func TestHelmManifestFromChartRepo(t *testing.T) { - service := newService(".") + root := t.TempDir() + service, gitMocks, mockCache := newServiceWithMocks(t, root, false) source := &argoappv1.ApplicationSource{Chart: "my-chart", TargetRevision: ">= 1.0.0"} request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true, ProjectName: "something", ProjectSourceRepos: []string{"*"}} @@ -309,10 +477,14 @@ func TestHelmManifestFromChartRepo(t *testing.T) { Revision: "1.1.0", SourceType: "Helm", }, response) + mockCache.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ + ExternalSets: 1, + ExternalGets: 0}) + gitMocks.AssertNotCalled(t, "LsRemote", mock.Anything) } func TestHelmChartReferencingExternalValues(t *testing.T) { - service := newService(".") + service := newService(t, ".") spec := argoappv1.ApplicationSpec{ Sources: []argoappv1.ApplicationSource{ {RepoURL: "https://helm.example.com", Chart: "my-chart", TargetRevision: ">= 1.0.0", Helm: &argoappv1.ApplicationSourceHelm{ @@ -342,7 +514,7 @@ func TestHelmChartReferencingExternalValues(t *testing.T) { } func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) { - service := newService(".") + service := newService(t, ".") err := os.Mkdir("testdata/oob-symlink", 0755) require.NoError(t, err) t.Cleanup(func() { @@ -376,7 +548,7 @@ func TestHelmChartReferencingExternalValues_OutOfBounds_Symlink(t *testing.T) { } func TestGenerateManifestsUseExactRevision(t *testing.T) { - service, gitClient := newServiceWithMocks(".", false) + service, gitClient, _ := newServiceWithMocks(t, ".", false) src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} @@ -390,7 +562,7 @@ func TestGenerateManifestsUseExactRevision(t *testing.T) { } func TestRecurseManifestsInDir(t *testing.T) { - service := newService(".") + service := newService(t, ".") src := argoappv1.ApplicationSource{Path: "./testdata/recurse", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} @@ -403,7 +575,7 @@ func TestRecurseManifestsInDir(t *testing.T) { } func TestInvalidManifestsInDir(t *testing.T) { - service := newService(".") + service := newService(t, ".") src := argoappv1.ApplicationSource{Path: "./testdata/invalid-manifests", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} @@ -414,7 +586,7 @@ func TestInvalidManifestsInDir(t *testing.T) { } func TestInvalidMetadata(t *testing.T) { - service := newService(".") + service := newService(t, ".") src := argoappv1.ApplicationSource{Path: "./testdata/invalid-metadata", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, AppLabelKey: "test", AppName: "invalid-metadata", TrackingMethod: "annotation+label"} @@ -424,7 +596,7 @@ func TestInvalidMetadata(t *testing.T) { } func TestNilMetadataAccessors(t *testing.T) { - service := newService(".") + service := newService(t, ".") expected := "{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"annotations\":{\"argocd.argoproj.io/tracking-id\":\"nil-metadata-accessors:/ConfigMap:/my-map\"},\"labels\":{\"test\":\"nil-metadata-accessors\"},\"name\":\"my-map\"},\"stringData\":{\"foo\":\"bar\"}}" src := argoappv1.ApplicationSource{Path: "./testdata/nil-metadata-accessors", Directory: &argoappv1.ApplicationSourceDirectory{Recurse: true}} @@ -436,7 +608,7 @@ func TestNilMetadataAccessors(t *testing.T) { } func TestGenerateJsonnetManifestInDir(t *testing.T) { - service := newService(".") + service := newService(t, ".") q := apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -459,7 +631,7 @@ func TestGenerateJsonnetManifestInDir(t *testing.T) { } func TestGenerateJsonnetManifestInRootDir(t *testing.T) { - service := newService("testdata/jsonnet-1") + service := newService(t, "testdata/jsonnet-1") q := apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -482,7 +654,7 @@ func TestGenerateJsonnetManifestInRootDir(t *testing.T) { } func TestGenerateJsonnetLibOutside(t *testing.T) { - service := newService(".") + service := newService(t, ".") q := apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -553,7 +725,7 @@ func TestManifestGenErrorCacheByNumRequests(t *testing.T) { for _, tt := range tests { testName := fmt.Sprintf("gen-attempts-%d-pause-%d-total-%d", tt.PauseGenerationAfterFailedGenerationAttempts, tt.PauseGenerationOnFailureForRequests, tt.TotalCacheInvocations) t.Run(testName, func(t *testing.T) { - service := newService(".") + service := newService(t, ".") service.initConstants = RepoServerInitConstants{ ParallelismLimit: 1, @@ -631,7 +803,7 @@ func TestManifestGenErrorCacheFileContentsChange(t *testing.T) { tmpDir := t.TempDir() - service := newService(tmpDir) + service := newService(t, tmpDir) service.initConstants = RepoServerInitConstants{ ParallelismLimit: 1, @@ -701,7 +873,7 @@ func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) { for _, tt := range tests { testName := fmt.Sprintf("pause-time-%d", tt.PauseGenerationOnFailureForMinutes) t.Run(testName, func(t *testing.T) { - service := newService(".") + service := newService(t, ".") // Here we simulate the passage of time by overriding the now() function of Service currentTime := time.Now() @@ -771,7 +943,7 @@ func TestManifestGenErrorCacheByMinutesElapsed(t *testing.T) { func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) { - service := newService(".") + service := newService(t, ".") service.initConstants = RepoServerInitConstants{ ParallelismLimit: 1, @@ -828,7 +1000,7 @@ func TestManifestGenErrorCacheRespectsNoCache(t *testing.T) { } func TestGenerateHelmWithValues(t *testing.T) { - service := newService("../../util/helm/testdata/redis") + service := newService(t, "../../util/helm/testdata/redis") res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -865,7 +1037,7 @@ func TestGenerateHelmWithValues(t *testing.T) { } func TestHelmWithMissingValueFiles(t *testing.T) { - service := newService("../../util/helm/testdata/redis") + service := newService(t, "../../util/helm/testdata/redis") missingValuesFile := "values-prod-overrides.yaml" req := &apiclient.ManifestRequest{ @@ -893,7 +1065,7 @@ func TestHelmWithMissingValueFiles(t *testing.T) { } func TestGenerateHelmWithEnvVars(t *testing.T) { - service := newService("../../util/helm/testdata/redis") + service := newService(t, "../../util/helm/testdata/redis") res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -930,7 +1102,7 @@ func TestGenerateHelmWithEnvVars(t *testing.T) { // The requested value file (`../minio/values.yaml`) is outside the app path (`./util/helm/testdata/redis`), however // since the requested value is still under the repo directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) { - service := newService("../../util/helm/testdata") + service := newService(t, "../../util/helm/testdata") _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, AppName: "test", @@ -947,7 +1119,7 @@ func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) { assert.NoError(t, err) // Test the case where the path is "." - service = newService("./testdata") + service = newService(t, "./testdata") _, err = service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, AppName: "test", @@ -961,7 +1133,7 @@ func TestGenerateHelmWithValuesDirectoryTraversal(t *testing.T) { } func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) { - service := newService(".") + service := newService(t, ".") source := &argoappv1.ApplicationSource{Chart: "out-of-bounds-chart", TargetRevision: ">= 1.0.0"} request := &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: source, NoCache: true} _, err := service.GenerateManifest(context.Background(), request) @@ -971,7 +1143,7 @@ func TestChartRepoWithOutOfBoundsSymlink(t *testing.T) { // This is a Helm first-class app with a values file inside the repo directory // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is allowed func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) { - service := newService(".") + service := newService(t, ".") source := &argoappv1.ApplicationSource{ Chart: "my-chart", TargetRevision: ">= 1.0.0", @@ -1000,7 +1172,7 @@ func TestHelmManifestFromChartRepoWithValueFile(t *testing.T) { // This is a Helm first-class app with a values file outside the repo directory // (`~/go/src/github.com/argoproj/argo-cd/reposerver/repository`), so it is not allowed func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) { - service := newService(".") + service := newService(t, ".") source := &argoappv1.ApplicationSource{ Chart: "my-chart", TargetRevision: ">= 1.0.0", @@ -1015,7 +1187,7 @@ func TestHelmManifestFromChartRepoWithValueFileOutsideRepo(t *testing.T) { func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) { t.Run("Valid symlink", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") source := &argoappv1.ApplicationSource{ Chart: "my-chart", TargetRevision: ">= 1.0.0", @@ -1031,7 +1203,7 @@ func TestHelmManifestFromChartRepoWithValueFileLinks(t *testing.T) { } func TestGenerateHelmWithURL(t *testing.T) { - service := newService("../../util/helm/testdata/redis") + service := newService(t, "../../util/helm/testdata/redis") _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -1054,7 +1226,7 @@ func TestGenerateHelmWithURL(t *testing.T) { // (`~/go/src/github.com/argoproj/argo-cd/util/helm/testdata/redis`), so it is blocked func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { t.Run("Values file with relative path pointing outside repo root", func(t *testing.T) { - service := newService("../../util/helm/testdata/redis") + service := newService(t, "../../util/helm/testdata/redis") _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, AppName: "test", @@ -1073,7 +1245,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { }) t.Run("Values file with relative path pointing inside repo root", func(t *testing.T) { - service := newService("./testdata") + service := newService(t, "./testdata") _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, AppName: "test", @@ -1091,7 +1263,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { }) t.Run("Values file with absolute path stays within repo root", func(t *testing.T) { - service := newService("./testdata") + service := newService(t, "./testdata") _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, AppName: "test", @@ -1109,7 +1281,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { }) t.Run("Values file with absolute path using back-references outside repo root", func(t *testing.T) { - service := newService("./testdata") + service := newService(t, "./testdata") _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, AppName: "test", @@ -1128,7 +1300,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { }) t.Run("Remote values file from forbidden protocol", func(t *testing.T) { - service := newService("./testdata") + service := newService(t, "./testdata") _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, AppName: "test", @@ -1147,7 +1319,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { }) t.Run("Remote values file from custom allowed protocol", func(t *testing.T) { - service := newService("./testdata") + service := newService(t, "./testdata") _, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, AppName: "test", @@ -1168,7 +1340,7 @@ func TestGenerateHelmWithValuesDirectoryTraversalOutsideRepo(t *testing.T) { // File parameter should not allow traversal outside of the repository root func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) { - service := newService("../..") + service := newService(t, "../..") file, err := os.CreateTemp("", "external-secret.txt") assert.NoError(t, err) @@ -1209,7 +1381,7 @@ func TestGenerateHelmWithAbsoluteFileParameter(t *testing.T) { // directory (`~/go/src/github.com/argoproj/argo-cd`), it is allowed. It is used as a means of // providing direct content to a helm chart via a specific key. func TestGenerateHelmWithFileParameter(t *testing.T) { - service := newService("../../util/helm/testdata") + service := newService(t, "../../util/helm/testdata") res, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -1234,7 +1406,7 @@ func TestGenerateHelmWithFileParameter(t *testing.T) { } func TestGenerateNullList(t *testing.T) { - service := newService(".") + service := newService(t, ".") t.Run("null list", func(t *testing.T) { res1, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ @@ -1302,7 +1474,7 @@ func TestGenerateFromUTF16(t *testing.T) { } func TestListApps(t *testing.T) { - service := newService("./testdata") + service := newService(t, "./testdata") res, err := service.ListApps(context.Background(), &apiclient.ListAppsRequest{Repo: &argoappv1.Repository{}}) assert.NoError(t, err) @@ -1324,12 +1496,30 @@ func TestListApps(t *testing.T) { "out-of-bounds-values-file-link": "Helm", "values-files": "Helm", "helm-with-dependencies": "Helm", + "helm-with-dependencies-alias": "Helm", } assert.Equal(t, expectedApps, res.Apps) } func TestGetAppDetailsHelm(t *testing.T) { - service := newService("../../util/helm/testdata/dependency") + service := newService(t, "../../util/helm/testdata/dependency") + + res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ + Repo: &argoappv1.Repository{}, + Source: &argoappv1.ApplicationSource{ + Path: ".", + }, + }) + + assert.NoError(t, err) + assert.NotNil(t, res.Helm) + + assert.Equal(t, "Helm", res.Type) + assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles) +} + +func TestGetAppDetailsHelmUsesCache(t *testing.T) { + service := newService(t, "../../util/helm/testdata/dependency") res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1344,8 +1534,9 @@ func TestGetAppDetailsHelm(t *testing.T) { assert.Equal(t, "Helm", res.Type) assert.EqualValues(t, []string{"values-production.yaml", "values.yaml"}, res.Helm.ValueFiles) } + func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) { - service := newService("../../util/helm/testdata/api-versions") + service := newService(t, "../../util/helm/testdata/api-versions") res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1363,7 +1554,7 @@ func TestGetAppDetailsHelm_WithNoValuesFile(t *testing.T) { } func TestGetAppDetailsKustomize(t *testing.T) { - service := newService("../../util/kustomize/testdata/kustomization_yaml") + service := newService(t, "../../util/kustomize/testdata/kustomization_yaml") res, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1380,7 +1571,7 @@ func TestGetAppDetailsKustomize(t *testing.T) { } func TestGetHelmCharts(t *testing.T) { - service := newService("../..") + service := newService(t, "../..") res, err := service.GetHelmCharts(context.Background(), &apiclient.HelmChartsRequest{Repo: &argoappv1.Repository{}}) // fix flakiness @@ -1401,7 +1592,7 @@ func TestGetHelmCharts(t *testing.T) { } func TestGetRevisionMetadata(t *testing.T) { - service, gitClient := newServiceWithMocks("../..", false) + service, gitClient, _ := newServiceWithMocks(t, "../..", false) now := time.Now() gitClient.On("RevisionMetadata", mock.Anything).Return(&git.RevisionMetadata{ @@ -1469,7 +1660,7 @@ func TestGetRevisionMetadata(t *testing.T) { func TestGetSignatureVerificationResult(t *testing.T) { // Commit with signature and verification requested { - service := newServiceWithSignature("../../manifests/base") + service := newServiceWithSignature(t, "../../manifests/base") src := argoappv1.ApplicationSource{Path: "."} q := apiclient.ManifestRequest{ @@ -1486,7 +1677,7 @@ func TestGetSignatureVerificationResult(t *testing.T) { } // Commit with signature and verification not requested { - service := newServiceWithSignature("../../manifests/base") + service := newServiceWithSignature(t, "../../manifests/base") src := argoappv1.ApplicationSource{Path: "."} q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, ProjectName: "something", @@ -1498,7 +1689,7 @@ func TestGetSignatureVerificationResult(t *testing.T) { } // Commit without signature and verification requested { - service := newService("../../manifests/base") + service := newService(t, "../../manifests/base") src := argoappv1.ApplicationSource{Path: "."} q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something", @@ -1510,7 +1701,7 @@ func TestGetSignatureVerificationResult(t *testing.T) { } // Commit without signature and verification not requested { - service := newService("../../manifests/base") + service := newService(t, "../../manifests/base") src := argoappv1.ApplicationSource{Path: "."} q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, VerifySignature: true, ProjectName: "something", @@ -1543,7 +1734,7 @@ func Test_newEnv(t *testing.T) { } func TestService_newHelmClientResolveRevision(t *testing.T) { - service := newService(".") + service := newService(t, ".") t.Run("EmptyRevision", func(t *testing.T) { _, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "", true) @@ -1557,7 +1748,7 @@ func TestService_newHelmClientResolveRevision(t *testing.T) { func TestGetAppDetailsWithAppParameterFile(t *testing.T) { t.Run("No app name set and app specific file exists", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "multi", func(t *testing.T, path string) { details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1570,7 +1761,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) { }) }) t.Run("No app specific override", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1584,7 +1775,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) { }) }) t.Run("Only app specific override", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1598,7 +1789,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) { }) }) t.Run("App specific override", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "multi", func(t *testing.T, path string) { details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1612,7 +1803,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) { }) }) t.Run("App specific overrides containing non-mergeable field", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "multi", func(t *testing.T, path string) { details, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1626,7 +1817,7 @@ func TestGetAppDetailsWithAppParameterFile(t *testing.T) { }) }) t.Run("Broken app-specific overrides", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "multi", func(t *testing.T, path string) { _, err := service.GetAppDetails(context.Background(), &apiclient.RepoServerAppDetailsQuery{ Repo: &argoappv1.Repository{}, @@ -1668,7 +1859,7 @@ func runWithTempTestdata(t *testing.T, path string, runner func(t *testing.T, pa func TestGenerateManifestsWithAppParameterFile(t *testing.T) { t.Run("Single global override", func(t *testing.T) { runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { - service := newService(".") + service := newService(t, ".") manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{ @@ -1699,7 +1890,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) { t.Run("Single global override Helm", func(t *testing.T) { runWithTempTestdata(t, "single-global-helm", func(t *testing.T, path string) { - service := newService(".") + service := newService(t, ".") manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, ApplicationSource: &argoappv1.ApplicationSource{ @@ -1729,7 +1920,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) { }) t.Run("Application specific override", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -1760,8 +1951,29 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) { }) }) + t.Run("Multi-source with source as ref only does not generate manifests", func(t *testing.T) { + service := newService(t, ".") + runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { + manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ + Repo: &argoappv1.Repository{}, + ApplicationSource: &argoappv1.ApplicationSource{ + Path: "", + Chart: "", + Ref: "test", + }, + AppName: "testapp-multi-ref-only", + ProjectName: "something", + ProjectSourceRepos: []string{"*"}, + HasMultipleSources: true, + }) + assert.NoError(t, err) + assert.Empty(t, manifests.Manifests) + assert.NotEmpty(t, manifests.Revision) + }) + }) + t.Run("Application specific override for other app", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "single-app-only", func(t *testing.T, path string) { manifests, err := service.GenerateManifest(context.Background(), &apiclient.ManifestRequest{ Repo: &argoappv1.Repository{}, @@ -1793,7 +2005,7 @@ func TestGenerateManifestsWithAppParameterFile(t *testing.T) { }) t.Run("Override info does not appear in cache key", func(t *testing.T) { - service := newService(".") + service := newService(t, ".") runWithTempTestdata(t, "single-global", func(t *testing.T, path string) { source := &argoappv1.ApplicationSource{ Path: path, @@ -1843,7 +2055,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) { ProjectSourceRepos: []string{"*"}, }, wantError: false, - service: newServiceWithCommitSHA(".", regularGitTagHash), + service: newServiceWithCommitSHA(t, ".", regularGitTagHash), }, { @@ -1859,7 +2071,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) { ProjectSourceRepos: []string{"*"}, }, wantError: false, - service: newServiceWithCommitSHA(".", annotatedGitTaghash), + service: newServiceWithCommitSHA(t, ".", annotatedGitTaghash), }, { @@ -1875,7 +2087,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) { ProjectSourceRepos: []string{"*"}, }, wantError: true, - service: newServiceWithCommitSHA(".", invalidGitTaghash), + service: newServiceWithCommitSHA(t, ".", invalidGitTaghash), }, } for _, tt := range tests { @@ -1900,7 +2112,7 @@ func TestGenerateManifestWithAnnotatedAndRegularGitTagHashes(t *testing.T) { func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) { annotatedGitTaghash := "95249be61b028d566c29d47b19e65c5603388a41" - service := newServiceWithCommitSHA(".", annotatedGitTaghash) + service := newServiceWithCommitSHA(t, ".", annotatedGitTaghash) refSources := map[string]*argoappv1.RefTarget{} @@ -2485,7 +2697,7 @@ func Test_findManifests(t *testing.T) { } func TestTestRepoOCI(t *testing.T) { - service := newService(".") + service := newService(t, ".") _, err := service.TestRepository(context.Background(), &apiclient.TestRepositoryRequest{ Repo: &argoappv1.Repository{ Repo: "https://demo.goharbor.io", @@ -2510,7 +2722,7 @@ func Test_getHelmDependencyRepos(t *testing.T) { func TestResolveRevision(t *testing.T) { - service := newService(".") + service := newService(t, ".") repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"} app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}} resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{ @@ -2532,7 +2744,7 @@ func TestResolveRevision(t *testing.T) { func TestResolveRevisionNegativeScenarios(t *testing.T) { - service := newService(".") + service := newService(t, ".") repo := &argoappv1.Repository{Repo: "https://github.com/argoproj/argo-cd"} app := &argoappv1.Application{Spec: argoappv1.ApplicationSpec{Source: &argoappv1.ApplicationSource{}}} resolveRevisionResponse, err := service.ResolveRevision(context.Background(), &apiclient.ResolveRevisionRequest{ @@ -2579,19 +2791,57 @@ func TestDirectoryPermissionInitializer(t *testing.T) { require.Error(t, err) } -func initGitRepo(repoPath string, remote string) error { - if err := os.Mkdir(repoPath, 0755); err != nil { - return err +func addHelmToGitRepo(t *testing.T, options newGitRepoOptions) { + err := os.WriteFile(filepath.Join(options.path, "Chart.yaml"), []byte("name: test\nversion: v1.0.0"), 0777) + assert.NoError(t, err) + for valuesFileName, values := range options.helmChartOptions.valuesFiles { + valuesFileContents, err := yaml.Marshal(values) + assert.NoError(t, err) + err = os.WriteFile(filepath.Join(options.path, valuesFileName), valuesFileContents, 0777) + assert.NoError(t, err) + } + assert.NoError(t, err) + cmd := exec.Command("git", "add", "-A") + cmd.Dir = options.path + assert.NoError(t, cmd.Run()) + cmd = exec.Command("git", "commit", "-m", "Initial commit") + cmd.Dir = options.path + assert.NoError(t, cmd.Run()) +} + +func initGitRepo(t *testing.T, options newGitRepoOptions) (revision string) { + if options.createPath { + assert.NoError(t, os.Mkdir(options.path, 0755)) + } + + cmd := exec.Command("git", "init", "-b", "main", options.path) + cmd.Dir = options.path + assert.NoError(t, cmd.Run()) + + if options.remote != "" { + cmd = exec.Command("git", "remote", "add", "origin", options.path) + cmd.Dir = options.path + assert.NoError(t, cmd.Run()) + } + + commitAdded := options.addEmptyCommit || options.helmChartOptions.chartName != "" + if options.addEmptyCommit { + cmd = exec.Command("git", "commit", "-m", "Initial commit", "--allow-empty") + cmd.Dir = options.path + assert.NoError(t, cmd.Run()) + } else if options.helmChartOptions.chartName != "" { + addHelmToGitRepo(t, options) } - cmd := exec.Command("git", "init", repoPath) - cmd.Dir = repoPath - if err := cmd.Run(); err != nil { - return err + if commitAdded { + var revB bytes.Buffer + cmd = exec.Command("git", "rev-parse", "HEAD", options.path) + cmd.Dir = options.path + cmd.Stdout = &revB + assert.NoError(t, cmd.Run()) + revision = strings.Split(revB.String(), "\n")[0] } - cmd = exec.Command("git", "remote", "add", "origin", remote) - cmd.Dir = repoPath - return cmd.Run() + return revision } func TestInit(t *testing.T) { @@ -2604,16 +2854,16 @@ func TestInit(t *testing.T) { }) repoPath := path.Join(dir, "repo1") - require.NoError(t, initGitRepo(repoPath, "https://github.com/argo-cd/test-repo1")) + initGitRepo(t, newGitRepoOptions{path: repoPath, remote: "https://github.com/argo-cd/test-repo1", createPath: true, addEmptyCommit: false}) - service := newService(".") + service := newService(t, ".") service.rootDir = dir require.NoError(t, service.Init()) _, err := os.ReadDir(dir) require.Error(t, err) - require.NoError(t, initGitRepo(path.Join(dir, "repo2"), "https://github.com/argo-cd/test-repo2")) + initGitRepo(t, newGitRepoOptions{path: path.Join(dir, "repo2"), remote: "https://github.com/argo-cd/test-repo2", createPath: true, addEmptyCommit: false}) } // TestCheckoutRevisionCanGetNonstandardRefs shows that we can fetch a revision that points to a non-standard ref. In @@ -2756,6 +3006,22 @@ func TestGetHelmRepo_NamedRepos(t *testing.T) { assert.Equal(t, helmRepos[0].Repo, "https://example.com") } +func TestGetHelmRepo_NamedReposAlias(t *testing.T) { + src := argoappv1.ApplicationSource{Path: "."} + q := apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &src, Repos: []*argoappv1.Repository{{ + Name: "custom-repo-alias", + Repo: "https://example.com", + Username: "test-alias", + }}} + + helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies-alias", q.Repos, q.HelmRepoCreds) + assert.Nil(t, err) + + assert.Equal(t, len(helmRepos), 1) + assert.Equal(t, helmRepos[0].Username, "test-alias") + assert.Equal(t, helmRepos[0].Repo, "https://example.com") +} + func Test_getResolvedValueFiles(t *testing.T) { tempDir := t.TempDir() paths := io.NewRandomizedTempPaths(tempDir) @@ -2926,7 +3192,7 @@ func TestErrorGetGitDirectories(t *testing.T) { want *apiclient.GitDirectoriesResponse wantErr assert.ErrorAssertionFunc }{ - {name: "InvalidRepo", fields: fields{service: newService(".")}, args: args{ + {name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{ ctx: context.TODO(), request: &apiclient.GitDirectoriesRequest{ Repo: nil, @@ -2935,7 +3201,7 @@ func TestErrorGetGitDirectories(t *testing.T) { }, }, want: nil, wantErr: assert.Error}, {name: "InvalidResolveRevision", fields: fields{service: func() *Service { - s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { + s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil) gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error")) paths.On("GetPath", mock.Anything).Return(".", nil) @@ -2966,7 +3232,7 @@ func TestErrorGetGitDirectories(t *testing.T) { func TestGetGitDirectories(t *testing.T) { // test not using the cache root := "./testdata/git-files-dirs" - s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { + s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { gitClient.On("Init").Return(nil) gitClient.On("Fetch", mock.Anything).Return(nil) gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil) @@ -2989,6 +3255,10 @@ func TestGetGitDirectories(t *testing.T) { directories, err = s.GetGitDirectories(context.TODO(), dirRequest) assert.Nil(t, err) assert.ElementsMatch(t, []string{"app", "app/bar", "app/foo/bar", "somedir", "app/foo"}, directories.GetPaths()) + cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ + ExternalSets: 1, + ExternalGets: 2, + }) } func TestErrorGetGitFiles(t *testing.T) { @@ -3006,7 +3276,7 @@ func TestErrorGetGitFiles(t *testing.T) { want *apiclient.GitFilesResponse wantErr assert.ErrorAssertionFunc }{ - {name: "InvalidRepo", fields: fields{service: newService(".")}, args: args{ + {name: "InvalidRepo", fields: fields{service: newService(t, ".")}, args: args{ ctx: context.TODO(), request: &apiclient.GitFilesRequest{ Repo: nil, @@ -3015,7 +3285,7 @@ func TestErrorGetGitFiles(t *testing.T) { }, }, want: nil, wantErr: assert.Error}, {name: "InvalidResolveRevision", fields: fields{service: func() *Service { - s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { + s, _, _ := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { gitClient.On("Checkout", mock.Anything, mock.Anything).Return(nil) gitClient.On("LsRemote", mock.Anything).Return("", fmt.Errorf("ah error")) paths.On("GetPath", mock.Anything).Return(".", nil) @@ -3048,7 +3318,7 @@ func TestGetGitFiles(t *testing.T) { files := []string{"./testdata/git-files-dirs/somedir/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/config.yaml", "./testdata/git-files-dirs/app/foo/bar/config.yaml"} root := "" - s, _ := newServiceWithOpt(func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { + s, _, cacheMocks := newServiceWithOpt(t, func(gitClient *gitmocks.Client, helmClient *helmmocks.Client, paths *iomocks.TempPaths) { gitClient.On("Init").Return(nil) gitClient.On("Fetch", mock.Anything).Return(nil) gitClient.On("Checkout", mock.Anything, mock.Anything).Once().Return(nil) @@ -3081,6 +3351,10 @@ func TestGetGitFiles(t *testing.T) { fileResponse, err = s.GetGitFiles(context.TODO(), filesRequest) assert.Nil(t, err) assert.Equal(t, expected, fileResponse.GetMap()) + cacheMocks.mockCache.AssertCacheCalledTimes(t, &repositorymocks.CacheCallCounts{ + ExternalSets: 1, + ExternalGets: 2, + }) } func Test_getRepoSanitizerRegex(t *testing.T) { @@ -3090,3 +3364,45 @@ func Test_getRepoSanitizerRegex(t *testing.T) { msg = r.ReplaceAllString("error message containing /tmp/_argocd-repo/SENSITIVE/with/trailing/path and other stuff", "") assert.Equal(t, "error message containing /with/trailing/path and other stuff", msg) } + +func TestGetRevisionChartDetails(t *testing.T) { + t.Run("Test revision semvar", func(t *testing.T) { + root := t.TempDir() + service := newService(t, root) + _, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{ + Repo: &v1alpha1.Repository{ + Repo: fmt.Sprintf("file://%s", root), + Name: "test-repo-name", + Type: "helm", + }, + Name: "test-name", + Revision: "test-revision", + }) + assert.ErrorContains(t, err, "invalid revision") + }) + + t.Run("Test GetRevisionChartDetails", func(t *testing.T) { + root := t.TempDir() + service := newService(t, root) + repoUrl := fmt.Sprintf("file://%s", root) + err := service.cache.SetRevisionChartDetails(repoUrl, "my-chart", "1.1.0", &argoappv1.ChartDetails{ + Description: "test-description", + Home: "test-home", + Maintainers: []string{"test-maintainer"}, + }) + assert.NoError(t, err) + chartDetails, err := service.GetRevisionChartDetails(context.Background(), &apiclient.RepoServerRevisionChartDetailsRequest{ + Repo: &v1alpha1.Repository{ + Repo: fmt.Sprintf("file://%s", root), + Name: "test-repo-name", + Type: "helm", + }, + Name: "my-chart", + Revision: "1.1.0", + }) + assert.NoError(t, err) + assert.Equal(t, "test-description", chartDetails.Description) + assert.Equal(t, "test-home", chartDetails.Home) + assert.Equal(t, []string{"test-maintainer"}, chartDetails.Maintainers) + }) +} diff --git a/reposerver/repository/testdata/helm-with-dependencies-alias/Chart.yaml b/reposerver/repository/testdata/helm-with-dependencies-alias/Chart.yaml new file mode 100644 index 00000000000000..8a38d551070c79 --- /dev/null +++ b/reposerver/repository/testdata/helm-with-dependencies-alias/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: helm-with-dependencies-alias +version: v1.0.0 +dependencies: + - name: helm + repository: "alias:custom-repo-alias" + version: v1.0.0 diff --git a/reposerver/server.go b/reposerver/server.go index 007b7136e41ed6..e1d611801c3ec1 100644 --- a/reposerver/server.go +++ b/reposerver/server.go @@ -102,7 +102,7 @@ func NewServer(metricsServer *metrics.MetricsServer, cache *reposervercache.Cach } repoService := repository.NewService(metricsServer, cache, initConstants, argo.NewResourceTracking(), gitCredsStore, filepath.Join(os.TempDir(), "_argocd-repo")) if err := repoService.Init(); err != nil { - return nil, err + return nil, fmt.Errorf("failed to initialize the repo service: %w", err) } return &ArgoCDRepoServer{ diff --git a/resource_customizations/apps.kruise.io/AdvancedCronJob/health.lua b/resource_customizations/apps.kruise.io/AdvancedCronJob/health.lua new file mode 100644 index 00000000000000..1e68d862722e1f --- /dev/null +++ b/resource_customizations/apps.kruise.io/AdvancedCronJob/health.lua @@ -0,0 +1,36 @@ +hs = { status = "Progressing", message = "AdvancedCronJobs has active jobs" } +-- Extract lastScheduleTime and convert to time objects +lastScheduleTime = nil + +if obj.status.lastScheduleTime ~= nil then + local year, month, day, hour, min, sec = string.match(obj.status.lastScheduleTime, "(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z") + lastScheduleTime = os.time({year=year, month=month, day=day, hour=hour, min=min, sec=sec}) +end + + +if lastScheduleTime == nil and obj.spec.paused == true then + hs.status = "Suspended" + hs.message = "AdvancedCronJob is Paused" + return hs +end + +-- AdvancedCronJobs are progressing if they have any object in the "active" state +if obj.status.active ~= nil and #obj.status.active > 0 then + hs.status = "Progressing" + hs.message = "AdvancedCronJobs has active jobs" + return hs +end +-- AdvancedCronJobs are Degraded if they don't have lastScheduleTime +if lastScheduleTime == nil then + hs.status = "Degraded" + hs.message = "AdvancedCronJobs has not run successfully" + return hs +end +-- AdvancedCronJobs are healthy if they have lastScheduleTime +if lastScheduleTime ~= nil then + hs.status = "Healthy" + hs.message = "AdvancedCronJobs has run successfully" + return hs +end + +return hs diff --git a/resource_customizations/apps.kruise.io/AdvancedCronJob/health_test.yaml b/resource_customizations/apps.kruise.io/AdvancedCronJob/health_test.yaml new file mode 100644 index 00000000000000..939c701955abb3 --- /dev/null +++ b/resource_customizations/apps.kruise.io/AdvancedCronJob/health_test.yaml @@ -0,0 +1,17 @@ +tests: + - healthStatus: + status: Healthy + message: AdvancedCronJobs has run successfully + inputPath: testdata/lastScheduleTime.yaml + - healthStatus: + status: Degraded + message: AdvancedCronJobs has not run successfully + inputPath: testdata/notScheduled.yaml + - healthStatus: + status: Progressing + message: AdvancedCronJobs has active jobs + inputPath: testdata/activeJobs.yaml + - healthStatus: + status: Suspended + message: AdvancedCronJob is Paused + inputPath: testdata/suspended.yaml diff --git a/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/activeJobs.yaml b/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/activeJobs.yaml new file mode 100644 index 00000000000000..5748143874d5ee --- /dev/null +++ b/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/activeJobs.yaml @@ -0,0 +1,30 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: AdvancedCronJob +metadata: + name: acj-test +spec: + schedule: "*/1 * * * *" + template: + broadcastJobTemplate: + spec: + template: + spec: + containers: + - name: pi + image: perl + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + completionPolicy: + type: Always + ttlSecondsAfterFinished: 30 + +status: + active: + - apiVersion: apps.kruise.io/v1alpha1 + kind: BroadcastJob + name: acj-test-1694882400 + namespace: default + resourceVersion: '4012' + uid: 2b08a429-a43b-4382-8e5d-3db0c72b5b13 + lastScheduleTime: '2023-09-16T16:40:00Z' + type: BroadcastJob diff --git a/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/lastScheduleTime.yaml b/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/lastScheduleTime.yaml new file mode 100644 index 00000000000000..bf48bdba777dc8 --- /dev/null +++ b/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/lastScheduleTime.yaml @@ -0,0 +1,23 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: AdvancedCronJob +metadata: + name: acj-test +spec: + schedule: "*/1 * * * *" + template: + broadcastJobTemplate: + spec: + template: + spec: + containers: + - name: pi + image: perl + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + completionPolicy: + type: Always + ttlSecondsAfterFinished: 30 + +status: + lastScheduleTime: "2023-09-16T16:29:00Z" + type: BroadcastJob diff --git a/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/notScheduled.yaml b/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/notScheduled.yaml new file mode 100644 index 00000000000000..cc8a9dd436d803 --- /dev/null +++ b/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/notScheduled.yaml @@ -0,0 +1,22 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: AdvancedCronJob +metadata: + name: acj-test +spec: + schedule: "*/1 * * * *" + template: + broadcastJobTemplate: + spec: + template: + spec: + containers: + - name: pi + image: perl + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + completionPolicy: + type: Always + ttlSecondsAfterFinished: 30 + +status: + lastScheduleTime: null diff --git a/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/suspended.yaml b/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/suspended.yaml new file mode 100644 index 00000000000000..dc79f1b41218be --- /dev/null +++ b/resource_customizations/apps.kruise.io/AdvancedCronJob/testdata/suspended.yaml @@ -0,0 +1,23 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: AdvancedCronJob +metadata: + name: acj-test +spec: + schedule: "*/1 * * * *" + template: + broadcastJobTemplate: + spec: + template: + spec: + containers: + - name: pi + image: perl + command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"] + restartPolicy: Never + completionPolicy: + type: Always + ttlSecondsAfterFinished: 30 + paused: true + +status: + type: BroadcastJob diff --git a/resource_customizations/apps.kruise.io/BroadcastJob/health.lua b/resource_customizations/apps.kruise.io/BroadcastJob/health.lua new file mode 100644 index 00000000000000..3b20ca88499759 --- /dev/null +++ b/resource_customizations/apps.kruise.io/BroadcastJob/health.lua @@ -0,0 +1,32 @@ +hs={ status= "Progressing", message= "BroadcastJob is still running" } + +if obj.status ~= nil then + +-- BroadcastJob are healthy if desired number and succeeded number is equal + if obj.status.desired == obj.status.succeeded and obj.status.phase == "completed" then + hs.status = "Healthy" + hs.message = "BroadcastJob is completed successfully" + return hs + end +-- BroadcastJob are progressing if active is not equal to 0 + if obj.status.active ~= 0 and obj.status.phase == "running" then + hs.status = "Progressing" + hs.message = "BroadcastJob is still running" + return hs + end +-- BroadcastJob are progressing if failed is not equal to 0 + if obj.status.failed ~= 0 and obj.status.phase == "failed" then + hs.status = "Degraded" + hs.message = "BroadcastJob failed" + return hs + end + + if obj.status.phase == "paused" and obj.spec.paused == true then + hs.status = "Suspended" + hs.message = "BroadcastJob is Paused" + return hs + end + +end + +return hs diff --git a/resource_customizations/apps.kruise.io/BroadcastJob/health_test.yaml b/resource_customizations/apps.kruise.io/BroadcastJob/health_test.yaml new file mode 100644 index 00000000000000..e3e16e22bfeef3 --- /dev/null +++ b/resource_customizations/apps.kruise.io/BroadcastJob/health_test.yaml @@ -0,0 +1,17 @@ +tests: + - healthStatus: + status: Healthy + message: "BroadcastJob is completed successfully" + inputPath: testdata/succeeded.yaml + - healthStatus: + status: Degraded + message: "BroadcastJob failed" + inputPath: testdata/failed.yaml + - healthStatus: + status: Progressing + message: "BroadcastJob is still running" + inputPath: testdata/running.yaml + - healthStatus: + status: Suspended + message: "BroadcastJob is Paused" + inputPath: testdata/suspended.yaml diff --git a/resource_customizations/apps.kruise.io/BroadcastJob/testdata/failed.yaml b/resource_customizations/apps.kruise.io/BroadcastJob/testdata/failed.yaml new file mode 100644 index 00000000000000..88b85cae281899 --- /dev/null +++ b/resource_customizations/apps.kruise.io/BroadcastJob/testdata/failed.yaml @@ -0,0 +1,31 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: BroadcastJob +metadata: + name: failed-job +spec: + template: + spec: + containers: + - name: guestbook + image: openkruise/guestbook:v3 + command: ["exit", "1"] # a dummy command to fail + restartPolicy: Never + completionPolicy: + type: Always + ttlSecondsAfterFinished: 60 # the job will be deleted after 60 seconds + +status: + active: 0 + completionTime: '2023-09-17T14:31:38Z' + conditions: + - lastProbeTime: '2023-09-17T14:31:38Z' + lastTransitionTime: '2023-09-17T14:31:38Z' + message: failure policy is FailurePolicyTypeFailFast and failed pod is found + reason: Failed + status: 'True' + type: Failed + desired: 1 + failed: 1 + phase: failed + startTime: '2023-09-17T14:31:32Z' + succeeded: 0 diff --git a/resource_customizations/apps.kruise.io/BroadcastJob/testdata/running.yaml b/resource_customizations/apps.kruise.io/BroadcastJob/testdata/running.yaml new file mode 100644 index 00000000000000..f679fa3ee0d508 --- /dev/null +++ b/resource_customizations/apps.kruise.io/BroadcastJob/testdata/running.yaml @@ -0,0 +1,22 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: BroadcastJob +metadata: + name: download-image +spec: + template: + spec: + containers: + - name: guestbook + image: openkruise/guestbook:v3 + command: ["echo", "started"] # a dummy command to do nothing + restartPolicy: Never + completionPolicy: + type: Always + ttlSecondsAfterFinished: 60 # the job will be deleted after 60 seconds +status: + active: 1 + desired: 1 + failed: 0 + phase: running + startTime: '2023-09-17T14:43:30Z' + succeeded: 0 diff --git a/resource_customizations/apps.kruise.io/BroadcastJob/testdata/succeeded.yaml b/resource_customizations/apps.kruise.io/BroadcastJob/testdata/succeeded.yaml new file mode 100644 index 00000000000000..61746b20cd9074 --- /dev/null +++ b/resource_customizations/apps.kruise.io/BroadcastJob/testdata/succeeded.yaml @@ -0,0 +1,31 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: BroadcastJob +metadata: + name: download-image +spec: + template: + spec: + containers: + - name: guestbook + image: openkruise/guestbook:v3 + command: ["echo", "started"] # a dummy command to do nothing + restartPolicy: Never + completionPolicy: + type: Always + ttlSecondsAfterFinished: 60 # the job will be deleted after 60 seconds +status: + active: 0 + completionTime: '2023-09-17T14:35:14Z' + conditions: + - lastProbeTime: '2023-09-17T14:35:14Z' + lastTransitionTime: '2023-09-17T14:35:14Z' + message: Job completed, 1 pods succeeded, 0 pods failed + reason: Complete + status: 'True' + type: Complete + desired: 1 + failed: 0 + phase: completed + startTime: '2023-09-17T14:35:07Z' + succeeded: 1 + diff --git a/resource_customizations/apps.kruise.io/BroadcastJob/testdata/suspended.yaml b/resource_customizations/apps.kruise.io/BroadcastJob/testdata/suspended.yaml new file mode 100644 index 00000000000000..60a9b587b8ec02 --- /dev/null +++ b/resource_customizations/apps.kruise.io/BroadcastJob/testdata/suspended.yaml @@ -0,0 +1,31 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: BroadcastJob +metadata: + name: download-image +spec: + template: + spec: + containers: + - name: guestbook + image: openkruise/guestbook:v3 + command: ["echo", "started"] # a dummy command to do nothing + restartPolicy: Never + paused: true + completionPolicy: + type: Always + ttlSecondsAfterFinished: 60 # the job will be deleted after 60 seconds +status: + active: 0 + completionTime: '2023-09-17T14:35:14Z' + conditions: + - lastProbeTime: '2023-09-17T14:35:14Z' + lastTransitionTime: '2023-09-17T14:35:14Z' + message: Job completed, 1 pods succeeded, 0 pods failed + reason: Complete + status: 'True' + type: Complete + desired: 1 + failed: 0 + phase: paused + startTime: '2023-09-17T14:35:07Z' + succeeded: 0 diff --git a/resource_customizations/apps.kruise.io/CloneSet/health.lua b/resource_customizations/apps.kruise.io/CloneSet/health.lua new file mode 100644 index 00000000000000..197ab7573dfe8a --- /dev/null +++ b/resource_customizations/apps.kruise.io/CloneSet/health.lua @@ -0,0 +1,33 @@ +hs={ status = "Progressing", message = "Waiting for initialization" } + +if obj.status ~= nil then + + if obj.metadata.generation == obj.status.observedGeneration then + + if obj.spec.updateStrategy.paused == true or not obj.status.updatedAvailableReplicas then + hs.status = "Suspended" + hs.message = "Cloneset is paused" + return hs + elseif obj.spec.updateStrategy.partition ~= 0 and obj.metadata.generation > 1 then + if obj.status.updatedReplicas >= obj.status.expectedUpdatedReplicas then + hs.status = "Suspended" + hs.message = "Cloneset needs manual intervention" + return hs + end + + elseif obj.status.updatedAvailableReplicas == obj.status.replicas then + hs.status = "Healthy" + hs.message = "All Cloneset workloads are ready and updated" + return hs + + else + if obj.status.updatedAvailableReplicas ~= obj.status.replicas then + hs.status = "Degraded" + hs.message = "Some replicas are not ready or available" + return hs + end + end + end +end + +return hs diff --git a/resource_customizations/apps.kruise.io/CloneSet/health_test.yaml b/resource_customizations/apps.kruise.io/CloneSet/health_test.yaml new file mode 100644 index 00000000000000..e740eca850778a --- /dev/null +++ b/resource_customizations/apps.kruise.io/CloneSet/health_test.yaml @@ -0,0 +1,21 @@ +tests: + - healthStatus: + status: Healthy + message: "All Cloneset workloads are ready and updated" + inputPath: testdata/healthy.yaml + - healthStatus: + status: Degraded + message: "Some replicas are not ready or available" + inputPath: testdata/degraded.yaml + - healthStatus: + status: Progressing + message: "Waiting for initialization" + inputPath: testdata/unknown.yaml + - healthStatus: + status: Suspended + message: "Cloneset is paused" + inputpath: testdata/suspended.yaml + - healthStatus: + status: Suspended + message: "Cloneset needs manual intervention" + inputpath: testdata/partition_suspended.yaml diff --git a/resource_customizations/apps.kruise.io/CloneSet/testdata/degraded.yaml b/resource_customizations/apps.kruise.io/CloneSet/testdata/degraded.yaml new file mode 100644 index 00000000000000..36e9a0d537c85c --- /dev/null +++ b/resource_customizations/apps.kruise.io/CloneSet/testdata/degraded.yaml @@ -0,0 +1,35 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: CloneSet +metadata: + name: cloneset-test + namespace: kruise + generation: 1 + labels: + app: sample +spec: + replicas: 2 + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + paused: false + +status: + observedGeneration: 1 + replicas: 2 + updatedReadyReplicas: 1 + updatedAvailableReplicas: 1 + conditions: + - lastTransitionTime: "2021-09-21T22:35:31Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: 'True' + type: FailedScale diff --git a/resource_customizations/apps.kruise.io/CloneSet/testdata/healthy.yaml b/resource_customizations/apps.kruise.io/CloneSet/testdata/healthy.yaml new file mode 100644 index 00000000000000..8a1935381e04ef --- /dev/null +++ b/resource_customizations/apps.kruise.io/CloneSet/testdata/healthy.yaml @@ -0,0 +1,36 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: CloneSet +metadata: + name: cloneset-test + namespace: kruise + generation: 1 + labels: + app: sample +spec: + replicas: 1 + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + paused: false + + +status: + observedGeneration: 1 + replicas: 2 + updatedReadyReplicas: 2 + updatedAvailableReplicas: 2 + conditions: + - lastTransitionTime: "2021-09-21T22:35:31Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: 'True' + type: FailedScale diff --git a/resource_customizations/apps.kruise.io/CloneSet/testdata/partition_suspended.yaml b/resource_customizations/apps.kruise.io/CloneSet/testdata/partition_suspended.yaml new file mode 100644 index 00000000000000..674c5226b30729 --- /dev/null +++ b/resource_customizations/apps.kruise.io/CloneSet/testdata/partition_suspended.yaml @@ -0,0 +1,31 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: CloneSet +metadata: + name: cloneset-test + namespace: kruise + generation: 2 + labels: + app: sample +spec: + replicas: 5 + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + partition: 3 + +status: + observedGeneration: 2 + replicas: 5 + expectedUpdatedReplicas: 2 + updatedReadyReplicas: 1 + updatedAvailableReplicas: 1 + updatedReplicas: 3 diff --git a/resource_customizations/apps.kruise.io/CloneSet/testdata/suspended.yaml b/resource_customizations/apps.kruise.io/CloneSet/testdata/suspended.yaml new file mode 100644 index 00000000000000..9edfaca6a51495 --- /dev/null +++ b/resource_customizations/apps.kruise.io/CloneSet/testdata/suspended.yaml @@ -0,0 +1,35 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: CloneSet +metadata: + name: cloneset-test + namespace: kruise + generation: 2 + labels: + app: sample +spec: + replicas: 1 + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + paused: true + +status: + observedGeneration: 2 + replicas: 2 + updatedReadyReplicas: 2 + updatedAvailableReplicas: 2 + conditions: + - lastTransitionTime: "2021-09-21T22:35:31Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: 'True' + type: FailedScale diff --git a/resource_customizations/apps.kruise.io/CloneSet/testdata/unknown.yaml b/resource_customizations/apps.kruise.io/CloneSet/testdata/unknown.yaml new file mode 100644 index 00000000000000..c1ccdb22fc76ee --- /dev/null +++ b/resource_customizations/apps.kruise.io/CloneSet/testdata/unknown.yaml @@ -0,0 +1,5 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: CloneSet +metadata: + name: cloneset-test + namespace: kruise diff --git a/resource_customizations/apps.kruise.io/DaemonSet/health.lua b/resource_customizations/apps.kruise.io/DaemonSet/health.lua new file mode 100644 index 00000000000000..7705bcc3325e52 --- /dev/null +++ b/resource_customizations/apps.kruise.io/DaemonSet/health.lua @@ -0,0 +1,35 @@ +hs={ status = "Progressing", message = "Waiting for initialization" } + +if obj.status ~= nil then + + if obj.metadata.generation == obj.status.observedGeneration then + + if obj.spec.updateStrategy.rollingUpdate.paused == true or not obj.status.updatedNumberScheduled then + hs.status = "Suspended" + hs.message = "Daemonset is paused" + return hs + elseif obj.spec.updateStrategy.rollingUpdate.partition ~= 0 and obj.metadata.generation > 1 then + if obj.status.updatedNumberScheduled > (obj.status.desiredNumberScheduled - obj.spec.updateStrategy.rollingUpdate.partition) then + hs.status = "Suspended" + hs.message = "Daemonset needs manual intervention" + return hs + end + + elseif (obj.status.updatedNumberScheduled == obj.status.desiredNumberScheduled) and (obj.status.numberAvailable == obj.status.desiredNumberScheduled) then + hs.status = "Healthy" + hs.message = "All Daemonset workloads are ready and updated" + return hs + + else + if (obj.status.updatedNumberScheduled == obj.status.desiredNumberScheduled) and (obj.status.numberUnavailable == obj.status.desiredNumberScheduled) then + hs.status = "Degraded" + hs.message = "Some pods are not ready or available" + return hs + end + end + + end + +end + +return hs diff --git a/resource_customizations/apps.kruise.io/DaemonSet/health_test.yaml b/resource_customizations/apps.kruise.io/DaemonSet/health_test.yaml new file mode 100644 index 00000000000000..0a8c8292672f30 --- /dev/null +++ b/resource_customizations/apps.kruise.io/DaemonSet/health_test.yaml @@ -0,0 +1,21 @@ +tests: + - healthStatus: + status: Healthy + message: "All Daemonset workloads are ready and updated" + inputPath: testdata/healthy.yaml + - healthStatus: + status: Degraded + message: "Some pods are not ready or available" + inputPath: testdata/degraded.yaml + - healthStatus: + status: Progressing + message: "Waiting for initialization" + inputPath: testdata/unknown.yaml + - healthStatus: + status: Suspended + message: "Daemonset is paused" + inputPath: testdata/suspended.yaml + - healthStatus: + status: Suspended + message: "Daemonset needs manual intervention" + inputPath: testdata/partition_suspended.yaml diff --git a/resource_customizations/apps.kruise.io/DaemonSet/testdata/degraded.yaml b/resource_customizations/apps.kruise.io/DaemonSet/testdata/degraded.yaml new file mode 100644 index 00000000000000..ed8cbc0b4699ed --- /dev/null +++ b/resource_customizations/apps.kruise.io/DaemonSet/testdata/degraded.yaml @@ -0,0 +1,34 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: DaemonSet +metadata: + name: daemonset-test + namespace: kruise + generation: 1 + labels: + app: sample +spec: + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + rollingUpdate: + partition: 0 + paused: false + +status: + currentNumberScheduled: 1 + daemonSetHash: 5dffcdfcd7 + desiredNumberScheduled: 1 + numberUnavailable: 1 + numberMisscheduled: 0 + numberReady: 0 + observedGeneration: 1 + updatedNumberScheduled: 1 diff --git a/resource_customizations/apps.kruise.io/DaemonSet/testdata/healthy.yaml b/resource_customizations/apps.kruise.io/DaemonSet/testdata/healthy.yaml new file mode 100644 index 00000000000000..6224ebf35e1644 --- /dev/null +++ b/resource_customizations/apps.kruise.io/DaemonSet/testdata/healthy.yaml @@ -0,0 +1,34 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: DaemonSet +metadata: + name: daemonset-test + namespace: kruise + generation: 1 + labels: + app: sample +spec: + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + rollingUpdate: + partition: 0 + paused: false + +status: + currentNumberScheduled: 1 + daemonSetHash: 5dffcdfcd7 + desiredNumberScheduled: 1 + numberAvailable: 1 + numberMisscheduled: 0 + numberReady: 1 + observedGeneration: 1 + updatedNumberScheduled: 1 diff --git a/resource_customizations/apps.kruise.io/DaemonSet/testdata/partition_suspended.yaml b/resource_customizations/apps.kruise.io/DaemonSet/testdata/partition_suspended.yaml new file mode 100644 index 00000000000000..4c0819cdc8703a --- /dev/null +++ b/resource_customizations/apps.kruise.io/DaemonSet/testdata/partition_suspended.yaml @@ -0,0 +1,33 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: DaemonSet +metadata: + name: daemonset-test + namespace: kruise + generation: 6 + labels: + app: sample +spec: + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + rollingUpdate: + partition: 4 + +status: + currentNumberScheduled: 1 + daemonSetHash: 5f8cdcdc65 + desiredNumberScheduled: 10 + numberAvailable: 10 + numberMisscheduled: 0 + numberReady: 10 + observedGeneration: 6 + updatedNumberScheduled: 7 diff --git a/resource_customizations/apps.kruise.io/DaemonSet/testdata/suspended.yaml b/resource_customizations/apps.kruise.io/DaemonSet/testdata/suspended.yaml new file mode 100644 index 00000000000000..fb705f5578176e --- /dev/null +++ b/resource_customizations/apps.kruise.io/DaemonSet/testdata/suspended.yaml @@ -0,0 +1,33 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: DaemonSet +metadata: + name: daemonset-test + namespace: kruise + generation: 1 + labels: + app: sample +spec: + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + rollingUpdate: + paused: true + +status: + currentNumberScheduled: 1 + daemonSetHash: 5dffcdfcd7 + desiredNumberScheduled: 1 + numberAvailable: 1 + numberMisscheduled: 0 + numberReady: 1 + observedGeneration: 1 + updatedNumberScheduled: 1 diff --git a/resource_customizations/apps.kruise.io/DaemonSet/testdata/unknown.yaml b/resource_customizations/apps.kruise.io/DaemonSet/testdata/unknown.yaml new file mode 100644 index 00000000000000..aa5791c52bc6c1 --- /dev/null +++ b/resource_customizations/apps.kruise.io/DaemonSet/testdata/unknown.yaml @@ -0,0 +1,5 @@ +apiVersion: apps.kruise.io/v1alpha1 +kind: DaemonSet +metadata: + name: daemonset-test + namespace: kruise diff --git a/resource_customizations/apps.kruise.io/StatefulSet/health.lua b/resource_customizations/apps.kruise.io/StatefulSet/health.lua new file mode 100644 index 00000000000000..47340452db2dc9 --- /dev/null +++ b/resource_customizations/apps.kruise.io/StatefulSet/health.lua @@ -0,0 +1,35 @@ +hs={ status = "Progressing", message = "Waiting for initialization" } + +if obj.status ~= nil then + + if obj.metadata.generation == obj.status.observedGeneration then + + if obj.spec.updateStrategy.rollingUpdate.paused == true or not obj.status.updatedAvailableReplicas then + hs.status = "Suspended" + hs.message = "Statefulset is paused" + return hs + elseif obj.spec.updateStrategy.rollingUpdate.partition ~= 0 and obj.metadata.generation > 1 then + if obj.status.updatedReplicas > (obj.status.replicas - obj.spec.updateStrategy.rollingUpdate.partition) then + hs.status = "Suspended" + hs.message = "Statefulset needs manual intervention" + return hs + end + + elseif obj.status.updatedAvailableReplicas == obj.status.replicas then + hs.status = "Healthy" + hs.message = "All Statefulset workloads are ready and updated" + return hs + + else + if obj.status.updatedAvailableReplicas ~= obj.status.replicas then + hs.status = "Degraded" + hs.message = "Some replicas are not ready or available" + return hs + end + end + + end + +end + +return hs diff --git a/resource_customizations/apps.kruise.io/StatefulSet/health_test.yaml b/resource_customizations/apps.kruise.io/StatefulSet/health_test.yaml new file mode 100644 index 00000000000000..6672b9f46d4f41 --- /dev/null +++ b/resource_customizations/apps.kruise.io/StatefulSet/health_test.yaml @@ -0,0 +1,21 @@ +tests: + - healthStatus: + status: Healthy + message: "All Statefulset workloads are ready and updated" + inputPath: testdata/healthy.yaml + - healthStatus: + status: Degraded + message: "Some replicas are not ready or available" + inputPath: testdata/degraded.yaml + - healthStatus: + status: Progressing + message: "Waiting for initialization" + inputPath: testdata/unknown.yaml + - healthStatus: + status: Suspended + message: "Statefulset is paused" + inputPath: testdata/suspended.yaml + - healthStatus: + status: Suspended + message: "Statefulset needs manual intervention" + inputPath: testdata/partition_suspended.yaml diff --git a/resource_customizations/apps.kruise.io/StatefulSet/testdata/degraded.yaml b/resource_customizations/apps.kruise.io/StatefulSet/testdata/degraded.yaml new file mode 100644 index 00000000000000..88e58914940fcb --- /dev/null +++ b/resource_customizations/apps.kruise.io/StatefulSet/testdata/degraded.yaml @@ -0,0 +1,42 @@ +apiVersion: apps.kruise.io/v1beta1 +kind: StatefulSet +metadata: + name: statefulset-test + namespace: kruise + generation: 5 + labels: + app: sample +spec: + replicas: 2 + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + rollingUpdate: + maxUnavailable: 1 + minReadySeconds: 0 + paused: false + partition: 0 + podUpdatePolicy: ReCreate + type: RollingUpdate + +status: + observedGeneration: 5 + replicas: 2 + updatedAvailableReplicas: 1 + updatedReadyReplicas: 1 + conditions: + - lastTransitionTime: "2021-09-21T22:35:31Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: 'True' + type: FailedCreatePod + diff --git a/resource_customizations/apps.kruise.io/StatefulSet/testdata/healthy.yaml b/resource_customizations/apps.kruise.io/StatefulSet/testdata/healthy.yaml new file mode 100644 index 00000000000000..793de25d3da1cd --- /dev/null +++ b/resource_customizations/apps.kruise.io/StatefulSet/testdata/healthy.yaml @@ -0,0 +1,41 @@ +apiVersion: apps.kruise.io/v1beta1 +kind: StatefulSet +metadata: + name: statefulset-test + namespace: kruise + generation: 2 + labels: + app: sample +spec: + replicas: 2 + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + rollingUpdate: + maxUnavailable: 1 + minReadySeconds: 0 + paused: false + partition: 0 + podUpdatePolicy: ReCreate + type: RollingUpdate + +status: + observedGeneration: 2 + replicas: 2 + updatedAvailableReplicas: 2 + updatedReadyReplicas: 2 + conditions: + - lastTransitionTime: "2021-09-21T22:35:31Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: 'False' + type: FailedCreatePod diff --git a/resource_customizations/apps.kruise.io/StatefulSet/testdata/partition_suspended.yaml b/resource_customizations/apps.kruise.io/StatefulSet/testdata/partition_suspended.yaml new file mode 100644 index 00000000000000..b09a7726bf5d7d --- /dev/null +++ b/resource_customizations/apps.kruise.io/StatefulSet/testdata/partition_suspended.yaml @@ -0,0 +1,36 @@ +apiVersion: apps.kruise.io/v1beta1 +kind: StatefulSet +metadata: + name: statefulset-test + namespace: kruise + generation: 3 + labels: + app: sample +spec: + replicas: 10 + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - image: nginx:mainline + updateStrategy: + rollingUpdate: + partition: 4 + +status: + availableReplicas: 10 + currentReplicas: 4 + currentRevision: statefulset-test-d4d4fb5bd + labelSelector: app=sample + observedGeneration: 3 + readyReplicas: 10 + replicas: 10 + updateRevision: statefulset-test-56dfb978d4 + updatedAvailableReplicas: 7 + updatedReadyReplicas: 7 + updatedReplicas: 7 diff --git a/resource_customizations/apps.kruise.io/StatefulSet/testdata/suspended.yaml b/resource_customizations/apps.kruise.io/StatefulSet/testdata/suspended.yaml new file mode 100644 index 00000000000000..42dae9cf5e322f --- /dev/null +++ b/resource_customizations/apps.kruise.io/StatefulSet/testdata/suspended.yaml @@ -0,0 +1,36 @@ +apiVersion: apps.kruise.io/v1beta1 +kind: StatefulSet +metadata: + name: statefulset-test + namespace: kruise + generation: 2 + labels: + app: sample +spec: + replicas: 2 + selector: + matchLabels: + app: sample + template: + metadata: + labels: + app: sample + spec: + containers: + - name: nginx + image: nginx:alpine + updateStrategy: + rollingUpdate: + paused: true + +status: + observedGeneration: 2 + replicas: 2 + updatedAvailableReplicas: 2 + updatedReadyReplicas: 2 + conditions: + - lastTransitionTime: "2021-09-21T22:35:31Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: 'False' + type: FailedCreatePod diff --git a/resource_customizations/apps.kruise.io/StatefulSet/testdata/unknown.yaml b/resource_customizations/apps.kruise.io/StatefulSet/testdata/unknown.yaml new file mode 100644 index 00000000000000..67d28de6dae645 --- /dev/null +++ b/resource_customizations/apps.kruise.io/StatefulSet/testdata/unknown.yaml @@ -0,0 +1,5 @@ +apiVersion: apps.kruise.io/v1beta1 +kind: StatefulSet +metadata: + name: statefulset-test + namespace: kruise diff --git a/resource_customizations/batch/CronJob/actions/create-job/action.lua b/resource_customizations/batch/CronJob/actions/create-job/action.lua index 8753144d404e7f..a6f3253a5b7573 100644 --- a/resource_customizations/batch/CronJob/actions/create-job/action.lua +++ b/resource_customizations/batch/CronJob/actions/create-job/action.lua @@ -38,12 +38,18 @@ if job.metadata == nil then end job.metadata.name = obj.metadata.name .. "-" ..os.date("!%Y%m%d%H%M") job.metadata.namespace = obj.metadata.namespace +if job.metadata.annotations == nil then + job.metadata.annotations = {} +end +job.metadata.annotations['cronjob.kubernetes.io/instantiate'] = "manual" local ownerRef = {} ownerRef.apiVersion = obj.apiVersion ownerRef.kind = obj.kind ownerRef.name = obj.metadata.name ownerRef.uid = obj.metadata.uid +ownerRef.blockOwnerDeletion = true +ownerRef.controller = true job.metadata.ownerReferences = {} job.metadata.ownerReferences[1] = ownerRef diff --git a/resource_customizations/batch/CronJob/actions/testdata/job.yaml b/resource_customizations/batch/CronJob/actions/testdata/job.yaml index 1ef281afdcdb4e..322ab0480beb5b 100644 --- a/resource_customizations/batch/CronJob/actions/testdata/job.yaml +++ b/resource_customizations/batch/CronJob/actions/testdata/job.yaml @@ -8,6 +8,7 @@ labels: my: label annotations: + cronjob.kubernetes.io/instantiate: manual my: annotation spec: ttlSecondsAfterFinished: 100 @@ -26,4 +27,4 @@ - /bin/sh - -c - date; echo Hello from the Kubernetes cluster - restartPolicy: OnFailure \ No newline at end of file + restartPolicy: OnFailure diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/health.lua b/resource_customizations/beat.k8s.elastic.co/Beat/health.lua new file mode 100644 index 00000000000000..c7639dbbd94f0d --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/health.lua @@ -0,0 +1,31 @@ +local hs = {} + +if obj.status ~= nil and (obj.status.health ~= nil or obj.status.expectedNodes ~= nil) then + if obj.status.health == "red" then + hs.status = "Degraded" + hs.message = "Elastic Beat status is Red" + return hs + elseif obj.status.health == "green" then + hs.status = "Healthy" + hs.message = "Elastic Beat status is Green" + return hs + elseif obj.status.health == "yellow" then + if obj.status.availableNodes ~= nil and obj.status.expectedNodes ~= nil then + hs.status = "Progressing" + hs.message = "Elastic Beat status is deploying, there is " .. obj.status.availableNodes .. " instance(s) on " .. obj.status.expectedNodes .. " expected" + return hs + else + hs.status = "Progressing" + hs.message = "Elastic Beat phase is progressing" + return hs + end + elseif obj.status.health == nil then + hs.status = "Progressing" + hs.message = "Elastic Beat phase is progressing" + return hs + end +end + +hs.status = "Unknown" +hs.message = "Elastic Beat status is unknown. Ensure your ArgoCD is current and then check for/file a bug report: https://github.com/argoproj/argo-cd/issues" +return hs diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/health_test.yaml b/resource_customizations/beat.k8s.elastic.co/Beat/health_test.yaml new file mode 100644 index 00000000000000..fb44e998ffaf1f --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/health_test.yaml @@ -0,0 +1,29 @@ +tests: +- healthStatus: + status: Healthy + message: "Elastic Beat status is Green" + inputPath: testdata/ready_green.yaml +- healthStatus: + status: Progressing + message: "Elastic Beat phase is progressing" + inputPath: testdata/ready_yellow_single_node.yaml +- healthStatus: + status: Progressing + message: "Elastic Beat status is deploying, there is 1 instance(s) on 2 expected" + inputPath: testdata/ready_yellow.yaml +- healthStatus: + status: Progressing + message: "Elastic Beat phase is progressing" + inputPath: testdata/progressing.yaml +- healthStatus: + status: Degraded + message: "Elastic Beat status is Red" + inputPath: testdata/ready_red.yaml +- healthStatus: + status: Unknown + message: "Elastic Beat status is unknown. Ensure your ArgoCD is current and then check for/file a bug report: https://github.com/argoproj/argo-cd/issues" + inputPath: testdata/unknown.yaml +- healthStatus: + status: Unknown + message: "Elastic Beat status is unknown. Ensure your ArgoCD is current and then check for/file a bug report: https://github.com/argoproj/argo-cd/issues" + inputPath: testdata/invalid.yaml diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/testdata/invalid.yaml b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/invalid.yaml new file mode 100644 index 00000000000000..3eca183165a5cc --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/invalid.yaml @@ -0,0 +1,12 @@ +apiVersion: beat.k8s.elastic.co/v1beta1 +kind: Beat +metadata: + name: quickstart +spec: + version: 8.8.8 + type: metricbeat +status: + expectedNodes: 1 + health: invalid + observedGeneration: 1 + version: 8.8.1 diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/testdata/progressing.yaml b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/progressing.yaml new file mode 100644 index 00000000000000..b007ad72ae3fe4 --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/progressing.yaml @@ -0,0 +1,11 @@ +apiVersion: beat.k8s.elastic.co/v1beta1 +kind: Beat +metadata: + name: quickstart +spec: + version: 8.8.8 + type: metricbeat +status: + expectedNodes: 1 + observedGeneration: 1 + version: 8.8.1 diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_green.yaml b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_green.yaml new file mode 100644 index 00000000000000..3f3c1866793d87 --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_green.yaml @@ -0,0 +1,13 @@ +apiVersion: beat.k8s.elastic.co/v1beta1 +kind: Beat +metadata: + name: quickstart +spec: + version: 8.8.8 + type: metricbeat +status: + expectedNodes: 1 + availableNodes: 1 + health: green + observedGeneration: 1 + version: 8.8.1 diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_red.yaml b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_red.yaml new file mode 100644 index 00000000000000..fc2433c8076a8b --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_red.yaml @@ -0,0 +1,10 @@ +apiVersion: beat.k8s.elastic.co/v1beta1 +kind: Beat +metadata: + name: quickstart +spec: + version: 8.8.8 + type: metricbeat +status: + expectedNodes: 1 + health: red diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_yellow.yaml b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_yellow.yaml new file mode 100644 index 00000000000000..831ee281ef02df --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_yellow.yaml @@ -0,0 +1,11 @@ +apiVersion: beat.k8s.elastic.co/v1beta1 +kind: Beat +metadata: + name: quickstart +spec: + version: 8.8.8 + type: metricbeat +status: + availableNodes: 1 + expectedNodes: 2 + health: yellow diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_yellow_single_node.yaml b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_yellow_single_node.yaml new file mode 100644 index 00000000000000..d652b5a55d0fff --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/ready_yellow_single_node.yaml @@ -0,0 +1,10 @@ +apiVersion: beat.k8s.elastic.co/v1beta1 +kind: Beat +metadata: + name: quickstart +spec: + version: 8.8.8 + type: metricbeat +status: + expectedNodes: 1 + health: yellow diff --git a/resource_customizations/beat.k8s.elastic.co/Beat/testdata/unknown.yaml b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/unknown.yaml new file mode 100644 index 00000000000000..dbcca36c9e691d --- /dev/null +++ b/resource_customizations/beat.k8s.elastic.co/Beat/testdata/unknown.yaml @@ -0,0 +1,8 @@ +apiVersion: beat.k8s.elastic.co/v1beta1 +kind: Beat +metadata: + name: quickstart +spec: + version: 8.8.8 + type: metricbeat +status: {} diff --git a/resource_customizations/cert-manager.io/Certificate/health.lua b/resource_customizations/cert-manager.io/Certificate/health.lua index ddecb2631b39ad..fce5bcbe3d1d0a 100644 --- a/resource_customizations/cert-manager.io/Certificate/health.lua +++ b/resource_customizations/cert-manager.io/Certificate/health.lua @@ -1,12 +1,17 @@ local hs = {} if obj.status ~= nil then if obj.status.conditions ~= nil then + + -- Always Handle Issuing First to ensure consistent behaviour for i, condition in ipairs(obj.status.conditions) do if condition.type == "Issuing" and condition.status == "True" then hs.status = "Progressing" hs.message = condition.message return hs end + end + + for i, condition in ipairs(obj.status.conditions) do if condition.type == "Ready" and condition.status == "False" then hs.status = "Degraded" hs.message = condition.message diff --git a/resource_customizations/cert-manager.io/Certificate/health_test.yaml b/resource_customizations/cert-manager.io/Certificate/health_test.yaml index ebf8e75e890647..1af7b1a759a604 100644 --- a/resource_customizations/cert-manager.io/Certificate/health_test.yaml +++ b/resource_customizations/cert-manager.io/Certificate/health_test.yaml @@ -7,6 +7,10 @@ tests: status: Progressing message: Issuing certificate as Secret does not exist inputPath: testdata/progressing_issuing.yaml +- healthStatus: + status: Progressing + message: Issuing certificate as Secret does not exist + inputPath: testdata/progressing_issuing_last.yaml - healthStatus: status: Degraded message: 'Resource validation failed: spec.acme.config: Required value: no ACME diff --git a/resource_customizations/cert-manager.io/Certificate/testdata/progressing_issuing_last.yaml b/resource_customizations/cert-manager.io/Certificate/testdata/progressing_issuing_last.yaml new file mode 100644 index 00000000000000..4d21a9b3610f12 --- /dev/null +++ b/resource_customizations/cert-manager.io/Certificate/testdata/progressing_issuing_last.yaml @@ -0,0 +1,36 @@ +apiVersion: cert-manager.io/v1alpha2 +kind: Certificate +metadata: + creationTimestamp: '2018-11-07T00:06:12Z' + generation: 1 + name: test-cert + namespace: argocd + resourceVersion: '64763033' + selfLink: /apis/cert-manager.io/v1alpha2/namespaces/argocd/certificates/test-cert + uid: e6cfba50-314d-11e9-be3f-42010a800011 +spec: + acme: + config: + - domains: + - cd.apps.argoproj.io + http01: + ingress: http01 + commonName: cd.apps.argoproj.io + dnsNames: + - cd.apps.argoproj.io + issuerRef: + kind: Issuer + name: argo-cd-issuer + secretName: test-secret +status: + conditions: + - lastTransitionTime: '2021-09-15T02:10:00Z' + message: Issuing certificate as Secret does not exist + reason: DoesNotExist + status: 'False' + type: Ready + - lastTransitionTime: '2021-09-15T02:10:00Z' + message: Issuing certificate as Secret does not exist + reason: DoesNotExist + status: 'True' + type: Issuing diff --git a/resource_customizations/cert-manager.io/ClusterIssuer/testdata/degraded_acmeFailed.yaml b/resource_customizations/cert-manager.io/ClusterIssuer/testdata/degraded_acmeFailed.yaml index 75a249feb3da63..c99c1f4f84ba45 100644 --- a/resource_customizations/cert-manager.io/ClusterIssuer/testdata/degraded_acmeFailed.yaml +++ b/resource_customizations/cert-manager.io/ClusterIssuer/testdata/degraded_acmeFailed.yaml @@ -8,7 +8,7 @@ metadata: uid: 37f408e3-3157-11e9-be3f-42010a800011 spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/cert-manager.io/ClusterIssuer/testdata/healthy_registered.yaml b/resource_customizations/cert-manager.io/ClusterIssuer/testdata/healthy_registered.yaml index edad50241c40b5..e883b51e3a793e 100644 --- a/resource_customizations/cert-manager.io/ClusterIssuer/testdata/healthy_registered.yaml +++ b/resource_customizations/cert-manager.io/ClusterIssuer/testdata/healthy_registered.yaml @@ -8,7 +8,7 @@ metadata: uid: b0045219-e219-11e8-9f93-42010a80021d spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/cert-manager.io/ClusterIssuer/testdata/progressing_noStatus.yaml b/resource_customizations/cert-manager.io/ClusterIssuer/testdata/progressing_noStatus.yaml index b05c4aeb7d13f6..4571d229ffed7d 100644 --- a/resource_customizations/cert-manager.io/ClusterIssuer/testdata/progressing_noStatus.yaml +++ b/resource_customizations/cert-manager.io/ClusterIssuer/testdata/progressing_noStatus.yaml @@ -8,7 +8,7 @@ metadata: uid: b0045219-e219-11e8-9f93-42010a80021d spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/cert-manager.io/Issuer/testdata/degraded_acmeFailed.yaml b/resource_customizations/cert-manager.io/Issuer/testdata/degraded_acmeFailed.yaml index 62226e3b3be620..a5abcf57a5ac22 100644 --- a/resource_customizations/cert-manager.io/Issuer/testdata/degraded_acmeFailed.yaml +++ b/resource_customizations/cert-manager.io/Issuer/testdata/degraded_acmeFailed.yaml @@ -10,7 +10,7 @@ metadata: uid: 37f408e3-3157-11e9-be3f-42010a800011 spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/cert-manager.io/Issuer/testdata/healthy_registered.yaml b/resource_customizations/cert-manager.io/Issuer/testdata/healthy_registered.yaml index 08b96394ec8231..07181567145f21 100644 --- a/resource_customizations/cert-manager.io/Issuer/testdata/healthy_registered.yaml +++ b/resource_customizations/cert-manager.io/Issuer/testdata/healthy_registered.yaml @@ -10,7 +10,7 @@ metadata: uid: b0045219-e219-11e8-9f93-42010a80021d spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/cert-manager.io/Issuer/testdata/progressing_noStatus.yaml b/resource_customizations/cert-manager.io/Issuer/testdata/progressing_noStatus.yaml index 820182e3e1e6ad..f2e7b80e7f0b55 100644 --- a/resource_customizations/cert-manager.io/Issuer/testdata/progressing_noStatus.yaml +++ b/resource_customizations/cert-manager.io/Issuer/testdata/progressing_noStatus.yaml @@ -10,7 +10,7 @@ metadata: uid: b0045219-e219-11e8-9f93-42010a80021d spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/certmanager.k8s.io/Issuer/testdata/degraded_acmeFailed.yaml b/resource_customizations/certmanager.k8s.io/Issuer/testdata/degraded_acmeFailed.yaml index dbd819ca9f1136..5f0dbec676917f 100644 --- a/resource_customizations/certmanager.k8s.io/Issuer/testdata/degraded_acmeFailed.yaml +++ b/resource_customizations/certmanager.k8s.io/Issuer/testdata/degraded_acmeFailed.yaml @@ -10,7 +10,7 @@ metadata: uid: 37f408e3-3157-11e9-be3f-42010a800011 spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/certmanager.k8s.io/Issuer/testdata/healthy_registered.yaml b/resource_customizations/certmanager.k8s.io/Issuer/testdata/healthy_registered.yaml index db0a81b941bab9..a5f6aa14986d68 100644 --- a/resource_customizations/certmanager.k8s.io/Issuer/testdata/healthy_registered.yaml +++ b/resource_customizations/certmanager.k8s.io/Issuer/testdata/healthy_registered.yaml @@ -10,7 +10,7 @@ metadata: uid: b0045219-e219-11e8-9f93-42010a80021d spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/certmanager.k8s.io/Issuer/testdata/progressing_noStatus.yaml b/resource_customizations/certmanager.k8s.io/Issuer/testdata/progressing_noStatus.yaml index 68f35fa773256a..501b7aa20060f9 100644 --- a/resource_customizations/certmanager.k8s.io/Issuer/testdata/progressing_noStatus.yaml +++ b/resource_customizations/certmanager.k8s.io/Issuer/testdata/progressing_noStatus.yaml @@ -10,7 +10,7 @@ metadata: uid: b0045219-e219-11e8-9f93-42010a80021d spec: acme: - email: myemail@test.com + email: myemail@example.com http01: {} privateKeySecretRef: key: "" diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/health.lua b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/health.lua new file mode 100644 index 00000000000000..3e07226b3cf893 --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/health.lua @@ -0,0 +1,42 @@ +local hs = {} +if obj.status ~= nil then + if obj.status.conditions ~= nil then + local ready = false + local synced = false + local suspended = false + + for i, condition in ipairs(obj.status.conditions) do + + if condition.type == "Ready" then + ready = condition.status == "True" + ready_message = condition.reason + elseif condition.type == "Synced" then + synced = condition.status == "True" + if condition.reason == "ReconcileError" then + synced_message = condition.message + elseif condition.reason == "ReconcilePaused" then + suspended = true + suspended_message = condition.reason + end + end + end + if ready and synced then + hs.status = "Healthy" + hs.message = ready_message + elseif synced == false and suspended == true then + hs.status = "Suspended" + hs.message = suspended_message + elseif ready == false and synced == true and suspended == false then + hs.status = "Progressing" + hs.message = "Waiting for distribution to be available" + else + hs.status = "Degraded" + hs.message = synced_message + end + return hs + end +end + +hs.status = "Progressing" +hs.message = "Waiting for distribution to be created" +return hs \ No newline at end of file diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/health_test.yaml b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/health_test.yaml new file mode 100644 index 00000000000000..981a6000ecb889 --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/health_test.yaml @@ -0,0 +1,37 @@ +tests: +- healthStatus: + status: Progressing + message: Waiting for distribution to be available + inputPath: testdata/progressing_creating.yaml +- healthStatus: + status: Progressing + message: Waiting for distribution to be available + inputPath: testdata/progressing_noavailable.yaml +- healthStatus: + status: Progressing + message: Waiting for distribution to be available + inputPath: testdata/progressing.yaml +- healthStatus: + status: Progressing + message: Waiting for distribution to be created + inputPath: testdata/progressing_noStatus.yaml +- healthStatus: + status: Degraded + message: > + update failed: cannot update Distribution in AWS: InvalidParameter: 2 + validation error(s) found. + + - missing required field, + UpdateDistributionInput.DistributionConfig.Origins.Items[0].DomainName. + + - missing required field, + UpdateDistributionInput.DistributionConfig.Origins.Items[0].Id. + inputPath: testdata/degraded_reconcileError.yaml +- healthStatus: + status: Suspended + message: ReconcilePaused + inputPath: testdata/suspended.yaml +- healthStatus: + status: Healthy + message: Available + inputPath: testdata/healthy.yaml diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/degraded_reconcileError.yaml b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/degraded_reconcileError.yaml new file mode 100644 index 00000000000000..80ea7930574ac0 --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/degraded_reconcileError.yaml @@ -0,0 +1,96 @@ +apiVersion: cloudfront.aws.crossplane.io/v1alpha1 +kind: Distribution +metadata: + creationTimestamp: '2024-01-17T07:26:02Z' + generation: 2 + name: crossplane.io + resourceVersion: '261942288' + uid: 4b50c88b-165c-4176-be8e-aa28fdec0a94 +spec: + deletionPolicy: Orphan + forProvider: + distributionConfig: + comment: 'crossplane' + customErrorResponses: + items: [] + defaultCacheBehavior: + allowedMethods: + cachedMethods: + items: + - HEAD + - GET + items: + - HEAD + - GET + compress: false + defaultTTL: 600 + fieldLevelEncryptionID: '' + forwardedValues: + cookies: + forward: none + headers: + items: [] + queryString: false + queryStringCacheKeys: {} + functionAssociations: {} + lambdaFunctionAssociations: {} + maxTTL: 600 + minTTL: 0 + smoothStreaming: false + targetOriginID: crossplane.io + trustedKeyGroups: + enabled: false + trustedSigners: + enabled: false + viewerProtocolPolicy: allow-all + defaultRootObject: index.html + enabled: true + httpVersion: http2 + isIPV6Enabled: true + logging: + bucket: '' + enabled: false + includeCookies: false + prefix: '' + originGroups: {} + origins: + items: + - connectionAttempts: 3 + connectionTimeout: 10 + customOriginConfig: + httpPort: 8080 + httpSPort: 443 + originKeepaliveTimeout: 5 + originProtocolPolicy: http-only + originReadTimeout: 10 + originSSLProtocols: + items: + - TLSv1 + - TLSv1.1 + - TLSv1.2 + priceClass: PriceClass_200 + restrictions: + geoRestriction: + restrictionType: none + region: ap-northeast-2 + providerConfigRef: + name: crossplane +status: + conditions: + - lastTransitionTime: '2024-01-17T07:26:02Z' + message: > + update failed: cannot update Distribution in AWS: InvalidParameter: 2 + validation error(s) found. + + - missing required field, + UpdateDistributionInput.DistributionConfig.Origins.Items[0].DomainName. + + - missing required field, + UpdateDistributionInput.DistributionConfig.Origins.Items[0].Id. + reason: ReconcileError + status: 'False' + type: Synced + - lastTransitionTime: '2024-01-17T07:26:03Z' + reason: Available + status: 'True' + type: Ready diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/healthy.yaml b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/healthy.yaml new file mode 100644 index 00000000000000..23d0287445e83f --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/healthy.yaml @@ -0,0 +1,92 @@ +apiVersion: cloudfront.aws.crossplane.io/v1alpha1 +kind: Distribution +metadata: + creationTimestamp: "2023-09-07T01:01:16Z" + generation: 121 + name: crossplane.io + resourceVersion: "254225966" + uid: 531d989c-a3d2-4ab4-841d-ab380cce0bdb +spec: + deletionPolicy: Orphan + forProvider: + distributionConfig: + comment: 'crossplane' + customErrorResponses: + items: [] + defaultCacheBehavior: + allowedMethods: + cachedMethods: + items: + - HEAD + - GET + items: + - HEAD + - GET + compress: false + defaultTTL: 600 + fieldLevelEncryptionID: '' + forwardedValues: + cookies: + forward: none + headers: + items: [] + queryString: false + queryStringCacheKeys: {} + functionAssociations: {} + lambdaFunctionAssociations: {} + maxTTL: 600 + minTTL: 0 + smoothStreaming: false + targetOriginID: crossplane.io + trustedKeyGroups: + enabled: false + trustedSigners: + enabled: false + viewerProtocolPolicy: allow-all + defaultRootObject: index.html + enabled: true + httpVersion: http2 + isIPV6Enabled: true + logging: + bucket: '' + enabled: false + includeCookies: false + prefix: '' + originGroups: {} + origins: + items: + - connectionAttempts: 3 + connectionTimeout: 10 + customHeaders: {} + customOriginConfig: + httpPort: 8080 + httpSPort: 443 + originKeepaliveTimeout: 5 + originProtocolPolicy: http-only + originReadTimeout: 10 + originSSLProtocols: + items: + - TLSv1 + - TLSv1.1 + - TLSv1.2 + domainName: crossplane.io + id: crossplane.io + originShield: + enabled: false + priceClass: PriceClass_200 + restrictions: + geoRestriction: + restrictionType: none + region: ap-northeast-2 + providerConfigRef: + name: crossplane +status: + conditions: + - lastTransitionTime: "2024-01-11T06:23:18Z" + reason: ReconcileSuccess + status: "True" + type: Synced + - lastTransitionTime: "2024-01-10T03:23:02Z" + reason: Available + status: "True" + type: Ready diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing.yaml b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing.yaml new file mode 100644 index 00000000000000..3dbde7e0408672 --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing.yaml @@ -0,0 +1,92 @@ +apiVersion: cloudfront.aws.crossplane.io/v1alpha1 +kind: Distribution +metadata: + creationTimestamp: '2023-06-16T04:42:04Z' + generation: 37 + name: crossplane.io + resourceVersion: '254326453' + uid: fd357670-b762-4285-ae83-00859c40dd6b +spec: + deletionPolicy: Orphan + forProvider: + distributionConfig: + comment: 'crossplane' + customErrorResponses: + items: [] + defaultCacheBehavior: + allowedMethods: + cachedMethods: + items: + - HEAD + - GET + items: + - GET + - HEAD + compress: false + defaultTTL: 600 + fieldLevelEncryptionID: "" + forwardedValues: + cookies: + forward: none + headers: + items: [] + queryString: false + queryStringCacheKeys: {} + functionAssociations: {} + lambdaFunctionAssociations: {} + maxTTL: 600 + minTTL: 0 + smoothStreaming: false + targetOriginID: crossplane.io + trustedKeyGroups: + enabled: false + trustedSigners: + enabled: false + viewerProtocolPolicy: allow-all + defaultRootObject: index.html + enabled: true + httpVersion: http2 + isIPV6Enabled: true + logging: + bucket: "" + enabled: false + includeCookies: false + prefix: "" + originGroups: {} + origins: + items: + - connectionAttempts: 3 + connectionTimeout: 10 + customHeaders: {} + customOriginConfig: + httpPort: 8080 + httpSPort: 443 + originKeepaliveTimeout: 5 + originProtocolPolicy: http-only + originReadTimeout: 10 + originSSLProtocols: + items: + - TLSv1 + - TLSv1.1 + - TLSv1.2 + domainName: crossplane.io + id: crossplane.io + originShield: + enabled: false + priceClass: PriceClass_200 + restrictions: + geoRestriction: + restrictionType: none + region: ap-northeast-2 + providerConfigRef: + name: crossplane +status: + conditions: + - lastTransitionTime: '2024-01-11T08:11:27Z' + reason: Unavailable + status: 'False' + type: Ready + - lastTransitionTime: '2024-01-11T08:11:02Z' + reason: ReconcileSuccess + status: 'True' + type: Synced diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_creating.yaml b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_creating.yaml new file mode 100644 index 00000000000000..122ab330d593b1 --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_creating.yaml @@ -0,0 +1,92 @@ +apiVersion: cloudfront.aws.crossplane.io/v1alpha1 +kind: Distribution +metadata: + creationTimestamp: "2023-09-07T01:01:16Z" + generation: 121 + name: crossplane.io + resourceVersion: "254225966" + uid: 531d989c-a3d2-4ab4-841d-ab380cce0bdb +spec: + deletionPolicy: Orphan + forProvider: + distributionConfig: + comment: 'crossplane' + customErrorResponses: + items: [] + defaultCacheBehavior: + allowedMethods: + cachedMethods: + items: + - HEAD + - GET + items: + - GET + - HEAD + compress: false + defaultTTL: 600 + fieldLevelEncryptionID: "" + forwardedValues: + cookies: + forward: none + headers: + items: [] + queryString: false + queryStringCacheKeys: {} + functionAssociations: {} + lambdaFunctionAssociations: {} + maxTTL: 600 + minTTL: 0 + smoothStreaming: false + targetOriginID: crossplane.io + trustedKeyGroups: + enabled: false + trustedSigners: + enabled: false + viewerProtocolPolicy: allow-all + defaultRootObject: index.html + enabled: true + httpVersion: http2 + isIPV6Enabled: true + logging: + bucket: "" + enabled: false + includeCookies: false + prefix: "" + originGroups: {} + origins: + items: + - connectionAttempts: 3 + connectionTimeout: 10 + customHeaders: {} + customOriginConfig: + httpPort: 8080 + httpSPort: 443 + originKeepaliveTimeout: 5 + originProtocolPolicy: http-only + originReadTimeout: 10 + originSSLProtocols: + items: + - TLSv1 + - TLSv1.1 + - TLSv1.2 + domainName: crossplane.io + id: crossplane.io + originShield: + enabled: false + priceClass: PriceClass_200 + restrictions: + geoRestriction: + restrictionType: none + region: ap-northeast-2 + providerConfigRef: + name: crossplane +status: + conditions: + - lastTransitionTime: "2023-11-16T04:44:27Z" + reason: Creating + status: "False" + type: Ready + - lastTransitionTime: "2023-11-16T04:44:25Z" + reason: ReconcileSuccess + status: "True" + type: Synced diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_noStatus.yaml b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_noStatus.yaml new file mode 100644 index 00000000000000..2985ec2dea657b --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_noStatus.yaml @@ -0,0 +1,82 @@ +apiVersion: cloudfront.aws.crossplane.io/v1alpha1 +kind: Distribution +metadata: + creationTimestamp: "2023-09-07T01:01:16Z" + generation: 121 + name: crossplane.io + resourceVersion: "254225966" + uid: 531d989c-a3d2-4ab4-841d-ab380cce0bdb +spec: + deletionPolicy: Orphan + forProvider: + distributionConfig: + comment: 'crossplane' + customErrorResponses: + items: [] + defaultCacheBehavior: + allowedMethods: + cachedMethods: + items: + - HEAD + - GET + items: + - GET + - HEAD + compress: false + defaultTTL: 600 + fieldLevelEncryptionID: "" + forwardedValues: + cookies: + forward: none + headers: + items: [] + queryString: false + queryStringCacheKeys: {} + functionAssociations: {} + lambdaFunctionAssociations: {} + maxTTL: 600 + minTTL: 0 + smoothStreaming: false + targetOriginID: crossplane.io + trustedKeyGroups: + enabled: false + trustedSigners: + enabled: false + viewerProtocolPolicy: allow-all + defaultRootObject: index.html + enabled: true + httpVersion: http2 + isIPV6Enabled: true + logging: + bucket: "" + enabled: false + includeCookies: false + prefix: "" + originGroups: {} + origins: + items: + - connectionAttempts: 3 + connectionTimeout: 10 + customHeaders: {} + customOriginConfig: + httpPort: 8080 + httpSPort: 443 + originKeepaliveTimeout: 5 + originProtocolPolicy: http-only + originReadTimeout: 10 + originSSLProtocols: + items: + - TLSv1 + - TLSv1.1 + - TLSv1.2 + domainName: crossplane.io + id: crossplane.io + originShield: + enabled: false + priceClass: PriceClass_200 + restrictions: + geoRestriction: + restrictionType: none + region: ap-northeast-2 + providerConfigRef: + name: crossplane diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_noavailable.yaml b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_noavailable.yaml new file mode 100644 index 00000000000000..7a47b0f48eea71 --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/progressing_noavailable.yaml @@ -0,0 +1,88 @@ +apiVersion: cloudfront.aws.crossplane.io/v1alpha1 +kind: Distribution +metadata: + generation: 1 + name: crossplane.io + resourceVersion: "261937039" + uid: a52c105f-b0e1-4027-aa19-7e93f269f2a6 +spec: + deletionPolicy: Orphan + forProvider: + distributionConfig: + comment: 'crossplane' + customErrorResponses: + items: [] + defaultCacheBehavior: + allowedMethods: + cachedMethods: + items: + - HEAD + - GET + items: + - GET + - HEAD + compress: false + defaultTTL: 600 + fieldLevelEncryptionID: "" + forwardedValues: + cookies: + forward: none + headers: + items: [] + queryString: false + queryStringCacheKeys: {} + functionAssociations: {} + lambdaFunctionAssociations: {} + maxTTL: 600 + minTTL: 0 + smoothStreaming: false + targetOriginID: crossplane.io + trustedKeyGroups: + enabled: false + trustedSigners: + enabled: false + viewerProtocolPolicy: allow-all + defaultRootObject: index.html + enabled: true + httpVersion: http2 + isIPV6Enabled: true + logging: + bucket: "" + enabled: false + includeCookies: false + prefix: "" + originGroups: {} + origins: + items: + - connectionAttempts: 3 + connectionTimeout: 10 + customHeaders: {} + customOriginConfig: + httpPort: 8080 + httpSPort: 443 + originKeepaliveTimeout: 5 + originProtocolPolicy: http-only + originReadTimeout: 10 + originSSLProtocols: + items: + - TLSv1 + - TLSv1.1 + - TLSv1.2 + domainName: crossplane.io + id: crossplane.io + originShield: + enabled: false + priceClass: PriceClass_200 + restrictions: + geoRestriction: + restrictionType: none + region: ap-northeast-2 + providerConfigRef: + name: crossplane +status: + atProvider: {} + conditions: + - lastTransitionTime: "2024-01-17T07:20:35Z" + reason: ReconcileSuccess + status: "True" + type: Synced diff --git a/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/suspended.yaml b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/suspended.yaml new file mode 100644 index 00000000000000..d15713737ff72a --- /dev/null +++ b/resource_customizations/cloudfront.aws.crossplane.io/Distribution/testdata/suspended.yaml @@ -0,0 +1,94 @@ +apiVersion: cloudfront.aws.crossplane.io/v1alpha1 +kind: Distribution +metadata: + annotations: + crossplane.io/paused: "true" + creationTimestamp: "2023-06-16T04:42:04Z" + generation: 34 + name: crossplane.io + resourceVersion: "254259056" + uid: fd357670-b762-4285-ae83-00859c40dd6b +spec: + deletionPolicy: Orphan + forProvider: + distributionConfig: + comment: 'crossplane' + customErrorResponses: + items: [] + defaultCacheBehavior: + allowedMethods: + cachedMethods: + items: + - HEAD + - GET + items: + - GET + - HEAD + compress: false + defaultTTL: 600 + fieldLevelEncryptionID: "" + forwardedValues: + cookies: + forward: none + headers: + items: [] + queryString: false + queryStringCacheKeys: {} + functionAssociations: {} + lambdaFunctionAssociations: {} + maxTTL: 600 + minTTL: 0 + smoothStreaming: false + targetOriginID: crossplane.io + trustedKeyGroups: + enabled: false + trustedSigners: + enabled: false + viewerProtocolPolicy: allow-all + defaultRootObject: index.html + enabled: true + httpVersion: http2 + isIPV6Enabled: true + logging: + bucket: "" + enabled: false + includeCookies: false + prefix: "" + originGroups: {} + origins: + items: + - connectionAttempts: 3 + connectionTimeout: 10 + customHeaders: {} + customOriginConfig: + httpPort: 8080 + httpSPort: 443 + originKeepaliveTimeout: 5 + originProtocolPolicy: http-only + originReadTimeout: 10 + originSSLProtocols: + items: + - TLSv1 + - TLSv1.1 + - TLSv1.2 + domainName: crossplane.io + id: crossplane.io + originShield: + enabled: false + priceClass: PriceClass_200 + restrictions: + geoRestriction: + restrictionType: none + region: ap-northeast-2 + providerConfigRef: + name: crossplane +status: + conditions: + - lastTransitionTime: "2023-10-16T07:40:47Z" + reason: Available + status: "True" + type: Ready + - lastTransitionTime: "2024-01-11T06:59:47Z" + reason: ReconcilePaused + status: "False" + type: Synced diff --git a/resource_customizations/db.atlasgo.io/AtlasMigration/health.lua b/resource_customizations/db.atlasgo.io/AtlasMigration/health.lua new file mode 100644 index 00000000000000..332b43ec213141 --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasMigration/health.lua @@ -0,0 +1,37 @@ +hs = {} + +local function readyCond(obj) + if obj.status ~= nil and obj.status.conditions ~= nil then + for _, condition in ipairs(obj.status.conditions) do + if condition.type == "Ready" then + return condition + end + end + end + return nil +end + +local ready = readyCond(obj) + +if ready == nil then + hs.status = "Progressing" + hs.message = "Waiting for Atlas Operator" + return hs +end + +if ready.status == "True" then + hs.status = "Healthy" + hs.message = ready.reason + return hs +end + +if ready.reason == "Reconciling" then + hs.status = "Progressing" +else + hs.status = "Degraded" +end + +hs.message = ready.reason + +return hs + diff --git a/resource_customizations/db.atlasgo.io/AtlasMigration/health_test.yaml b/resource_customizations/db.atlasgo.io/AtlasMigration/health_test.yaml new file mode 100644 index 00000000000000..b827f89c0bdf2d --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasMigration/health_test.yaml @@ -0,0 +1,13 @@ +tests: +- healthStatus: + status: Progressing + message: "Reconciling" + inputPath: testdata/progressing.yaml +- healthStatus: + status: Degraded + message: "Migrating" + inputPath: testdata/degraded.yaml +- healthStatus: + status: Healthy + message: "Applied" + inputPath: testdata/healthy.yaml diff --git a/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/degraded.yaml b/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/degraded.yaml new file mode 100644 index 00000000000000..ee51f15e482419 --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/degraded.yaml @@ -0,0 +1,29 @@ +apiVersion: db.atlasgo.io/v1alpha1 +kind: AtlasMigration +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"db.atlasgo.io/v1alpha1","kind":"AtlasMigration","metadata":{"annotations":{},"name":"atlasmigration-sample","namespace":"default"},"spec":{"dir":{"configMapRef":{"name":"migration-dir"}},"urlFrom":{"secretKeyRef":{"key":"url","name":"mysql-credentials"}}}} + creationTimestamp: "2023-11-16T08:37:23Z" + generation: 1 + name: atlasmigration-sample + namespace: default + resourceVersion: "49923" + uid: 0d5bc3d6-750e-4f5a-82a3-8b9173106ef4 +spec: + dir: + configMapRef: + name: migration-dir + urlFrom: + secretKeyRef: + key: url + name: mysql-credentials +status: + conditions: + - lastTransitionTime: "2023-11-16T08:37:23Z" + message: 'Error: checksum mismatch' + reason: Migrating + status: "False" + type: Ready + lastApplied: 0 + observed_hash: "" diff --git a/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/healthy.yaml b/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/healthy.yaml new file mode 100644 index 00000000000000..4a7a91324d196c --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/healthy.yaml @@ -0,0 +1,30 @@ +apiVersion: db.atlasgo.io/v1alpha1 +kind: AtlasMigration +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"db.atlasgo.io/v1alpha1","kind":"AtlasMigration","metadata":{"annotations":{},"name":"atlasmigration-sample","namespace":"default"},"spec":{"dir":{"configMapRef":{"name":"migration-dir"}},"urlFrom":{"secretKeyRef":{"key":"url","name":"mysql-credentials"}}}} + creationTimestamp: "2023-11-16T08:37:23Z" + generation: 1 + name: atlasmigration-sample + namespace: default + resourceVersion: "50387" + uid: 0d5bc3d6-750e-4f5a-82a3-8b9173106ef4 +spec: + dir: + configMapRef: + name: migration-dir + urlFrom: + secretKeyRef: + key: url + name: mysql-credentials +status: + conditions: + - lastTransitionTime: "2023-11-16T08:46:27Z" + message: "" + reason: Applied + status: "True" + type: Ready + lastApplied: 1700124387 + lastAppliedVersion: "20230316085611" + observed_hash: 4969b3c84c097ff61a9f9722b595a66c1a4473bd85fdd282107b98a92db8a43b diff --git a/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/progressing.yaml b/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/progressing.yaml new file mode 100644 index 00000000000000..024f9f7558d784 --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasMigration/testdata/progressing.yaml @@ -0,0 +1,30 @@ +apiVersion: db.atlasgo.io/v1alpha1 +kind: AtlasMigration +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"db.atlasgo.io/v1alpha1","kind":"AtlasMigration","metadata":{"annotations":{},"name":"atlasmigration-sample","namespace":"default"},"spec":{"dir":{"configMapRef":{"name":"migration-dir"}},"urlFrom":{"secretKeyRef":{"key":"url","name":"mysql-credentials"}}}} + creationTimestamp: "2023-11-16T08:37:23Z" + generation: 1 + name: atlasmigration-sample + namespace: default + resourceVersion: "50387" + uid: 0d5bc3d6-750e-4f5a-82a3-8b9173106ef4 +spec: + dir: + configMapRef: + name: migration-dir + urlFrom: + secretKeyRef: + key: url + name: mysql-credentials +status: + conditions: + - lastTransitionTime: "2023-11-16T08:46:27Z" + message: "Current migration data has changed" + reason: "Reconciling" + status: "False" + type: Ready + lastApplied: 1700124387 + lastAppliedVersion: "20230316085611" + observed_hash: 4969b3c84c097ff61a9f9722b595a66c1a4473bd85fdd282107b98a92db8a43b diff --git a/resource_customizations/db.atlasgo.io/AtlasSchema/health.lua b/resource_customizations/db.atlasgo.io/AtlasSchema/health.lua new file mode 100644 index 00000000000000..c66d66d15b5a8d --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasSchema/health.lua @@ -0,0 +1,37 @@ +hs = {} + +local function readyCond(obj) + if obj.status ~= nil and obj.status.conditions ~= nil then + for _, condition in ipairs(obj.status.conditions) do + if condition.type == "Ready" then + return condition + end + end + end + return nil +end + +local ready = readyCond(obj) + +if ready == nil then + hs.status = "Progressing" + hs.message = "Waiting for Atlas Operator" + return hs +end + +if ready.status == "True" then + hs.status = "Healthy" + hs.message = ready.reason + return hs +end + +if ready.message == "Reconciling" or ready.message == "GettingDevDB" then + hs.status = "Progressing" +else + hs.status = "Degraded" +end + +hs.message = ready.reason + +return hs + diff --git a/resource_customizations/db.atlasgo.io/AtlasSchema/health_test.yaml b/resource_customizations/db.atlasgo.io/AtlasSchema/health_test.yaml new file mode 100644 index 00000000000000..0fe102f2991389 --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasSchema/health_test.yaml @@ -0,0 +1,13 @@ +tests: +- healthStatus: + status: Progressing + message: "Reconciling" + inputPath: testdata/progressing.yaml +- healthStatus: + status: Degraded + message: "ApplyingSchema" + inputPath: testdata/degraded.yaml +- healthStatus: + status: Healthy + message: "Applied" + inputPath: testdata/healthy.yaml diff --git a/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/degraded.yaml b/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/degraded.yaml new file mode 100644 index 00000000000000..08383988e996ae --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/degraded.yaml @@ -0,0 +1,38 @@ +apiVersion: db.atlasgo.io/v1alpha1 +kind: AtlasSchema +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"db.atlasgo.io/v1alpha1","kind":"AtlasSchema","metadata":{"annotations":{},"name":"atlasschema-mysql","namespace":"default"},"spec":{"schema":{"sql":"create table users (\n id int not null auto_increment,\n name varchar(255) not null,\n email varchar(255) unique not null,\n short_bio varchar(255) not null,\n primary key (id)\n);\n"},"urlFrom":{"secretKeyRef":{"key":"url","name":"mysql-credentials"}}}} + creationTimestamp: "2023-11-15T14:33:18Z" + generation: 2 + name: atlasschema-mysql + namespace: default + resourceVersion: "46659" + uid: 54a4cdfc-e4f9-4c3d-934c-e08b6122e38a +spec: + schema: + sql: | + xcreate table users ( + id int not null auto_increment, + name varchar(255) not null, + email varchar(255) unique not null, + short_bio varchar(255) not null, + primary key (id) + ); + urlFrom: + secretKeyRef: + key: url + name: mysql-credentials +status: + conditions: + - lastTransitionTime: "2023-11-15T14:38:41Z" + message: |- + Error: sql/migrate: read migration directory state: sql/migrate: execute: executing statement "xcreate table users (\n id int not null auto_increment,\n name varchar(255) not null,\n email varchar(255) unique not null,\n short_bio varchar(255) not null,\n primary key (id)\n);" from version "schema": Error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'xcreate table users ( + id int not null auto_increment, + name varchar(255) not ' at line 1 + reason: ApplyingSchema + status: "False" + type: Ready + last_applied: 1700058814 + observed_hash: ddfe666707ddf5c2cc7625c2a0de89da51e54fc7caa6403db307146430d20d85 diff --git a/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/healthy.yaml b/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/healthy.yaml new file mode 100644 index 00000000000000..eca8ec497f09ad --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/healthy.yaml @@ -0,0 +1,39 @@ +apiVersion: db.atlasgo.io/v1alpha1 +kind: AtlasSchema +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"db.atlasgo.io/v1alpha1","kind":"AtlasSchema","metadata":{"annotations":{},"name":"atlasschema-mysql","namespace":"default"},"spec":{"schema":{"sql":"create table users (\n id int not null auto_increment,\n name varchar(255) not null,\n email varchar(255) unique not null,\n short_bio varchar(255) not null,\n primary key (id)\n);\n"},"urlFrom":{"secretKeyRef":{"key":"url","name":"mysql-credentials"}}}} + creationTimestamp: "2023-11-15T14:33:18Z" + generation: 1 + name: atlasschema-mysql + namespace: default + resourceVersion: "46390" + uid: 54a4cdfc-e4f9-4c3d-934c-e08b6122e38a +spec: + schema: + sql: | + create table users ( + id int not null auto_increment, + name varchar(255) not null, + email varchar(255) unique not null, + short_bio varchar(255) not null, + primary key (id) + ); + urlFrom: + secretKeyRef: + key: url + name: mysql-credentials +status: + conditions: + - lastTransitionTime: "2023-11-15T14:33:34Z" + message: 'The schema has been applied successfully. Apply response: {"Driver":"mysql","URL":{"Scheme":"mysql","Opaque":"","User":{},"Host":"mysql.default:3306","Path":"/myapp","RawPath":"","OmitHost":false,"ForceQuery":false,"RawQuery":"parseTime=true","Fragment":"","RawFragment":"","Schema":"myapp"},"Changes":{"Applied":["CREATE + TABLE `users` (\n `id` int NOT NULL AUTO_INCREMENT,\n `name` varchar(255) + NOT NULL,\n `email` varchar(255) NOT NULL,\n `short_bio` varchar(255) NOT + NULL,\n PRIMARY KEY (`id`),\n UNIQUE INDEX `email` (`email`)\n) CHARSET utf8mb4 + COLLATE utf8mb4_0900_ai_ci"]}}' + reason: Applied + status: "True" + type: Ready + last_applied: 1700058814 + observed_hash: ddfe666707ddf5c2cc7625c2a0de89da51e54fc7caa6403db307146430d20d85 diff --git a/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/progressing.yaml b/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/progressing.yaml new file mode 100644 index 00000000000000..79d59ca7681410 --- /dev/null +++ b/resource_customizations/db.atlasgo.io/AtlasSchema/testdata/progressing.yaml @@ -0,0 +1,35 @@ +apiVersion: db.atlasgo.io/v1alpha1 +kind: AtlasSchema +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"db.atlasgo.io/v1alpha1","kind":"AtlasSchema","metadata":{"annotations":{},"name":"atlasschema-mysql","namespace":"default"},"spec":{"schema":{"sql":"create table users (\n id int not null auto_increment,\n name varchar(255) not null,\n email varchar(255) unique not null,\n short_bio varchar(255) not null,\n primary key (id)\n);\n"},"urlFrom":{"secretKeyRef":{"key":"url","name":"mysql-credentials"}}}} + creationTimestamp: "2023-11-15T14:33:18Z" + generation: 1 + name: atlasschema-mysql + namespace: default + resourceVersion: "46390" + uid: 54a4cdfc-e4f9-4c3d-934c-e08b6122e38a +spec: + schema: + sql: | + create table users ( + id int not null auto_increment, + name varchar(255) not null, + email varchar(255) unique not null, + short_bio varchar(255) not null, + primary key (id) + ); + urlFrom: + secretKeyRef: + key: url + name: mysql-credentials +status: + conditions: + - lastTransitionTime: "2023-11-15T14:33:34Z" + message: 'Reconciling' + reason: Reconciling + status: "False" + type: Ready + last_applied: 1700058814 + observed_hash: ddfe666707ddf5c2cc7625c2a0de89da51e54fc7caa6403db307146430d20d85 diff --git a/resource_customizations/pxc.percona.com/PerconaXtraDBCluster/health.lua b/resource_customizations/pxc.percona.com/PerconaXtraDBCluster/health.lua index 9de2180197571c..d614828d461f21 100644 --- a/resource_customizations/pxc.percona.com/PerconaXtraDBCluster/health.lua +++ b/resource_customizations/pxc.percona.com/PerconaXtraDBCluster/health.lua @@ -27,7 +27,7 @@ if obj.status ~= nil then if obj.status.state == "error" then hs.status = "Degraded" - hs.message = "Cluster is on error: " .. table.concat(obj.status.messages, ", ") + hs.message = "Cluster is on error: " .. table.concat(obj.status.message, ", ") return hs end diff --git a/resource_customizations/pxc.percona.com/PerconaXtraDBCluster/testdata/error.yaml b/resource_customizations/pxc.percona.com/PerconaXtraDBCluster/testdata/error.yaml index b6f1884be08198..4a373358dcd8cb 100644 --- a/resource_customizations/pxc.percona.com/PerconaXtraDBCluster/testdata/error.yaml +++ b/resource_customizations/pxc.percona.com/PerconaXtraDBCluster/testdata/error.yaml @@ -12,7 +12,7 @@ status: pmm: {} proxysql: {} pxc: - image: '' + image: "" ready: 1 size: 2 status: error @@ -20,5 +20,5 @@ status: ready: 1 size: 2 state: error - messages: - - we lost node + message: + - we lost node diff --git a/resource_customizations/rollouts.kruise.io/Rollout/health.lua b/resource_customizations/rollouts.kruise.io/Rollout/health.lua new file mode 100644 index 00000000000000..5fd4ddb2a5486f --- /dev/null +++ b/resource_customizations/rollouts.kruise.io/Rollout/health.lua @@ -0,0 +1,31 @@ +hs={ status = "Progressing", message = "Rollout is still progressing" } + +if obj.metadata.generation == obj.status.observedGeneration then + + if obj.status.canaryStatus.currentStepState == "StepUpgrade" and obj.status.phase == "Progressing" then + hs.status = "Progressing" + hs.message = "Rollout is still progressing" + return hs + end + + if obj.status.canaryStatus.currentStepState == "StepPaused" and obj.status.phase == "Progressing" then + hs.status = "Suspended" + hs.message = "Rollout is Paused need manual intervention" + return hs + end + + if obj.status.canaryStatus.currentStepState == "Completed" and obj.status.phase == "Healthy" then + hs.status = "Healthy" + hs.message = "Rollout is Completed" + return hs + end + + if obj.status.canaryStatus.currentStepState == "StepPaused" and (obj.status.phase == "Terminating" or obj.status.phase == "Disabled") then + hs.status = "Degraded" + hs.message = "Rollout is Disabled or Terminating" + return hs + end + +end + +return hs diff --git a/resource_customizations/rollouts.kruise.io/Rollout/health_test.yaml b/resource_customizations/rollouts.kruise.io/Rollout/health_test.yaml new file mode 100644 index 00000000000000..c89ea3409ec77e --- /dev/null +++ b/resource_customizations/rollouts.kruise.io/Rollout/health_test.yaml @@ -0,0 +1,17 @@ +tests: + - healthStatus: + status: Healthy + message: "Rollout is Completed" + inputPath: testdata/healthy.yaml + - healthStatus: + status: Degraded + message: "Rollout is Disabled or Terminating" + inputPath: testdata/degraded.yaml + - healthStatus: + status: Progressing + message: "Rollout is still progressing" + inputPath: testdata/progressing.yaml + - healthStatus: + status: Suspended + message: "Rollout is Paused need manual intervention" + inputPath: testdata/suspended.yaml diff --git a/resource_customizations/rollouts.kruise.io/Rollout/testdata/degraded.yaml b/resource_customizations/rollouts.kruise.io/Rollout/testdata/degraded.yaml new file mode 100644 index 00000000000000..97c40f10a0c960 --- /dev/null +++ b/resource_customizations/rollouts.kruise.io/Rollout/testdata/degraded.yaml @@ -0,0 +1,50 @@ +apiVersion: rollouts.kruise.io/v1alpha1 +kind: Rollout +metadata: + name: rollouts-demo + namespace: default + annotations: + rollouts.kruise.io/rolling-style: partition + generation: 5 +spec: + objectRef: + workloadRef: + apiVersion: apps/v1 + kind: Deployment + name: workload-demo + strategy: + canary: + steps: + - replicas: 1 + pause: + duration: 0 + - replicas: 50% + pause: + duration: 0 + - replicas: 100% + +status: + canaryStatus: + canaryReadyReplicas: 1 + canaryReplicas: 1 + canaryRevision: 76fd76f75b + currentStepIndex: 1 + currentStepState: StepPaused + lastUpdateTime: '2023-09-23T11:44:39Z' + message: BatchRelease is at state Ready, rollout-id , step 1 + observedWorkloadGeneration: 7 + podTemplateHash: 76fd76f75b + rolloutHash: 77cxd69w47b7bwddwv2w7vxvb4xxdbwcx9x289vw69w788w4w6z4x8dd4vbz2zbw + stableRevision: 6bfdfb5bfb + conditions: + - lastTransitionTime: '2023-09-23T11:44:09Z' + lastUpdateTime: '2023-09-23T11:44:09Z' + message: Rollout is in Progressing + reason: InRolling + status: 'True' + type: Progressing + message: >- + Rollout is in step(1/3), and you need manually confirm to enter the next + step + observedGeneration: 5 + phase: Disabled diff --git a/resource_customizations/rollouts.kruise.io/Rollout/testdata/healthy.yaml b/resource_customizations/rollouts.kruise.io/Rollout/testdata/healthy.yaml new file mode 100644 index 00000000000000..77743b50007ad5 --- /dev/null +++ b/resource_customizations/rollouts.kruise.io/Rollout/testdata/healthy.yaml @@ -0,0 +1,56 @@ +apiVersion: rollouts.kruise.io/v1alpha1 +kind: Rollout +metadata: + name: rollouts-demo + namespace: default + annotations: + rollouts.kruise.io/rolling-style: partition + generation: 7 +spec: + objectRef: + workloadRef: + apiVersion: apps/v1 + kind: Deployment + name: workload-demo + strategy: + canary: + steps: + - replicas: 1 + pause: + duration: 0 + - replicas: 50% + pause: + duration: 0 + - replicas: 100% + +status: + canaryStatus: + canaryReadyReplicas: 10 + canaryReplicas: 10 + canaryRevision: 76fd76f75b + currentStepIndex: 3 + currentStepState: Completed + lastUpdateTime: '2023-09-23T11:48:58Z' + message: BatchRelease is at state Ready, rollout-id , step 3 + observedWorkloadGeneration: 22 + podTemplateHash: 76fd76f75b + rolloutHash: 77cxd69w47b7bwddwv2w7vxvb4xxdbwcx9x289vw69w788w4w6z4x8dd4vbz2zbw + stableRevision: 6bfdfb5bfb + conditions: + - lastTransitionTime: '2023-09-23T11:44:09Z' + lastUpdateTime: '2023-09-23T11:44:09Z' + message: Rollout progressing has been completed + reason: Completed + status: 'False' + type: Progressing + - lastTransitionTime: '2023-09-23T11:49:01Z' + lastUpdateTime: '2023-09-23T11:49:01Z' + message: '' + reason: '' + status: 'True' + type: Succeeded + message: Rollout progressing has been completed + observedGeneration: 7 + phase: Healthy + + diff --git a/resource_customizations/rollouts.kruise.io/Rollout/testdata/progressing.yaml b/resource_customizations/rollouts.kruise.io/Rollout/testdata/progressing.yaml new file mode 100644 index 00000000000000..f84d395867530c --- /dev/null +++ b/resource_customizations/rollouts.kruise.io/Rollout/testdata/progressing.yaml @@ -0,0 +1,48 @@ +apiVersion: rollouts.kruise.io/v1alpha1 +kind: Rollout +metadata: + name: rollouts-demo + namespace: default + annotations: + rollouts.kruise.io/rolling-style: partition + generation: 5 +spec: + objectRef: + workloadRef: + apiVersion: apps/v1 + kind: Deployment + name: workload-demo + strategy: + canary: + steps: + - replicas: 1 + pause: + duration: 0 + - replicas: 50% + pause: + duration: 0 + - replicas: 100% + +status: + canaryStatus: + canaryReadyReplicas: 0 + canaryReplicas: 1 + canaryRevision: 76fd76f75b + currentStepIndex: 1 + currentStepState: StepUpgrade + lastUpdateTime: '2023-09-23T11:44:12Z' + message: BatchRelease is at state Verifying, rollout-id , step 1 + observedWorkloadGeneration: 6 + podTemplateHash: 76fd76f75b + rolloutHash: 77cxd69w47b7bwddwv2w7vxvb4xxdbwcx9x289vw69w788w4w6z4x8dd4vbz2zbw + stableRevision: 6bfdfb5bfb + conditions: + - lastTransitionTime: '2023-09-23T11:44:09Z' + lastUpdateTime: '2023-09-23T11:44:09Z' + message: Rollout is in Progressing + reason: InRolling + status: 'True' + type: Progressing + message: Rollout is in step(1/3), and upgrade workload to new version + observedGeneration: 5 + phase: Progressing diff --git a/resource_customizations/rollouts.kruise.io/Rollout/testdata/suspended.yaml b/resource_customizations/rollouts.kruise.io/Rollout/testdata/suspended.yaml new file mode 100644 index 00000000000000..77a67129a248e7 --- /dev/null +++ b/resource_customizations/rollouts.kruise.io/Rollout/testdata/suspended.yaml @@ -0,0 +1,50 @@ +apiVersion: rollouts.kruise.io/v1alpha1 +kind: Rollout +metadata: + name: rollouts-demo + namespace: default + annotations: + rollouts.kruise.io/rolling-style: partition + generation: 5 +spec: + objectRef: + workloadRef: + apiVersion: apps/v1 + kind: Deployment + name: workload-demo + strategy: + canary: + steps: + - replicas: 1 + pause: + duration: 0 + - replicas: 50% + pause: + duration: 0 + - replicas: 100% + +status: + canaryStatus: + canaryReadyReplicas: 1 + canaryReplicas: 1 + canaryRevision: 76fd76f75b + currentStepIndex: 1 + currentStepState: StepPaused + lastUpdateTime: '2023-09-23T11:44:39Z' + message: BatchRelease is at state Ready, rollout-id , step 1 + observedWorkloadGeneration: 7 + podTemplateHash: 76fd76f75b + rolloutHash: 77cxd69w47b7bwddwv2w7vxvb4xxdbwcx9x289vw69w788w4w6z4x8dd4vbz2zbw + stableRevision: 6bfdfb5bfb + conditions: + - lastTransitionTime: '2023-09-23T11:44:09Z' + lastUpdateTime: '2023-09-23T11:44:09Z' + message: Rollout is in Progressing + reason: InRolling + status: 'True' + type: Progressing + message: >- + Rollout is in step(1/3), and you need manually confirm to enter the next + step + observedGeneration: 5 + phase: Progressing diff --git a/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/health_test.yaml b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/health_test.yaml new file mode 100644 index 00000000000000..aa83951d5a2db4 --- /dev/null +++ b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/health_test.yaml @@ -0,0 +1,25 @@ +tests: +- healthStatus: + status: Progressing + message: Waiting for resourcrecordset to be available + inputPath: testdata/progressing_creating.yaml +- healthStatus: + status: Progressing + message: Waiting for resourcrecordset to be created + inputPath: testdata/progressing_noStatus.yaml +- healthStatus: + status: Degraded + message: >- + create failed: failed to create the ResourceRecordSet resource: + InvalidChangeBatch: [RRSet of type CNAME with DNS name + www.crossplane.io. is not permitted as it conflicts with other + records with the same DNS name in zone crossplane.io.] + inputPath: testdata/degraded_reconcileError.yaml +- healthStatus: + status: Suspended + message: ReconcilePaused + inputPath: testdata/suspended_reconcilePaused.yaml +- healthStatus: + status: Healthy + message: Available + inputPath: testdata/healthy.yaml diff --git a/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/heatlh.lua b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/heatlh.lua new file mode 100644 index 00000000000000..0cf5253e910ff1 --- /dev/null +++ b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/heatlh.lua @@ -0,0 +1,41 @@ +local hs = {} +if obj.status ~= nil then + if obj.status.conditions ~= nil then + local ready = false + local synced = false + local suspended = false + for i, condition in ipairs(obj.status.conditions) do + + if condition.type == "Ready" then + ready = condition.status == "True" + ready_message = condition.reason + elseif condition.type == "Synced" then + synced = condition.status == "True" + if condition.reason == "ReconcileError" then + synced_message = condition.message + elseif condition.reason == "ReconcilePaused" then + suspended = true + suspended_message = condition.reason + end + end + end + if ready and synced then + hs.status = "Healthy" + hs.message = ready_message + elseif synced == false and suspended == true then + hs.status = "Suspended" + hs.message = suspended_message + elseif ready == false and synced == true and suspended == false then + hs.status = "Progressing" + hs.message = "Waiting for resourcrecordset to be available" + else + hs.status = "Degraded" + hs.message = synced_message + end + return hs + end +end + +hs.status = "Progressing" +hs.message = "Waiting for resourcrecordset to be created" +return hs diff --git a/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/degraded_reconcileError.yaml b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/degraded_reconcileError.yaml new file mode 100644 index 00000000000000..31bc5123c7bfd6 --- /dev/null +++ b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/degraded_reconcileError.yaml @@ -0,0 +1,35 @@ +apiVersion: route53.aws.crossplane.io/v1alpha1 +kind: ResourceRecordSet +metadata: + creationTimestamp: '2024-01-11T03:48:32Z' + generation: 1 + name: www-domain + resourceVersion: '187731157' + selfLink: /apis/route53.aws.crossplane.io/v1alpha1/resourcerecordsets/www-domain + uid: c9c85395-0830-4549-b255-e9e426663547 +spec: + providerConfigRef: + name: crossplane + forProvider: + resourceRecords: + - value: www.crossplane.io + setIdentifier: www + ttl: 60 + type: CNAME + weight: 0 + zoneId: ABCDEFGAB07CD +status: + conditions: + - lastTransitionTime: '2024-01-11T03:48:57Z' + message: >- + create failed: failed to create the ResourceRecordSet resource: + InvalidChangeBatch: [RRSet of type CNAME with DNS name + www.crossplane.io. is not permitted as it conflicts with other + records with the same DNS name in zone crossplane.io.] + reason: ReconcileError + status: 'False' + type: Synced + - lastTransitionTime: '2024-01-11T03:48:34Z' + reason: Creating + status: 'False' + type: Ready diff --git a/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/healthy.yaml b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/healthy.yaml new file mode 100644 index 00000000000000..f808e46cc8c929 --- /dev/null +++ b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/healthy.yaml @@ -0,0 +1,29 @@ +apiVersion: route53.aws.crossplane.io/v1alpha1 +kind: ResourceRecordSet +metadata: + creationTimestamp: "2023-11-16T04:44:19Z" + generation: 4 + name: www-domain + resourceVersion: "140397563" + selfLink: /apis/route53.aws.crossplane.io/v1alpha1/resourcerecordsets/www-domain + uid: 11f0d48d-134f-471b-9340-b6d45d953fcb +spec: + providerConfigRef: + name: crossplane + forProvider: + zoneId: A1B2C3D4 + type: A + aliasTarget: + dnsName: abcdefg.cloudfront.net. + evaluateTargetHealth: false + hostedZoneId: AZBZCZDEFG +status: + conditions: + - lastTransitionTime: "2023-11-16T04:44:27Z" + reason: Available + status: "True" + type: Ready + - lastTransitionTime: "2023-11-16T04:44:25Z" + reason: ReconcileSuccess + status: "True" + type: Synced diff --git a/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/progressing_creating.yaml b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/progressing_creating.yaml new file mode 100644 index 00000000000000..abf59775fb8e0c --- /dev/null +++ b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/progressing_creating.yaml @@ -0,0 +1,29 @@ +apiVersion: route53.aws.crossplane.io/v1alpha1 +kind: ResourceRecordSet +metadata: + creationTimestamp: "2023-11-16T04:44:19Z" + generation: 4 + name: www-domain + resourceVersion: "140397563" + selfLink: /apis/route53.aws.crossplane.io/v1alpha1/resourcerecordsets/www-domain + uid: 11f0d48d-134f-471b-9340-b6d45d953fcb +spec: + providerConfigRef: + name: crossplane + forProvider: + zoneId: A1B2C3D4 + type: A + aliasTarget: + dnsName: abcdefg.cloudfront.net. + evaluateTargetHealth: false + hostedZoneId: AZBZCZDEFG +status: + conditions: + - lastTransitionTime: "2023-11-16T04:44:27Z" + reason: Creating + status: "False" + type: Ready + - lastTransitionTime: "2023-11-16T04:44:25Z" + reason: ReconcileSuccess + status: "True" + type: Synced diff --git a/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/progressing_noStatus.yaml b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/progressing_noStatus.yaml new file mode 100644 index 00000000000000..28d778d0550502 --- /dev/null +++ b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/progressing_noStatus.yaml @@ -0,0 +1,19 @@ +apiVersion: route53.aws.crossplane.io/v1alpha1 +kind: ResourceRecordSet +metadata: + creationTimestamp: "2023-11-16T04:44:19Z" + generation: 4 + name: www-domain + resourceVersion: "140397563" + selfLink: /apis/route53.aws.crossplane.io/v1alpha1/resourcerecordsets/www-domain + uid: 11f0d48d-134f-471b-9340-b6d45d953fcb +spec: + providerConfigRef: + name: crossplane + forProvider: + zoneId: A1B2C3D4 + type: A + aliasTarget: + dnsName: abcdefg.cloudfront.net. + evaluateTargetHealth: false + hostedZoneId: AZBZCZDEFG diff --git a/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/suspended_reconcilePaused.yaml b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/suspended_reconcilePaused.yaml new file mode 100644 index 00000000000000..522c0e878dcf8f --- /dev/null +++ b/resource_customizations/route53.aws.crossplane.io/ResourceRecordSet/testdata/suspended_reconcilePaused.yaml @@ -0,0 +1,27 @@ +apiVersion: route53.aws.crossplane.io/v1alpha1 +kind: ResourceRecordSet +metadata: + annotations: + crossplane.io/paused: "true" + creationTimestamp: "2024-01-11T04:16:15Z" + generation: 1 + name: www-domain + resourceVersion: "187746011" + uid: 5517b419-5052-43d9-941e-c32f60d8c7e5 +spec: + providerConfigRef: + name: crossplane + forProvider: + resourceRecords: + - value: www.crossplane.io + setIdentifier: www + ttl: 60 + type: CNAME + weight: 0 + zoneId: ABCDEFGAB07CD +status: + conditions: + - lastTransitionTime: "2024-01-11T04:16:16Z" + reason: ReconcilePaused + status: "False" + type: Synced diff --git a/server/application/application.go b/server/application/application.go index ec600d29b3c6b1..8ee16b93494c8b 100644 --- a/server/application/application.go +++ b/server/application/application.go @@ -152,7 +152,12 @@ func NewServer( // If the user does provide a "project," we can respond more specifically. If the user does not have access to the given // app name in the given project, we return "permission denied." If the app exists, but the project is different from func (s *Server) getAppEnforceRBAC(ctx context.Context, action, project, namespace, name string, getApp func() (*appv1.Application, error)) (*appv1.Application, error) { + user := session.Username(ctx) + if user == "" { + user = "Unknown user" + } logCtx := log.WithFields(map[string]interface{}{ + "user": user, "application": name, "namespace": namespace, }) @@ -1752,7 +1757,7 @@ func (s *Server) Sync(ctx context.Context, syncReq *application.ApplicationSyncR if a.DeletionTimestamp != nil { return nil, status.Errorf(codes.FailedPrecondition, "application is deleting") } - if a.Spec.SyncPolicy != nil && a.Spec.SyncPolicy.Automated != nil { + if a.Spec.SyncPolicy != nil && a.Spec.SyncPolicy.Automated != nil && !syncReq.GetDryRun() { if syncReq.GetRevision() != "" && syncReq.GetRevision() != text.FirstNonEmpty(source.TargetRevision, "HEAD") { return nil, status.Errorf(codes.FailedPrecondition, "Cannot sync to %s: auto-sync currently set to %s", syncReq.GetRevision(), source.TargetRevision) } diff --git a/server/application/application_test.go b/server/application/application_test.go index c0f7d000b15065..65600ad629d3f3 100644 --- a/server/application/application_test.go +++ b/server/application/application_test.go @@ -76,7 +76,7 @@ func fakeRepo() *appsv1.Repository { func fakeCluster() *appsv1.Cluster { return &appsv1.Cluster{ - Server: "https://cluster-api.com", + Server: "https://cluster-api.example.com", Name: "fake-cluster", Config: appsv1.ClusterConfig{}, } @@ -503,7 +503,7 @@ spec: environment: default destination: namespace: ` + test.FakeDestNamespace + ` - server: https://cluster-api.com + server: https://cluster-api.example.com ` const fakeAppWithDestName = ` @@ -541,7 +541,7 @@ spec: environment: default destination: namespace: ` + test.FakeDestNamespace + ` - server: https://cluster-api.com + server: https://cluster-api.example.com ` func newTestAppWithDestName(opts ...func(app *appsv1.Application)) *appsv1.Application { @@ -797,22 +797,22 @@ func TestNoAppEnumeration(t *testing.T) { t.Run("UpdateSpec", func(t *testing.T) { _, err := appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("test"), Spec: &appsv1.ApplicationSpec{ - Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.com"}, + Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, Source: &appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, }}) assert.NoError(t, err) _, err = appServer.UpdateSpec(noRoleCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("test"), Spec: &appsv1.ApplicationSpec{ - Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.com"}, + Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, Source: &appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, }}) assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("doest-not-exist"), Spec: &appsv1.ApplicationSpec{ - Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.com"}, + Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, Source: &appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, }}) assert.Equal(t, permissionDeniedErr.Error(), err.Error(), "error message must be _only_ the permission error, to avoid leaking information about app existence") _, err = appServer.UpdateSpec(adminCtx, &application.ApplicationUpdateSpecRequest{Name: pointer.String("doest-not-exist"), Project: pointer.String("test"), Spec: &appsv1.ApplicationSpec{ - Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.com"}, + Destination: appsv1.ApplicationDestination{Namespace: "default", Server: "https://cluster-api.example.com"}, Source: &appsv1.ApplicationSource{RepoURL: "https://some-fake-source", Path: "."}, }}) assert.Equal(t, "rpc error: code = NotFound desc = applications.argoproj.io \"doest-not-exist\" not found", err.Error(), "when the request specifies a project, we can return the standard k8s error message") @@ -1436,7 +1436,7 @@ func TestCreateAppWithDestName(t *testing.T) { app, err := appServer.Create(context.Background(), &createReq) assert.NoError(t, err) assert.NotNil(t, app) - assert.Equal(t, app.Spec.Destination.Server, "https://cluster-api.com") + assert.Equal(t, app.Spec.Destination.Server, "https://cluster-api.example.com") } func TestUpdateApp(t *testing.T) { diff --git a/server/applicationset/applicationset_test.go b/server/applicationset/applicationset_test.go index c5a299c85f3587..c49ddb35a7970f 100644 --- a/server/applicationset/applicationset_test.go +++ b/server/applicationset/applicationset_test.go @@ -38,7 +38,7 @@ func fakeRepo() *appsv1.Repository { func fakeCluster() *appsv1.Cluster { return &appsv1.Cluster{ - Server: "https://cluster-api.com", + Server: "https://cluster-api.example.com", Name: "fake-cluster", Config: appsv1.ClusterConfig{}, } diff --git a/server/badge/badge.go b/server/badge/badge.go index cf291d589501e1..5787d530c15f71 100644 --- a/server/badge/badge.go +++ b/server/badge/badge.go @@ -42,10 +42,28 @@ var ( leftTextPattern = regexp.MustCompile(`id="leftText" [^>]*>([^<]*)`) rightTextPattern = regexp.MustCompile(`id="rightText" [^>]*>([^<]*)`) revisionTextPattern = regexp.MustCompile(`id="revisionText" [^>]*>([^<]*)`) + titleTextPattern = regexp.MustCompile(`id="titleText" [^>]*>([^<]*)`) + titleRectWidthPattern = regexp.MustCompile(`(id="titleRect" .* width=)("0")`) + rightRectWidthPattern = regexp.MustCompile(`(id="rightRect" .* width=)("\d*")`) + leftRectYCoodPattern = regexp.MustCompile(`(id="leftRect" .* y=)("\d*")`) + rightRectYCoodPattern = regexp.MustCompile(`(id="rightRect" .* y=)("\d*")`) + revisionRectYCoodPattern = regexp.MustCompile(`(id="revisionRect" .* y=)("\d*")`) + leftTextYCoodPattern = regexp.MustCompile(`(id="leftText" .* y=)("\d*")`) + rightTextYCoodPattern = regexp.MustCompile(`(id="rightText" .* y=)("\d*")`) + revisionTextYCoodPattern = regexp.MustCompile(`(id="revisionText" .* y=)("\d*")`) + svgHeightPattern = regexp.MustCompile(`^( 0 { + handler = enforceContentTypes(handler, a.ContentTypes) + } else { + log.WithField(common.SecurityField, common.SecurityHigh).Warnf("Content-Type enforcement is disabled, which may make your API vulnerable to CSRF attacks") + } mux.Handle("/api/", handler) terminal := application.NewHandler(a.appLister, a.Namespace, a.ApplicationNamespaces, a.db, a.enf, a.Cache, appResourceTreeFn, a.settings.ExecShells, *a.sessionMgr). @@ -1002,7 +1022,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl // API server won't panic if extensions fail to register. In // this case an error log will be sent and no extension route // will be added in mux. - registerExtensions(mux, a) + registerExtensions(mux, a, metricsReg) } mustRegisterGWHandler(versionpkg.RegisterVersionServiceHandler, ctx, gwmux, conn) @@ -1028,7 +1048,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl // Webhook handler for git events (Note: cache timeouts are hardcoded because API server does not write to cache and not really using them) argoDB := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset) - acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, repocache.NewCache(a.Cache.GetCache(), 24*time.Hour, 3*time.Minute), a.Cache, argoDB) + acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, a.RepoServerCache, a.Cache, argoDB) mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler) @@ -1055,16 +1075,32 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl return &httpS } +func enforceContentTypes(handler http.Handler, types []string) http.Handler { + allowedTypes := map[string]bool{} + for _, t := range types { + allowedTypes[strings.ToLower(t)] = true + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet || allowedTypes[strings.ToLower(r.Header.Get("Content-Type"))] { + handler.ServeHTTP(w, r) + } else { + http.Error(w, "Invalid content type", http.StatusUnsupportedMediaType) + } + }) +} + // registerExtensions will try to register all configured extensions // in the given mux. If any error is returned while registering // extensions handlers, no route will be added in the given mux. -func registerExtensions(mux *http.ServeMux, a *ArgoCDServer) { +func registerExtensions(mux *http.ServeMux, a *ArgoCDServer, metricsReg HTTPMetricsRegistry) { a.log.Info("Registering extensions...") extHandler := http.HandlerFunc(a.extensionManager.CallExtension()) authMiddleware := a.sessionMgr.AuthMiddlewareFunc(a.DisableAuth) // auth middleware ensures that requests to all extensions are authenticated first mux.Handle(fmt.Sprintf("%s/", extension.URLPrefix), authMiddleware(extHandler)) + a.extensionManager.AddMetricsRegistry(metricsReg) + err := a.extensionManager.RegisterExtensions() if err != nil { a.log.Errorf("Error registering extensions: %s", err) @@ -1121,7 +1157,7 @@ func (a *ArgoCDServer) registerDexHandlers(mux *http.ServeMux) { // Run dex OpenID Connect Identity Provider behind a reverse proxy (served at /api/dex) var err error mux.HandleFunc(common.DexAPIEndpoint+"/", dexutil.NewDexHTTPReverseProxy(a.DexServerAddr, a.BaseHRef, a.DexTLSConfig)) - a.ssoClientApp, err = oidc.NewClientApp(a.settings, a.DexServerAddr, a.DexTLSConfig, a.BaseHRef) + a.ssoClientApp, err = oidc.NewClientApp(a.settings, a.DexServerAddr, a.DexTLSConfig, a.BaseHRef, cacheutil.NewRedisCache(a.RedisClient, a.settings.UserInfoCacheExpiration(), cacheutil.RedisCompressionNone)) errorsutil.CheckError(err) mux.HandleFunc(common.LoginEndpoint, a.ssoClientApp.HandleLogin) mux.HandleFunc(common.CallbackEndpoint, a.ssoClientApp.HandleCallback) @@ -1315,7 +1351,35 @@ func (a *ArgoCDServer) getClaims(ctx context.Context) (jwt.Claims, string, error if err != nil { return claims, "", status.Errorf(codes.Unauthenticated, "invalid session: %v", err) } - return claims, newToken, nil + + // Some SSO implementations (Okta) require a call to + // the OIDC user info path to get attributes like groups + // we assume that everywhere in argocd jwt.MapClaims is used as type for interface jwt.Claims + // otherwise this would cause a panic + var groupClaims jwt.MapClaims + if groupClaims, ok = claims.(jwt.MapClaims); !ok { + if tmpClaims, ok := claims.(*jwt.MapClaims); ok { + groupClaims = *tmpClaims + } + } + iss := jwtutil.StringField(groupClaims, "iss") + if iss != util_session.SessionManagerClaimsIssuer && a.settings.UserInfoGroupsEnabled() && a.settings.UserInfoPath() != "" { + userInfo, unauthorized, err := a.ssoClientApp.GetUserInfo(groupClaims, a.settings.IssuerURL(), a.settings.UserInfoPath()) + if unauthorized { + log.Errorf("error while quering userinfo endpoint: %v", err) + return claims, "", status.Errorf(codes.Unauthenticated, "invalid session") + } + if err != nil { + log.Errorf("error fetching user info endpoint: %v", err) + return claims, "", status.Errorf(codes.Internal, "invalid userinfo response") + } + if groupClaims["sub"] != userInfo["sub"] { + return claims, "", status.Error(codes.Unknown, "subject of claims from user info endpoint didn't match subject of idToken, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfo") + } + groupClaims["groups"] = userInfo["groups"] + } + + return groupClaims, newToken, nil } // getToken extracts the token from gRPC metadata or cookie headers diff --git a/server/server_test.go b/server/server_test.go index 303f938871f383..c4f4153f24d895 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -32,8 +32,10 @@ import ( "github.com/argoproj/argo-cd/v2/server/rbacpolicy" "github.com/argoproj/argo-cd/v2/test" "github.com/argoproj/argo-cd/v2/util/assets" + "github.com/argoproj/argo-cd/v2/util/cache" cacheutil "github.com/argoproj/argo-cd/v2/util/cache" appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate" + "github.com/argoproj/argo-cd/v2/util/oidc" "github.com/argoproj/argo-cd/v2/util/rbac" settings_util "github.com/argoproj/argo-cd/v2/util/settings" testutil "github.com/argoproj/argo-cd/v2/util/test" @@ -533,7 +535,7 @@ func dexMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Re } } -func getTestServer(t *testing.T, anonymousEnabled bool, withFakeSSO bool, useDexForSSO bool) (argocd *ArgoCDServer, oidcURL string) { +func getTestServer(t *testing.T, anonymousEnabled bool, withFakeSSO bool, useDexForSSO bool, additionalOIDCConfig settings_util.OIDCConfig) (argocd *ArgoCDServer, oidcURL string) { cm := test.NewFakeConfigMap() if anonymousEnabled { cm.Data["users.anonymous.enabled"] = "true" @@ -562,13 +564,12 @@ connectors: clientID: test-client clientSecret: $dex.oidc.clientSecret` } else { - oidcConfig := settings_util.OIDCConfig{ - Name: "Okta", - Issuer: oidcServer.URL, - ClientID: "argo-cd", - ClientSecret: "$oidc.okta.clientSecret", - } - oidcConfigString, err := yaml.Marshal(oidcConfig) + // override required oidc config fields but keep other configs as passed in + additionalOIDCConfig.Name = "Okta" + additionalOIDCConfig.Issuer = oidcServer.URL + additionalOIDCConfig.ClientID = "argo-cd" + additionalOIDCConfig.ClientSecret = "$oidc.okta.clientSecret" + oidcConfigString, err := yaml.Marshal(additionalOIDCConfig) require.NoError(t, err) cm.Data["oidc.config"] = string(oidcConfigString) // Avoid bothering with certs for local tests. @@ -589,9 +590,109 @@ connectors: argoCDOpts.DexServerAddr = ts.URL } argocd = NewServer(context.Background(), argoCDOpts) + var err error + argocd.ssoClientApp, err = oidc.NewClientApp(argocd.settings, argocd.DexServerAddr, argocd.DexTLSConfig, argocd.BaseHRef, cache.NewInMemoryCache(24*time.Hour)) + require.NoError(t, err) return argocd, oidcServer.URL } +func TestGetClaims(t *testing.T) { + + defaultExpiry := jwt.NewNumericDate(time.Now().Add(time.Hour * 24)) + defaultExpiryUnix := float64(defaultExpiry.Unix()) + + type testData struct { + test string + claims jwt.MapClaims + expectedErrorContains string + expectedClaims jwt.MapClaims + expectNewToken bool + additionalOIDCConfig settings_util.OIDCConfig + } + var tests = []testData{ + { + test: "GetClaims", + claims: jwt.MapClaims{ + "aud": "argo-cd", + "exp": defaultExpiry, + "sub": "randomUser", + }, + expectedErrorContains: "", + expectedClaims: jwt.MapClaims{ + "aud": "argo-cd", + "exp": defaultExpiryUnix, + "sub": "randomUser", + }, + expectNewToken: false, + additionalOIDCConfig: settings_util.OIDCConfig{}, + }, + { + // note: a passing test with user info groups can never be achieved since the user never logged in properly + // therefore the oidcClient's cache contains no accessToken for the user info endpoint + // and since the oidcClient cache is unexported (for good reasons) we can't mock this behaviour + test: "GetClaimsWithUserInfoGroupsEnabled", + claims: jwt.MapClaims{ + "aud": common.ArgoCDClientAppID, + "exp": defaultExpiry, + "sub": "randomUser", + }, + expectedErrorContains: "invalid session", + expectedClaims: jwt.MapClaims{ + "aud": common.ArgoCDClientAppID, + "exp": defaultExpiryUnix, + "sub": "randomUser", + }, + expectNewToken: false, + additionalOIDCConfig: settings_util.OIDCConfig{ + EnableUserInfoGroups: true, + UserInfoPath: "/userinfo", + UserInfoCacheExpiration: "5m", + }, + }, + } + + for _, testData := range tests { + testDataCopy := testData + + t.Run(testDataCopy.test, func(t *testing.T) { + t.Parallel() + + // Must be declared here to avoid race. + ctx := context.Background() //nolint:ineffassign,staticcheck + + argocd, oidcURL := getTestServer(t, false, true, false, testDataCopy.additionalOIDCConfig) + + // create new JWT and store it on the context to simulate an incoming request + testDataCopy.claims["iss"] = oidcURL + testDataCopy.expectedClaims["iss"] = oidcURL + token := jwt.NewWithClaims(jwt.SigningMethodRS512, testDataCopy.claims) + key, err := jwt.ParseRSAPrivateKeyFromPEM(testutil.PrivateKey) + require.NoError(t, err) + tokenString, err := token.SignedString(key) + require.NoError(t, err) + ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString)) + + gotClaims, newToken, err := argocd.getClaims(ctx) + + // Note: testutil.oidcMockHandler currently doesn't implement reissuing expired tokens + // so newToken will always be empty + if testDataCopy.expectNewToken { + assert.NotEmpty(t, newToken) + } + if testDataCopy.expectedClaims == nil { + assert.Nil(t, gotClaims) + } else { + assert.Equal(t, testDataCopy.expectedClaims, gotClaims) + } + if testDataCopy.expectedErrorContains != "" { + assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "getClaims should have thrown an error and return an error") + } else { + assert.NoError(t, err) + } + }) + } +} + func TestAuthenticate_3rd_party_JWTs(t *testing.T) { // Marshaling single strings to strings is typical, so we test for this relatively common behavior. jwt.MarshalSingleStringAsArray = false @@ -723,7 +824,7 @@ func TestAuthenticate_3rd_party_JWTs(t *testing.T) { // Must be declared here to avoid race. ctx := context.Background() //nolint:ineffassign,staticcheck - argocd, oidcURL := getTestServer(t, testDataCopy.anonymousEnabled, true, testDataCopy.useDex) + argocd, oidcURL := getTestServer(t, testDataCopy.anonymousEnabled, true, testDataCopy.useDex, settings_util.OIDCConfig{}) if testDataCopy.useDex { testDataCopy.claims.Issuer = fmt.Sprintf("%s/api/dex", oidcURL) @@ -779,7 +880,7 @@ func TestAuthenticate_no_request_metadata(t *testing.T) { t.Run(testDataCopy.test, func(t *testing.T) { t.Parallel() - argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true, true) + argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true, true, settings_util.OIDCConfig{}) ctx := context.Background() ctx, err := argocd.Authenticate(ctx) @@ -825,7 +926,7 @@ func TestAuthenticate_no_SSO(t *testing.T) { // Must be declared here to avoid race. ctx := context.Background() //nolint:ineffassign,staticcheck - argocd, dexURL := getTestServer(t, testDataCopy.anonymousEnabled, false, true) + argocd, dexURL := getTestServer(t, testDataCopy.anonymousEnabled, false, true, settings_util.OIDCConfig{}) token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{Issuer: fmt.Sprintf("%s/api/dex", dexURL)}) tokenString, err := token.SignedString([]byte("key")) require.NoError(t, err) @@ -933,7 +1034,7 @@ func TestAuthenticate_bad_request_metadata(t *testing.T) { // Must be declared here to avoid race. ctx := context.Background() //nolint:ineffassign,staticcheck - argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true, true) + argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true, true, settings_util.OIDCConfig{}) ctx = metadata.NewIncomingContext(context.Background(), testDataCopy.metadata) ctx, err := argocd.Authenticate(ctx) @@ -1425,3 +1526,46 @@ func TestReplaceBaseHRef(t *testing.T) { }) } } + +func Test_enforceContentTypes(t *testing.T) { + getBaseHandler := func(t *testing.T, allow bool) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + assert.True(t, allow, "http handler was hit when it should have been blocked by content type enforcement") + writer.WriteHeader(200) + }) + } + + t.Parallel() + + t.Run("GET - not providing a content type, should still succeed", func(t *testing.T) { + handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc) + req := httptest.NewRequest("GET", "/", nil) + w := httptest.NewRecorder() + handler(w, req) + resp := w.Result() + assert.Equal(t, 200, resp.StatusCode) + }) + + t.Run("POST", func(t *testing.T) { + handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc) + req := httptest.NewRequest("POST", "/", nil) + w := httptest.NewRecorder() + handler(w, req) + resp := w.Result() + assert.Equal(t, 415, resp.StatusCode, "didn't provide a content type, should have gotten an error") + + req = httptest.NewRequest("POST", "/", nil) + req.Header = map[string][]string{"Content-Type": {"application/json"}} + w = httptest.NewRecorder() + handler(w, req) + resp = w.Result() + assert.Equal(t, 200, resp.StatusCode, "should have passed, since an allowed content type was provided") + + req = httptest.NewRequest("POST", "/", nil) + req.Header = map[string][]string{"Content-Type": {"not-allowed"}} + w = httptest.NewRecorder() + handler(w, req) + resp = w.Result() + assert.Equal(t, 415, resp.StatusCode, "should not have passed, since a disallowed content type was provided") + }) +} diff --git a/test/container/Dockerfile b/test/container/Dockerfile index c86fbb1f387b1f..8258be1af72aa6 100644 --- a/test/container/Dockerfile +++ b/test/container/Dockerfile @@ -1,20 +1,20 @@ -FROM docker.io/library/redis:7.0.11@sha256:f50031a49f41e493087fb95f96fdb3523bb25dcf6a3f0b07c588ad3cdbe1d0aa as redis +FROM docker.io/library/redis:7.2.4@sha256:e647cfe134bf5e8e74e620f66346f93418acfc240b71dd85640325cb7cd01402 as redis # There are libraries we will want to copy from here in the final stage of the # build, but the COPY directive does not have a way to determine system # architecture, so we create a symlink here to facilitate copying. -RUN ln -s /usr/lib/$(uname -m)-linux-gnu /usr/lib/linux-gnu +RUN ln -s /usr/lib/$(uname -m)-linux-gnu /usr/lib/linux-gnu # Please make sure to also check the contained yarn version and update the references below when upgrading this image's version -FROM docker.io/library/node:20.7.0@sha256:f08c20b9f9c55dd47b1841793f0ee480c5395aa165cd02edfd68b068ed64bfb5 as node +FROM docker.io/library/node:21.6.2@sha256:65998e325b06014d4f1417a8a6afb1540d1ac66521cca76f2221a6953947f9ee as node FROM docker.io/library/golang:1.21.3@sha256:02d7116222536a5cf0fcf631f90b507758b669648e0f20186d2dc94a9b419a9b as golang -FROM docker.io/library/registry:2.8@sha256:41f413c22d6156587e2a51f3e80c09808b8c70e82be149b82b5e0196a88d49b4 as registry +FROM docker.io/library/registry:2.8@sha256:f4e1b878d4bc40a1f65532d68c94dcfbab56aa8cba1f00e355a206e7f6cc9111 as registry -FROM docker.io/bitnami/kubectl:1.27@sha256:670fe3f50d45c0511bb0f2af018e2fc082ac8cdfaea02dba4e32866296036926 as kubectl +FROM docker.io/bitnami/kubectl:1.27@sha256:14ab746e857d96c105df4989cc2bf841292f2d143f7c60f9d7f549ae660eab43 as kubectl -FROM docker.io/library/ubuntu:22.04@sha256:0bced47fffa3361afa981854fcabcd4577cd43cebbb808cea2b1f33a3dd7f508 +FROM docker.io/library/ubuntu:22.04@sha256:f9d633ff6640178c2d0525017174a688e2c1aef28f0a0130b26bd5554491f0da ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update && apt-get install --fix-missing -y \ @@ -72,10 +72,10 @@ COPY --from=redis /usr/local/bin/* /usr/local/bin/ # Copy redis dependencies/shared libraries # Ubuntu 22.04+ has moved to OpenSSL3 and no longer provides these libraries -COPY --from=redis /usr/lib/linux-gnu/libssl.so.1.1 /usr/lib/linux-gnu/ -COPY --from=redis /usr/lib/linux-gnu/libcrypto.so.1.1 /usr/lib/linux-gnu/ -RUN mv /usr/lib/linux-gnu/libssl.so.1.1 /usr/lib/$(uname -m)-linux-gnu/ && \ - mv /usr/lib/linux-gnu/libcrypto.so.1.1 /usr/lib/$(uname -m)-linux-gnu/ && \ +COPY --from=redis /usr/lib/linux-gnu/libssl.so.3 /usr/lib/linux-gnu/ +COPY --from=redis /usr/lib/linux-gnu/libcrypto.so.3 /usr/lib/linux-gnu/ +RUN mv /usr/lib/linux-gnu/libssl.so.3 /usr/lib/$(uname -m)-linux-gnu/ && \ + mv /usr/lib/linux-gnu/libcrypto.so.3 /usr/lib/$(uname -m)-linux-gnu/ && \ rm -rf /usr/lib/linux-gnu/ # Copy registry binaries to the image diff --git a/test/container/Procfile b/test/container/Procfile index ef5100e71bab38..3ec9add44d5a77 100644 --- a/test/container/Procfile +++ b/test/container/Procfile @@ -1,6 +1,6 @@ controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''}" api-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_BINARY_NAME=argocd-server $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} " -dex: sh -c "test $ARGOCD_IN_CI = true && exit 0; ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/cmd gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.37.0 serve /dex.yaml" +dex: sh -c "test $ARGOCD_IN_CI = true && exit 0; ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/cmd gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.38.0 serve /dex.yaml" redis: sh -c "/usr/local/bin/redis-server --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}" repo-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_BINARY_NAME=argocd-repo-server $COMMAND --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}" ui: sh -c "test $ARGOCD_IN_CI = true && exit 0; cd ui && ARGOCD_E2E_YARN_HOST=0.0.0.0 ${ARGOCD_E2E_YARN_CMD:-yarn} start" diff --git a/test/e2e/app_deletion_test.go b/test/e2e/app_deletion_test.go index 1194edcb37df3c..9158dddffa06a0 100644 --- a/test/e2e/app_deletion_test.go +++ b/test/e2e/app_deletion_test.go @@ -67,3 +67,18 @@ func TestDeletingAppByLabel(t *testing.T) { // delete is successful Expect(DoesNotExist()) } + +func TestDeletingAppByLabelWait(t *testing.T) { + Given(t). + Path(guestbookPath). + When(). + CreateApp("--label=foo=bar"). + Sync(). + Then(). + Expect(SyncStatusIs(SyncStatusCode(SyncStatusCodeSynced))). + When(). + DeleteBySelectorWithWait("foo=bar"). + Then(). + // delete is successful + Expect(DoesNotExistNow()) +} diff --git a/test/e2e/app_management_ns_test.go b/test/e2e/app_management_ns_test.go index 3e13131791ab9a..32636e2b52c49d 100644 --- a/test/e2e/app_management_ns_test.go +++ b/test/e2e/app_management_ns_test.go @@ -748,7 +748,7 @@ func TestNamespacedResourceDiffing(t *testing.T) { Then(). Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). And(func(app *Application) { - diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local", "testdata/guestbook") + diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local-repo-root", ".", "--local", "testdata/guestbook") assert.Error(t, err) assert.Contains(t, diffOutput, fmt.Sprintf("===== apps/Deployment %s/guestbook-ui ======", DeploymentNamespace())) }). @@ -761,7 +761,7 @@ func TestNamespacedResourceDiffing(t *testing.T) { Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(app *Application) { - diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local", "testdata/guestbook") + diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local-repo-root", ".", "--local", "testdata/guestbook") assert.NoError(t, err) assert.Empty(t, diffOutput) }). @@ -897,7 +897,7 @@ func testNSEdgeCasesApplicationResources(t *testing.T, appPath string, statusCod expect. Expect(HealthIs(statusCode)). And(func(app *Application) { - diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local", path.Join("testdata", appPath)) + diffOutput, err := RunCli("app", "diff", ctx.AppQualifiedName(), "--local-repo-root", ".", "--local", path.Join("testdata", appPath)) assert.Empty(t, diffOutput) assert.NoError(t, err) }) @@ -998,7 +998,7 @@ func TestNamespacedLocalManifestSync(t *testing.T) { Given(). LocalPath(guestbookPathLocal). When(). - Sync(). + Sync("--local-repo-root", "."). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(app *Application) { @@ -1066,7 +1066,7 @@ func TestNamespacedLocalSyncDryRunWithASEnabled(t *testing.T) { assert.NoError(t, err) appBefore := app.DeepCopy() - _, err = RunCli("app", "sync", app.QualifiedName(), "--dry-run", "--local", guestbookPathLocal) + _, err = RunCli("app", "sync", app.QualifiedName(), "--dry-run", "--local-repo-root", ".", "--local", guestbookPathLocal) assert.NoError(t, err) appAfter := app.DeepCopy() diff --git a/test/e2e/app_management_test.go b/test/e2e/app_management_test.go index d2902e27c97d85..10b2cf926723c9 100644 --- a/test/e2e/app_management_test.go +++ b/test/e2e/app_management_test.go @@ -1324,7 +1324,7 @@ func TestLocalManifestSync(t *testing.T) { Given(). LocalPath(guestbookPathLocal). When(). - Sync(). + Sync("--local-repo-root", "."). Then(). Expect(SyncStatusIs(SyncStatusCodeSynced)). And(func(app *Application) { @@ -1385,7 +1385,7 @@ func TestLocalSyncDryRunWithAutosyncEnabled(t *testing.T) { assert.NoError(t, err) appBefore := app.DeepCopy() - _, err = RunCli("app", "sync", app.Name, "--dry-run", "--local", guestbookPathLocal) + _, err = RunCli("app", "sync", app.Name, "--dry-run", "--local-repo-root", ".", "--local", guestbookPathLocal) assert.NoError(t, err) appAfter := app.DeepCopy() diff --git a/test/e2e/app_sync_options_test.go b/test/e2e/app_sync_options_test.go new file mode 100644 index 00000000000000..7d0a0ffeabb990 --- /dev/null +++ b/test/e2e/app_sync_options_test.go @@ -0,0 +1,61 @@ +package e2e + +import ( + "testing" + + "github.com/argoproj/gitops-engine/pkg/health" + . "github.com/argoproj/gitops-engine/pkg/sync/common" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + + . "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + . "github.com/argoproj/argo-cd/v2/test/e2e/fixture" + . "github.com/argoproj/argo-cd/v2/test/e2e/fixture/app" +) + +// Given application is set with --sync-option CreateNamespace=true and --sync-option ServerSideApply=true +// +// application --dest-namespace exists +// +// Then, --dest-namespace is created with server side apply +// application is synced and healthy with resource +// application resources created with server side apply in the newly created namespace. +func TestNamespaceCreationWithSSA(t *testing.T) { + SkipOnEnv(t, "OPENSHIFT") + namespace := "guestbook-ui-with-ssa" + defer func() { + if !t.Skipped() { + _, err := Run("", "kubectl", "delete", "namespace", namespace) + assert.NoError(t, err) + } + }() + + ctx := Given(t) + ctx. + SetAppNamespace(AppNamespace()). + Timeout(30). + Path("guestbook"). + When(). + CreateFromFile(func(app *Application) { + app.Spec.SyncPolicy = &SyncPolicy{ + SyncOptions: SyncOptions{"CreateNamespace=true", "ServerSideApply=true"}, + } + }). + Then(). + Expect(NoNamespace(namespace)). + When(). + AppSet("--dest-namespace", namespace). + Sync(). + Then(). + Expect(Success("")). + Expect(Namespace(namespace, func(app *Application, ns *v1.Namespace) { + assert.NotContains(t, ns.Annotations, "kubectl.kubernetes.io/last-applied-configuration") + })). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(ResourceHealthWithNamespaceIs("Deployment", "guestbook-ui", namespace, health.HealthStatusHealthy)). + Expect(ResourceSyncStatusWithNamespaceIs("Deployment", "guestbook-ui", namespace, SyncStatusCodeSynced)). + Expect(ResourceHealthWithNamespaceIs("Service", "guestbook-ui", namespace, health.HealthStatusHealthy)). + Expect(ResourceSyncStatusWithNamespaceIs("Service", "guestbook-ui", namespace, SyncStatusCodeSynced)) +} diff --git a/test/e2e/applicationset_test.go b/test/e2e/applicationset_test.go index 2fef0ace555835..5b9b8190c5437d 100644 --- a/test/e2e/applicationset_test.go +++ b/test/e2e/applicationset_test.go @@ -599,6 +599,134 @@ func TestRenderHelmValuesObject(t *testing.T) { } +func TestTemplatePatch(t *testing.T) { + + expectedApp := argov1alpha1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: application.ApplicationKind, + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-guestbook", + Namespace: fixture.TestNamespace(), + Finalizers: []string{"resources-finalizer.argocd.argoproj.io"}, + Annotations: map[string]string{ + "annotation-some-key": "annotation-some-value", + }, + }, + Spec: argov1alpha1.ApplicationSpec{ + Project: "default", + Source: &argov1alpha1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + }, + Destination: argov1alpha1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "guestbook", + }, + SyncPolicy: &argov1alpha1.SyncPolicy{ + SyncOptions: argov1alpha1.SyncOptions{"CreateNamespace=true"}, + }, + }, + } + + templatePatch := `{ + "metadata": { + "annotations": { + {{- range $k, $v := .annotations }} + "{{ $k }}": "{{ $v }}" + {{- end }} + } + }, + {{- if .createNamespace }} + "spec": { + "syncPolicy": { + "syncOptions": [ + "CreateNamespace=true" + ] + } + } + {{- end }} + } + ` + + var expectedAppNewNamespace *argov1alpha1.Application + var expectedAppNewMetadata *argov1alpha1.Application + + Given(t). + // Create a ListGenerator-based ApplicationSet + When().Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{ + Name: "patch-template", + }, + Spec: v1alpha1.ApplicationSetSpec{ + GoTemplate: true, + Template: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster}}-guestbook"}, + Spec: argov1alpha1.ApplicationSpec{ + Project: "default", + Source: &argov1alpha1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + }, + Destination: argov1alpha1.ApplicationDestination{ + Server: "{{.url}}", + Namespace: "guestbook", + }, + }, + }, + TemplatePatch: &templatePatch, + Generators: []v1alpha1.ApplicationSetGenerator{ + { + List: &v1alpha1.ListGenerator{ + Elements: []apiextensionsv1.JSON{{ + Raw: []byte(`{ + "cluster": "my-cluster", + "url": "https://kubernetes.default.svc", + "createNamespace": true, + "annotations": { + "annotation-some-key": "annotation-some-value" + } + }`), + }}, + }, + }, + }, + }, + }).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})). + + // Update the ApplicationSet template namespace, and verify it updates the Applications + When(). + And(func() { + expectedAppNewNamespace = expectedApp.DeepCopy() + expectedAppNewNamespace.Spec.Destination.Namespace = "guestbook2" + }). + Update(func(appset *v1alpha1.ApplicationSet) { + appset.Spec.Template.Spec.Destination.Namespace = "guestbook2" + }).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewNamespace})). + + // Update the metadata fields in the appset template, and make sure it propagates to the apps + When(). + And(func() { + expectedAppNewMetadata = expectedAppNewNamespace.DeepCopy() + expectedAppNewMetadata.ObjectMeta.Labels = map[string]string{ + "label-key": "label-value", + } + }). + Update(func(appset *v1alpha1.ApplicationSet) { + appset.Spec.Template.Labels = map[string]string{"label-key": "label-value"} + }).Then().Expect(ApplicationsExist([]argov1alpha1.Application{*expectedAppNewMetadata})). + + // verify the ApplicationSet status conditions were set correctly + Expect(ApplicationSetHasConditions("patch-template", ExpectedConditions)). + + // Delete the ApplicationSet, and verify it deletes the Applications + When(). + Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewMetadata})) + +} + func TestSyncPolicyCreateUpdate(t *testing.T) { expectedApp := argov1alpha1.Application{ diff --git a/test/e2e/cluster_test.go b/test/e2e/cluster_test.go index e57b2132b74727..2074a6aa1b7b1a 100644 --- a/test/e2e/cluster_test.go +++ b/test/e2e/cluster_test.go @@ -38,7 +38,7 @@ https://kubernetes.default.svc in-cluster %v Successful `, GetVe When(). CreateApp() - tries := 2 + tries := 5 for i := 0; i <= tries; i += 1 { clusterFixture.GivenWithSameState(t). When(). diff --git a/test/e2e/fixture/app/actions.go b/test/e2e/fixture/app/actions.go index f4fd167db1024d..a2b1d5e01371b1 100644 --- a/test/e2e/fixture/app/actions.go +++ b/test/e2e/fixture/app/actions.go @@ -417,6 +417,12 @@ func (a *Actions) DeleteBySelector(selector string) *Actions { return a } +func (a *Actions) DeleteBySelectorWithWait(selector string) *Actions { + a.context.t.Helper() + a.runCli("app", "delete", fmt.Sprintf("--selector=%s", selector), "--yes", "--wait") + return a +} + func (a *Actions) Wait(args ...string) *Actions { a.context.t.Helper() args = append([]string{"app", "wait"}, args...) diff --git a/test/e2e/fixture/app/expectation.go b/test/e2e/fixture/app/expectation.go index c7cf20ab277297..4d4918e981751c 100644 --- a/test/e2e/fixture/app/expectation.go +++ b/test/e2e/fixture/app/expectation.go @@ -216,6 +216,19 @@ func DoesNotExist() Expectation { } } +func DoesNotExistNow() Expectation { + return func(c *Consequences) (state, string) { + _, err := c.get() + if err != nil { + if apierr.IsNotFound(err) { + return succeeded, "app does not exist" + } + return failed, err.Error() + } + return failed, "app should not exist" + } +} + func Pod(predicate func(p v1.Pod) bool) Expectation { return func(c *Consequences) (state, string) { pods, err := pods() diff --git a/test/e2e/fixture/applicationsets/context.go b/test/e2e/fixture/applicationsets/context.go index c10b2c99bfe5fb..a7e91f4d0c8ffe 100644 --- a/test/e2e/fixture/applicationsets/context.go +++ b/test/e2e/fixture/applicationsets/context.go @@ -5,7 +5,6 @@ import ( "time" "github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils" - . "github.com/argoproj/argo-cd/v2/test/e2e/fixture/applicationsets/utils" ) // Context implements the "given" part of given/when/then @@ -19,7 +18,7 @@ type Context struct { } func Given(t *testing.T) *Context { - EnsureCleanState(t) + utils.EnsureCleanState(t) return &Context{t: t} } diff --git a/test/e2e/fixture/http.go b/test/e2e/fixture/http.go index 1e818f5262024f..00c123ab5d893c 100644 --- a/test/e2e/fixture/http.go +++ b/test/e2e/fixture/http.go @@ -28,6 +28,7 @@ func DoHttpRequest(method string, path string, data ...byte) (*http.Response, er return nil, err } req.AddCookie(&http.Cookie{Name: common.AuthCookieName, Value: token}) + req.Header.Set("Content-Type", "application/json") httpClient := &http.Client{ Transport: &http.Transport{ diff --git a/test/e2e/hook_test.go b/test/e2e/hook_test.go index f6bb1be872ac60..2db8ff87795adb 100644 --- a/test/e2e/hook_test.go +++ b/test/e2e/hook_test.go @@ -1,12 +1,14 @@ package e2e import ( + "context" "fmt" "testing" "time" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/argoproj/gitops-engine/pkg/health" . "github.com/argoproj/gitops-engine/pkg/sync/common" @@ -48,6 +50,24 @@ func testHookSuccessful(t *testing.T, hookType HookType) { Expect(ResourceResultIs(ResourceResult{Version: "v1", Kind: "Pod", Namespace: DeploymentNamespace(), Name: "hook", Message: "pod/hook created", HookType: hookType, HookPhase: OperationSucceeded, SyncPhase: SyncPhase(hookType)})) } +func TestPostDeleteHook(t *testing.T) { + Given(t). + Path("post-delete-hook"). + When(). + CreateApp(). + Refresh(RefreshTypeNormal). + Delete(true). + Then(). + Expect(DoesNotExist()). + AndAction(func() { + hooks, err := KubeClientset.CoreV1().Pods(DeploymentNamespace()).List(context.Background(), metav1.ListOptions{}) + CheckError(err) + assert.Len(t, hooks.Items, 1) + assert.Equal(t, "hook", hooks.Items[0].Name) + }) + +} + // make sure that that hooks do not appear in "argocd app diff" func TestHookDiff(t *testing.T) { Given(t). diff --git a/test/e2e/multiarch-container/Dockerfile b/test/e2e/multiarch-container/Dockerfile index d4d49adc0d7464..fb9b9224f24c46 100644 --- a/test/e2e/multiarch-container/Dockerfile +++ b/test/e2e/multiarch-container/Dockerfile @@ -1,2 +1,2 @@ -FROM docker.io/library/busybox@sha256:2376a0c12759aa1214ba83e771ff252c7b1663216b192fbe5e0fb364e952f85c +FROM docker.io/library/busybox@sha256:3fbc632167424a6d997e74f52b878d7cc478225cffac6bc977eedfe51c7f4e79 CMD exec sh -c "trap : TERM INT; echo 'Hi' && tail -f /dev/null" diff --git a/test/e2e/notification_test.go b/test/e2e/notification_test.go index 363cb87454a0fc..eebe4d8991ae5d 100644 --- a/test/e2e/notification_test.go +++ b/test/e2e/notification_test.go @@ -12,7 +12,7 @@ import ( func TestNotificationsListServices(t *testing.T) { ctx := notifFixture.Given(t) ctx.When(). - SetParamInNotificationConfigMap("service.webhook.test", "url: https://test.com"). + SetParamInNotificationConfigMap("service.webhook.test", "url: https://test.example.com"). Then().Services(func(services *notification.ServiceList, err error) { assert.Nil(t, err) assert.Equal(t, []*notification.Service{{Name: pointer.String("test")}}, services.Items) diff --git a/test/e2e/sync_waves_test.go b/test/e2e/sync_waves_test.go index ac5db15eee57d0..8d0ee14e487d1b 100644 --- a/test/e2e/sync_waves_test.go +++ b/test/e2e/sync_waves_test.go @@ -9,6 +9,8 @@ import ( "github.com/argoproj/gitops-engine/pkg/health" . "github.com/argoproj/gitops-engine/pkg/sync/common" + + v1 "k8s.io/api/core/v1" ) func TestFixingDegradedApp(t *testing.T) { @@ -100,3 +102,46 @@ func TestDegradedDeploymentIsSucceededAndSynced(t *testing.T) { Expect(SyncStatusIs(SyncStatusCodeSynced)). Expect(ResourceResultNumbering(1)) } + +// resources should be pruned in reverse of creation order(syncwaves order) +func TestSyncPruneOrderWithSyncWaves(t *testing.T) { + ctx := Given(t).Timeout(60) + + // remove finalizer to ensure proper cleanup if test fails at early stage + defer func() { + _, _ = RunCli("app", "patch-resource", ctx.AppQualifiedName(), + "--kind", "Pod", + "--resource-name", "pod-with-finalizers", + "--patch", `[{"op": "remove", "path": "/metadata/finalizers"}]`, + "--patch-type", "application/json-patch+json", "--all", + ) + }() + + ctx.Path("syncwaves-prune-order"). + When(). + CreateApp(). + // creation order: sa & role -> rolebinding -> pod + Sync(). + Wait(). + Then(). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + When(). + // delete files to remove resources + DeleteFile("pod.yaml"). + DeleteFile("rbac.yaml"). + Refresh(RefreshTypeHard). + IgnoreErrors(). + Then(). + Expect(SyncStatusIs(SyncStatusCodeOutOfSync)). + When(). + // prune order: pod -> rolebinding -> sa & role + Sync("--prune"). + Wait(). + Then(). + Expect(OperationPhaseIs(OperationSucceeded)). + Expect(SyncStatusIs(SyncStatusCodeSynced)). + Expect(HealthIs(health.HealthStatusHealthy)). + Expect(NotPod(func(p v1.Pod) bool { return p.Name == "pod-with-finalizers" })). + Expect(ResourceResultNumbering(4)) +} diff --git a/test/e2e/testdata/networking/guestbook-ui-svc-ingress.yaml b/test/e2e/testdata/networking/guestbook-ui-svc-ingress.yaml index d499de1e9c3086..a4427135b193d0 100644 --- a/test/e2e/testdata/networking/guestbook-ui-svc-ingress.yaml +++ b/test/e2e/testdata/networking/guestbook-ui-svc-ingress.yaml @@ -9,7 +9,7 @@ metadata: ingress.kubernetes.io/app-root: "/" spec: rules: - - host: myhost.com + - host: example.com http: paths: - path: / @@ -27,7 +27,7 @@ metadata: ingress.kubernetes.io/app-root: "/" spec: rules: - - host: myhost.com + - host: example.com http: paths: - path: / diff --git a/test/e2e/testdata/post-delete-hook/hook.yaml b/test/e2e/testdata/post-delete-hook/hook.yaml new file mode 100644 index 00000000000000..5631db681f1d08 --- /dev/null +++ b/test/e2e/testdata/post-delete-hook/hook.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + annotations: + argocd.argoproj.io/hook: PostDelete + name: hook +spec: + containers: + - command: + - "true" + image: quay.io/argoprojlabs/argocd-e2e-container:0.1 + imagePullPolicy: IfNotPresent + name: main + restartPolicy: Never \ No newline at end of file diff --git a/test/e2e/testdata/syncwaves-prune-order/README.md b/test/e2e/testdata/syncwaves-prune-order/README.md new file mode 100644 index 00000000000000..92a62fdfe109df --- /dev/null +++ b/test/e2e/testdata/syncwaves-prune-order/README.md @@ -0,0 +1,15 @@ +## Test Scenario + +This test example is for testing the reverse pruning of resources with syncwaves during sync operation. + +Resource creation happens in below order +- wave 0: sa & role +- wave 1: rolebinding +- wave 2: pod + +They are setup in such a way that the resources will be cleaned up properly only if they are deleted in the reverse order of creation i.e +- wave 0: pod +- wave 1: rolebinding +- wave 2: sa & role + +If above delete order is not followed the pod gets stuck in terminating state due to a finalizer which is supposed to be removed by k8s container lifecycle hook on delete if delete order is correct. \ No newline at end of file diff --git a/test/e2e/testdata/syncwaves-prune-order/pod.yaml b/test/e2e/testdata/syncwaves-prune-order/pod.yaml new file mode 100644 index 00000000000000..f801a3992aa373 --- /dev/null +++ b/test/e2e/testdata/syncwaves-prune-order/pod.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pod-with-finalizers + annotations: + argocd.argoproj.io/sync-wave: "2" + # remove this finalizers using container preStop lifecycle hook on delete + finalizers: + - example.com/block-delete +spec: + serviceAccountName: modify-pods-sa # sa with permissions to modify pods + terminationGracePeriodSeconds: 15 + containers: + - name: container + image: nginx:alpine + command: ["/bin/sh", "-c"] + args: ["sleep 10h"] + env: + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + lifecycle: + # remove finalizers for successful delete of pod + preStop: + exec: + command: + - /bin/sh + - -c + - | + set -e + + SERVICE_ACCOUNT_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + POD_URL="https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods/$POD_NAME" + PATCH_PAYLOAD='[{"op": "remove", "path": "/metadata/finalizers"}]' + + curl -k -v -H "Authorization: Bearer $SERVICE_ACCOUNT_TOKEN" -H "Content-Type: application/json-patch+json" -X PATCH --data "$PATCH_PAYLOAD" $POD_URL diff --git a/test/e2e/testdata/syncwaves-prune-order/rbac.yaml b/test/e2e/testdata/syncwaves-prune-order/rbac.yaml new file mode 100644 index 00000000000000..9512644b731db0 --- /dev/null +++ b/test/e2e/testdata/syncwaves-prune-order/rbac.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: modify-pods-sa + annotations: + argocd.argoproj.io/sync-wave: "0" +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: modify-pods-role + annotations: + argocd.argoproj.io/sync-wave: "0" +rules: + - apiGroups: [""] + resources: + - pods + verbs: + - get + - list + - delete + - update + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: modify-pods-rolebinding + annotations: + argocd.argoproj.io/sync-wave: "1" +subjects: + - kind: ServiceAccount + name: modify-pods-sa +roleRef: + kind: Role + name: modify-pods-role + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/test/remote/Dockerfile b/test/remote/Dockerfile index 8d03d1321d25b9..cf43ee355567d5 100644 --- a/test/remote/Dockerfile +++ b/test/remote/Dockerfile @@ -1,6 +1,6 @@ ARG BASE_IMAGE=docker.io/library/ubuntu:22.04 -FROM docker.io/library/golang:1.21.3@sha256:02d7116222536a5cf0fcf631f90b507758b669648e0f20186d2dc94a9b419a9b AS go +FROM docker.io/library/golang:1.22.0@sha256:7b297d9abee021bab9046e492506b3c2da8a3722cbf301653186545ecc1e00bb AS go RUN go install github.com/mattn/goreman@latest && \ go install github.com/kisielk/godepgraph@latest diff --git a/test/testutil.go b/test/testutil.go index 34264772fa54f8..e97de4a762bcf5 100644 --- a/test/testutil.go +++ b/test/testutil.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" log "github.com/sirupsen/logrus" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/tools/cache" @@ -84,6 +85,15 @@ func YamlToUnstructured(yamlStr string) *unstructured.Unstructured { return &unstructured.Unstructured{Object: obj} } +func YamlToApplication(yamlStr string) *v1alpha1.Application { + app := v1alpha1.Application{} + err := yaml.Unmarshal([]byte(yamlStr), &app) + if err != nil { + panic(err) + } + return &app +} + // ToMap converts any object to a map[string]interface{} func ToMap(obj interface{}) map[string]interface{} { data, err := json.Marshal(obj) diff --git a/ui-test/Dockerfile b/ui-test/Dockerfile index a5a77710eca52f..4868c11d66056d 100644 --- a/ui-test/Dockerfile +++ b/ui-test/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/library/node:20.7.0@sha256:f08c20b9f9c55dd47b1841793f0ee480c5395aa165cd02edfd68b068ed64bfb5 as node +FROM docker.io/library/node:21.6.2@sha256:65998e325b06014d4f1417a8a6afb1540d1ac66521cca76f2221a6953947f9ee as node RUN apt-get update && apt-get install --no-install-recommends -y \ software-properties-common diff --git a/ui-test/package.json b/ui-test/package.json index 1875e31b6fd627..fd34ca2edab4ab 100644 --- a/ui-test/package.json +++ b/ui-test/package.json @@ -27,6 +27,6 @@ "tslint-config-prettier": "^1.18.0", "tslint-plugin-prettier": "^2.0.1", "typescript": "^4.0.3", - "yarn": "^1.22.10" + "yarn": "^1.22.13" } } diff --git a/ui-test/yarn.lock b/ui-test/yarn.lock index b80910028fb7f1..6765cbf79d61b1 100644 --- a/ui-test/yarn.lock +++ b/ui-test/yarn.lock @@ -540,9 +540,9 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== follow-redirects@^1.14.0: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== foreach@^2.0.5: version "2.0.5" @@ -1510,10 +1510,10 @@ yargs@13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yarn@^1.22.10: - version "1.22.10" - resolved "https://registry.npmjs.org/yarn/-/yarn-1.22.10.tgz" - integrity sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA== +yarn@^1.22.13: + version "1.22.13" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.13.tgz#8789ef23b630fe99b819b044f4b7b93ab1bc1b8f" + integrity sha512-G8qG4t7Ef5cLVpzbM3HWWsow4hpfeSCfKtMnjfERmp9V5qSCOKz0uGAIQCM/x3gWfCzH8Bvb4hl3ZfhG/XD1Jg== yauzl@^2.10.0: version "2.10.0" diff --git a/ui/.nvmrc b/ui/.nvmrc index 376d26203e61e8..a8d3ff91fa10d4 100644 --- a/ui/.nvmrc +++ b/ui/.nvmrc @@ -1 +1 @@ -v20.7.0 +v21.6.1 diff --git a/ui/jest.config.js b/ui/jest.config.js index abd8a45bcecd6f..524b493f546fc3 100644 --- a/ui/jest.config.js +++ b/ui/jest.config.js @@ -1,12 +1,11 @@ module.exports = { preset: 'ts-jest', - testEnvironment: 'node', + testEnvironment: 'jsdom', reporters: ['default', 'jest-junit'], collectCoverage: true, transformIgnorePatterns: ['node_modules/(?!(argo-ui)/)'], globals: { 'self': {}, - 'window': {localStorage: { getItem: () => '{}', setItem: () => null }}, 'ts-jest': { isolatedModules: true, }, @@ -17,20 +16,3 @@ module.exports = { '.+\\.(css|styl|less|sass|scss)$': 'jest-transform-css', }, }; - -const localStorageMock = (() => { - let store = {}; - return { - getItem: (key) => store[key], - setItem: (key, value) => { - store[key] = value.toString(); - }, - clear: () => { - store = {}; - }, - removeItem: (key) => { - delete store[key]; - } - }; -})(); -global.localStorage = localStorageMock; \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index 7c7b0df9c4f522..828d0c6e7f97d8 100644 --- a/ui/package.json +++ b/ui/package.json @@ -13,7 +13,7 @@ "dependencies": { "@fortawesome/fontawesome-free": "^6.4.0", "@types/react-virtualized": "^9.21.21", - "@types/superagent": "^4.1.15", + "@types/superagent": "^4.1.21", "ansi-to-react": "^6.1.6", "argo-ui": "git+https://github.com/argoproj/argo-ui.git", "buffer": "^6.0.3", @@ -40,7 +40,7 @@ "react-dom": "^16.9.3", "react-form": "2.16.3", "react-ga": "^2.7.0", - "react-helmet": "^5.2.0", + "react-helmet": "^6.1.0", "react-hot-loader": "^3.1.3", "react-moment": "^0.9.7", "react-paginate": "^8.1.4", @@ -50,7 +50,7 @@ "react-virtualized": "^9.22.3", "redoc": "^2.0.0-rc.64", "rxjs": "^6.6.6", - "superagent": "^8.0.9", + "superagent": "^8.1.2", "timezones-list": "3.0.1", "tsx": "^3.4.0", "unidiff": "^1.0.2", @@ -84,7 +84,7 @@ "@types/react-autocomplete": "^1.8.4", "@types/react-dom": "^16.9.14", "@types/react-form": "^2.16.0", - "@types/react-helmet": "^5.0.17", + "@types/react-helmet": "^6.1.6", "@types/react-paginate": "^6.2.0", "@types/react-router": "^4.0.27", "@types/react-router-dom": "^4.2.3", @@ -102,7 +102,7 @@ "jest-junit": "^6.4.0", "jest-transform-css": "^2.0.0", "monaco-editor-webpack-plugin": "^7.0.0", - "postcss": "^8.2.13", + "postcss": "^8.4.35", "prettier": "1.19", "raw-loader": "^0.5.1", "react-test-renderer": "16.8.3", @@ -120,6 +120,6 @@ "webpack": "^5.84.1", "webpack-cli": "^4.9.2", "webpack-dev-server": "^4.7.4", - "yarn": "^1.22.10" + "yarn": "^1.22.21" } } diff --git a/ui/src/app/applications/components/application-deployment-history/application-deployment-history.tsx b/ui/src/app/applications/components/application-deployment-history/application-deployment-history.tsx index 55734b69ea0c41..37908fb1a35b88 100644 --- a/ui/src/app/applications/components/application-deployment-history/application-deployment-history.tsx +++ b/ui/src/app/applications/components/application-deployment-history/application-deployment-history.tsx @@ -1,4 +1,5 @@ import {DataLoader, DropDownMenu, Duration} from 'argo-ui'; +import {InitiatedBy} from './initiated-by'; import * as moment from 'moment'; import * as React from 'react'; import {Revision, Timestamp} from '../../../shared/components'; @@ -42,6 +43,12 @@ export const ApplicationDeploymentHistory = ({
{(info.deployStartedAt && ) || 'Unknown'} +
+
+ Initiated by: +
+ +

Active for: diff --git a/ui/src/app/applications/components/application-deployment-history/initiated-by.tsx b/ui/src/app/applications/components/application-deployment-history/initiated-by.tsx new file mode 100644 index 00000000000000..f691389b5dacab --- /dev/null +++ b/ui/src/app/applications/components/application-deployment-history/initiated-by.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; + +export const InitiatedBy = (props: {username: string; automated: boolean}) => { + const initiator = props.automated ? 'automated sync policy' : props.username || 'Unknown'; + return {initiator}; +}; diff --git a/ui/src/app/applications/components/application-details/application-details.scss b/ui/src/app/applications/components/application-details/application-details.scss index af1ca3e9d07dc9..82402ffd0d8b41 100644 --- a/ui/src/app/applications/components/application-details/application-details.scss +++ b/ui/src/app/applications/components/application-details/application-details.scss @@ -8,16 +8,16 @@ $header: 120px; .application-details { height: 100vh; width: 100%; - &__status-panel { - position: fixed; - left: $sidebar-width; - right: 0; - z-index: 3; - @media screen and (max-width: map-get($breakpoints, xlarge)) { - top: 150px; - } - @media screen and (max-width: map-get($breakpoints, large)) { - top: 146px; + + &__wrapper { + display: flex; + flex-direction: column; + height: calc(100vh - 2 * $top-bar-height); + overflow: hidden; + + @media screen and (max-width: map-get($breakpoints, xxlarge)) { + height: calc(100vh - 3 * $top-bar-height); + margin-top: $top-bar-height; } } @@ -27,13 +27,11 @@ $header: 120px; &__tree { padding: 1em; + + flex: 1; overflow-x: auto; overflow-y: auto; - margin-top: 150px; - height: calc(100vh - 2 * 70px - 115px); - @media screen and (max-width: map-get($breakpoints, xlarge)) { - margin-top: 165px; - } + overscroll-behavior-x: none; } &__sliding-panel-pagination-wrap { diff --git a/ui/src/app/applications/components/application-details/application-details.tsx b/ui/src/app/applications/components/application-details/application-details.tsx index 3f12660fb01918..a3e8175591dde8 100644 --- a/ui/src/app/applications/components/application-details/application-details.tsx +++ b/ui/src/app/applications/components/application-details/application-details.tsx @@ -30,7 +30,7 @@ import {ApplicationsDetailsAppDropdown} from './application-details-app-dropdown import {useSidebarTarget} from '../../../sidebar/sidebar'; import './application-details.scss'; -import {AppViewExtension} from '../../../shared/services/extensions-service'; +import {AppViewExtension, StatusPanelExtension} from '../../../shared/services/extensions-service'; interface ApplicationDetailsState { page: number; @@ -42,6 +42,8 @@ interface ApplicationDetailsState { collapsedNodes?: string[]; extensions?: AppViewExtension[]; extensionsMap?: {[key: string]: AppViewExtension}; + statusExtensions?: StatusPanelExtension[]; + statusExtensionsMap?: {[key: string]: StatusPanelExtension}; } interface FilterInput { @@ -87,6 +89,11 @@ export class ApplicationDetails extends React.Component { extensionsMap[ext.title] = ext; }); + const statusExtensions = services.extensions.getStatusPanelExtensions(); + const statusExtensionsMap: {[key: string]: StatusPanelExtension} = {}; + statusExtensions.forEach(ext => { + statusExtensionsMap[ext.id] = ext; + }); this.state = { page: 0, groupedResources: [], @@ -95,7 +102,9 @@ export class ApplicationDetails extends React.Component ) }}> -
- this.selectNode(appFullName, 0, 'diff')} - showOperation={() => this.setOperationStatusVisible(true)} - showConditions={() => this.setConditionsStatusVisible(true)} - showMetadataInfo={revision => this.setState({...this.state, revision})} - /> -
-
- {refreshing &&

Refreshing

} - {((pref.view === 'tree' || pref.view === 'network') && ( - <> - services.viewPreferences.getPreferences()}> - {viewPref => ( - - )} - - - this.filterTreeNode(node, treeFilter)} - selectedNodeFullName={this.selectedNodeKey} - onNodeClick={fullName => this.selectNode(fullName)} - nodeMenu={node => - AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () => - this.getApplicationActionMenu(application, false) - ) - } - showCompactNodes={pref.groupNodes} - userMsgs={pref.userHelpTipMsgs} - tree={tree} - app={application} - showOrphanedResources={pref.orphanedResources} - useNetworkingHierarchy={pref.view === 'network'} - onClearFilter={clearFilter} - onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)} - zoom={pref.zoom} - podGroupCount={pref.podGroupCount} - appContext={this.appContext} - nameDirection={this.state.truncateNameOnRight} - filters={pref.resourceFilter} - setTreeFilterGraph={setFilterGraph} - updateUsrHelpTipMsgs={updateHelpTipState} - setShowCompactNodes={setShowCompactNodes} - setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)} - getNodeExpansion={node => this.getNodeExpansion(node)} - /> - - )) || - (pref.view === 'pods' && ( - this.selectNode(fullName)} - nodeMenu={node => - AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () => - this.getApplicationActionMenu(application, false) - ) - } - quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)} - /> - )) || - (this.state.extensionsMap[pref.view] != null && ( - - )) || ( -
+
+
+ this.selectNode(appFullName, 0, 'diff')} + showOperation={() => this.setOperationStatusVisible(true)} + showConditions={() => this.setConditionsStatusVisible(true)} + showExtension={id => this.setExtensionPanelVisible(id)} + showMetadataInfo={revision => this.setState({...this.state, revision})} + /> +
+
+ {refreshing &&

Refreshing

} + {((pref.view === 'tree' || pref.view === 'network') && ( + <> services.viewPreferences.getPreferences()}> {viewPref => ( )} - {(filteredRes.length > 0 && ( - this.setState({page})} - preferencesKey='application-details'> - {data => ( - this.selectNode(fullName)} - resources={data} - nodeMenu={node => - AppUtils.renderResourceMenu( - {...node, root: node}, - application, - tree, - this.appContext.apis, - this.appChanged, - () => this.getApplicationActionMenu(application, false) - ) - } + + this.filterTreeNode(node, treeFilter)} + selectedNodeFullName={this.selectedNodeKey} + onNodeClick={fullName => this.selectNode(fullName)} + nodeMenu={node => + AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () => + this.getApplicationActionMenu(application, false) + ) + } + showCompactNodes={pref.groupNodes} + userMsgs={pref.userHelpTipMsgs} + tree={tree} + app={application} + showOrphanedResources={pref.orphanedResources} + useNetworkingHierarchy={pref.view === 'network'} + onClearFilter={clearFilter} + onGroupdNodeClick={groupdedNodeIds => openGroupNodeDetails(groupdedNodeIds)} + zoom={pref.zoom} + podGroupCount={pref.podGroupCount} + appContext={this.appContext} + nameDirection={this.state.truncateNameOnRight} + filters={pref.resourceFilter} + setTreeFilterGraph={setFilterGraph} + updateUsrHelpTipMsgs={updateHelpTipState} + setShowCompactNodes={setShowCompactNodes} + setNodeExpansion={(node, isExpanded) => this.setNodeExpansion(node, isExpanded)} + getNodeExpansion={node => this.getNodeExpansion(node)} + /> + + )) || + (pref.view === 'pods' && ( + this.selectNode(fullName)} + nodeMenu={node => + AppUtils.renderResourceMenu(node, application, tree, this.appContext.apis, this.appChanged, () => + this.getApplicationActionMenu(application, false) + ) + } + quickStarts={node => AppUtils.renderResourceButtons(node, application, tree, this.appContext.apis, this.appChanged)} + /> + )) || + (this.state.extensionsMap[pref.view] != null && ( + + )) || ( +
+ services.viewPreferences.getPreferences()}> + {viewPref => ( + )} - - )) || ( - -

No resources found

-
Try to change filter criteria
-
- )} -
- )} + + {(filteredRes.length > 0 && ( + this.setState({page})} + preferencesKey='application-details'> + {data => ( + this.selectNode(fullName)} + resources={data} + nodeMenu={node => + AppUtils.renderResourceMenu( + {...node, root: node}, + application, + tree, + this.appContext.apis, + this.appChanged, + () => this.getApplicationActionMenu(application, false) + ) + } + tree={tree} + /> + )} + + )) || ( + +

No resources found

+
Try to change filter criteria
+
+ )} +
+ )} +
0} onClose={() => this.closeGroupedNodesPanel()}>
@@ -730,6 +749,13 @@ export class ApplicationDetails extends React.Component ))} + this.setExtensionPanelVisible('')}> + {this.selectedExtension !== '' && activeExtension && activeExtension.flyout && ( + + )} +
); @@ -748,12 +774,12 @@ export class ApplicationDetails extends React.Component, + title: , action: () => this.selectNode(fullName) }, { iconClassName: 'fa fa-file-medical', - title: , + title: , action: () => this.selectNode(fullName, 0, 'diff'), disabled: app.status.sync.status === appModels.SyncStatuses.Synced }, @@ -964,6 +990,10 @@ export class ApplicationDetails extends React.Component 0 && (getResNode(tree.nodes, nodeKey(resources[0])) as ResourceNode)?.parentRefs?.[0]) || ({} as ResourceRef); + const searchParams = new URLSearchParams(window.location.search); + const view = searchParams.get('view'); + const ParentRefDetails = () => { + return Object.keys(parentNode).length > 0 ? ( +
+
Parent Node Info
+
+
Name:
+
{parentNode?.name}
+
+
+
Kind:
+
{parentNode?.kind}
+
+
+ ) : ( +
+ ); + }; return (
-
- {Object.keys(parentNode).length > 0 && ( -
-
Parent Node Info
-
-
Name:
-
{parentNode?.name}
-
-
-
Kind:
-
{parentNode?.kind}
-
-
- )} -
+ {/* Display only when the view is set to or network */} + {(view === 'tree' || view === 'network') && ( +
+ +
+ )}
-
NAME
+
NAME
GROUP/KIND
SYNC ORDER
-
NAMESPACE
+
NAMESPACE
{(parentNode.kind === 'Rollout' || parentNode.kind === 'Deployment') &&
REVISION
} -
CREATED AT
-
STATUS
+
CREATED AT
+
STATUS
{resources @@ -79,8 +90,8 @@ export const ApplicationResourceList = ({
{ResourceLabel({kind: res.kind})}
-
- {res.name} +
+ {res.name} {res.kind === 'Application' && ( {ctx => ( @@ -98,7 +109,7 @@ export const ApplicationResourceList = ({
{[res.group, res.kind].filter(item => !!item).join('/')}
{res.syncWave || '-'}
-
{res.namespace}
+
{res.namespace}
{res.kind === 'ReplicaSet' && ((getResNode(tree.nodes, nodeKey(res)) as ResourceNode).info || []) .filter(tag => !tag.name.includes('Node')) @@ -111,7 +122,7 @@ export const ApplicationResourceList = ({ ); })} -
+
{res.createdAt && ( @@ -121,7 +132,7 @@ export const ApplicationResourceList = ({ )}
-
+
{res.health && ( {res.health.status}   diff --git a/ui/src/app/applications/components/application-node-info/application-node-info.scss b/ui/src/app/applications/components/application-node-info/application-node-info.scss index f50e67279cc525..27ab11d776c17f 100644 --- a/ui/src/app/applications/components/application-node-info/application-node-info.scss +++ b/ui/src/app/applications/components/application-node-info/application-node-info.scss @@ -1,4 +1,5 @@ @import 'node_modules/argo-ui/src/styles/config'; +@import 'node_modules/argo-ui/src/styles/theme'; .application-node-info { &__manifest { @@ -6,6 +7,9 @@ .tabs__content { background-color: white; + @include themify($themes){ + background-color: themed('background-2'); + } } &--raw { @@ -37,6 +41,9 @@ label { padding-right: 2em; color: $argo-color-gray-8; + @include themify($themes){ + color: themed('text-2'); + } } } &__err_msg { diff --git a/ui/src/app/applications/components/application-node-info/application-node-info.tsx b/ui/src/app/applications/components/application-node-info/application-node-info.tsx index 18ff44e381c557..edd787e0240c19 100644 --- a/ui/src/app/applications/components/application-node-info/application-node-info.tsx +++ b/ui/src/app/applications/components/application-node-info/application-node-info.tsx @@ -21,7 +21,20 @@ const RenderContainerState = (props: {container: any}) => { return (
-
{props.container.name}
+
+ {props.container.state?.running && ( + + + + )} + {(props.container.state.terminated && props.container.state.terminated?.exitCode !== 0) || + (lastState && lastState?.exitCode !== 0 && ( + + + + ))} + {props.container.name} +
{state && ( <> diff --git a/ui/src/app/applications/components/application-parameters/application-parameters.tsx b/ui/src/app/applications/components/application-parameters/application-parameters.tsx index 27f292ff7d2e85..38a6d151a90c29 100644 --- a/ui/src/app/applications/components/application-parameters/application-parameters.tsx +++ b/ui/src/app/applications/components/application-parameters/application-parameters.tsx @@ -286,7 +286,7 @@ export const ApplicationParameters = (props: { } else if (props.details.type === 'Plugin') { attributes.push({ title: 'NAME', - view:
{ValueEditor(app.spec.source.plugin && app.spec.source.plugin.name, null)}
, + view:
{ValueEditor(app.spec.source?.plugin?.name, null)}
, edit: (formApi: FormApi) => ( services.authService.plugins()}> {(plugins: Plugin[]) => ( @@ -299,12 +299,11 @@ export const ApplicationParameters = (props: { title: 'ENV', view: (
- {app.spec.source.plugin && - (app.spec.source.plugin.env || []).map(val => ( - - {NameValueEditor(val, null)} - - ))} + {(app.spec.source?.plugin?.env || []).map(val => ( + + {NameValueEditor(val, null)} + + ))}
), edit: (formApi: FormApi) => @@ -315,7 +314,7 @@ export const ApplicationParameters = (props: { parametersSet.add(announcement.name); } } - if (app.spec.source.plugin?.parameters) { + if (app.spec.source?.plugin?.parameters) { for (const appParameter of app.spec.source.plugin.parameters) { parametersSet.add(appParameter.name); } @@ -326,7 +325,7 @@ export const ApplicationParameters = (props: { } parametersSet.forEach(name => { const announcement = props.details.plugin.parametersAnnouncement?.find(param => param.name === name); - const liveParam = app.spec.source.plugin?.parameters?.find(param => param.name === name); + const liveParam = app.spec.source?.plugin?.parameters?.find(param => param.name === name); const pluginIcon = announcement && liveParam ? 'This parameter has been provided by plugin, but is overridden in application manifest.' : 'This parameter is provided by the plugin.'; const isPluginPar = !!announcement; diff --git a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx index 6f28a40ea50464..3d5b1782a0e0cb 100644 --- a/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx +++ b/ui/src/app/applications/components/application-resource-tree/application-resource-tree.tsx @@ -564,11 +564,13 @@ function renderPodGroup(props: ApplicationResourceTreeProps, id: string, node: R
{[podGroupHealthy, podGroupDegraded, podGroupInProgress].map((pods, index) => { - return ( -
- {renderPodGroupByStatus(props, node, pods, showPodGroupByStatus)} -
- ); + if (pods.length > 0) { + return ( +
+ {renderPodGroupByStatus(props, node, pods, showPodGroupByStatus)} +
+ ); + } })}
diff --git a/ui/src/app/applications/components/application-resources-diff/application-resources-diff.scss b/ui/src/app/applications/components/application-resources-diff/application-resources-diff.scss index fbf23c95796bf5..fb139f273a24c8 100644 --- a/ui/src/app/applications/components/application-resources-diff/application-resources-diff.scss +++ b/ui/src/app/applications/components/application-resources-diff/application-resources-diff.scss @@ -7,6 +7,9 @@ label { padding-right: 2em; color: $argo-color-gray-8; + @include themify($themes){ + color: themed('text-2'); + } } } &__diff { @@ -28,4 +31,8 @@ .custom-diff-hunk { color: $argo-color-gray-6; + border-bottom: 1px dashed; + @include themify($themes){ + border-bottom: 1px dashed themed('text-2'); + } } \ No newline at end of file diff --git a/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx b/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx index 0e4104f3f12b35..7c2b65cd3ce279 100644 --- a/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx +++ b/ui/src/app/applications/components/application-status-panel/application-status-panel.tsx @@ -16,6 +16,7 @@ interface Props { showDiff?: () => any; showOperation?: () => any; showConditions?: () => any; + showExtension?: (id: string) => any; showMetadataInfo?: (revision: string) => any; } @@ -45,7 +46,7 @@ const sectionHeader = (info: SectionInfo, hasMultipleSources: boolean, onClick?: ); }; -export const ApplicationStatusPanel = ({application, showDiff, showOperation, showConditions, showMetadataInfo}: Props) => { +export const ApplicationStatusPanel = ({application, showDiff, showOperation, showConditions, showExtension, showMetadataInfo}: Props) => { const today = new Date(); let daysSinceLastSynchronized = 0; @@ -63,6 +64,8 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh showOperation = null; } + const statusExtensions = services.extensions.getStatusPanelExtensions(); + const infos = cntByCategory.get('info'); const warnings = cntByCategory.get('warning'); const errors = cntByCategory.get('error'); @@ -203,6 +206,7 @@ export const ApplicationStatusPanel = ({application, showDiff, showOperation, sh
)} + {statusExtensions && statusExtensions.map(ext => showExtension && showExtension(ext.id)} />)}
); }; diff --git a/ui/src/app/applications/components/application-summary/application-summary.tsx b/ui/src/app/applications/components/application-summary/application-summary.tsx index 5e8fa2db22ba1e..63bab3be0364c7 100644 --- a/ui/src/app/applications/components/application-summary/application-summary.tsx +++ b/ui/src/app/applications/components/application-summary/application-summary.tsx @@ -15,7 +15,7 @@ import { RevisionHelpIcon } from '../../../shared/components'; import {BadgePanel, Spinner} from '../../../shared/components'; -import {Consumer, ContextApis} from '../../../shared/context'; +import {AuthSettingsCtx, Consumer, ContextApis} from '../../../shared/context'; import * as models from '../../../shared/models'; import {services} from '../../../shared/services'; @@ -37,6 +37,16 @@ function swap(array: any[], a: number, b: number) { return array; } +function processPath(path: string) { + if (path !== null && path !== undefined) { + if (path === '.') { + return '(root)'; + } + return path; + } + return ''; +} + export interface ApplicationSummaryProps { app: models.Application; updateApp: (app: models.Application, query: {validate?: boolean}) => Promise; @@ -47,6 +57,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => { const source = getAppDefaultSource(app); const isHelm = source.hasOwnProperty('chart'); const initialState = app.spec.destination.server === undefined ? 'NAME' : 'URL'; + const useAuthSettingsCtx = React.useContext(AuthSettingsCtx); const [destFormat, setDestFormat] = React.useState(initialState); const [changeSync, setChangeSync] = React.useState(false); @@ -238,7 +249,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => { title: 'PATH', view: ( - {source.path ?? ''} + {processPath(source.path)} ), edit: (formApi: FormApi) => @@ -255,7 +266,12 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => { view: app.spec.revisionHistoryLimit, edit: (formApi: FormApi) => (
- +
{ setPending(true); - let resources = appResources.filter((_, i) => params.resources[i]); - if (resources.length === appResources.length) { - resources = null; + let selectedResources = appResources.filter((_, i) => params.resources[i]); + const allResourcesAreSelected = selectedResources.length === appResources.length; + const syncFlags = {...params.syncFlags} as SyncFlags; + + const allRequirePruning = !selectedResources.some(resource => !resource?.requiresPruning); + if (syncFlags.Prune && allRequirePruning && allResourcesAreSelected) { + const confirmed = await ctx.popup.confirm('Prune all resources?', () => ( +
+ + {PRUNE_ALL_WARNING} Are you sure you want to continue? +
+ )); + if (!confirmed) { + setPending(false); + return; + } + } + if (allResourcesAreSelected) { + selectedResources = null; } const replace = params.syncOptions?.findIndex((opt: string) => opt === 'Replace=true') > -1; if (replace) { @@ -74,7 +97,6 @@ export const ApplicationSyncPanel = ({application, selectedResource, hide}: {app } } - const syncFlags = {...params.syncFlags} as SyncFlags; const force = syncFlags.Force || false; if (syncFlags.ApplyOnly) { @@ -102,7 +124,7 @@ export const ApplicationSyncPanel = ({application, selectedResource, hide}: {app syncFlags.Prune || false, syncFlags.DryRun || false, syncStrategy, - resources, + selectedResources, params.syncOptions, params.retryStrategy ); diff --git a/ui/src/app/applications/components/applications-list/applications-tiles.scss b/ui/src/app/applications/components/applications-list/applications-tiles.scss index 2e63152d532014..a4c18567652e23 100644 --- a/ui/src/app/applications/components/applications-list/applications-tiles.scss +++ b/ui/src/app/applications/components/applications-list/applications-tiles.scss @@ -3,7 +3,7 @@ .applications-tiles { display: grid; gap: 24px; - grid-template-columns: repeat(auto-fill,minmax(380px,1fr)); + grid-template-columns: repeat(auto-fill,minmax(370px,1fr)); padding: 0 12px; &__wrapper { diff --git a/ui/src/app/applications/components/applications-list/applications-tiles.tsx b/ui/src/app/applications/components/applications-list/applications-tiles.tsx index b69d4e45403489..3467d3b952a878 100644 --- a/ui/src/app/applications/components/applications-list/applications-tiles.tsx +++ b/ui/src/app/applications/components/applications-list/applications-tiles.tsx @@ -105,9 +105,7 @@ export const ApplicationTiles = ({applications, syncApplication, refreshApplicat {pref => { const favList = pref.appList.favoritesAppList || []; return ( -
+
{applications.map((app, i) => { const source = getAppDefaultSource(app); return ( diff --git a/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.scss b/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.scss index 44e350f3251373..9530d82dd11462 100644 --- a/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.scss +++ b/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.scss @@ -1,12 +1,19 @@ @import 'node_modules/argo-ui/src/styles/config'; +@import 'node_modules/argo-ui/src/styles/theme'; .pod-logs-viewer { height: 90%; font-size: 14px; font-family: monospace; background-color: white; + @include themify($themes){ + background-color: themed('background-2'); + } padding: 0; color: black; + @include themify($themes){ + color: themed('text-2'); + } &--inverted { background-color: black; diff --git a/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.tsx b/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.tsx index 1ef2d83815821c..309287fab2f371 100644 --- a/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.tsx +++ b/ui/src/app/applications/components/pod-logs-viewer/pod-logs-viewer.tsx @@ -149,9 +149,9 @@ export const PodsLogsViewer = (props: PodLogsProps) => { const logsContent = (width: number, height: number, isWrapped: boolean) => (
{logs.map((log, lineNum) => ( -
+                
{renderLog(log, lineNum)} -
+
))}
); diff --git a/ui/src/app/applications/components/resource-details/resource-details.tsx b/ui/src/app/applications/components/resource-details/resource-details.tsx index 6477509370905f..52d2fef1847031 100644 --- a/ui/src/app/applications/components/resource-details/resource-details.tsx +++ b/ui/src/app/applications/components/resource-details/resource-details.tsx @@ -280,7 +280,7 @@ export const ResourceDetails = (props: ResourceDetailsProps) => { const settings = await services.authService.settings(); const execEnabled = settings.execEnabled; const logsAllowed = await services.accounts.canI('logs', 'get', application.spec.project + '/' + application.metadata.name); - const execAllowed = await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name); + const execAllowed = execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name)); const links = await services.applications.getResourceLinks(application.metadata.name, application.metadata.namespace, selectedNode).catch(() => null); return {controlledState, liveState, events, podState, execEnabled, execAllowed, logsAllowed, links}; }}> diff --git a/ui/src/app/applications/components/utils.tsx b/ui/src/app/applications/components/utils.tsx index 674ffc6728db46..cd39470bfb25b2 100644 --- a/ui/src/app/applications/components/utils.tsx +++ b/ui/src/app/applications/components/utils.tsx @@ -361,7 +361,7 @@ export const deletePopup = async (ctx: ContextApis, resource: ResourceTreeNode, handleStateChange('force')} style={{marginRight: '5px'}} id='force-delete-radio' /> handleStateChange('orphan')} style={{marginRight: '5px'}} id='cascade-delete-radio' /> @@ -473,8 +473,8 @@ function getActionItems( const execAction = services.authService .settings() .then(async settings => { - const execAllowed = await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name); - if (resource.kind === 'Pod' && settings.execEnabled && execAllowed) { + const execAllowed = settings.execEnabled && (await services.accounts.canI('exec', 'create', application.spec.project + '/' + application.metadata.name)); + if (resource.kind === 'Pod' && execAllowed) { return [ { title: 'Exec', diff --git a/ui/src/app/login/components/utils.ts b/ui/src/app/login/components/utils.ts index 90453ced77d4af..6c715077cc9ccb 100644 --- a/ui/src/app/login/components/utils.ts +++ b/ui/src/app/login/components/utils.ts @@ -74,10 +74,6 @@ export const pkceLogin = async (oidcConfig: AuthSettings['oidcConfig'], redirect throw new PKCELoginError('No Authorization Server endpoint found'); } - if (!authorizationServer?.code_challenge_methods_supported?.includes('S256')) { - throw new PKCELoginError('Authorization Server does not support S256 code challenge method'); - } - const codeVerifier = generateRandomCodeVerifier(); const codeChallange = await calculatePKCECodeChallenge(codeVerifier); diff --git a/ui/src/app/settings/components/project-details/project-details.tsx b/ui/src/app/settings/components/project-details/project-details.tsx index 224c2e1e45e123..8b00c8590edb75 100644 --- a/ui/src/app/settings/components/project-details/project-details.tsx +++ b/ui/src/app/settings/components/project-details/project-details.tsx @@ -6,7 +6,7 @@ import {FormApi, Text} from 'react-form'; import {RouteComponentProps} from 'react-router'; import {BadgePanel, CheckboxField, DataLoader, EditablePanel, ErrorNotification, MapInputField, Page, Query} from '../../../shared/components'; -import {AppContext, Consumer} from '../../../shared/context'; +import {AppContext, Consumer, AuthSettingsCtx} from '../../../shared/context'; import {GroupKind, Groups, Project, DetailedProjectsResponse, ProjectSpec, ResourceKinds} from '../../../shared/models'; import {CreateJWTTokenParams, DeleteJWTTokenParams, ProjectRoleParams, services} from '../../../shared/services'; @@ -52,6 +52,7 @@ function reduceGlobal(projs: Project[]): ProjectSpec & {count: number} { merged.namespaceResourceWhitelist = merged.namespaceResourceWhitelist.concat(proj.spec.namespaceResourceWhitelist || []); merged.sourceRepos = merged.sourceRepos.concat(proj.spec.sourceRepos || []); merged.destinations = merged.destinations.concat(proj.spec.destinations || []); + merged.sourceNamespaces = merged.sourceNamespaces.concat(proj.spec.sourceNamespaces || []); merged.sourceRepos = merged.sourceRepos.filter((item, index) => { return ( @@ -106,6 +107,15 @@ function reduceGlobal(projs: Project[]): ProjectSpec & {count: number} { }) ); }); + + merged.sourceNamespaces = merged.sourceNamespaces.filter((item, index) => { + return ( + index === + merged.sourceNamespaces.findIndex(obj => { + return obj === item; + }) + ); + }); merged.count += 1; return merged; @@ -116,6 +126,7 @@ function reduceGlobal(projs: Project[]): ProjectSpec & {count: number} { namespaceResourceWhitelist: new Array(), clusterResourceWhitelist: new Array(), sourceRepos: [], + sourceNamespaces: [], signatureKeys: [], destinations: [], description: '', @@ -648,7 +659,51 @@ export class ProjectDetails extends React.Component - + + {authCtx => + authCtx.appsInAnyNamespaceEnabled && ( + this.saveProject(item)} + values={proj} + title={ + SOURCE NAMESPACES {helpTip('Kubernetes namespaces where application resources are allowed to be created in')} + } + view={ + + {proj.spec.sourceNamespaces + ? proj.spec.sourceNamespaces.map((namespace, i) => ( +
+
{namespace}
+
+ )) + : emptyMessage('source namespaces')} +
+ } + edit={formApi => ( + + {(formApi.values.spec.sourceNamespaces || []).map((_: Project, i: number) => ( +
+
+ + formApi.setValue('spec.sourceNamespaces', removeEl(formApi.values.spec.sourceNamespaces, i))} + /> +
+
+ ))} + +
+ )} + items={[]} + /> + ) + } +
this.saveProject(item)} values={proj} diff --git a/ui/src/app/settings/components/project-details/resource-lists-panel.tsx b/ui/src/app/settings/components/project-details/resource-lists-panel.tsx index 2e5c1d1fedd723..ec9e617ac9122d 100644 --- a/ui/src/app/settings/components/project-details/resource-lists-panel.tsx +++ b/ui/src/app/settings/components/project-details/resource-lists-panel.tsx @@ -99,6 +99,36 @@ function viewSourceReposInfoList(type: field, proj: Project) { ); } +const sourceNamespacesInfoByField: {[type: string]: {title: string; helpText: string}} = { + sourceNamespaces: { + title: 'source namespaces', + helpText: 'Kubernetes namespaces where application resources are allowed to be created in' + } +}; + +function viewSourceNamespacesInfoList(type: field, proj: Project) { + const info = sourceNamespacesInfoByField[type]; + const list = proj.spec[type] as Array; + return ( + +

+ {info.title} {helpTip(info.helpText)} +

+ {(list || []).length > 0 ? ( + + {list.map((namespace, i) => ( +
+
{namespace}
+
+ ))} +
+ ) : ( +

The {info.title} is empty

+ )} +
+ ); +} + const destinationsInfoByField: {[type: string]: {title: string; helpText: string}} = { destinations: { title: 'destinations', @@ -180,6 +210,8 @@ export const ResourceListsPanel = ({proj, saveProject, title}: {proj: Project; t {viewList(key as field, proj)} ))} {!proj.metadata && Object.keys(sourceReposInfoByField).map(key => {viewSourceReposInfoList(key as field, proj)})} + {!proj.metadata && + Object.keys(sourceNamespacesInfoByField).map(key => {viewSourceNamespacesInfoList(key as field, proj)})} {!proj.metadata && Object.keys(destinationsInfoByField).map(key => {viewDestinationsInfoList(key as field, proj)})} } diff --git a/ui/src/app/settings/components/repo-details/repo-details.tsx b/ui/src/app/settings/components/repo-details/repo-details.tsx index 6075dfbde3b227..25fa9831727004 100644 --- a/ui/src/app/settings/components/repo-details/repo-details.tsx +++ b/ui/src/app/settings/components/repo-details/repo-details.tsx @@ -65,7 +65,8 @@ export const RepoDetails = (props: {repo: models.Repository; save?: (params: New enableLfs: repo.enableLfs || false, proxy: repo.proxy || '', project: repo.project || '', - enableOCI: repo.enableOCI || false + enableOCI: repo.enableOCI || false, + forceHttpBasicAuth: repo.forceHttpBasicAuth || false }; return ( diff --git a/ui/src/app/shared/components/badge-panel/badge-panel.tsx b/ui/src/app/shared/components/badge-panel/badge-panel.tsx index ad6a4f6c187e29..d6ab8fa95ffd23 100644 --- a/ui/src/app/shared/components/badge-panel/badge-panel.tsx +++ b/ui/src/app/shared/components/badge-panel/badge-panel.tsx @@ -6,7 +6,7 @@ import {Context} from '../../context'; require('./badge-panel.scss'); -export const BadgePanel = ({app, project}: {app?: string; project?: string}) => { +export const BadgePanel = ({app, project, appNamespace, nsEnabled}: {app?: string; project?: string; appNamespace?: string; nsEnabled?: boolean}) => { const [badgeType, setBadgeType] = React.useState('URL'); const context = React.useContext(Context); if (!app && !project) { @@ -19,7 +19,10 @@ export const BadgePanel = ({app, project}: {app?: string; project?: string}) => let entityURL = ''; let alt = ''; if (app) { - badgeURL = `${root}api/badge?name=${app}&revision=true`; + badgeURL = `${root}api/badge?name=${app}&revision=true&showAppName=true`; + if (nsEnabled) { + badgeURL += `&namespace=${appNamespace}`; + } entityURL = `${root}applications/${app}`; alt = 'App Status'; } else if (project) { diff --git a/ui/src/app/shared/components/layout/layout.tsx b/ui/src/app/shared/components/layout/layout.tsx index dcf98dde565ebd..096fdde68e99b0 100644 --- a/ui/src/app/shared/components/layout/layout.tsx +++ b/ui/src/app/shared/components/layout/layout.tsx @@ -14,14 +14,21 @@ export interface LayoutProps { const getBGColor = (theme: string): string => (theme === 'light' ? '#dee6eb' : '#100f0f'); -export const Layout = (props: LayoutProps) => ( -
-
- - {props.pref.theme ? (document.body.style.background = getBGColor(props.pref.theme)) : null} -
- {props.children} +export const Layout = (props: LayoutProps) => { + React.useEffect(() => { + if (props.pref.theme) { + document.body.style.background = getBGColor(props.pref.theme); + } + }, [props.pref.theme]); + + return ( +
+
+ +
+ {props.children} +
-
-); + ); +}; diff --git a/ui/src/app/shared/components/monaco-editor.tsx b/ui/src/app/shared/components/monaco-editor.tsx index 817e4c8ae6bcbb..a30381638f0b5c 100644 --- a/ui/src/app/shared/components/monaco-editor.tsx +++ b/ui/src/app/shared/components/monaco-editor.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import * as monacoEditor from 'monaco-editor'; +import {services} from '../services'; export interface EditorInput { text: string; @@ -28,6 +29,16 @@ const MonacoEditorLazy = React.lazy(() => const Component = (props: MonacoProps) => { const [height, setHeight] = React.useState(0); + React.useEffect(() => { + const subscription = services.viewPreferences.getPreferences().subscribe(preferences => { + monaco.editor.setTheme(preferences.theme === 'dark' ? 'vs-dark' : 'vs'); + }); + + return () => { + subscription.unsubscribe(); + }; + }, []); + return (
(), systemLevelExtensions: new Array(), - appViewExtensions: new Array() + appViewExtensions: new Array(), + statusPanelExtensions: new Array() }; function registerResourceExtension(component: ExtensionComponent, group: string, kind: string, tabTitle: string, opts?: {icon: string}) { @@ -21,6 +22,10 @@ function registerAppViewExtension(component: ExtensionComponent, title: string, extensions.appViewExtensions.push({component, title, icon}); } +function registerStatusPanelExtension(component: StatusPanelExtensionComponent, title: string, id: string, flyout?: ExtensionComponent) { + extensions.statusPanelExtensions.push({component, flyout, title, id}); +} + let legacyInitialized = false; function initLegacyExtensions() { @@ -56,9 +61,18 @@ export interface AppViewExtension { icon?: string; } +export interface StatusPanelExtension { + component: StatusPanelExtensionComponent; + flyout?: StatusPanelExtensionFlyoutComponent; + title: string; + id: string; +} + export type ExtensionComponent = React.ComponentType; export type SystemExtensionComponent = React.ComponentType; export type AppViewExtensionComponent = React.ComponentType; +export type StatusPanelExtensionComponent = React.ComponentType; +export type StatusPanelExtensionFlyoutComponent = React.ComponentType; export interface Extension { component: ExtensionComponent; @@ -75,6 +89,16 @@ export interface AppViewComponentProps { tree: ApplicationTree; } +export interface StatusPanelComponentProps { + application: Application; + openFlyout: () => any; +} + +export interface StatusPanelFlyoutProps { + application: Application; + tree: ApplicationTree; +} + export class ExtensionsService { public getResourceTabs(group: string, kind: string): ResourceTabExtension[] { initLegacyExtensions(); @@ -89,6 +113,10 @@ export class ExtensionsService { public getAppViewExtensions(): AppViewExtension[] { return extensions.appViewExtensions.slice(); } + + public getStatusPanelExtensions(): StatusPanelExtension[] { + return extensions.statusPanelExtensions.slice(); + } } ((window: any) => { @@ -97,6 +125,7 @@ export class ExtensionsService { window.extensionsAPI = { registerResourceExtension, registerSystemLevelExtension, - registerAppViewExtension + registerAppViewExtension, + registerStatusPanelExtension }; })(window); diff --git a/ui/src/app/shared/services/repo-service.ts b/ui/src/app/shared/services/repo-service.ts index 09f8c169ac9ae0..94378bee8352bb 100644 --- a/ui/src/app/shared/services/repo-service.ts +++ b/ui/src/app/shared/services/repo-service.ts @@ -62,7 +62,9 @@ export class RepositoriesService { insecure, enableLfs, proxy, - project + project, + forceHttpBasicAuth, + enableOCI }: { type: string; name: string; @@ -75,10 +77,12 @@ export class RepositoriesService { enableLfs: boolean; proxy: string; project?: string; + forceHttpBasicAuth?: boolean; + enableOCI: boolean; }): Promise { return requests .put(`/repositories/${encodeURIComponent(url)}`) - .send({type, name, repo: url, username, password, tlsClientCertData, tlsClientCertKey, insecure, enableLfs, proxy, project}) + .send({type, name, repo: url, username, password, tlsClientCertData, tlsClientCertKey, insecure, enableLfs, proxy, project, forceHttpBasicAuth, enableOCI}) .then(res => res.body as models.Repository); } diff --git a/ui/src/app/shared/services/requests.ts b/ui/src/app/shared/services/requests.ts index 207917a318529e..4df6d1e4ddf199 100644 --- a/ui/src/app/shared/services/requests.ts +++ b/ui/src/app/shared/services/requests.ts @@ -51,19 +51,19 @@ export default { }, post(url: string) { - return initHandlers(agent.post(`${apiRoot()}${url}`)); + return initHandlers(agent.post(`${apiRoot()}${url}`)).set('Content-Type', 'application/json'); }, put(url: string) { - return initHandlers(agent.put(`${apiRoot()}${url}`)); + return initHandlers(agent.put(`${apiRoot()}${url}`)).set('Content-Type', 'application/json'); }, patch(url: string) { - return initHandlers(agent.patch(`${apiRoot()}${url}`)); + return initHandlers(agent.patch(`${apiRoot()}${url}`)).set('Content-Type', 'application/json'); }, delete(url: string) { - return initHandlers(agent.del(`${apiRoot()}${url}`)); + return initHandlers(agent.del(`${apiRoot()}${url}`)).set('Content-Type', 'application/json'); }, loadEventSource(url: string): Observable { diff --git a/ui/src/app/sidebar/sidebar.scss b/ui/src/app/sidebar/sidebar.scss index a3ff7a0355c281..d41cbeed3f7cfb 100644 --- a/ui/src/app/sidebar/sidebar.scss +++ b/ui/src/app/sidebar/sidebar.scss @@ -58,6 +58,13 @@ $deselected-text: #818d94; text-overflow: ellipsis; } + &__tooltip { + max-width: 300px; + @media screen and (max-width: 590px) { + max-width: 250px; + } + } + &__nav-item { cursor: pointer; display: flex; @@ -81,6 +88,7 @@ $deselected-text: #818d94; margin-left: 2px; margin-right: -2px; margin-top: 12px; + width: 32px; } &--active { diff --git a/ui/src/app/sidebar/sidebar.tsx b/ui/src/app/sidebar/sidebar.tsx index c690565d01cb52..1aeb77c9112ee0 100644 --- a/ui/src/app/sidebar/sidebar.tsx +++ b/ui/src/app/sidebar/sidebar.tsx @@ -64,7 +64,7 @@ export const Sidebar = (props: SidebarProps) => {
{(props.navItems || []).map(item => ( - + {item?.tooltip || item.title}
} {...tooltipProps}>
) => { chatUrl = 'invalid-url'; } } - const shouldRenderTop = position === 'top' || position === 'both'; + const shouldRenderTop = position === 'top' || position === 'both' || (!position && content); const shouldRenderBottom = position === 'bottom' || position === 'both'; return ( diff --git a/ui/yarn.lock b/ui/yarn.lock index 369ef20d7790d5..4e6dee439d56fb 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1903,12 +1903,7 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== -"@types/node@*": - version "16.3.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.3.1.tgz#24691fa2b0c3ec8c0d34bfcfd495edac5593ebb4" - integrity sha512-N87VuQi7HEeRJkhzovao/JviiqKjDKMVKxKMfUvSKw+MbkbW8R0nA3fi/MQhhlxV2fQ+2ReM+/Nt4efdrJx3zA== - -"@types/node@20.6.3": +"@types/node@*", "@types/node@20.6.3": version "20.6.3" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.6.3.tgz#5b763b321cd3b80f6b8dde7a37e1a77ff9358dd9" integrity sha512-HksnYH4Ljr4VQgEy2lTStbCKv/P590tmPe5HqOnv9Gprffgv5WXAY+Y5Gqniu0GGqeTCUdBnzC3QSrzPkBkAMA== @@ -1966,10 +1961,10 @@ dependencies: "@types/react" "*" -"@types/react-helmet@^5.0.17": - version "5.0.19" - resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-5.0.19.tgz#e709f192eac0b873693a6f831bb2dbc7085a5c19" - integrity sha512-Ub4sOSkg/64UYRJG33kYLzMV2OsgD923DXS09K3Pp2Qsq0AId5Ih59NvX2eXDIUQPfxO3rj0rFeVxurN8IUksQ== +"@types/react-helmet@^6.1.6": + version "6.1.6" + resolved "https://registry.yarnpkg.com/@types/react-helmet/-/react-helmet-6.1.6.tgz#7d1afd8cbf099616894e8240e9ef70e3c6d7506d" + integrity sha512-ZKcoOdW/Tg+kiUbkFCBtvDw0k3nD4HJ/h/B9yWxN4uDO8OkRksWTO+EL+z/Qu3aHTeTll3Ro0Cc/8UhwBCMG5A== dependencies: "@types/react" "*" @@ -2058,10 +2053,10 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/superagent@^4.1.15": - version "4.1.15" - resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" - integrity sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ== +"@types/superagent@^4.1.21": + version "4.1.21" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.21.tgz#78e2c2d6894c5f8ece228f0df4912906133d97c3" + integrity sha512-yrbAccEEY9+BSa1wji3ry8R3/BdW9kyWnjkRKctrtw5ebn/k2a2CsMeaQ7dD4iLfomgHkomBVIVgOFRMV4XYHA== dependencies: "@types/cookiejar" "*" "@types/node" "*" @@ -2513,7 +2508,7 @@ arg@^4.1.0: "argo-ui@git+https://github.com/argoproj/argo-ui.git": version "1.0.0" - resolved "git+https://github.com/argoproj/argo-ui.git#002d01b18e8aaf4b21307a3b87341ab05230483f" + resolved "git+https://github.com/argoproj/argo-ui.git#5ff344ac9692c14dd108468bd3c020c3c75181cb" dependencies: "@fortawesome/fontawesome-free" "^6.2.1" "@tippy.js/react" "^3.1.1" @@ -3206,11 +3201,6 @@ colorette@^1.2.0: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.4.0.tgz#5190fbb87276259a86ad700bff2c6d6faa3fca40" integrity sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g== -colorette@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" - integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== - colorette@^2.0.10, colorette@^2.0.14: version "2.0.16" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" @@ -4525,9 +4515,9 @@ find-up@^4.0.0, find-up@^4.1.0: path-exists "^4.0.0" follow-redirects@^1.0.0: - version "1.14.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" - integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + version "1.15.5" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== for-in@^1.0.2: version "1.0.2" @@ -6502,10 +6492,10 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -nanoid@^3.1.22: - version "3.2.0" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== nanomatch@^1.2.9: version "1.2.13" @@ -7138,14 +7128,14 @@ postcss@^7.0.1: source-map "^0.6.1" supports-color "^6.1.0" -postcss@^8.2.13: - version "8.2.13" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.13.tgz#dbe043e26e3c068e45113b1ed6375d2d37e2129f" - integrity sha512-FCE5xLH+hjbzRdpbRb1IMCvPv9yZx2QnDarBEYSN0N0HYk+TcXsEhwdFcFb+SRWOKzKGErhIEbBK2ogyLdTtfQ== +postcss@^8.4.35: + version "8.4.35" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" + integrity sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA== dependencies: - colorette "^1.2.2" - nanoid "^3.1.22" - source-map "^0.6.1" + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" prelude-ls@~1.1.2: version "1.1.2" @@ -7798,11 +7788,6 @@ react-dom@^16.9.3: prop-types "^15.6.2" scheduler "^0.19.1" -react-fast-compare@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" - integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== - react-fast-compare@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" @@ -7827,16 +7812,6 @@ react-ga@^2.7.0: resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.7.0.tgz#24328f157f31e8cffbf4de74a3396536679d8d7c" integrity sha512-AjC7UOZMvygrWTc2hKxTDvlMXEtbmA0IgJjmkhgmQQ3RkXrWR11xEagLGFGaNyaPnmg24oaIiaNPnEoftUhfXA== -react-helmet@^5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-5.2.1.tgz#16a7192fdd09951f8e0fe22ffccbf9bb3e591ffa" - integrity sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA== - dependencies: - object-assign "^4.1.1" - prop-types "^15.5.4" - react-fast-compare "^2.0.2" - react-side-effect "^1.1.0" - react-helmet@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/react-helmet/-/react-helmet-6.1.0.tgz#a750d5165cb13cf213e44747502652e794468726" @@ -7935,13 +7910,6 @@ react-router@^4.3.1: prop-types "^15.6.1" warning "^4.0.1" -react-side-effect@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-1.2.0.tgz#0e940c78faba0c73b9b0eba9cd3dda8dfb7e7dae" - integrity sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w== - dependencies: - shallowequal "^1.0.1" - react-side-effect@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3" @@ -8452,20 +8420,20 @@ semver@7.0.0: integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== semver@7.x, semver@^7.3.2, semver@^7.3.8: - version "7.5.2" - resolved "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb" - integrity sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: version "5.7.2" - resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@^6.0.0, semver@^6.3.0: version "6.3.1" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== send@0.17.2: @@ -8556,7 +8524,7 @@ shallow-equal@^1.2.1: resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA== -shallowequal@^1.0.1, shallowequal@^1.1.0: +shallowequal@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== @@ -8714,7 +8682,7 @@ source-list-map@^2.0.0, source-list-map@^2.0.1: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== -"source-map-js@>=0.6.2 <2.0.0": +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -8974,10 +8942,10 @@ stylis@^4.0.13: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -superagent@^8.0.9: - version "8.0.9" - resolved "https://registry.npmjs.org/superagent/-/superagent-8.0.9.tgz#2c6fda6fadb40516515f93e9098c0eb1602e0535" - integrity sha512-4C7Bh5pyHTvU33KpZgwrNKh/VQnvgtCSqPRfJAUdmrtSYePVzVg4E4OzsrbkhJj9O7SO6Bnv75K/F8XVZT8YHA== +superagent@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== dependencies: component-emitter "^1.3.0" cookiejar "^2.1.4" @@ -9946,10 +9914,10 @@ yargs@^17.0.1: y18n "^5.0.5" yargs-parser "^20.2.2" -yarn@^1.22.10: - version "1.22.10" - resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.10.tgz#c99daa06257c80f8fa2c3f1490724e394c26b18c" - integrity sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA== +yarn@^1.22.21: + version "1.22.21" + resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.21.tgz#1959a18351b811cdeedbd484a8f86c3cc3bbaf72" + integrity sha512-ynXaJsADJ9JiZ84zU25XkPGOvVMmZ5b7tmTSpKURYwgELdjucAOydqIOrOfTxVYcNXe91xvLZwcRh68SR3liCg== yn@3.1.1: version "3.1.1" diff --git a/util/argo/audit_logger.go b/util/argo/audit_logger.go index 1645e8d7d65d89..104b0dcd6143ec 100644 --- a/util/argo/audit_logger.go +++ b/util/argo/audit_logger.go @@ -2,6 +2,7 @@ package argo import ( "context" + "encoding/json" "fmt" "time" @@ -45,7 +46,7 @@ const ( EventReasonOperationCompleted = "OperationCompleted" ) -func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, info EventInfo, message string, logFields map[string]string) { +func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, info EventInfo, message string, logFields map[string]interface{}) { logCtx := log.WithFields(log.Fields{ "type": info.Type, "reason": info.Reason, @@ -53,6 +54,19 @@ func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, i for field, val := range logFields { logCtx = logCtx.WithField(field, val) } + logFieldStrings := make(map[string]string) + for field, val := range logFields { + if valStr, ok := val.(string); ok { + logFieldStrings[field] = valStr + continue + } + vJsonStr, err := json.Marshal(val) + if err != nil { + logCtx.Errorf("Unable to marshal audit event field %v: %v", field, err) + continue + } + logFieldStrings[field] = string(vJsonStr) + } switch gvk.Kind { case application.ApplicationKind: @@ -66,7 +80,7 @@ func (l *AuditLogger) logEvent(objMeta ObjectRef, gvk schema.GroupVersionKind, i event := v1.Event{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%v.%x", objMeta.Name, t.UnixNano()), - Annotations: logFields, + Annotations: logFieldStrings, }, Source: v1.EventSource{ Component: l.component, @@ -101,13 +115,14 @@ func (l *AuditLogger) LogAppEvent(app *v1alpha1.Application, info EventInfo, mes ResourceVersion: app.ObjectMeta.ResourceVersion, UID: app.ObjectMeta.UID, } - fields := map[string]string{ + fields := map[string]interface{}{ "dest-server": app.Spec.Destination.Server, "dest-namespace": app.Spec.Destination.Namespace, } if user != "" { fields["user"] = user } + fields["spec"] = app.Spec l.logEvent(objectMeta, v1alpha1.ApplicationSchemaGroupVersionKind, info, message, fields) } @@ -118,7 +133,7 @@ func (l *AuditLogger) LogAppSetEvent(app *v1alpha1.ApplicationSet, info EventInf ResourceVersion: app.ObjectMeta.ResourceVersion, UID: app.ObjectMeta.UID, } - fields := map[string]string{} + fields := make(map[string]interface{}) if user != "" { fields["user"] = user } @@ -132,7 +147,7 @@ func (l *AuditLogger) LogResourceEvent(res *v1alpha1.ResourceNode, info EventInf ResourceVersion: res.ResourceRef.Version, UID: types.UID(res.ResourceRef.UID), } - fields := map[string]string{} + fields := make(map[string]interface{}) if user != "" { fields["user"] = user } diff --git a/util/argo/diff/diff.go b/util/argo/diff/diff.go index 9b104719c56165..c99a04354c7513 100644 --- a/util/argo/diff/diff.go +++ b/util/argo/diff/diff.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/go-logr/logr" + log "github.com/sirupsen/logrus" k8smanagedfields "k8s.io/apimachinery/pkg/util/managedfields" @@ -26,7 +27,9 @@ type DiffConfigBuilder struct { // NewDiffConfigBuilder create a new DiffConfigBuilder instance. func NewDiffConfigBuilder() *DiffConfigBuilder { return &DiffConfigBuilder{ - diffConfig: &diffConfig{}, + diffConfig: &diffConfig{ + ignoreMutationWebhook: true, + }, } } @@ -63,7 +66,6 @@ func (b *DiffConfigBuilder) WithNoCache() *DiffConfigBuilder { // WithCache sets the appstatecache.Cache and the appName in the diff config. Those the // are two objects necessary to retrieve a cached diff. func (b *DiffConfigBuilder) WithCache(s *appstatecache.Cache, appName string) *DiffConfigBuilder { - b.diffConfig.noCache = false b.diffConfig.stateCache = s b.diffConfig.appName = appName return b @@ -95,6 +97,21 @@ func (b *DiffConfigBuilder) WithManager(manager string) *DiffConfigBuilder { return b } +func (b *DiffConfigBuilder) WithServerSideDryRunner(ssdr diff.ServerSideDryRunner) *DiffConfigBuilder { + b.diffConfig.serverSideDryRunner = ssdr + return b +} + +func (b *DiffConfigBuilder) WithServerSideDiff(ssd bool) *DiffConfigBuilder { + b.diffConfig.serverSideDiff = ssd + return b +} + +func (b *DiffConfigBuilder) WithIgnoreMutationWebhook(m bool) *DiffConfigBuilder { + b.diffConfig.ignoreMutationWebhook = m + return b +} + // Build will first validate the current state of the diff config and return the // DiffConfig implementation if no errors are found. Will return nil and the error // details otherwise. @@ -140,6 +157,10 @@ type DiffConfig interface { // Manager returns the manager that should be used by the diff while // calculating the structured merge diff. Manager() string + + ServerSideDiff() bool + ServerSideDryRunner() diff.ServerSideDryRunner + IgnoreMutationWebhook() bool } // diffConfig defines the configurations used while applying diffs. @@ -156,6 +177,9 @@ type diffConfig struct { gvkParser *k8smanagedfields.GvkParser structuredMergeDiff bool manager string + serverSideDiff bool + serverSideDryRunner diff.ServerSideDryRunner + ignoreMutationWebhook bool } func (c *diffConfig) Ignores() []v1alpha1.ResourceIgnoreDifferences { @@ -194,6 +218,15 @@ func (c *diffConfig) StructuredMergeDiff() bool { func (c *diffConfig) Manager() string { return c.manager } +func (c *diffConfig) ServerSideDryRunner() diff.ServerSideDryRunner { + return c.serverSideDryRunner +} +func (c *diffConfig) ServerSideDiff() bool { + return c.serverSideDiff +} +func (c *diffConfig) IgnoreMutationWebhook() bool { + return c.ignoreMutationWebhook +} // Validate will check the current state of this diffConfig and return // error if it finds any required configuration missing. @@ -213,6 +246,9 @@ func (c *diffConfig) Validate() error { return fmt.Errorf("%s: StateCache must be set when retrieving from cache", msg) } } + if c.serverSideDiff && c.serverSideDryRunner == nil { + return fmt.Errorf("%s: serverSideDryRunner must be set when using server side diff", msg) + } return nil } @@ -254,6 +290,9 @@ func StateDiffs(lives, configs []*unstructured.Unstructured, diffConfig DiffConf diff.WithStructuredMergeDiff(diffConfig.StructuredMergeDiff()), diff.WithGVKParser(diffConfig.GVKParser()), diff.WithManager(diffConfig.Manager()), + diff.WithServerSideDiff(diffConfig.ServerSideDiff()), + diff.WithServerSideDryRunner(diffConfig.ServerSideDryRunner()), + diff.WithIgnoreMutationWebhook(diffConfig.IgnoreMutationWebhook()), } if diffConfig.Logger() != nil { @@ -282,9 +321,8 @@ func diffArrayCached(configArray []*unstructured.Unstructured, liveArray []*unst } diffByKey := map[kube.ResourceKey]*v1alpha1.ResourceDiff{} - for i := range cachedDiff { - res := cachedDiff[i] - diffByKey[kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)] = cachedDiff[i] + for _, res := range cachedDiff { + diffByKey[kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)] = res } diffResultList := diff.DiffResultList{ @@ -335,7 +373,12 @@ func (c *diffConfig) DiffFromCache(appName string) (bool, []*v1alpha1.ResourceDi return false, nil } cachedDiff := make([]*v1alpha1.ResourceDiff, 0) - if c.stateCache != nil && c.stateCache.GetAppManagedResources(appName, &cachedDiff) == nil { + if c.stateCache != nil { + err := c.stateCache.GetAppManagedResources(appName, &cachedDiff) + if err != nil { + log.Errorf("DiffFromCache error: error getting managed resources for app %s: %s", appName, err) + return false, nil + } return true, cachedDiff } return false, nil diff --git a/util/argo/normalizers/knowntypes_normalizer.go b/util/argo/normalizers/knowntypes_normalizer.go index 403065c58a6df9..f96a366a75d6a1 100644 --- a/util/argo/normalizers/knowntypes_normalizer.go +++ b/util/argo/normalizers/knowntypes_normalizer.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" @@ -32,6 +33,9 @@ func init() { knownTypes["core/Quantity"] = func() interface{} { return &resource.Quantity{} } + knownTypes["meta/v1/Duration"] = func() interface{} { + return &metav1.Duration{} + } } // NewKnownTypesNormalizer create a normalizer that re-format custom resource fields using built-in Kubernetes types. diff --git a/util/argo/normalizers/knowntypes_normalizer_test.go b/util/argo/normalizers/knowntypes_normalizer_test.go index 57e436195f8902..37c34d37509b3d 100644 --- a/util/argo/normalizers/knowntypes_normalizer_test.go +++ b/util/argo/normalizers/knowntypes_normalizer_test.go @@ -11,6 +11,7 @@ import ( "github.com/argoproj/pkg/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/yaml" @@ -228,6 +229,33 @@ spec: assert.Equal(t, "1250M", ram) } +func TestNormalize_Duration(t *testing.T) { + cert := mustUnmarshalYAML(` +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: my-cert +spec: + duration: 8760h +`) + normalizer, err := NewKnownTypesNormalizer(map[string]v1alpha1.ResourceOverride{ + "cert-manager.io/Certificate": { + KnownTypeFields: []v1alpha1.KnownTypeField{{ + Type: "meta/v1/Duration", + Field: "spec.duration", + }}, + }, + }) + require.NoError(t, err) + + require.NoError(t, normalizer.Normalize(cert)) + + duration, ok, err := unstructured.NestedFieldNoCopy(cert.Object, "spec", "duration") + require.NoError(t, err) + require.True(t, ok) + require.Equal(t, "8760h0m0s", duration) +} + func TestFieldDoesNotExist(t *testing.T) { rollout := mustUnmarshalYAML(someCRDYaml) normalizer, err := NewKnownTypesNormalizer(map[string]v1alpha1.ResourceOverride{ diff --git a/util/cache/appstate/cache.go b/util/cache/appstate/cache.go index d59d31befb12e3..bb161a429eff9c 100644 --- a/util/cache/appstate/cache.go +++ b/util/cache/appstate/cache.go @@ -6,7 +6,6 @@ import ( "sort" "time" - "github.com/redis/go-redis/v9" "github.com/spf13/cobra" appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" @@ -29,7 +28,7 @@ func NewCache(cache *cacheutil.Cache, appStateCacheExpiration time.Duration) *Ca return &Cache{cache, appStateCacheExpiration} } -func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) func() (*Cache, error) { +func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...cacheutil.Options) func() (*Cache, error) { var appStateCacheExpiration time.Duration cmd.Flags().DurationVar(&appStateCacheExpiration, "app-state-cache-expiration", env.ParseDurationFromEnv("ARGOCD_APP_STATE_CACHE_EXPIRATION", 1*time.Hour, 0, 10*time.Hour), "Cache expiration for app state") diff --git a/util/cache/cache.go b/util/cache/cache.go index fdea46cdea0d27..b632824e9c96bb 100644 --- a/util/cache/cache.go +++ b/util/cache/cache.go @@ -5,17 +5,17 @@ import ( "fmt" "math" "os" + "strings" "time" "crypto/tls" "crypto/x509" - "github.com/redis/go-redis/v9" - "github.com/spf13/cobra" - "github.com/argoproj/argo-cd/v2/common" certutil "github.com/argoproj/argo-cd/v2/util/cert" "github.com/argoproj/argo-cd/v2/util/env" + "github.com/redis/go-redis/v9" + "github.com/spf13/cobra" ) const ( @@ -27,6 +27,15 @@ const ( envRedisRetryCount = "REDIS_RETRY_COUNT" // defaultRedisRetryCount holds default number of retries defaultRedisRetryCount = 3 + // envRedisSentinelPassword is an env variable name which stores redis sentinel password + envRedisSentinelPassword = "REDIS_SENTINEL_PASSWORD" + // envRedisSentinelUsername is an env variable name which stores redis sentinel username + envRedisSentinelUsername = "REDIS_SENTINEL_USERNAME" +) + +const ( + // CLIFlagRedisCompress is a cli flag name to define the redis compression setting for data sent to redis + CLIFlagRedisCompress = "redis-compress" ) func NewCache(client CacheClient) *Cache { @@ -52,28 +61,74 @@ func buildRedisClient(redisAddress, password, username string, redisDB, maxRetri return client } -func buildFailoverRedisClient(sentinelMaster, password, username string, redisDB, maxRetries int, tlsConfig *tls.Config, sentinelAddresses []string) *redis.Client { +func buildFailoverRedisClient(sentinelMaster, sentinelUsername, sentinelPassword, password, username string, redisDB, maxRetries int, tlsConfig *tls.Config, sentinelAddresses []string) *redis.Client { opts := &redis.FailoverOptions{ - MasterName: sentinelMaster, - SentinelAddrs: sentinelAddresses, - DB: redisDB, - Password: password, - MaxRetries: maxRetries, - TLSConfig: tlsConfig, - Username: username, + MasterName: sentinelMaster, + SentinelAddrs: sentinelAddresses, + DB: redisDB, + Password: password, + MaxRetries: maxRetries, + TLSConfig: tlsConfig, + Username: username, + SentinelUsername: sentinelUsername, + SentinelPassword: sentinelPassword, } client := redis.NewFailoverClient(opts) client.AddHook(redis.Hook(NewArgoRedisHook(func() { - *client = *buildFailoverRedisClient(sentinelMaster, password, username, redisDB, maxRetries, tlsConfig, sentinelAddresses) + *client = *buildFailoverRedisClient(sentinelMaster, sentinelUsername, sentinelPassword, password, username, redisDB, maxRetries, tlsConfig, sentinelAddresses) }))) return client } +type Options struct { + FlagPrefix string + OnClientCreated func(client *redis.Client) +} + +func (o *Options) callOnClientCreated(client *redis.Client) { + if o.OnClientCreated != nil { + o.OnClientCreated(client) + } +} + +func (o *Options) getEnvPrefix() string { + return strings.Replace(strings.ToUpper(o.FlagPrefix), "-", "_", -1) +} + +func mergeOptions(opts ...Options) Options { + var result Options + for _, o := range opts { + if o.FlagPrefix != "" { + result.FlagPrefix = o.FlagPrefix + } + if o.OnClientCreated != nil { + result.OnClientCreated = o.OnClientCreated + } + } + return result +} + +func getFlagVal[T any](cmd *cobra.Command, o Options, name string, getVal func(name string) (T, error)) func() T { + return func() T { + var res T + var err error + if o.FlagPrefix != "" && cmd.Flags().Changed(o.FlagPrefix+name) { + res, err = getVal(o.FlagPrefix + name) + } else { + res, err = getVal(name) + } + if err != nil { + panic(err) + } + return res + } +} + // AddCacheFlagsToCmd adds flags which control caching to the specified command -func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) func() (*Cache, error) { +func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...Options) func() (*Cache, error) { redisAddress := "" sentinelAddresses := make([]string, 0) sentinelMaster := "" @@ -84,20 +139,44 @@ func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) redisUseTLS := false insecureRedis := false compressionStr := "" + opt := mergeOptions(opts...) var defaultCacheExpiration time.Duration - cmd.Flags().StringVar(&redisAddress, "redis", env.StringFromEnv("REDIS_SERVER", ""), "Redis server hostname and port (e.g. argocd-redis:6379). ") - cmd.Flags().IntVar(&redisDB, "redisdb", env.ParseNumFromEnv("REDISDB", 0, 0, math.MaxInt32), "Redis database.") - cmd.Flags().StringArrayVar(&sentinelAddresses, "sentinel", []string{}, "Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). ") - cmd.Flags().StringVar(&sentinelMaster, "sentinelmaster", "master", "Redis sentinel master group name.") - cmd.Flags().DurationVar(&defaultCacheExpiration, "default-cache-expiration", env.ParseDurationFromEnv("ARGOCD_DEFAULT_CACHE_EXPIRATION", 24*time.Hour, 0, math.MaxInt64), "Cache expiration default") - cmd.Flags().BoolVar(&redisUseTLS, "redis-use-tls", false, "Use TLS when connecting to Redis. ") - cmd.Flags().StringVar(&redisClientCertificate, "redis-client-certificate", "", "Path to Redis client certificate (e.g. /etc/certs/redis/client.crt).") - cmd.Flags().StringVar(&redisClientKey, "redis-client-key", "", "Path to Redis client key (e.g. /etc/certs/redis/client.crt).") - cmd.Flags().BoolVar(&insecureRedis, "redis-insecure-skip-tls-verify", false, "Skip Redis server certificate validation.") - cmd.Flags().StringVar(&redisCACertificate, "redis-ca-certificate", "", "Path to Redis server CA certificate (e.g. /etc/certs/redis/ca.crt). If not specified, system trusted CAs will be used for server certificate validation.") - cmd.Flags().StringVar(&compressionStr, "redis-compress", env.StringFromEnv("REDIS_COMPRESSION", string(RedisCompressionGZip)), "Enable compression for data sent to Redis with the required compression algorithm. (possible values: gzip, none)") + cmd.Flags().StringVar(&redisAddress, opt.FlagPrefix+"redis", env.StringFromEnv(opt.getEnvPrefix()+"REDIS_SERVER", ""), "Redis server hostname and port (e.g. argocd-redis:6379). ") + redisAddressSrc := getFlagVal(cmd, opt, "redis", cmd.Flags().GetString) + cmd.Flags().IntVar(&redisDB, opt.FlagPrefix+"redisdb", env.ParseNumFromEnv(opt.getEnvPrefix()+"REDISDB", 0, 0, math.MaxInt32), "Redis database.") + redisDBSrc := getFlagVal(cmd, opt, "redisdb", cmd.Flags().GetInt) + cmd.Flags().StringArrayVar(&sentinelAddresses, opt.FlagPrefix+"sentinel", []string{}, "Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). ") + sentinelAddressesSrc := getFlagVal(cmd, opt, "sentinel", cmd.Flags().GetStringArray) + cmd.Flags().StringVar(&sentinelMaster, opt.FlagPrefix+"sentinelmaster", "master", "Redis sentinel master group name.") + sentinelMasterSrc := getFlagVal(cmd, opt, "sentinelmaster", cmd.Flags().GetString) + cmd.Flags().DurationVar(&defaultCacheExpiration, opt.FlagPrefix+"default-cache-expiration", env.ParseDurationFromEnv("ARGOCD_DEFAULT_CACHE_EXPIRATION", 24*time.Hour, 0, math.MaxInt64), "Cache expiration default") + defaultCacheExpirationSrc := getFlagVal(cmd, opt, "default-cache-expiration", cmd.Flags().GetDuration) + cmd.Flags().BoolVar(&redisUseTLS, opt.FlagPrefix+"redis-use-tls", false, "Use TLS when connecting to Redis. ") + redisUseTLSSrc := getFlagVal(cmd, opt, "redis-use-tls", cmd.Flags().GetBool) + cmd.Flags().StringVar(&redisClientCertificate, opt.FlagPrefix+"redis-client-certificate", "", "Path to Redis client certificate (e.g. /etc/certs/redis/client.crt).") + redisClientCertificateSrc := getFlagVal(cmd, opt, "redis-client-certificate", cmd.Flags().GetString) + cmd.Flags().StringVar(&redisClientKey, opt.FlagPrefix+"redis-client-key", "", "Path to Redis client key (e.g. /etc/certs/redis/client.crt).") + redisClientKeySrc := getFlagVal(cmd, opt, "redis-client-key", cmd.Flags().GetString) + cmd.Flags().BoolVar(&insecureRedis, opt.FlagPrefix+"redis-insecure-skip-tls-verify", false, "Skip Redis server certificate validation.") + insecureRedisSrc := getFlagVal(cmd, opt, "redis-insecure-skip-tls-verify", cmd.Flags().GetBool) + cmd.Flags().StringVar(&redisCACertificate, opt.FlagPrefix+"redis-ca-certificate", "", "Path to Redis server CA certificate (e.g. /etc/certs/redis/ca.crt). If not specified, system trusted CAs will be used for server certificate validation.") + redisCACertificateSrc := getFlagVal(cmd, opt, "redis-ca-certificate", cmd.Flags().GetString) + cmd.Flags().StringVar(&compressionStr, opt.FlagPrefix+CLIFlagRedisCompress, env.StringFromEnv(opt.getEnvPrefix()+"REDIS_COMPRESSION", string(RedisCompressionGZip)), "Enable compression for data sent to Redis with the required compression algorithm. (possible values: gzip, none)") + compressionStrSrc := getFlagVal(cmd, opt, CLIFlagRedisCompress, cmd.Flags().GetString) return func() (*Cache, error) { + redisAddress := redisAddressSrc() + redisDB := redisDBSrc() + sentinelAddresses := sentinelAddressesSrc() + sentinelMaster := sentinelMasterSrc() + defaultCacheExpiration := defaultCacheExpirationSrc() + redisUseTLS := redisUseTLSSrc() + redisClientCertificate := redisClientCertificateSrc() + redisClientKey := redisClientKeySrc() + insecureRedis := insecureRedisSrc() + redisCACertificate := redisCACertificateSrc() + compressionStr := compressionStrSrc() + var tlsConfig *tls.Config = nil if redisUseTLS { tlsConfig = &tls.Config{} @@ -126,16 +205,31 @@ func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) } password := os.Getenv(envRedisPassword) username := os.Getenv(envRedisUsername) + sentinelUsername := os.Getenv(envRedisSentinelUsername) + sentinelPassword := os.Getenv(envRedisSentinelPassword) + if opt.FlagPrefix != "" { + if val := os.Getenv(opt.getEnvPrefix() + envRedisUsername); val != "" { + username = val + } + if val := os.Getenv(opt.getEnvPrefix() + envRedisPassword); val != "" { + password = val + } + if val := os.Getenv(opt.getEnvPrefix() + envRedisSentinelUsername); val != "" { + sentinelUsername = val + } + if val := os.Getenv(opt.getEnvPrefix() + envRedisSentinelPassword); val != "" { + sentinelPassword = val + } + } + maxRetries := env.ParseNumFromEnv(envRedisRetryCount, defaultRedisRetryCount, 0, math.MaxInt32) compression, err := CompressionTypeFromString(compressionStr) if err != nil { return nil, err } if len(sentinelAddresses) > 0 { - client := buildFailoverRedisClient(sentinelMaster, password, username, redisDB, maxRetries, tlsConfig, sentinelAddresses) - for i := range opts { - opts[i](client) - } + client := buildFailoverRedisClient(sentinelMaster, sentinelUsername, sentinelPassword, password, username, redisDB, maxRetries, tlsConfig, sentinelAddresses) + opt.callOnClientCreated(client) return NewCache(NewRedisCache(client, defaultCacheExpiration, compression)), nil } if redisAddress == "" { @@ -143,9 +237,7 @@ func AddCacheFlagsToCmd(cmd *cobra.Command, opts ...func(client *redis.Client)) } client := buildRedisClient(redisAddress, password, username, redisDB, maxRetries, tlsConfig) - for i := range opts { - opts[i](client) - } + opt.callOnClientCreated(client) return NewCache(NewRedisCache(client, defaultCacheExpiration, compression)), nil } } @@ -163,6 +255,10 @@ func (c *Cache) SetClient(client CacheClient) { c.client = client } +func (c *Cache) RenameItem(oldKey string, newKey string, expiration time.Duration) error { + return c.client.Rename(fmt.Sprintf("%s|%s", oldKey, common.CacheVersion), fmt.Sprintf("%s|%s", newKey, common.CacheVersion), expiration) +} + func (c *Cache) SetItem(key string, item interface{}, expiration time.Duration, delete bool) error { key = fmt.Sprintf("%s|%s", key, common.CacheVersion) if delete { diff --git a/util/cache/client.go b/util/cache/client.go index 434c2a8da187a5..c8c7b4a6baa80b 100644 --- a/util/cache/client.go +++ b/util/cache/client.go @@ -17,6 +17,7 @@ type Item struct { type CacheClient interface { Set(item *Item) error + Rename(oldKey string, newKey string, expiration time.Duration) error Get(key string, obj interface{}) error Delete(key string) error OnUpdated(ctx context.Context, key string, callback func() error) error diff --git a/util/cache/inmemory.go b/util/cache/inmemory.go index 53e690925d940b..6d970c1d4f567f 100644 --- a/util/cache/inmemory.go +++ b/util/cache/inmemory.go @@ -16,6 +16,10 @@ func NewInMemoryCache(expiration time.Duration) *InMemoryCache { } } +func init() { + gob.Register([]interface{}{}) +} + // compile-time validation of adherance of the CacheClient contract var _ CacheClient = &InMemoryCache{} @@ -33,6 +37,16 @@ func (i *InMemoryCache) Set(item *Item) error { return nil } +func (i *InMemoryCache) Rename(oldKey string, newKey string, expiration time.Duration) error { + bufIf, found := i.memCache.Get(oldKey) + if !found { + return ErrCacheMiss + } + i.memCache.Set(newKey, bufIf, expiration) + i.memCache.Delete(oldKey) + return nil +} + // HasSame returns true if key with the same value already present in cache func (i *InMemoryCache) HasSame(key string, obj interface{}) (bool, error) { var buf bytes.Buffer diff --git a/util/cache/mocks/cacheclient.go b/util/cache/mocks/cacheclient.go new file mode 100644 index 00000000000000..2fdd9fc37f8be9 --- /dev/null +++ b/util/cache/mocks/cacheclient.go @@ -0,0 +1,74 @@ +package mocks + +import ( + "context" + "time" + + "github.com/stretchr/testify/mock" + + "github.com/argoproj/argo-cd/v2/util/cache" +) + +type MockCacheClient struct { + mock.Mock + BaseCache cache.CacheClient + ReadDelay time.Duration + WriteDelay time.Duration +} + +func (c *MockCacheClient) Rename(oldKey string, newKey string, expiration time.Duration) error { + args := c.Called(oldKey, newKey, expiration) + if len(args) > 0 && args.Get(0) != nil { + return args.Get(0).(error) + } + return c.BaseCache.Rename(oldKey, newKey, expiration) +} + +func (c *MockCacheClient) Set(item *cache.Item) error { + args := c.Called(item) + if len(args) > 0 && args.Get(0) != nil { + return args.Get(0).(error) + } + if c.WriteDelay > 0 { + time.Sleep(c.WriteDelay) + } + return c.BaseCache.Set(item) +} + +func (c *MockCacheClient) Get(key string, obj interface{}) error { + args := c.Called(key, obj) + if len(args) > 0 && args.Get(0) != nil { + return args.Get(0).(error) + } + if c.ReadDelay > 0 { + time.Sleep(c.ReadDelay) + } + return c.BaseCache.Get(key, obj) +} + +func (c *MockCacheClient) Delete(key string) error { + args := c.Called(key) + if len(args) > 0 && args.Get(0) != nil { + return args.Get(0).(error) + } + if c.WriteDelay > 0 { + time.Sleep(c.WriteDelay) + } + return c.BaseCache.Delete(key) +} + +func (c *MockCacheClient) OnUpdated(ctx context.Context, key string, callback func() error) error { + args := c.Called(ctx, key, callback) + if len(args) > 0 && args.Get(0) != nil { + return args.Get(0).(error) + } + return c.BaseCache.OnUpdated(ctx, key, callback) +} + +func (c *MockCacheClient) NotifyUpdated(key string) error { + args := c.Called(key) + if len(args) > 0 && args.Get(0) != nil { + return args.Get(0).(error) + } + return c.BaseCache.NotifyUpdated(key) +} diff --git a/util/cache/redis.go b/util/cache/redis.go index c5365c4984e21d..4648a553f08cc1 100644 --- a/util/cache/redis.go +++ b/util/cache/redis.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "net" "time" ioutil "github.com/argoproj/argo-cd/v2/util/io" @@ -95,6 +96,10 @@ func (r *redisCache) unmarshal(data []byte, obj interface{}) error { return nil } +func (r *redisCache) Rename(oldKey string, newKey string, _ time.Duration) error { + return r.client.Rename(context.TODO(), r.getKey(oldKey), r.getKey(newKey)).Err() +} + func (r *redisCache) Set(item *Item) error { expiration := item.Expiration if expiration == 0 { @@ -155,41 +160,27 @@ type MetricsRegistry interface { ObserveRedisRequestDuration(duration time.Duration) } -var metricStartTimeKey = struct{}{} - type redisHook struct { registry MetricsRegistry } -func (rh *redisHook) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { - return context.WithValue(ctx, metricStartTimeKey, time.Now()), nil -} - -func (rh *redisHook) AfterProcess(ctx context.Context, cmd redis.Cmder) error { - cmdErr := cmd.Err() - rh.registry.IncRedisRequest(cmdErr != nil && cmdErr != redis.Nil) - - startTime := ctx.Value(metricStartTimeKey).(time.Time) - duration := time.Since(startTime) - rh.registry.ObserveRedisRequestDuration(duration) - - return nil -} - -func (redisHook) BeforeProcessPipeline(ctx context.Context, _ []redis.Cmder) (context.Context, error) { - return ctx, nil +func (rh *redisHook) DialHook(next redis.DialHook) redis.DialHook { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := next(ctx, network, addr) + return conn, err + } } -func (redisHook) AfterProcessPipeline(_ context.Context, _ []redis.Cmder) error { - return nil -} +func (rh *redisHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { + return func(ctx context.Context, cmd redis.Cmder) error { + startTime := time.Now() -func (redisHook) DialHook(next redis.DialHook) redis.DialHook { - return nil -} + err := next(ctx, cmd) + rh.registry.IncRedisRequest(err != nil && err != redis.Nil) + rh.registry.ObserveRedisRequestDuration(time.Since(startTime)) -func (redisHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { - return nil + return err + } } func (redisHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { diff --git a/util/cache/redis_hook.go b/util/cache/redis_hook.go index 455ad03eb5bbfe..e7cc3f4bcc68ed 100644 --- a/util/cache/redis_hook.go +++ b/util/cache/redis_hook.go @@ -2,14 +2,13 @@ package cache import ( "context" - "strings" + "errors" + "net" "github.com/redis/go-redis/v9" log "github.com/sirupsen/logrus" ) -const NoSuchHostErr = "no such host" - type argoRedisHooks struct { reconnectCallback func() } @@ -18,32 +17,23 @@ func NewArgoRedisHook(reconnectCallback func()) *argoRedisHooks { return &argoRedisHooks{reconnectCallback: reconnectCallback} } -func (hook *argoRedisHooks) BeforeProcess(ctx context.Context, cmd redis.Cmder) (context.Context, error) { - return ctx, nil -} - -func (hook *argoRedisHooks) AfterProcess(ctx context.Context, cmd redis.Cmder) error { - if cmd.Err() != nil && strings.Contains(cmd.Err().Error(), NoSuchHostErr) { - log.Warnf("Reconnect to redis because error: \"%v\"", cmd.Err()) - hook.reconnectCallback() - } - return nil -} - -func (hook *argoRedisHooks) BeforeProcessPipeline(ctx context.Context, cmds []redis.Cmder) (context.Context, error) { - return ctx, nil -} - -func (hook *argoRedisHooks) AfterProcessPipeline(ctx context.Context, cmds []redis.Cmder) error { - return nil -} - func (hook *argoRedisHooks) DialHook(next redis.DialHook) redis.DialHook { - return nil + return func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := next(ctx, network, addr) + return conn, err + } } func (hook *argoRedisHooks) ProcessHook(next redis.ProcessHook) redis.ProcessHook { - return nil + return func(ctx context.Context, cmd redis.Cmder) error { + var dnsError *net.DNSError + err := next(ctx, cmd) + if err != nil && errors.As(err, &dnsError) { + log.Warnf("Reconnect to redis because error: \"%v\"", err) + hook.reconnectCallback() + } + return err + } } func (hook *argoRedisHooks) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { diff --git a/util/cache/redis_hook_test.go b/util/cache/redis_hook_test.go index ef9e6a1c855379..4d7d9b7aaf41d0 100644 --- a/util/cache/redis_hook_test.go +++ b/util/cache/redis_hook_test.go @@ -1,38 +1,53 @@ package cache import ( - "context" - "errors" "testing" + "time" + "github.com/alicebob/miniredis/v2" "github.com/stretchr/testify/assert" "github.com/redis/go-redis/v9" ) func Test_ReconnectCallbackHookCalled(t *testing.T) { + mr, err := miniredis.Run() + if err != nil { + panic(err) + } + defer mr.Close() + called := false hook := NewArgoRedisHook(func() { called = true }) - cmd := &redis.StringCmd{} - cmd.SetErr(errors.New("Failed to resync revoked tokens. retrying again in 1 minute: dial tcp: lookup argocd-redis on 10.179.0.10:53: no such host")) - - _ = hook.AfterProcess(context.Background(), cmd) + faultyDNSRedisClient := redis.NewClient(&redis.Options{Addr: "invalidredishost.invalid:12345"}) + faultyDNSRedisClient.AddHook(hook) + faultyDNSClient := NewRedisCache(faultyDNSRedisClient, 60*time.Second, RedisCompressionNone) + err = faultyDNSClient.Set(&Item{Key: "baz", Object: "foo"}) assert.Equal(t, called, true) + assert.Error(t, err) } func Test_ReconnectCallbackHookNotCalled(t *testing.T) { + mr, err := miniredis.Run() + if err != nil { + panic(err) + } + defer mr.Close() + called := false hook := NewArgoRedisHook(func() { called = true }) - cmd := &redis.StringCmd{} - cmd.SetErr(errors.New("Something wrong")) - _ = hook.AfterProcess(context.Background(), cmd) + redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()}) + redisClient.AddHook(hook) + client := NewRedisCache(redisClient, 60*time.Second, RedisCompressionNone) + err = client.Set(&Item{Key: "foo", Object: "bar"}) assert.Equal(t, called, false) + assert.NoError(t, err) } diff --git a/util/cache/redis_test.go b/util/cache/redis_test.go index 3800753cee3ec3..e05c7541f5ff11 100644 --- a/util/cache/redis_test.go +++ b/util/cache/redis_test.go @@ -2,14 +2,59 @@ package cache import ( "context" + "strconv" "testing" "time" + promcm "github.com/prometheus/client_model/go" + "github.com/alicebob/miniredis/v2" + "github.com/prometheus/client_golang/prometheus" "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" ) +var ( + redisRequestCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_redis_request_total", + }, + []string{"initiator", "failed"}, + ) + redisRequestHistogram = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "argocd_redis_request_duration", + Buckets: []float64{0.1, 0.25, .5, 1, 2}, + }, + []string{"initiator"}, + ) +) + +type MockMetricsServer struct { + registry *prometheus.Registry + redisRequestCounter *prometheus.CounterVec + redisRequestHistogram *prometheus.HistogramVec +} + +func NewMockMetricsServer() *MockMetricsServer { + registry := prometheus.NewRegistry() + registry.MustRegister(redisRequestCounter) + registry.MustRegister(redisRequestHistogram) + return &MockMetricsServer{ + registry: registry, + redisRequestCounter: redisRequestCounter, + redisRequestHistogram: redisRequestHistogram, + } +} + +func (m *MockMetricsServer) IncRedisRequest(failed bool) { + m.redisRequestCounter.WithLabelValues("mock", strconv.FormatBool(failed)).Inc() +} + +func (m *MockMetricsServer) ObserveRedisRequestDuration(duration time.Duration) { + m.redisRequestHistogram.WithLabelValues("mock").Observe(duration.Seconds()) +} + func TestRedisSetCache(t *testing.T) { mr, err := miniredis.Run() if err != nil { @@ -70,3 +115,50 @@ func TestRedisSetCacheCompressed(t *testing.T) { assert.Equal(t, testValue, result) } + +func TestRedisMetrics(t *testing.T) { + mr, err := miniredis.Run() + if err != nil { + panic(err) + } + defer mr.Close() + + metric := &promcm.Metric{} + ms := NewMockMetricsServer() + redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()}) + faultyRedisClient := redis.NewClient(&redis.Options{Addr: "invalidredishost.invalid:12345"}) + CollectMetrics(redisClient, ms) + CollectMetrics(faultyRedisClient, ms) + + client := NewRedisCache(redisClient, 60*time.Second, RedisCompressionNone) + faultyClient := NewRedisCache(faultyRedisClient, 60*time.Second, RedisCompressionNone) + var res string + + //client successful request + err = client.Set(&Item{Key: "foo", Object: "bar"}) + assert.NoError(t, err) + err = client.Get("foo", &res) + assert.NoError(t, err) + + c, err := ms.redisRequestCounter.GetMetricWithLabelValues("mock", "false") + assert.NoError(t, err) + err = c.Write(metric) + assert.NoError(t, err) + assert.Equal(t, metric.Counter.GetValue(), float64(2)) + + //faulty client failed request + err = faultyClient.Get("foo", &res) + assert.Error(t, err) + c, err = ms.redisRequestCounter.GetMetricWithLabelValues("mock", "true") + assert.NoError(t, err) + err = c.Write(metric) + assert.NoError(t, err) + assert.Equal(t, metric.Counter.GetValue(), float64(1)) + + //both clients histogram count + o, err := ms.redisRequestHistogram.GetMetricWithLabelValues("mock") + assert.NoError(t, err) + err = o.(prometheus.Metric).Write(metric) + assert.NoError(t, err) + assert.Equal(t, int(metric.Histogram.GetSampleCount()), 3) +} diff --git a/util/cache/twolevelclient.go b/util/cache/twolevelclient.go index 14a4279e87c89c..f2210998448763 100644 --- a/util/cache/twolevelclient.go +++ b/util/cache/twolevelclient.go @@ -18,6 +18,14 @@ type twoLevelClient struct { externalCache CacheClient } +func (c *twoLevelClient) Rename(oldKey string, newKey string, expiration time.Duration) error { + err := c.inMemoryCache.Rename(oldKey, newKey, expiration) + if err != nil { + log.Warnf("Failed to move key '%s' in in-memory cache: %v", oldKey, err) + } + return c.externalCache.Rename(oldKey, newKey, expiration) +} + // Set stores the given value in both in-memory and external cache. // Skip storing the value in external cache if the same value already exists in memory to avoid requesting external cache. func (c *twoLevelClient) Set(item *Item) error { diff --git a/util/db/cluster.go b/util/db/cluster.go index dad8a62010adca..7b5f5508ef5fa6 100644 --- a/util/db/cluster.go +++ b/util/db/cluster.go @@ -316,7 +316,7 @@ func (db *db) DeleteCluster(ctx context.Context, server string) error { return db.settingsMgr.ResyncInformers() } -// clusterToData converts a cluster object to string data for serialization to a secret +// clusterToSecret converts a cluster object to string data for serialization to a secret func clusterToSecret(c *appv1.Cluster, secret *apiv1.Secret) error { data := make(map[string][]byte) data["server"] = []byte(strings.TrimRight(c.Server, "/")) diff --git a/util/db/repository_secrets.go b/util/db/repository_secrets.go index 31152300b0b8ba..2d96c1c3a99eb3 100644 --- a/util/db/repository_secrets.go +++ b/util/db/repository_secrets.go @@ -489,6 +489,9 @@ func (s *secretsRepositoryBackend) getRepositoryCredentialIndex(repoCredentials for i, cred := range repoCredentials { credUrl := git.NormalizeGitURL(string(cred.Data["url"])) if strings.HasPrefix(repoURL, credUrl) { + if len(credUrl) == max { + log.Warnf("Found multiple credentials for repoURL: %s", repoURL) + } if len(credUrl) > max { max = len(credUrl) idx = i diff --git a/util/dex/dex_test.go b/util/dex/dex_test.go index b531e1470b7888..e15726d44f5018 100644 --- a/util/dex/dex_test.go +++ b/util/dex/dex_test.go @@ -42,7 +42,7 @@ connectors: id: acme-github name: Acme GitHub config: - hostName: github.acme.com + hostName: github.acme.example.com clientID: abcdefghijklmnopqrst clientSecret: $dex.acme.clientSecret orgs: @@ -79,7 +79,7 @@ connectors: id: acme-github name: Acme GitHub config: - hostName: github.acme.com + hostName: github.acme.example.com clientID: abcdefghijklmnopqrst clientSecret: $dex.acme.clientSecret orgs: diff --git a/util/env/env.go b/util/env/env.go index 221db7b0efaab2..0b8bf9e0c37080 100644 --- a/util/env/env.go +++ b/util/env/env.go @@ -151,8 +151,17 @@ func ParseDurationFromEnv(env string, defaultValue, min, max time.Duration) time return dur } -func StringFromEnv(env string, defaultValue string) string { - if str := os.Getenv(env); str != "" { +type StringFromEnvOpts struct { + // AllowEmpty allows the value to be empty as long as the environment variable is set. + AllowEmpty bool +} + +func StringFromEnv(env string, defaultValue string, opts ...StringFromEnvOpts) string { + opt := StringFromEnvOpts{} + for _, o := range opts { + opt.AllowEmpty = opt.AllowEmpty || o.AllowEmpty + } + if str, ok := os.LookupEnv(env); opt.AllowEmpty && ok || str != "" { return str } return defaultValue @@ -186,3 +195,30 @@ func ParseBoolFromEnv(envVar string, defaultValue bool) bool { } return defaultValue } + +// ParseStringToStringVar parses given value from the environment as a map of string. +// Returns default value if envVar is not set. +func ParseStringToStringFromEnv(envVar string, defaultValue map[string]string, seperator string) map[string]string { + str := os.Getenv(envVar) + str = strings.TrimSpace(str) + if str == "" { + return defaultValue + } + + parsed := make(map[string]string) + for _, pair := range strings.Split(str, seperator) { + keyvalue := strings.Split(pair, "=") + if len(keyvalue) != 2 { + log.Warnf("Invalid key-value pair when parsing environment '%s' as a string map", str) + return defaultValue + } + key := strings.TrimSpace(keyvalue[0]) + value := strings.TrimSpace(keyvalue[1]) + if _, ok := parsed[key]; ok { + log.Warnf("Duplicate key '%s' when parsing environment '%s' as a string map", key, str) + return defaultValue + } + parsed[key] = value + } + return parsed +} diff --git a/util/env/env_test.go b/util/env/env_test.go index 691d235805b236..95c78ccde6d9d1 100644 --- a/util/env/env_test.go +++ b/util/env/env_test.go @@ -7,6 +7,7 @@ import ( "time" "github.com/stretchr/testify/assert" + "k8s.io/utils/pointer" ) func TestParseNumFromEnv(t *testing.T) { @@ -167,19 +168,25 @@ func TestStringFromEnv(t *testing.T) { testCases := []struct { name string - env string + env *string expected string def string + opts []StringFromEnvOpts }{ - {"Some string", "true", "true", def}, - {"Empty string with default", "", def, def}, - {"Empty string without default", "", "", ""}, + {"Some string", pointer.String("true"), "true", def, nil}, + {"Empty string with default", pointer.String(""), def, def, nil}, + {"Empty string without default", pointer.String(""), "", "", nil}, + {"No env variable with default allow empty", nil, "default", "default", []StringFromEnvOpts{{AllowEmpty: true}}}, + {"Some variable with default allow empty", pointer.String("true"), "true", "default", []StringFromEnvOpts{{AllowEmpty: true}}}, + {"Empty variable with default allow empty", pointer.String(""), "", "default", []StringFromEnvOpts{{AllowEmpty: true}}}, } for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - t.Setenv(envKey, tt.env) - b := StringFromEnv(envKey, tt.def) + if tt.env != nil { + t.Setenv(envKey, *tt.env) + } + b := StringFromEnv(envKey, tt.def, tt.opts...) assert.Equal(t, tt.expected, b) }) } @@ -210,3 +217,41 @@ func TestStringsFromEnv(t *testing.T) { }) } } + +func TestParseStringToStringFromEnv(t *testing.T) { + envKey := "SOMEKEY" + def := map[string]string{} + + testCases := []struct { + name string + env string + expected map[string]string + def map[string]string + sep string + }{ + {"success, no key-value", "", map[string]string{}, def, ","}, + {"success, one key, no value", "key1=", map[string]string{"key1": ""}, def, ","}, + {"success, one key, no value, with spaces", "key1 = ", map[string]string{"key1": ""}, def, ","}, + {"success, one pair", "key1=value1", map[string]string{"key1": "value1"}, def, ","}, + {"success, one pair with spaces", "key1 = value1", map[string]string{"key1": "value1"}, def, ","}, + {"success, one pair with spaces and no value", "key1 = ", map[string]string{"key1": ""}, def, ","}, + {"success, two keys, no value", "key1=,key2=", map[string]string{"key1": "", "key2": ""}, def, ","}, + {"success, two keys, no value, with spaces", "key1 = , key2 = ", map[string]string{"key1": "", "key2": ""}, def, ","}, + {"success, two pairs", "key1=value1,key2=value2", map[string]string{"key1": "value1", "key2": "value2"}, def, ","}, + {"success, two pairs with semicolon as seperator", "key1=value1;key2=value2", map[string]string{"key1": "value1", "key2": "value2"}, def, ";"}, + {"success, two pairs with spaces", "key1 = value1, key2 = value2", map[string]string{"key1": "value1", "key2": "value2"}, def, ","}, + {"failure, one key", "key1", map[string]string{}, def, ","}, + {"failure, duplicate keys", "key1=value1,key1=value2", map[string]string{}, def, ","}, + {"failure, one key ending with two successive equals to", "key1==", map[string]string{}, def, ","}, + {"failure, one valid pair and invalid one key", "key1=value1,key2", map[string]string{}, def, ","}, + {"failure, two valid pairs and invalid two keys", "key1=value1,key2=value2,key3,key4", map[string]string{}, def, ","}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(envKey, tt.env) + got := ParseStringToStringFromEnv(envKey, tt.def, tt.sep) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/util/git/client.go b/util/git/client.go index 6a8828d13f4321..73c85b54f3c1fc 100644 --- a/util/git/client.go +++ b/util/git/client.go @@ -741,7 +741,6 @@ func (m *nativeGitClient) runCmdOutput(cmd *exec.Cmd, ropts runOpts) (string, er } } } - cmd.Env = proxy.UpsertEnv(cmd, m.proxy) opts := executil.ExecRunOpts{ TimeoutBehavior: argoexec.TimeoutBehavior{ diff --git a/util/git/creds.go b/util/git/creds.go index c3d09574eeb844..18698449082bf3 100644 --- a/util/git/creds.go +++ b/util/git/creds.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "net/url" "os" "strconv" "strings" @@ -241,10 +242,11 @@ type SSHCreds struct { caPath string insecure bool store CredsStore + proxy string } -func NewSSHCreds(sshPrivateKey string, caPath string, insecureIgnoreHostKey bool, store CredsStore) SSHCreds { - return SSHCreds{sshPrivateKey, caPath, insecureIgnoreHostKey, store} +func NewSSHCreds(sshPrivateKey string, caPath string, insecureIgnoreHostKey bool, store CredsStore, proxy string) SSHCreds { + return SSHCreds{sshPrivateKey, caPath, insecureIgnoreHostKey, store, proxy} } type sshPrivateKeyFile string @@ -303,7 +305,25 @@ func (c SSHCreds) Environ() (io.Closer, []string, error) { knownHostsFile := certutil.GetSSHKnownHostsDataPath() args = append(args, "-o", "StrictHostKeyChecking=yes", "-o", fmt.Sprintf("UserKnownHostsFile=%s", knownHostsFile)) } + // Handle SSH socks5 proxy settings + proxyEnv := []string{} + if c.proxy != "" { + parsedProxyURL, err := url.Parse(c.proxy) + if err != nil { + return nil, nil, fmt.Errorf("failed to set environment variables related to socks5 proxy, could not parse proxy URL '%s': %w", c.proxy, err) + } + args = append(args, "-o", fmt.Sprintf("ProxyCommand='connect-proxy -S %s:%s -5 %%h %%p'", + parsedProxyURL.Hostname(), + parsedProxyURL.Port())) + if parsedProxyURL.User != nil { + proxyEnv = append(proxyEnv, fmt.Sprintf("SOCKS5_USER=%s", parsedProxyURL.User.Username())) + if socks5_passwd, isPasswdSet := parsedProxyURL.User.Password(); isPasswdSet { + proxyEnv = append(proxyEnv, fmt.Sprintf("SOCKS5_PASSWD=%s", socks5_passwd)) + } + } + } env = append(env, []string{fmt.Sprintf("GIT_SSH_COMMAND=%s", strings.Join(args, " "))}...) + env = append(env, proxyEnv...) return sshPrivateKeyFile(file.Name()), env, nil } diff --git a/util/git/creds_test.go b/util/git/creds_test.go index 40cc39c10f1bcb..23a705ed335745 100644 --- a/util/git/creds_test.go +++ b/util/git/creds_test.go @@ -205,7 +205,7 @@ func Test_SSHCreds_Environ(t *testing.T) { caFile := path.Join(tempDir, "caFile") err := os.WriteFile(caFile, []byte(""), os.FileMode(0600)) require.NoError(t, err) - creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, &NoopCredsStore{}) + creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, &NoopCredsStore{}, "") closer, env, err := creds.Environ() require.NoError(t, err) require.Len(t, env, 2) @@ -232,6 +232,76 @@ func Test_SSHCreds_Environ(t *testing.T) { } } +func Test_SSHCreds_Environ_WithProxy(t *testing.T) { + for _, insecureIgnoreHostKey := range []bool{false, true} { + tempDir := t.TempDir() + caFile := path.Join(tempDir, "caFile") + err := os.WriteFile(caFile, []byte(""), os.FileMode(0600)) + require.NoError(t, err) + creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, &NoopCredsStore{}, "socks5://127.0.0.1:1080") + closer, env, err := creds.Environ() + require.NoError(t, err) + require.Len(t, env, 2) + + assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set") + + assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND=")) + + if insecureIgnoreHostKey { + assert.Contains(t, env[1], "-o StrictHostKeyChecking=no") + assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null") + } else { + assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes") + hostsPath := cert.GetSSHKnownHostsDataPath() + assert.Contains(t, env[1], fmt.Sprintf("-o UserKnownHostsFile=%s", hostsPath)) + } + assert.Contains(t, env[1], "-o ProxyCommand='connect-proxy -S 127.0.0.1:1080 -5 %h %p'") + + envRegex := regexp.MustCompile("-i ([^ ]+)") + assert.Regexp(t, envRegex, env[1]) + privateKeyFile := envRegex.FindStringSubmatch(env[1])[1] + assert.FileExists(t, privateKeyFile) + io.Close(closer) + assert.NoFileExists(t, privateKeyFile) + } +} + +func Test_SSHCreds_Environ_WithProxyUserNamePassword(t *testing.T) { + for _, insecureIgnoreHostKey := range []bool{false, true} { + tempDir := t.TempDir() + caFile := path.Join(tempDir, "caFile") + err := os.WriteFile(caFile, []byte(""), os.FileMode(0600)) + require.NoError(t, err) + creds := NewSSHCreds("sshPrivateKey", caFile, insecureIgnoreHostKey, &NoopCredsStore{}, "socks5://user:password@127.0.0.1:1080") + closer, env, err := creds.Environ() + require.NoError(t, err) + require.Len(t, env, 4) + + assert.Equal(t, fmt.Sprintf("GIT_SSL_CAINFO=%s/caFile", tempDir), env[0], "CAINFO env var must be set") + + assert.True(t, strings.HasPrefix(env[1], "GIT_SSH_COMMAND=")) + assert.Equal(t, "SOCKS5_USER=user", env[2], "SOCKS5 user env var must be set") + assert.Equal(t, "SOCKS5_PASSWD=password", env[3], "SOCKS5 password env var must be set") + + if insecureIgnoreHostKey { + assert.Contains(t, env[1], "-o StrictHostKeyChecking=no") + assert.Contains(t, env[1], "-o UserKnownHostsFile=/dev/null") + } else { + assert.Contains(t, env[1], "-o StrictHostKeyChecking=yes") + hostsPath := cert.GetSSHKnownHostsDataPath() + assert.Contains(t, env[1], fmt.Sprintf("-o UserKnownHostsFile=%s", hostsPath)) + } + assert.Contains(t, env[1], "-o ProxyCommand='connect-proxy -S 127.0.0.1:1080 -5 %h %p'") + + envRegex := regexp.MustCompile("-i ([^ ]+)") + assert.Regexp(t, envRegex, env[1]) + privateKeyFile := envRegex.FindStringSubmatch(env[1])[1] + assert.FileExists(t, privateKeyFile) + io.Close(closer) + assert.NoFileExists(t, privateKeyFile) + } +} + const gcpServiceAccountKeyJSON = `{ "type": "service_account", "project_id": "my-google-project", diff --git a/util/git/workaround.go b/util/git/workaround.go index c364c093c853ea..47636125cf3491 100644 --- a/util/git/workaround.go +++ b/util/git/workaround.go @@ -1,6 +1,9 @@ package git import ( + "fmt" + neturl "net/url" + "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/transport" @@ -30,6 +33,23 @@ func newClient(url string, insecure bool, creds Creds, proxy string) (transport. if !IsHTTPSURL(url) && !IsHTTPURL(url) { // use the default client for protocols other than HTTP/HTTPS + ep.InsecureSkipTLS = insecure + if proxy != "" { + parsedProxyURL, err := neturl.Parse(proxy) + if err != nil { + return nil, nil, fmt.Errorf("failed to create client for url '%s', error parsing proxy url '%s': %w", url, proxy, err) + } + var proxyUsername, proxyPasswd string + if parsedProxyURL.User != nil { + proxyUsername = parsedProxyURL.User.Username() + proxyPasswd, _ = parsedProxyURL.User.Password() + } + ep.Proxy = transport.ProxyOptions{ + URL: fmt.Sprintf("%s://%s:%s", parsedProxyURL.Scheme, parsedProxyURL.Hostname(), parsedProxyURL.Port()), + Username: proxyUsername, + Password: proxyPasswd, + } + } c, err := client.NewClient(ep) if err != nil { return nil, nil, err diff --git a/util/grpc/grpc.go b/util/grpc/grpc.go index 1741727a5d604c..536da792e30482 100644 --- a/util/grpc/grpc.go +++ b/util/grpc/grpc.go @@ -10,6 +10,7 @@ import ( "github.com/argoproj/argo-cd/v2/common" "github.com/sirupsen/logrus" + "golang.org/x/net/proxy" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" @@ -63,7 +64,7 @@ func BlockingDial(ctx context.Context, network, address string, creds credential dialer := func(ctx context.Context, address string) (net.Conn, error) { - conn, err := (&net.Dialer{Cancel: ctx.Done()}).Dial(network, address) + conn, err := proxy.Dial(ctx, network, address) if err != nil { writeResult(err) return nil, err diff --git a/util/helm/cmd.go b/util/helm/cmd.go index 419c7daff5d3ce..cc2a1388d65a22 100644 --- a/util/helm/cmd.go +++ b/util/helm/cmd.go @@ -91,6 +91,28 @@ func (c *Cmd) RegistryLogin(repo string, creds Creds) (string, error) { args = append(args, "--password", creds.Password) } + if creds.CAPath != "" { + args = append(args, "--ca-file", creds.CAPath) + } + + if len(creds.CertData) > 0 { + filePath, closer, err := writeToTmp(creds.CertData) + if err != nil { + return "", err + } + defer argoio.Close(closer) + args = append(args, "--cert-file", filePath) + } + + if len(creds.KeyData) > 0 { + filePath, closer, err := writeToTmp(creds.KeyData) + if err != nil { + return "", err + } + defer argoio.Close(closer) + args = append(args, "--key-file", filePath) + } + if creds.InsecureSkipVerify { args = append(args, "--insecure") } @@ -238,6 +260,25 @@ func (c *Cmd) PullOCI(repo string, chart string, version string, destination str if creds.CAPath != "" { args = append(args, "--ca-file", creds.CAPath) } + + if len(creds.CertData) > 0 { + filePath, closer, err := writeToTmp(creds.CertData) + if err != nil { + return "", err + } + defer argoio.Close(closer) + args = append(args, "--cert-file", filePath) + } + + if len(creds.KeyData) > 0 { + filePath, closer, err := writeToTmp(creds.KeyData) + if err != nil { + return "", err + } + defer argoio.Close(closer) + args = append(args, "--key-file", filePath) + } + if creds.InsecureSkipVerify && c.insecureSkipVerifySupported { args = append(args, "--insecure-skip-tls-verify") } diff --git a/util/http/http.go b/util/http/http.go index fcf8447e1e2f64..7c13c71fde223b 100644 --- a/util/http/http.go +++ b/util/http/http.go @@ -30,7 +30,7 @@ const ( // max number of chunks a cookie can be broken into. To be compatible with // widest range of browsers, you shouldn't create more than 30 cookies per domain -var maxCookieNumber = env.ParseNumFromEnv(common.EnvMaxCookieNumber, 20, 0, math.MaxInt64) +var maxCookieNumber = env.ParseNumFromEnv(common.EnvMaxCookieNumber, 20, 0, math.MaxInt) // MakeCookieMetadata generates a string representing a Web cookie. Yum! func MakeCookieMetadata(key, value string, flags ...string) ([]string, error) { diff --git a/util/kustomize/kustomize.go b/util/kustomize/kustomize.go index 2487b14b4903b3..5c7ac51ac6122d 100644 --- a/util/kustomize/kustomize.go +++ b/util/kustomize/kustomize.go @@ -35,8 +35,9 @@ type Kustomize interface { } // NewKustomizeApp create a new wrapper to run commands on the `kustomize` command-line tool. -func NewKustomizeApp(path string, creds git.Creds, fromRepo string, binaryPath string) Kustomize { +func NewKustomizeApp(repoRoot string, path string, creds git.Creds, fromRepo string, binaryPath string) Kustomize { return &kustomize{ + repoRoot: repoRoot, path: path, creds: creds, repo: fromRepo, @@ -45,6 +46,8 @@ func NewKustomizeApp(path string, creds git.Creds, fromRepo string, binaryPath s } type kustomize struct { + // path to the Git repository root + repoRoot string // path inside the checked out tree path string // creds structure @@ -181,6 +184,9 @@ func (k *kustomize) Build(opts *v1alpha1.ApplicationSourceKustomize, kustomizeOp if opts.ForceCommonLabels { args = append(args, "--force") } + if opts.LabelWithoutSelector { + args = append(args, "--without-selector") + } commonLabels := map[string]string{} for name, value := range opts.CommonLabels { commonLabels[name] = envVars.Envsubst(value) @@ -301,6 +307,7 @@ func (k *kustomize) Build(opts *v1alpha1.ApplicationSourceKustomize, kustomizeOp cmd = exec.Command(k.getBinaryPath(), "build", k.path) } cmd.Env = env + cmd.Dir = k.repoRoot out, err := executil.Run(cmd) if err != nil { return nil, nil, err @@ -315,7 +322,7 @@ func (k *kustomize) Build(opts *v1alpha1.ApplicationSourceKustomize, kustomizeOp } func parseKustomizeBuildOptions(path, buildOptions string) []string { - return append([]string{"build", path}, strings.Split(buildOptions, " ")...) + return append([]string{"build", path}, strings.Fields(buildOptions)...) } var KustomizationNames = []string{"kustomization.yaml", "kustomization.yml", "Kustomization"} diff --git a/util/kustomize/kustomize_test.go b/util/kustomize/kustomize_test.go index a6275cf01ae1b4..cf57daaf4128c5 100644 --- a/util/kustomize/kustomize_test.go +++ b/util/kustomize/kustomize_test.go @@ -24,6 +24,7 @@ const kustomization3 = "force_common" const kustomization4 = "custom_version" const kustomization5 = "kustomization_yaml_patches" const kustomization6 = "kustomization_yaml_components" +const kustomization7 = "label_without_selector" func testDataDir(tb testing.TB, testData string) (string, error) { res := tb.TempDir() @@ -40,7 +41,7 @@ func TestKustomizeBuild(t *testing.T) { namePrefix := "namePrefix-" nameSuffix := "-nameSuffix" namespace := "custom-namespace" - kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "") + kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "") env := &v1alpha1.Env{ &v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: "argo-cd-tests"}, } @@ -123,7 +124,7 @@ func TestKustomizeBuild(t *testing.T) { func TestFailKustomizeBuild(t *testing.T) { appPath, err := testDataDir(t, kustomization1) assert.Nil(t, err) - kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "") + kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "") kustomizeSource := v1alpha1.ApplicationSourceKustomize{ Replicas: []v1alpha1.KustomizeReplica{ { @@ -222,7 +223,7 @@ func TestKustomizeBuildForceCommonLabels(t *testing.T) { for _, tc := range testCases { appPath, err := testDataDir(t, tc.TestData) assert.Nil(t, err) - kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "") + kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "") objs, _, err := kustomize.Build(&tc.KustomizeSource, nil, tc.Env) switch tc.ExpectErr { case true: @@ -314,7 +315,7 @@ func TestKustomizeBuildForceCommonAnnotations(t *testing.T) { for _, tc := range testCases { appPath, err := testDataDir(t, tc.TestData) assert.Nil(t, err) - kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "") + kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "") objs, _, err := kustomize.Build(&tc.KustomizeSource, nil, tc.Env) switch tc.ExpectErr { case true: @@ -328,13 +329,91 @@ func TestKustomizeBuildForceCommonAnnotations(t *testing.T) { } } +func TestKustomizeLabelWithoutSelector(t *testing.T) { + type testCase struct { + TestData string + KustomizeSource v1alpha1.ApplicationSourceKustomize + ExpectedMetadataLabels map[string]string + ExpectedSelectorLabels map[string]string + ExpectedTemplateLabels map[string]string + ExpectErr bool + Env *v1alpha1.Env + } + testCases := []testCase{ + { + TestData: kustomization7, + KustomizeSource: v1alpha1.ApplicationSourceKustomize{ + CommonLabels: map[string]string{ + "foo": "bar", + }, + LabelWithoutSelector: true, + }, + ExpectedMetadataLabels: map[string]string{"app": "nginx", "managed-by": "helm", "foo": "bar"}, + ExpectedSelectorLabels: map[string]string{"app": "nginx"}, + ExpectedTemplateLabels: map[string]string{"app": "nginx"}, + Env: &v1alpha1.Env{ + &v1alpha1.EnvEntry{ + Name: "ARGOCD_APP_NAME", + Value: "argo-cd-tests", + }, + }, + }, + { + TestData: kustomization7, + KustomizeSource: v1alpha1.ApplicationSourceKustomize{ + CommonLabels: map[string]string{ + "managed-by": "argocd", + }, + LabelWithoutSelector: true, + ForceCommonLabels: true, + }, + ExpectedMetadataLabels: map[string]string{"app": "nginx", "managed-by": "argocd"}, + ExpectedSelectorLabels: map[string]string{"app": "nginx"}, + ExpectedTemplateLabels: map[string]string{"app": "nginx"}, + Env: &v1alpha1.Env{ + &v1alpha1.EnvEntry{ + Name: "ARGOCD_APP_NAME", + Value: "argo-cd-tests", + }, + }, + }, + } + + for _, tc := range testCases { + appPath, err := testDataDir(t, tc.TestData) + assert.Nil(t, err) + kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "") + objs, _, err := kustomize.Build(&tc.KustomizeSource, nil, tc.Env) + + switch tc.ExpectErr { + case true: + assert.Error(t, err) + default: + assert.Nil(t, err) + if assert.Equal(t, len(objs), 1) { + obj := objs[0] + sl, found, err := unstructured.NestedStringMap(obj.Object, "spec", "selector", "matchLabels") + assert.Nil(t, err) + assert.Equal(t, found, true) + tl, found, err := unstructured.NestedStringMap(obj.Object, "spec", "template", "metadata", "labels") + assert.Nil(t, err) + assert.Equal(t, found, true) + assert.Equal(t, tc.ExpectedMetadataLabels, obj.GetLabels()) + assert.Equal(t, tc.ExpectedSelectorLabels, sl) + assert.Equal(t, tc.ExpectedTemplateLabels, tl) + } + } + } + +} + func TestKustomizeCustomVersion(t *testing.T) { appPath, err := testDataDir(t, kustomization1) assert.Nil(t, err) kustomizePath, err := testDataDir(t, kustomization4) assert.Nil(t, err) envOutputFile := kustomizePath + "/env_output" - kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", kustomizePath+"/kustomize.special") + kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", kustomizePath+"/kustomize.special") kustomizeSource := v1alpha1.ApplicationSourceKustomize{ Version: "special", } @@ -356,7 +435,7 @@ func TestKustomizeCustomVersion(t *testing.T) { func TestKustomizeBuildComponents(t *testing.T) { appPath, err := testDataDir(t, kustomization6) assert.Nil(t, err) - kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "") + kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "") kustomizeSource := v1alpha1.ApplicationSourceKustomize{ Components: []string{"./components"}, @@ -377,7 +456,7 @@ func TestKustomizeBuildComponents(t *testing.T) { func TestKustomizeBuildPatches(t *testing.T) { appPath, err := testDataDir(t, kustomization5) assert.Nil(t, err) - kustomize := NewKustomizeApp(appPath, git.NopCreds{}, "", "") + kustomize := NewKustomizeApp(appPath, appPath, git.NopCreds{}, "", "") kustomizeSource := v1alpha1.ApplicationSourceKustomize{ Patches: []v1alpha1.KustomizePatch{ diff --git a/util/kustomize/testdata/label_without_selector/deployment.yaml b/util/kustomize/testdata/label_without_selector/deployment.yaml new file mode 100644 index 00000000000000..fa161556fb2b03 --- /dev/null +++ b/util/kustomize/testdata/label_without_selector/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx + managed-by: helm +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.15.4 + ports: + - containerPort: 80 diff --git a/util/kustomize/testdata/label_without_selector/kustomization.yaml b/util/kustomize/testdata/label_without_selector/kustomization.yaml new file mode 100644 index 00000000000000..9e90331c9bca0d --- /dev/null +++ b/util/kustomize/testdata/label_without_selector/kustomization.yaml @@ -0,0 +1,2 @@ +resources: + - ./deployment.yaml \ No newline at end of file diff --git a/util/notification/argocd/service.go b/util/notification/argocd/service.go index 61e0bebeca3c69..426217318ce317 100644 --- a/util/notification/argocd/service.go +++ b/util/notification/argocd/service.go @@ -18,7 +18,7 @@ import ( type Service interface { GetCommitMetadata(ctx context.Context, repoURL string, commitSHA string) (*shared.CommitMetadata, error) - GetAppDetails(ctx context.Context, appSource *v1alpha1.ApplicationSource) (*shared.AppDetail, error) + GetAppDetails(ctx context.Context, appSource *v1alpha1.ApplicationSource, appName string) (*shared.AppDetail, error) } func NewArgoCDService(clientset kubernetes.Interface, namespace string, repoClientset apiclient.Clientset) (*argoCDService, error) { @@ -76,7 +76,7 @@ func (svc *argoCDService) getKustomizeOptions(source *v1alpha1.ApplicationSource return kustomizeSettings.GetOptions(*source) } -func (svc *argoCDService) GetAppDetails(ctx context.Context, appSource *v1alpha1.ApplicationSource) (*shared.AppDetail, error) { +func (svc *argoCDService) GetAppDetails(ctx context.Context, appSource *v1alpha1.ApplicationSource, appName string) (*shared.AppDetail, error) { argocdDB := db.NewDB(svc.namespace, svc.settingsMgr, svc.clientset) repo, err := argocdDB.GetRepository(ctx, appSource.RepoURL) if err != nil { @@ -95,6 +95,7 @@ func (svc *argoCDService) GetAppDetails(ctx context.Context, appSource *v1alpha1 return nil, err } appDetail, err := svc.repoServerClient.GetAppDetails(ctx, &apiclient.RepoServerAppDetailsQuery{ + AppName: appName, Repo: repo, Source: appSource, Repos: helmRepos, diff --git a/util/notification/expression/repo/repo.go b/util/notification/expression/repo/repo.go index 060060cbccd685..a782c0b7c17256 100644 --- a/util/notification/expression/repo/repo.go +++ b/util/notification/expression/repo/repo.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "net/url" "regexp" "strings" @@ -22,25 +23,25 @@ var ( gitSuffix = regexp.MustCompile(`\.git$`) ) -func getApplicationSource(obj *unstructured.Unstructured) (*v1alpha1.ApplicationSource, error) { +func getApplicationSourceAndName(obj *unstructured.Unstructured) (*v1alpha1.ApplicationSource, string, error) { data, err := json.Marshal(obj) if err != nil { - return nil, err + return nil, "", err } application := &v1alpha1.Application{} err = json.Unmarshal(data, application) if err != nil { - return nil, err + return nil, "", err } - return application.Spec.GetSourcePtr(), nil + return application.Spec.GetSourcePtr(), application.GetName(), nil } func getAppDetails(app *unstructured.Unstructured, argocdService service.Service) (*shared.AppDetail, error) { - appSource, err := getApplicationSource(app) + appSource, appName, err := getApplicationSourceAndName(app) if err != nil { return nil, err } - appDetail, err := argocdService.GetAppDetails(context.Background(), appSource) + appDetail, err := argocdService.GetAppDetails(context.Background(), appSource, appName) if err != nil { return nil, err } @@ -90,6 +91,7 @@ func NewExprs(argocdService service.Service, app *unstructured.Unstructured) map return map[string]interface{}{ "RepoURLToHTTPS": repoURLToHTTPS, "FullNameByRepoURL": FullNameByRepoURL, + "QueryEscape": url.QueryEscape, "GetCommitMetadata": func(commitSHA string) interface{} { meta, err := getCommitMetadata(commitSHA, app, argocdService) if err != nil { diff --git a/util/notification/settings/settings.go b/util/notification/settings/settings.go index ed6a44b60f3656..79d70499aaea68 100644 --- a/util/notification/settings/settings.go +++ b/util/notification/settings/settings.go @@ -12,17 +12,20 @@ import ( service "github.com/argoproj/argo-cd/v2/util/notification/argocd" ) -func GetFactorySettings(argocdService service.Service, secretName, configMapName string) api.Settings { +func GetFactorySettings(argocdService service.Service, secretName, configMapName string, selfServiceNotificationEnabled bool) api.Settings { return api.Settings{ SecretName: secretName, ConfigMapName: configMapName, InitGetVars: func(cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (api.GetVars, error) { + if selfServiceNotificationEnabled { + return initGetVarsWithoutSecret(argocdService, cfg, configMap, secret) + } return initGetVars(argocdService, cfg, configMap, secret) }, } } -func initGetVars(argocdService service.Service, cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (api.GetVars, error) { +func getContext(cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (map[string]string, error) { context := map[string]string{} if contextYaml, ok := configMap.Data["context"]; ok { if err := yaml.Unmarshal([]byte(contextYaml), &context); err != nil { @@ -32,6 +35,28 @@ func initGetVars(argocdService service.Service, cfg *api.Config, configMap *v1.C if err := ApplyLegacyConfig(cfg, context, configMap, secret); err != nil { return nil, err } + return context, nil +} + +func initGetVarsWithoutSecret(argocdService service.Service, cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (api.GetVars, error) { + context, err := getContext(cfg, configMap, secret) + if err != nil { + return nil, err + } + + return func(obj map[string]interface{}, dest services.Destination) map[string]interface{} { + return expression.Spawn(&unstructured.Unstructured{Object: obj}, argocdService, map[string]interface{}{ + "app": obj, + "context": injectLegacyVar(context, dest.Service), + }) + }, nil +} + +func initGetVars(argocdService service.Service, cfg *api.Config, configMap *v1.ConfigMap, secret *v1.Secret) (api.GetVars, error) { + context, err := getContext(cfg, configMap, secret) + if err != nil { + return nil, err + } return func(obj map[string]interface{}, dest services.Destination) map[string]interface{} { return expression.Spawn(&unstructured.Unstructured{Object: obj}, argocdService, map[string]interface{}{ diff --git a/util/notification/settings/settings_test.go b/util/notification/settings/settings_test.go index 21c2eaf416d37a..176839b51740e7 100644 --- a/util/notification/settings/settings_test.go +++ b/util/notification/settings/settings_test.go @@ -27,7 +27,7 @@ func TestInitGetVars(t *testing.T) { }, Data: map[string]string{ "context": fmt.Sprintf("%s: %s", testContextKey, testContextKeyValue), - "service.webhook.test": "url: https://test.com", + "service.webhook.test": "url: https://test.example.com", "template.app-created": "email:\n subject: Application {{.app.metadata.name}} has been created.\nmessage: Application {{.app.metadata.name}} has been created.\nteams:\n title: Application {{.app.metadata.name}} has been created.\n", "trigger.on-created": "- description: Application is created.\n oncePer: app.metadata.name\n send:\n - app-created\n when: \"true\"\n", }, diff --git a/util/oidc/oidc.go b/util/oidc/oidc.go index 3df31664901729..2c376cc7e5b5ba 100644 --- a/util/oidc/oidc.go +++ b/util/oidc/oidc.go @@ -6,6 +6,7 @@ import ( "fmt" "html" "html/template" + "io" "net" "net/http" "net/url" @@ -21,9 +22,12 @@ import ( "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/server/settings/oidc" + "github.com/argoproj/argo-cd/v2/util/cache" "github.com/argoproj/argo-cd/v2/util/crypto" "github.com/argoproj/argo-cd/v2/util/dex" + httputil "github.com/argoproj/argo-cd/v2/util/http" + jwtutil "github.com/argoproj/argo-cd/v2/util/jwt" "github.com/argoproj/argo-cd/v2/util/rand" "github.com/argoproj/argo-cd/v2/util/settings" ) @@ -31,9 +35,11 @@ import ( var InvalidRedirectURLError = fmt.Errorf("invalid return URL") const ( - GrantTypeAuthorizationCode = "authorization_code" - GrantTypeImplicit = "implicit" - ResponseTypeCode = "code" + GrantTypeAuthorizationCode = "authorization_code" + GrantTypeImplicit = "implicit" + ResponseTypeCode = "code" + UserInfoResponseCachePrefix = "userinfo_response" + AccessTokenCachePrefix = "access_token" ) // OIDCConfiguration holds a subset of interested fields from the OIDC configuration spec @@ -57,6 +63,8 @@ type ClientApp struct { redirectURI string // URL of the issuer (e.g. https://argocd.example.com/api/dex) issuerURL string + // the path where the issuer providers user information (e.g /user-info for okta) + userInfoPath string // The URL endpoint at which the ArgoCD server is accessed. baseHRef string // client is the HTTP client which is used to query the IDp @@ -70,6 +78,8 @@ type ClientApp struct { encryptionKey []byte // provider is the OIDC provider provider Provider + // clientCache represent a cache of sso artifact + clientCache cache.CacheClient } func GetScopesOrDefault(scopes []string) []string { @@ -81,7 +91,7 @@ func GetScopesOrDefault(scopes []string) []string { // NewClientApp will register the Argo CD client app (either via Dex or external OIDC) and return an // object which has HTTP handlers for handling the HTTP responses for login and callback -func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTlsConfig *dex.DexTLSConfig, baseHRef string) (*ClientApp, error) { +func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTlsConfig *dex.DexTLSConfig, baseHRef string, cacheClient cache.CacheClient) (*ClientApp, error) { redirectURL, err := settings.RedirectURL() if err != nil { return nil, err @@ -95,8 +105,10 @@ func NewClientApp(settings *settings.ArgoCDSettings, dexServerAddr string, dexTl clientSecret: settings.OAuth2ClientSecret(), redirectURI: redirectURL, issuerURL: settings.IssuerURL(), + userInfoPath: settings.UserInfoPath(), baseHRef: baseHRef, encryptionKey: encryptionKey, + clientCache: cacheClient, } log.Infof("Creating client app (%s)", a.clientID) u, err := url.Parse(settings.URL) @@ -376,6 +388,26 @@ func (a *ClientApp) HandleCallback(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } + // save the accessToken in memory for later use + encToken, err := crypto.Encrypt([]byte(token.AccessToken), a.encryptionKey) + if err != nil { + claimsJSON, _ := json.Marshal(claims) + http.Error(w, "failed encrypting token", http.StatusInternalServerError) + log.Errorf("cannot encrypt accessToken: %v (claims=%s)", err, claimsJSON) + return + } + sub := jwtutil.StringField(claims, "sub") + err = a.clientCache.Set(&cache.Item{ + Key: formatAccessTokenCacheKey(AccessTokenCachePrefix, sub), + Object: encToken, + Expiration: getTokenExpiration(claims), + }) + if err != nil { + claimsJSON, _ := json.Marshal(claims) + http.Error(w, fmt.Sprintf("claims=%s, err=%v", claimsJSON, err), http.StatusInternalServerError) + return + } + if idTokenRAW != "" { cookies, err := httputil.MakeCookieMetadata(common.AuthCookieName, idTokenRAW, flags...) if err != nil { @@ -509,3 +541,145 @@ func createClaimsAuthenticationRequestParameter(requestedClaims map[string]*oidc } return oauth2.SetAuthURLParam("claims", string(claimsRequestRAW)), nil } + +// GetUserInfo queries the IDP userinfo endpoint for claims +func (a *ClientApp) GetUserInfo(actualClaims jwt.MapClaims, issuerURL, userInfoPath string) (jwt.MapClaims, bool, error) { + sub := jwtutil.StringField(actualClaims, "sub") + var claims jwt.MapClaims + var encClaims []byte + + // in case we got it in the cache, we just return the item + clientCacheKey := formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, sub) + if err := a.clientCache.Get(clientCacheKey, &encClaims); err == nil { + claimsRaw, err := crypto.Decrypt(encClaims, a.encryptionKey) + if err != nil { + log.Errorf("decrypting the cached claims failed (sub=%s): %s", sub, err) + } else { + err = json.Unmarshal(claimsRaw, &claims) + if err != nil { + log.Errorf("cannot unmarshal cached claims structure: %s", err) + } else { + // return the cached claims since they are not yet expired, were successfully decrypted and unmarshaled + return claims, false, err + } + } + } + + // check if the accessToken for the user is still present + var encAccessToken []byte + err := a.clientCache.Get(formatAccessTokenCacheKey(AccessTokenCachePrefix, sub), &encAccessToken) + // without an accessToken we can't query the user info endpoint + // thus the user needs to reauthenticate for argocd to get a new accessToken + if err == cache.ErrCacheMiss { + return claims, true, fmt.Errorf("no accessToken for %s: %w", sub, err) + } else if err != nil { + return claims, true, fmt.Errorf("couldn't read accessToken from cache for %s: %w", sub, err) + } + + accessToken, err := crypto.Decrypt(encAccessToken, a.encryptionKey) + if err != nil { + return claims, true, fmt.Errorf("couldn't decrypt accessToken for %s: %w", sub, err) + } + + url := issuerURL + userInfoPath + request, err := http.NewRequest("GET", url, nil) + + if err != nil { + err = fmt.Errorf("failed creating new http request: %w", err) + return claims, false, err + } + + bearer := fmt.Sprintf("Bearer %s", accessToken) + request.Header.Set("Authorization", bearer) + + response, err := a.client.Do(request) + if err != nil { + return claims, false, fmt.Errorf("failed to query userinfo endpoint of IDP: %w", err) + } + defer response.Body.Close() + if response.StatusCode == http.StatusUnauthorized { + return claims, true, err + } + + // according to https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponseValidation + // the response should be validated + header := response.Header.Get("content-type") + rawBody, err := io.ReadAll(response.Body) + if err != nil { + return claims, false, fmt.Errorf("got error reading response body: %w", err) + } + switch header { + case "application/jwt": + // if body is JWT, first validate it before extracting claims + idToken, err := a.provider.Verify(string(rawBody), a.settings) + if err != nil { + return claims, false, fmt.Errorf("user info response in jwt format not valid: %w", err) + } + err = idToken.Claims(claims) + if err != nil { + return claims, false, fmt.Errorf("cannot get claims from userinfo jwt: %w", err) + } + default: + // if body is json, unsigned and unencrypted claims can be deserialized + err = json.Unmarshal(rawBody, &claims) + if err != nil { + return claims, false, fmt.Errorf("failed to decode response body to struct: %w", err) + } + } + + // in case response was successfully validated and there was no error, put item in cache + // but first let's determine the expiry of the cache + var cacheExpiry time.Duration + settingExpiry := a.settings.UserInfoCacheExpiration() + tokenExpiry := getTokenExpiration(claims) + + // only use configured expiry if the token lives longer and the expiry is configured + // if the token has no expiry, use the expiry of the actual token + // otherwise use the expiry of the token + if settingExpiry < tokenExpiry && settingExpiry != 0 { + cacheExpiry = settingExpiry + } else if tokenExpiry < 0 { + cacheExpiry = getTokenExpiration(actualClaims) + } else { + cacheExpiry = tokenExpiry + } + + rawClaims, err := json.Marshal(claims) + if err != nil { + return claims, false, fmt.Errorf("couldn't marshal claim to json: %w", err) + } + encClaims, err = crypto.Encrypt(rawClaims, a.encryptionKey) + if err != nil { + return claims, false, fmt.Errorf("couldn't encrypt user info response: %w", err) + } + + err = a.clientCache.Set(&cache.Item{ + Key: clientCacheKey, + Object: encClaims, + Expiration: cacheExpiry, + }) + if err != nil { + return claims, false, fmt.Errorf("couldn't put item to cache: %w", err) + } + + return claims, false, nil +} + +// getTokenExpiration returns a time.Duration until the token expires +func getTokenExpiration(claims jwt.MapClaims) time.Duration { + // get duration until token expires + exp := jwtutil.Float64Field(claims, "exp") + tm := time.Unix(int64(exp), 0) + tokenExpiry := time.Until(tm) + return tokenExpiry +} + +// formatUserInfoResponseCacheKey returns the key which is used to store userinfo of user in cache +func formatUserInfoResponseCacheKey(prefix, sub string) string { + return fmt.Sprintf("%s_%s", UserInfoResponseCachePrefix, sub) +} + +// formatAccessTokenCacheKey returns the key which is used to store the accessToken of a user in cache +func formatAccessTokenCacheKey(prefix, sub string) string { + return fmt.Sprintf("%s_%s", prefix, sub) +} diff --git a/util/oidc/oidc_test.go b/util/oidc/oidc_test.go index fe5fa77eed3b5a..cd1d3fa1bf7890 100644 --- a/util/oidc/oidc_test.go +++ b/util/oidc/oidc_test.go @@ -11,8 +11,10 @@ import ( "os" "strings" "testing" + "time" gooidc "github.com/coreos/go-oidc/v3/oidc" + "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" @@ -20,6 +22,7 @@ import ( "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/server/settings/oidc" "github.com/argoproj/argo-cd/v2/util" + "github.com/argoproj/argo-cd/v2/util/cache" "github.com/argoproj/argo-cd/v2/util/crypto" "github.com/argoproj/argo-cd/v2/util/dex" "github.com/argoproj/argo-cd/v2/util/settings" @@ -126,7 +129,7 @@ clientID: xxx clientSecret: yyy requestedScopes: ["oidc"]`, oidcTestServer.URL), } - app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", nil) @@ -141,7 +144,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), cdSettings.OIDCTLSInsecureSkipVerify = true - app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) w = httptest.NewRecorder() @@ -166,7 +169,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), require.NoError(t, err) cdSettings.Certificate = &cert - app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/login", nil) @@ -179,7 +182,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), t.Fatal("did not receive expected certificate verification failure error") } - app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com") + app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) w = httptest.NewRecorder() @@ -211,7 +214,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), // The base href (the last argument for NewClientApp) is what HandleLogin will fall back to when no explicit // redirect URL is given. - app, err := NewClientApp(cdSettings, "", nil, "/") + app, err := NewClientApp(cdSettings, "", nil, "/", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) w := httptest.NewRecorder() @@ -254,7 +257,7 @@ clientID: xxx clientSecret: yyy requestedScopes: ["oidc"]`, oidcTestServer.URL), } - app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/callback", nil) @@ -269,7 +272,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), cdSettings.OIDCTLSInsecureSkipVerify = true - app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err = NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) w = httptest.NewRecorder() @@ -294,7 +297,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), require.NoError(t, err) cdSettings.Certificate = &cert - app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com") + app, err := NewClientApp(cdSettings, dexTestServer.URL, nil, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "https://argocd.example.com/auth/callback", nil) @@ -307,7 +310,7 @@ requestedScopes: ["oidc"]`, oidcTestServer.URL), t.Fatal("did not receive expected certificate verification failure error") } - app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com") + app, err = NewClientApp(cdSettings, dexTestServer.URL, &dex.DexTLSConfig{StrictValidation: false}, "https://argocd.example.com", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) w = httptest.NewRecorder() @@ -406,7 +409,7 @@ func TestGenerateAppState(t *testing.T) { signature, err := util.MakeSignature(32) require.NoError(t, err) expectedReturnURL := "http://argocd.example.com/" - app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", nil, "") + app, err := NewClientApp(&settings.ArgoCDSettings{ServerSignature: signature, URL: expectedReturnURL}, "", nil, "", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) generateResponse := httptest.NewRecorder() state, err := app.generateAppState(expectedReturnURL, generateResponse) @@ -443,7 +446,7 @@ func TestGenerateAppState_XSS(t *testing.T) { URL: "https://argocd.example.com", ServerSignature: signature, }, - "", nil, "", + "", nil, "", cache.NewInMemoryCache(24*time.Hour), ) require.NoError(t, err) @@ -495,7 +498,7 @@ func TestGenerateAppState_NoReturnURL(t *testing.T) { encrypted, err := crypto.Encrypt([]byte("123"), key) require.NoError(t, err) - app, err := NewClientApp(cdSettings, "", nil, "/argo-cd") + app, err := NewClientApp(cdSettings, "", nil, "/argo-cd", cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) req.AddCookie(&http.Cookie{Name: common.StateCookieName, Value: hex.EncodeToString(encrypted)}) @@ -503,3 +506,270 @@ func TestGenerateAppState_NoReturnURL(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "/argo-cd", returnURL) } + +func TestGetUserInfo(t *testing.T) { + + var tests = []struct { + name string + userInfoPath string + expectedOutput interface{} + expectError bool + expectUnauthenticated bool + expectedCacheItems []struct { // items to check in cache after function call + key string + value string + expectEncrypted bool + expectError bool + } + idpHandler func(w http.ResponseWriter, r *http.Request) + idpClaims jwt.MapClaims // as per specification sub and exp are REQUIRED fields + cache cache.CacheClient + cacheItems []struct { // items to put in cache before execution + key string + value string + encrypt bool + } + }{ + { + name: "call UserInfo with wrong userInfoPath", + userInfoPath: "/user", + expectedOutput: jwt.MapClaims(nil), + expectError: true, + expectUnauthenticated: false, + expectedCacheItems: []struct { + key string + value string + expectEncrypted bool + expectError bool + }{ + { + key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), + expectError: true, + }, + }, + idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, + idpHandler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + }, + cache: cache.NewInMemoryCache(24 * time.Hour), + cacheItems: []struct { + key string + value string + encrypt bool + }{ + { + key: formatAccessTokenCacheKey(AccessTokenCachePrefix, "randomUser"), + value: "FakeAccessToken", + encrypt: true, + }, + }, + }, + { + name: "call UserInfo with bad accessToken", + userInfoPath: "/user-info", + expectedOutput: jwt.MapClaims(nil), + expectError: false, + expectUnauthenticated: true, + expectedCacheItems: []struct { + key string + value string + expectEncrypted bool + expectError bool + }{ + { + key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), + expectError: true, + }, + }, + idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, + idpHandler: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusUnauthorized) + }, + cache: cache.NewInMemoryCache(24 * time.Hour), + cacheItems: []struct { + key string + value string + encrypt bool + }{ + { + key: formatAccessTokenCacheKey(AccessTokenCachePrefix, "randomUser"), + value: "FakeAccessToken", + encrypt: true, + }, + }, + }, + { + name: "call UserInfo with garbage returned", + userInfoPath: "/user-info", + expectedOutput: jwt.MapClaims(nil), + expectError: true, + expectUnauthenticated: false, + expectedCacheItems: []struct { + key string + value string + expectEncrypted bool + expectError bool + }{ + { + key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), + expectError: true, + }, + }, + idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, + idpHandler: func(w http.ResponseWriter, r *http.Request) { + userInfoBytes := ` + notevenJsongarbage + ` + _, err := w.Write([]byte(userInfoBytes)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusTeapot) + }, + cache: cache.NewInMemoryCache(24 * time.Hour), + cacheItems: []struct { + key string + value string + encrypt bool + }{ + { + key: formatAccessTokenCacheKey(AccessTokenCachePrefix, "randomUser"), + value: "FakeAccessToken", + encrypt: true, + }, + }, + }, + { + name: "call UserInfo without accessToken in cache", + userInfoPath: "/user-info", + expectedOutput: jwt.MapClaims(nil), + expectError: true, + expectUnauthenticated: true, + expectedCacheItems: []struct { + key string + value string + expectEncrypted bool + expectError bool + }{ + { + key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), + expectError: true, + }, + }, + idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, + idpHandler: func(w http.ResponseWriter, r *http.Request) { + userInfoBytes := ` + { + "groups":["githubOrg:engineers"] + }` + w.Header().Set("content-type", "application/json") + _, err := w.Write([]byte(userInfoBytes)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + }, + cache: cache.NewInMemoryCache(24 * time.Hour), + }, + { + name: "call UserInfo with valid accessToken in cache", + userInfoPath: "/user-info", + expectedOutput: jwt.MapClaims{"groups": []interface{}{"githubOrg:engineers"}}, + expectError: false, + expectUnauthenticated: false, + expectedCacheItems: []struct { + key string + value string + expectEncrypted bool + expectError bool + }{ + { + key: formatUserInfoResponseCacheKey(UserInfoResponseCachePrefix, "randomUser"), + value: "{\"groups\":[\"githubOrg:engineers\"]}", + expectEncrypted: true, + expectError: false, + }, + }, + idpClaims: jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())}, + idpHandler: func(w http.ResponseWriter, r *http.Request) { + userInfoBytes := ` + { + "groups":["githubOrg:engineers"] + }` + w.Header().Set("content-type", "application/json") + _, err := w.Write([]byte(userInfoBytes)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) + }, + cache: cache.NewInMemoryCache(24 * time.Hour), + cacheItems: []struct { + key string + value string + encrypt bool + }{ + { + key: formatAccessTokenCacheKey(AccessTokenCachePrefix, "randomUser"), + value: "FakeAccessToken", + encrypt: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(tt.idpHandler)) + defer ts.Close() + + signature, err := util.MakeSignature(32) + require.NoError(t, err) + cdSettings := &settings.ArgoCDSettings{ServerSignature: signature} + encryptionKey, err := cdSettings.GetServerEncryptionKey() + assert.NoError(t, err) + a, _ := NewClientApp(cdSettings, "", nil, "/argo-cd", tt.cache) + + for _, item := range tt.cacheItems { + var newValue []byte + newValue = []byte(item.value) + if item.encrypt { + newValue, err = crypto.Encrypt([]byte(item.value), encryptionKey) + assert.NoError(t, err) + } + err := a.clientCache.Set(&cache.Item{ + Key: item.key, + Object: newValue, + }) + require.NoError(t, err) + } + + got, unauthenticated, err := a.GetUserInfo(tt.idpClaims, ts.URL, tt.userInfoPath) + assert.Equal(t, tt.expectedOutput, got) + assert.Equal(t, tt.expectUnauthenticated, unauthenticated) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + for _, item := range tt.expectedCacheItems { + var tmpValue []byte + err := a.clientCache.Get(item.key, &tmpValue) + if item.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + if item.expectEncrypted { + tmpValue, err = crypto.Decrypt(tmpValue, encryptionKey) + require.NoError(t, err) + } + assert.Equal(t, item.value, string(tmpValue)) + } + } + }) + } + +} diff --git a/util/oidc/provider.go b/util/oidc/provider.go index fcb1a95b60f4fa..d75bcf97efecd5 100644 --- a/util/oidc/provider.go +++ b/util/oidc/provider.go @@ -73,6 +73,18 @@ func (p *providerImpl) newGoOIDCProvider() (*gooidc.Provider, error) { return prov, nil } +type tokenVerificationError struct { + errorsByAudience map[string]error +} + +func (t tokenVerificationError) Error() string { + var errorStrings []string + for aud, err := range t.errorsByAudience { + errorStrings = append(errorStrings, fmt.Sprintf("error for aud %q: %v", aud, err)) + } + return fmt.Sprintf("token verification failed for all audiences: %s", strings.Join(errorStrings, ", ")) +} + func (p *providerImpl) Verify(tokenString string, argoSettings *settings.ArgoCDSettings) (*gooidc.IDToken, error) { // According to the JWT spec, the aud claim is optional. The spec also says (emphasis mine): // @@ -104,6 +116,7 @@ func (p *providerImpl) Verify(tokenString string, argoSettings *settings.ArgoCDS if len(allowedAudiences) == 0 { return nil, errors.New("token has an audience claim, but no allowed audiences are configured") } + tokenVerificationErrors := make(map[string]error) // Token must be verified for at least one allowed audience for _, aud := range allowedAudiences { idToken, err = p.verify(aud, tokenString, false) @@ -117,6 +130,13 @@ func (p *providerImpl) Verify(tokenString string, argoSettings *settings.ArgoCDS if err == nil { break } + // We store the error for each audience so that we can return a more detailed error message to the user. + // If this gets merged, we'll be able to detect failures unrelated to audiences and short-circuit this loop + // to avoid logging irrelevant warnings: https://github.com/coreos/go-oidc/pull/406 + tokenVerificationErrors[aud] = err + } + if len(tokenVerificationErrors) > 0 { + err = tokenVerificationError{errorsByAudience: tokenVerificationErrors} } } diff --git a/util/settings/settings.go b/util/settings/settings.go index bc091e8b818ecc..82b4d72dc23c81 100644 --- a/util/settings/settings.go +++ b/util/settings/settings.go @@ -161,6 +161,9 @@ func (o *oidcConfig) toExported() *OIDCConfig { ClientID: o.ClientID, ClientSecret: o.ClientSecret, CLIClientID: o.CLIClientID, + UserInfoPath: o.UserInfoPath, + EnableUserInfoGroups: o.EnableUserInfoGroups, + UserInfoCacheExpiration: o.UserInfoCacheExpiration, RequestedScopes: o.RequestedScopes, RequestedIDTokenClaims: o.RequestedIDTokenClaims, LogoutURL: o.LogoutURL, @@ -175,6 +178,9 @@ type OIDCConfig struct { ClientID string `json:"clientID,omitempty"` ClientSecret string `json:"clientSecret,omitempty"` CLIClientID string `json:"cliClientID,omitempty"` + EnableUserInfoGroups bool `json:"enableUserInfoGroups,omitempty"` + UserInfoPath string `json:"userInfoPath,omitempty"` + UserInfoCacheExpiration string `json:"userInfoCacheExpiration,omitempty"` RequestedScopes []string `json:"requestedScopes,omitempty"` RequestedIDTokenClaims map[string]*oidc.Claim `json:"requestedIDTokenClaims,omitempty"` LogoutURL string `json:"logoutURL,omitempty"` @@ -1489,27 +1495,6 @@ func (mgr *SettingsManager) updateSettingsFromSecret(settings *ArgoCDSettings, a } else { errs = append(errs, &incompleteSettingsError{message: "server.secretkey is missing"}) } - if githubWebhookSecret := argoCDSecret.Data[settingsWebhookGitHubSecretKey]; len(githubWebhookSecret) > 0 { - settings.WebhookGitHubSecret = string(githubWebhookSecret) - } - if gitlabWebhookSecret := argoCDSecret.Data[settingsWebhookGitLabSecretKey]; len(gitlabWebhookSecret) > 0 { - settings.WebhookGitLabSecret = string(gitlabWebhookSecret) - } - if bitbucketWebhookUUID := argoCDSecret.Data[settingsWebhookBitbucketUUIDKey]; len(bitbucketWebhookUUID) > 0 { - settings.WebhookBitbucketUUID = string(bitbucketWebhookUUID) - } - if bitbucketserverWebhookSecret := argoCDSecret.Data[settingsWebhookBitbucketServerSecretKey]; len(bitbucketserverWebhookSecret) > 0 { - settings.WebhookBitbucketServerSecret = string(bitbucketserverWebhookSecret) - } - if gogsWebhookSecret := argoCDSecret.Data[settingsWebhookGogsSecretKey]; len(gogsWebhookSecret) > 0 { - settings.WebhookGogsSecret = string(gogsWebhookSecret) - } - if azureDevOpsUsername := argoCDSecret.Data[settingsWebhookAzureDevOpsUsernameKey]; len(azureDevOpsUsername) > 0 { - settings.WebhookAzureDevOpsUsername = string(azureDevOpsUsername) - } - if azureDevOpsPassword := argoCDSecret.Data[settingsWebhookAzureDevOpsPasswordKey]; len(azureDevOpsPassword) > 0 { - settings.WebhookAzureDevOpsPassword = string(azureDevOpsPassword) - } // The TLS certificate may be externally managed. We try to load it from an // external secret first. If the external secret doesn't exist, we either @@ -1549,6 +1534,15 @@ func (mgr *SettingsManager) updateSettingsFromSecret(settings *ArgoCDSettings, a if len(errs) > 0 { return errs[0] } + + settings.WebhookGitHubSecret = ReplaceStringSecret(string(argoCDSecret.Data[settingsWebhookGitHubSecretKey]), settings.Secrets) + settings.WebhookGitLabSecret = ReplaceStringSecret(string(argoCDSecret.Data[settingsWebhookGitLabSecretKey]), settings.Secrets) + settings.WebhookBitbucketUUID = ReplaceStringSecret(string(argoCDSecret.Data[settingsWebhookBitbucketUUIDKey]), settings.Secrets) + settings.WebhookBitbucketServerSecret = ReplaceStringSecret(string(argoCDSecret.Data[settingsWebhookBitbucketServerSecretKey]), settings.Secrets) + settings.WebhookGogsSecret = ReplaceStringSecret(string(argoCDSecret.Data[settingsWebhookGogsSecretKey]), settings.Secrets) + settings.WebhookAzureDevOpsUsername = ReplaceStringSecret(string(argoCDSecret.Data[settingsWebhookAzureDevOpsUsernameKey]), settings.Secrets) + settings.WebhookAzureDevOpsPassword = ReplaceStringSecret(string(argoCDSecret.Data[settingsWebhookAzureDevOpsPasswordKey]), settings.Secrets) + return nil } @@ -1850,6 +1844,34 @@ func (a *ArgoCDSettings) IssuerURL() string { return "" } +// UserInfoGroupsEnabled returns whether group claims should be fetch from UserInfo endpoint +func (a *ArgoCDSettings) UserInfoGroupsEnabled() bool { + if oidcConfig := a.OIDCConfig(); oidcConfig != nil { + return oidcConfig.EnableUserInfoGroups + } + return false +} + +// UserInfoPath returns the sub-path on which the IDP exposes the UserInfo endpoint +func (a *ArgoCDSettings) UserInfoPath() string { + if oidcConfig := a.OIDCConfig(); oidcConfig != nil { + return oidcConfig.UserInfoPath + } + return "" +} + +// UserInfoCacheExpiration returns the expiry time of the UserInfo cache +func (a *ArgoCDSettings) UserInfoCacheExpiration() time.Duration { + if oidcConfig := a.OIDCConfig(); oidcConfig != nil && oidcConfig.UserInfoCacheExpiration != "" { + userInfoCacheExpiration, err := time.ParseDuration(oidcConfig.UserInfoCacheExpiration) + if err != nil { + log.Warnf("Failed to parse 'oidc.config.userInfoCacheExpiration' key: %v", err) + } + return userInfoCacheExpiration + } + return 0 +} + func (a *ArgoCDSettings) OAuth2ClientID() string { if oidcConfig := a.OIDCConfig(); oidcConfig != nil { return oidcConfig.ClientID diff --git a/util/settings/settings_test.go b/util/settings/settings_test.go index 07a2c268a6bd77..e11fa401a9fc9b 100644 --- a/util/settings/settings_test.go +++ b/util/settings/settings_test.go @@ -1241,9 +1241,9 @@ func TestDownloadArgoCDBinaryUrls(t *testing.T) { func TestSecretKeyRef(t *testing.T) { data := map[string]string{ "oidc.config": `name: Okta -issuer: $acme:issuerSecret +issuer: $ext:issuerSecret clientID: aaaabbbbccccddddeee -clientSecret: $acme:clientSecret +clientSecret: $ext:clientSecret # Optional set of OIDC scopes to request. If omitted, defaults to: ["openid", "profile", "email", "groups"] requestedScopes: ["openid", "profile", "email"] # Optional set of OIDC claims to request on the ID token. @@ -1265,21 +1265,23 @@ requestedIDTokenClaims: {"groups": {"essential": true}}`, Namespace: "default", }, Data: map[string][]byte{ - "admin.password": nil, - "server.secretkey": nil, + "admin.password": nil, + "server.secretkey": nil, + "webhook.github.secret": []byte("$ext:webhook.github.secret"), }, } secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "acme", + Name: "ext", Namespace: "default", Labels: map[string]string{ "app.kubernetes.io/part-of": "argocd", }, }, Data: map[string][]byte{ - "issuerSecret": []byte("https://dev-123456.oktapreview.com"), - "clientSecret": []byte("deadbeef"), + "issuerSecret": []byte("https://dev-123456.oktapreview.com"), + "clientSecret": []byte("deadbeef"), + "webhook.github.secret": []byte("mywebhooksecret"), }, } kubeClient := fake.NewSimpleClientset(cm, secret, argocdSecret) @@ -1287,6 +1289,7 @@ requestedIDTokenClaims: {"groups": {"essential": true}}`, settings, err := settingsManager.GetSettings() assert.NoError(t, err) + assert.Equal(t, settings.WebhookGitHubSecret, "mywebhooksecret") oidcConfig := settings.OIDCConfig() assert.Equal(t, oidcConfig.Issuer, "https://dev-123456.oktapreview.com") diff --git a/util/test/testutil.go b/util/test/testutil.go index 6fdbd4151d82cf..bb6c43313358c7 100644 --- a/util/test/testutil.go +++ b/util/test/testutil.go @@ -8,9 +8,9 @@ import ( "net/http/httptest" "testing" + "github.com/go-jose/go-jose/v3" "github.com/golang-jwt/jwt/v4" "github.com/stretchr/testify/require" - "gopkg.in/square/go-jose.v2" ) // Cert is a certificate for tests. It was generated like this: @@ -168,6 +168,16 @@ func oidcMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.R "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"], "claims_supported": ["sub", "aud", "exp"] }`, url)) + require.NoError(t, err) + case "/userinfo": + w.Header().Set("content-type", "application/json") + _, err := io.WriteString(w, fmt.Sprintf(` +{ + "groups":["githubOrg:engineers"], + "iss": "%[1]s", + "sub": "randomUser" +}`, url)) + require.NoError(t, err) case "/keys": pubKey, err := jwt.ParseRSAPublicKeyFromPEM(Cert) diff --git a/util/trace/trace.go b/util/trace/trace.go index 64a0361dfa3d58..9d281bf0d4c768 100644 --- a/util/trace/trace.go +++ b/util/trace/trace.go @@ -13,10 +13,11 @@ import ( "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.6.1" + "google.golang.org/grpc/credentials" ) // InitTracer initializes the trace provider and the otel grpc exporter. -func InitTracer(ctx context.Context, serviceName, otlpAddress string, otlpAttrs []string) (func(), error) { +func InitTracer(ctx context.Context, serviceName, otlpAddress string, otlpInsecure bool, otlpHeaders map[string]string, otlpAttrs []string) (func(), error) { attrs := make([]attribute.KeyValue, 0, len(otlpAttrs)) for i := range otlpAttrs { attr := otlpAttrs[i] @@ -38,10 +39,19 @@ func InitTracer(ctx context.Context, serviceName, otlpAddress string, otlpAttrs return nil, fmt.Errorf("failed to create resource: %w", err) } - // Set up a trace exporter + // set up grpc options based on secure/insecure connection + var secureOption otlptracegrpc.Option + if otlpInsecure { + secureOption = otlptracegrpc.WithInsecure() + } else { + secureOption = otlptracegrpc.WithTLSCredentials(credentials.NewClientTLSFromCert(nil, "")) + } + + // set up a trace exporter exporter, err := otlptracegrpc.New(ctx, - otlptracegrpc.WithInsecure(), + secureOption, otlptracegrpc.WithEndpoint(otlpAddress), + otlptracegrpc.WithHeaders(otlpHeaders), ) if err != nil { return nil, fmt.Errorf("failed to create trace exporter: %w", err) diff --git a/util/webhook/webhook.go b/util/webhook/webhook.go index 9955540ea04a91..04746a1df0e37b 100644 --- a/util/webhook/webhook.go +++ b/util/webhook/webhook.go @@ -321,7 +321,7 @@ func getWebUrlRegex(webURL string) (*regexp.Regexp, error) { } regexEscapedHostname := regexp.QuoteMeta(urlObj.Hostname()) - regexEscapedPath := regexp.QuoteMeta(urlObj.Path[1:]) + regexEscapedPath := regexp.QuoteMeta(urlObj.EscapedPath()[1:]) regexpStr := fmt.Sprintf(`(?i)^(http://|https://|%s@|ssh://(%s@)?)%s(:[0-9]+|)[:/]%s(\.git)?$`, usernameRegex, usernameRegex, regexEscapedHostname, regexEscapedPath) repoRegexp, err := regexp.Compile(regexpStr) @@ -349,18 +349,12 @@ func (a *ArgoCDWebhookHandler) storePreviouslyCachedManifests(app *v1alpha1.Appl return fmt.Errorf("error getting ref sources: %w", err) } source := app.Spec.GetSource() - cache.LogDebugManifestCacheKeyFields("getting manifests cache", "webhook app revision changed", change.shaBefore, &source, refSources, &clusterInfo, app.Spec.Destination.Namespace, trackingMethod, appInstanceLabelKey, app.Name, nil) + cache.LogDebugManifestCacheKeyFields("moving manifests cache", "webhook app revision changed", change.shaBefore, &source, refSources, &clusterInfo, app.Spec.Destination.Namespace, trackingMethod, appInstanceLabelKey, app.Name, nil) - var cachedManifests cache.CachedManifestResponse - if err := a.repoCache.GetManifests(change.shaBefore, &source, refSources, &clusterInfo, app.Spec.Destination.Namespace, trackingMethod, appInstanceLabelKey, app.Name, &cachedManifests, nil); err != nil { + if err := a.repoCache.SetNewRevisionManifests(change.shaAfter, change.shaBefore, &source, refSources, &clusterInfo, app.Spec.Destination.Namespace, trackingMethod, appInstanceLabelKey, app.Name, nil); err != nil { return err } - cache.LogDebugManifestCacheKeyFields("setting manifests cache", "webhook app revision changed", change.shaAfter, &source, refSources, &clusterInfo, app.Spec.Destination.Namespace, trackingMethod, appInstanceLabelKey, app.Name, nil) - - if err = a.repoCache.SetManifests(change.shaAfter, &source, refSources, &clusterInfo, app.Spec.Destination.Namespace, trackingMethod, appInstanceLabelKey, app.Name, &cachedManifests, nil); err != nil { - return err - } return nil } diff --git a/util/webhook/webhook_test.go b/util/webhook/webhook_test.go index b241d7c6718416..3097dc58f574e7 100644 --- a/util/webhook/webhook_test.go +++ b/util/webhook/webhook_test.go @@ -670,6 +670,7 @@ func Test_getWebUrlRegex(t *testing.T) { {true, "https://example.com/org/repo", "ssh://user-name@example.com/org/repo", "valid usernames with hyphens in repo should match"}, {false, "https://example.com/org/repo", "ssh://-user-name@example.com/org/repo", "invalid usernames with hyphens in repo should not match"}, {true, "https://example.com:443/org/repo", "GIT@EXAMPLE.COM:22:ORG/REPO", "matches aren't case-sensitive"}, + {true, "https://example.com/org/repo%20", "https://example.com/org/repo%20", "escape codes in path are preserved"}, } for _, testCase := range tests { testCopy := testCase