diff --git a/.github/workflows/ci-build.yaml b/.github/workflows/ci-build.yaml index 3a596a9552d70..e01964e1e6a60 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,28 @@ permissions: contents: read jobs: + changes: + runs-on: ubuntu-latest + outputs: + backend: ${{ steps.filter.outputs.backend }} + frontend: ${{ steps.filter.outputs.frontend }} + steps: + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 + - uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # v2 + id: filter + with: + filters: | + backend: + - '!(ui/**)' + - '!(**/*.md)' + frontend: + - 'ui/**' 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 @@ -43,7 +62,10 @@ jobs: 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 +89,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 +108,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 +177,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 +241,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,7 +292,10 @@ 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 @@ -292,10 +327,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 +352,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 +373,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 +416,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 @@ -462,3 +501,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/USERS.md b/USERS.md index cdf4406b7f296..3f164796d099f 100644 --- a/USERS.md +++ b/USERS.md @@ -94,6 +94,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/) @@ -283,6 +284,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/cmd/argocd-application-controller/commands/argocd_application_controller.go b/cmd/argocd-application-controller/commands/argocd_application_controller.go index 8004340250611..0ff9fa33c8254 100644 --- a/cmd/argocd-application-controller/commands/argocd_application_controller.go +++ b/cmd/argocd-application-controller/commands/argocd_application_controller.go @@ -266,7 +266,7 @@ func getClusterSharding(kubeClient *kubernetes.Clientset, settingsMgr *settings. // If we still see conflicts after the retries, wait for next iteration of heartbeat process. for i := 0; i <= common.AppControllerHeartbeatUpdateRetryCount; i++ { shardNumber, err = sharding.GetOrUpdateShardFromConfigMap(kubeClient, settingsMgr, replicasCount, shardNumber) - if !kubeerrors.IsConflict(err) { + if err != nil && !kubeerrors.IsConflict(err) { err = fmt.Errorf("unable to get shard due to error updating the sharding config map: %s", err) break } diff --git a/docs/assets/api-management.png b/docs/assets/api-management.png deleted file mode 100644 index ae066f0a6a87d..0000000000000 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 d27e03b661f82..0000000000000 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 45557b51ead7f..0000000000000 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 0000000000000..bfc4570826b0a 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 0000000000000..dbf99a88ed6e3 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 0000000000000..4e85b062f357b 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 0000000000000..cf0b75b0e4a21 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 0000000000000..4edb93d42ea91 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 0000000000000..6cd1783c72653 Binary files /dev/null and b/docs/assets/okta-groups-scope.png differ diff --git a/docs/developer-guide/site.md b/docs/developer-guide/site.md index af32753a323e2..efd6aece9aedb 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/operator-manual/applicationset/Template.md b/docs/operator-manual/applicationset/Template.md index 573e297bff2e2..9a7cd574453b4 100644 --- a/docs/operator-manual/applicationset/Template.md +++ b/docs/operator-manual/applicationset/Template.md @@ -111,16 +111,15 @@ In this example, the ApplicationSet controller will generate an `Application` re ## Template Patch -Templating is only available on string type. However, some uses cases may require to apply templating on other types. +Templating is only available on string type. However, some use cases may require applying templating on other types. Example: -- Set the automated sync policy -- Switch prune boolean to true -- Add multiple helm value files - -Argo CD has a `templatePatch` feature to allow advanced templating. It supports both json and yaml. +- 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 @@ -174,3 +173,6 @@ spec: 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 ddc47ffc936d0..4355354d2faef 100644 --- a/docs/operator-manual/argocd-cm.yaml +++ b/docs/operator-manual/argocd-cm.yaml @@ -318,6 +318,12 @@ data: # 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" diff --git a/docs/operator-manual/dynamic-cluster-distribution.md b/docs/operator-manual/dynamic-cluster-distribution.md index a32258c3f2f0a..9d5d2104a1795 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/ingress.md b/docs/operator-manual/ingress.md index 5ea947345d507..aad2208c21873 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 diff --git a/docs/operator-manual/metrics.md b/docs/operator-manual/metrics.md index cfd2a8a8093ac..634684a430045 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. diff --git a/docs/operator-manual/notifications/services/alertmanager.md b/docs/operator-manual/notifications/services/alertmanager.md index e0f9d7e4e7889..033a76a29ea65 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 6b744f4744b93..5331533826348 100755 --- a/docs/operator-manual/notifications/services/awssqs.md +++ b/docs/operator-manual/notifications/services/awssqs.md @@ -1,8 +1,8 @@ -# 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 overridden with target destination annotation. * `region` - region of the sqs queue can be provided via env variable AWS_DEFAULT_REGION @@ -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 environment variables are 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 b81ab6cde8b4c..7fd3f0e22379c 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 be76ab150d1a1..1fa1a985d2682 100755 --- a/docs/operator-manual/notifications/services/github.md +++ b/docs/operator-manual/notifications/services/github.md @@ -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,7 @@ 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. diff --git a/docs/operator-manual/notifications/services/googlechat.md b/docs/operator-manual/notifications/services/googlechat.md index 885ce685a4511..821c23023e863 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: diff --git a/docs/operator-manual/notifications/services/grafana.md b/docs/operator-manual/notifications/services/grafana.md index a36672d0fa423..1f3e77701f044 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 98e0d0fd7b82f..d1f187e955b9c 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 d98288a846422..b0c7e340c9b28 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 665d0081e7c73..e92ee99756ab8 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 0e1ab965332e1..c6e1e41dac81d 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 21e8d942e4e93..549cdc937b150 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 37cb20b277dcc..a09b3660f9233 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 f1157050139d0..20aaa405c80d0 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 0f3fdf1739210..41bdddd7617c4 100755 --- a/docs/operator-manual/notifications/services/slack.md +++ b/docs/operator-manual/notifications/services/slack.md @@ -15,6 +15,7 @@ The Slack notification service configuration includes following settings: | `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 @@ -48,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 8b8c6b819c795..0e44456d4de19 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: diff --git a/docs/operator-manual/notifications/services/telegram.md b/docs/operator-manual/notifications/services/telegram.md index 953c2a9fca0bf..8612a09d1ca84 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 440ed1ddc738f..eba4c5e11b8dc 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 965098402236f..4b8ca38a685ad 100755 --- a/docs/operator-manual/notifications/services/webhook.md +++ b/docs/operator-manual/notifications/services/webhook.md @@ -31,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:/// @@ -50,7 +50,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: template.github-commit-status: | webhook: @@ -82,7 +82,7 @@ metadata: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.github: | url: https://api.github.com @@ -97,7 +97,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.github: | url: https://api.github.com @@ -128,7 +128,7 @@ data: apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.jenkins: | url: http:///job//build?token= @@ -145,7 +145,7 @@ type: Opaque apiVersion: v1 kind: ConfigMap metadata: - name: + name: argocd-notifications-cm data: service.webhook.form: | url: https://form.example.com @@ -166,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/upgrading/2.9-2.10.md b/docs/operator-manual/upgrading/2.9-2.10.md index 4cd7c379bdc81..cfb3e286649ac 100644 --- a/docs/operator-manual/upgrading/2.9-2.10.md +++ b/docs/operator-manual/upgrading/2.9-2.10.md @@ -10,3 +10,7 @@ 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.0. diff --git a/docs/operator-manual/user-management/okta.md b/docs/operator-manual/user-management/okta.md index 09d7099d19954..308254759de6e 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/proposals/decouple-application-sync-user-using-impersonation.md b/docs/proposals/decouple-application-sync-user-using-impersonation.md new file mode 100644 index 0000000000000..e7e459a7059c0 --- /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-oci-support.md b/docs/proposals/native-oci-support.md index 64918fde8904e..7ec0053729c2e 100644 --- a/docs/proposals/native-oci-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/user-guide/kustomize.md b/docs/user-guide/kustomize.md index 647e753649cce..3da35b7eede76 100644 --- a/docs/user-guide/kustomize.md +++ b/docs/user-guide/kustomize.md @@ -106,6 +106,37 @@ spec: 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. diff --git a/docs/user-guide/resource_hooks.md b/docs/user-guide/resource_hooks.md index a6fdaf8bd2e05..6e15a55bb20c2 100644 --- a/docs/user-guide/resource_hooks.md +++ b/docs/user-guide/resource_hooks.md @@ -8,9 +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 `PostDelete` hook to run clean-up or finalizer logic after an all Application resources are deleted. Please note that - `PostDelete` hooks are only deleted if delete policy matches to the aggregated deletion hooks status and not garbage collected after the application is deleted. +* 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 @@ -39,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 diff --git a/docs/user-guide/sync-waves.md b/docs/user-guide/sync-waves.md index 932ba396d68d2..8b17237c87571 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/go.mod b/go.mod index b8acf2282cdb1..cb024e3183404 100644 --- a/go.mod +++ b/go.mod @@ -13,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.20240122213038-792124280fcc - github.com/argoproj/notifications-engine v0.4.1-0.20231027194313-a8d185ecc0a9 + 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 @@ -25,7 +25,8 @@ 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.11.0 @@ -114,19 +115,20 @@ 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 @@ -178,7 +180,6 @@ 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 diff --git a/go.sum b/go.sum index 78a085ec08a73..2d33e5a248cce 100644 --- a/go.sum +++ b/go.sum @@ -694,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.20240122213038-792124280fcc h1:Fv94Mi2WvtvPkEH5WoWC3iy/VoQRLeSsE0hyg0n2UkY= -github.com/argoproj/gitops-engine v0.7.1-0.20240122213038-792124280fcc/go.mod h1:gWE8uROi7hIkWGNAVM+8FWkMfo0vZ03SLx/aFw/DBzg= -github.com/argoproj/notifications-engine v0.4.1-0.20231027194313-a8d185ecc0a9 h1:1lt0VXzmLK7Vv0kaeal3S6/JIfzPyBORkUWXhiqF3l0= -github.com/argoproj/notifications-engine v0.4.1-0.20231027194313-a8d185ecc0a9/go.mod h1:E/vv4+by868m0mmflaRfGBmKBtAupoF+mmyfekP8QCk= +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= @@ -713,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= @@ -875,8 +877,8 @@ github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBF 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= 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 0000000000000..6f9aaf5a270d5 --- /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 0000000000000..d0e09bd4b41f7 --- /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 0000000000000..d179322b99dd5 --- /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 0000000000000..31ff04397b29e --- /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/tool-versions.sh b/hack/tool-versions.sh index ecc1c424febfa..3cd1bc15aa4c4 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.2 +helm3_version=3.14.0 kubectl_version=1.17.8 kubectx_version=0.6.3 kustomize5_version=5.2.1 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 bcaf2d4bb5894..68dd75de2f47f 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: diff --git a/manifests/base/application-controller-deployment/argocd-application-controller-service.yaml b/manifests/base/application-controller-deployment/argocd-application-controller-service.yaml index f66c8055247f3..a769e75468483 100644 --- a/manifests/base/application-controller-deployment/argocd-application-controller-service.yaml +++ b/manifests/base/application-controller-deployment/argocd-application-controller-service.yaml @@ -14,7 +14,7 @@ spec: targetPort: 8082 - name: metrics protocol: TCP - port: 8082 - targetPort: 8082 + port: 8084 + targetPort: 8084 selector: app.kubernetes.io/name: argocd-application-controller \ No newline at end of file diff --git a/reposerver/gpgwatcher.go b/reposerver/gpgwatcher.go index 9c2c9be790813..5b43d6a24ac76 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/server.go b/reposerver/server.go index 007b7136e41ed..e1d611801c3ec 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/beat.k8s.elastic.co/Beat/health.lua b/resource_customizations/beat.k8s.elastic.co/Beat/health.lua new file mode 100644 index 0000000000000..c7639dbbd94f0 --- /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 0000000000000..fb44e998ffaf1 --- /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 0000000000000..3eca183165a5c --- /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 0000000000000..b007ad72ae3fe --- /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 0000000000000..3f3c1866793d8 --- /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 0000000000000..fc2433c8076a8 --- /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 0000000000000..831ee281ef02d --- /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 0000000000000..d652b5a55d0ff --- /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 0000000000000..dbcca36c9e691 --- /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/server/extension/extension.go b/server/extension/extension.go index aca924620756c..9f8edcd6184fc 100644 --- a/server/extension/extension.go +++ b/server/extension/extension.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/felixge/httpsnoop" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v3" @@ -300,6 +301,19 @@ type Manager struct { project ProjectGetter rbac RbacEnforcer registry ExtensionRegistry + metricsReg ExtensionMetricsRegistry +} + +// ExtensionMetricsRegistry exposes operations to update http metrics in the Argo CD +// API server. +type ExtensionMetricsRegistry interface { + // IncExtensionRequestCounter will increase the request counter for the given + // extension with the given status. + IncExtensionRequestCounter(extension string, status int) + // ObserveExtensionRequestDuration will register the request roundtrip duration + // between Argo CD API Server and the extension backend service for the given + // extension. + ObserveExtensionRequestDuration(extension string, duration time.Duration) } // NewManager will initialize a new manager. @@ -423,7 +437,8 @@ func validateConfigs(configs *ExtensionConfigs) error { } // NewProxy will instantiate a new reverse proxy based on the provided -// targetURL and config. +// targetURL and config. It will remove sensitive information from the +// incoming request such as the Authorization and Cookie headers. func NewProxy(targetURL string, headers []Header, config ProxyConfig) (*httputil.ReverseProxy, error) { url, err := url.Parse(targetURL) if err != nil { @@ -484,6 +499,10 @@ func (m *Manager) RegisterExtensions() error { if err != nil { return fmt.Errorf("error getting settings: %s", err) } + if settings.ExtensionConfig == "" { + m.log.Infof("No extensions configured.") + return nil + } err = m.UpdateExtensionRegistry(settings) if err != nil { return fmt.Errorf("error updating extension registry: %s", err) @@ -683,13 +702,26 @@ func (m *Manager) CallExtension() func(http.ResponseWriter, *http.Request) { prepareRequest(r, extName, app) m.log.Debugf("proxing request for extension %q", extName) - proxy.ServeHTTP(w, r) + // httpsnoop package is used to properly wrap the responseWriter + // and avoid optional intefaces issue: + // https://github.com/felixge/httpsnoop#why-this-package-exists + // CaptureMetrics will call the proxy and return the metrics from it. + metrics := httpsnoop.CaptureMetrics(proxy, w, r) + + go registerMetrics(extName, metrics, m.metricsReg) } } -// prepareRequest is reponsible for preparing and cleaning the given -// request, removing sensitive information before forwarding it to the -// proxy extension. +func registerMetrics(extName string, metrics httpsnoop.Metrics, extensionMetricsRegistry ExtensionMetricsRegistry) { + if extensionMetricsRegistry != nil { + extensionMetricsRegistry.IncExtensionRequestCounter(extName, metrics.Code) + extensionMetricsRegistry.ObserveExtensionRequestDuration(extName, metrics.Duration) + } +} + +// prepareRequest is reponsible for cleaning the incoming request URL removing +// the Argo CD extension API section from it. It will set the cluster destination name +// and cluster destination server in the headers as it is defined in the given app. func prepareRequest(r *http.Request, extName string, app *v1alpha1.Application) { r.URL.Path = strings.TrimPrefix(r.URL.Path, fmt.Sprintf("%s/%s", URLPrefix, extName)) if app.Spec.Destination.Name != "" { @@ -699,3 +731,8 @@ func prepareRequest(r *http.Request, extName string, app *v1alpha1.Application) r.Header.Set(HeaderArgoCDTargetClusterURL, app.Spec.Destination.Server) } } + +// AddMetricsRegistry will associate the given metricsReg in the Manager. +func (m *Manager) AddMetricsRegistry(metricsReg ExtensionMetricsRegistry) { + m.metricsReg = metricsReg +} diff --git a/server/extension/extension_test.go b/server/extension/extension_test.go index 273779d59ca29..ff287dde80424 100644 --- a/server/extension/extension_test.go +++ b/server/extension/extension_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "strings" + "sync" "testing" "github.com/sirupsen/logrus/hooks/test" @@ -188,10 +189,6 @@ func TestRegisterExtensions(t *testing.T) { configYaml string } cases := []testCase{ - { - name: "no config", - configYaml: "", - }, { name: "no name", configYaml: getExtensionConfigNoName(), @@ -234,7 +231,7 @@ func TestRegisterExtensions(t *testing.T) { err := f.manager.RegisterExtensions() // then - assert.Error(t, err) + assert.Error(t, err, fmt.Sprintf("expected error in test %s but got nil", tc.name)) }) } }) @@ -247,6 +244,7 @@ func TestCallExtension(t *testing.T) { settingsGetterMock *mocks.SettingsGetter rbacMock *mocks.RbacEnforcer projMock *mocks.ProjectGetter + metricsMock *mocks.ExtensionMetricsRegistry manager *extension.Manager } defaultProjectName := "project-name" @@ -256,10 +254,12 @@ func TestCallExtension(t *testing.T) { settMock := &mocks.SettingsGetter{} rbacMock := &mocks.RbacEnforcer{} projMock := &mocks.ProjectGetter{} + metricsMock := &mocks.ExtensionMetricsRegistry{} logger, _ := test.NewNullLogger() logEntry := logger.WithContext(context.Background()) m := extension.NewManager(logEntry, settMock, appMock, projMock, rbacMock) + m.AddMetricsRegistry(metricsMock) mux := http.NewServeMux() extHandler := http.HandlerFunc(m.CallExtension()) @@ -271,6 +271,7 @@ func TestCallExtension(t *testing.T) { settingsGetterMock: settMock, rbacMock: rbacMock, projMock: projMock, + metricsMock: metricsMock, manager: m, } } @@ -328,6 +329,11 @@ func TestCallExtension(t *testing.T) { f.projMock.On("Get", prj.GetName()).Return(prj, nil) } + withMetrics := func(f *fixture) { + f.metricsMock.On("IncExtensionRequestCounter", mock.Anything, mock.Anything) + f.metricsMock.On("ObserveExtensionRequestDuration", mock.Anything, mock.Anything) + } + withRbac := func(f *fixture, allowApp, allowExt bool) { var appAccessError error var extAccessError error @@ -406,6 +412,18 @@ func TestCallExtension(t *testing.T) { proj := getProjectWithDestinations("project-name", nil, []string{clusterURL}) f.appGetterMock.On("Get", mock.Anything, mock.Anything).Return(app, nil) withProject(proj, f) + var wg sync.WaitGroup + wg.Add(2) + f.metricsMock. + On("IncExtensionRequestCounter", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + wg.Done() + }) + f.metricsMock. + On("ObserveExtensionRequestDuration", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + wg.Done() + }) // when resp, err := http.DefaultClient.Do(r) @@ -420,6 +438,13 @@ func TestCallExtension(t *testing.T) { assert.Equal(t, backendResponse, actual) assert.Equal(t, clusterURL, resp.Header.Get(extension.HeaderArgoCDTargetClusterURL)) assert.Equal(t, "Bearer some-bearer-token", resp.Header.Get("Authorization")) + + // waitgroup is necessary to make sure assertions aren't executed before + // the goroutine initiated by extension.CallExtension concludes which would + // lead to flaky test. + wg.Wait() + f.metricsMock.AssertCalled(t, "IncExtensionRequestCounter", backendEndpoint, http.StatusOK) + f.metricsMock.AssertCalled(t, "ObserveExtensionRequestDuration", backendEndpoint, mock.Anything) }) t.Run("proxy will return 404 if extension endpoint not registered", func(t *testing.T) { // given @@ -427,6 +452,7 @@ func TestCallExtension(t *testing.T) { f := setup() withExtensionConfig(getExtensionConfigString(), f) withRbac(f, true, true) + withMetrics(f) cluster1Name := "cluster1" f.appGetterMock.On("Get", "namespace", "app-name").Return(getApp(cluster1Name, "", defaultProjectName), nil) withProject(getProjectWithDestinations("project-name", []string{cluster1Name}, []string{"some-url"}), f) @@ -466,6 +492,7 @@ func TestCallExtension(t *testing.T) { withRbac(f, true, true) withExtensionConfig(getExtensionConfigWith2Backends(extName, beSrv1.URL, cluster1Name, beSrv2.URL, cluster2URL), f) withProject(getProjectWithDestinations("project-name", []string{cluster1Name}, []string{cluster2URL}), f) + withMetrics(f) ts := startTestServer(t, f) defer ts.Close() @@ -511,6 +538,7 @@ func TestCallExtension(t *testing.T) { extName := "some-extension" withRbac(f, allowApp, allowExtension) withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) + withMetrics(f) ts := startTestServer(t, f) defer ts.Close() r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) @@ -533,6 +561,7 @@ func TestCallExtension(t *testing.T) { extName := "some-extension" withRbac(f, allowApp, allowExtension) withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) + withMetrics(f) ts := startTestServer(t, f) defer ts.Close() r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) @@ -556,6 +585,7 @@ func TestCallExtension(t *testing.T) { noCluster := []string{} withRbac(f, allowApp, allowExtension) withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) + withMetrics(f) ts := startTestServer(t, f) defer ts.Close() r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) @@ -580,6 +610,7 @@ func TestCallExtension(t *testing.T) { extName := "some-extension" withRbac(f, allowApp, allowExtension) withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) + withMetrics(f) ts := startTestServer(t, f) defer ts.Close() r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) @@ -604,6 +635,7 @@ func TestCallExtension(t *testing.T) { differentProject := "differentProject" withRbac(f, allowApp, allowExtension) withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) + withMetrics(f) ts := startTestServer(t, f) defer ts.Close() r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) @@ -634,6 +666,7 @@ func TestCallExtension(t *testing.T) { withRbac(f, true, true) withExtensionConfig(getExtensionConfigWith2Backends(extName, "url1", "clusterName", "url2", "clusterURL"), f) withProject(getProjectWithDestinations("project-name", nil, []string{"srv1", destinationServer}), f) + withMetrics(f) ts := startTestServer(t, f) defer ts.Close() @@ -666,6 +699,7 @@ func TestCallExtension(t *testing.T) { differentProject := "differentProject" withRbac(f, allowApp, allowExtension) withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) + withMetrics(f) ts := startTestServer(t, f) defer ts.Close() r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/", ts.URL)) diff --git a/server/extension/mocks/ExtensionMetricsRegistry.go b/server/extension/mocks/ExtensionMetricsRegistry.go new file mode 100644 index 0000000000000..78e583929f74d --- /dev/null +++ b/server/extension/mocks/ExtensionMetricsRegistry.go @@ -0,0 +1,38 @@ +// Code generated by mockery v2.38.0. DO NOT EDIT. + +package mocks + +import ( + time "time" + + mock "github.com/stretchr/testify/mock" +) + +// ExtensionMetricsRegistry is an autogenerated mock type for the ExtensionMetricsRegistry type +type ExtensionMetricsRegistry struct { + mock.Mock +} + +// IncExtensionRequestCounter provides a mock function with given fields: _a0, status +func (_m *ExtensionMetricsRegistry) IncExtensionRequestCounter(_a0 string, status int) { + _m.Called(_a0, status) +} + +// ObserveExtensionRequestDuration provides a mock function with given fields: _a0, duration +func (_m *ExtensionMetricsRegistry) ObserveExtensionRequestDuration(_a0 string, duration time.Duration) { + _m.Called(_a0, duration) +} + +// NewExtensionMetricsRegistry creates a new instance of ExtensionMetricsRegistry. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewExtensionMetricsRegistry(t interface { + mock.TestingT + Cleanup(func()) +}) *ExtensionMetricsRegistry { + mock := &ExtensionMetricsRegistry{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/server/metrics/metrics.go b/server/metrics/metrics.go index 40698e742b093..4afac9da26c02 100644 --- a/server/metrics/metrics.go +++ b/server/metrics/metrics.go @@ -14,8 +14,10 @@ import ( type MetricsServer struct { *http.Server - redisRequestCounter *prometheus.CounterVec - redisRequestHistogram *prometheus.HistogramVec + redisRequestCounter *prometheus.CounterVec + redisRequestHistogram *prometheus.HistogramVec + extensionRequestCounter *prometheus.CounterVec + extensionRequestDuration *prometheus.HistogramVec } var ( @@ -34,6 +36,21 @@ var ( }, []string{"initiator"}, ) + extensionRequestCounter = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "argocd_proxy_extension_request_total", + Help: "Number of requests sent to configured proxy extensions.", + }, + []string{"extension", "status"}, + ) + extensionRequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "argocd_proxy_extension_request_duration_seconds", + Help: "Request duration in seconds between the Argo CD API server and the extension backend.", + Buckets: []float64{0.1, 0.25, .5, 1, 2, 5, 10}, + }, + []string{"extension"}, + ) ) // NewMetricsServer returns a new prometheus server which collects api server metrics @@ -48,14 +65,18 @@ func NewMetricsServer(host string, port int) *MetricsServer { registry.MustRegister(redisRequestCounter) registry.MustRegister(redisRequestHistogram) + registry.MustRegister(extensionRequestCounter) + registry.MustRegister(extensionRequestDuration) return &MetricsServer{ Server: &http.Server{ Addr: fmt.Sprintf("%s:%d", host, port), Handler: mux, }, - redisRequestCounter: redisRequestCounter, - redisRequestHistogram: redisRequestHistogram, + redisRequestCounter: redisRequestCounter, + redisRequestHistogram: redisRequestHistogram, + extensionRequestCounter: extensionRequestCounter, + extensionRequestDuration: extensionRequestDuration, } } @@ -67,3 +88,11 @@ func (m *MetricsServer) IncRedisRequest(failed bool) { func (m *MetricsServer) ObserveRedisRequestDuration(duration time.Duration) { m.redisRequestHistogram.WithLabelValues("argocd-server").Observe(duration.Seconds()) } + +func (m *MetricsServer) IncExtensionRequestCounter(extension string, status int) { + m.extensionRequestCounter.WithLabelValues(extension, strconv.Itoa(status)).Inc() +} + +func (m *MetricsServer) ObserveExtensionRequestDuration(extension string, duration time.Duration) { + m.extensionRequestDuration.WithLabelValues(extension).Observe(duration.Seconds()) +} diff --git a/server/server.go b/server/server.go index 8f6aafc689e94..e42e6f59a49a3 100644 --- a/server/server.go +++ b/server/server.go @@ -223,6 +223,18 @@ type ArgoCDServerOpts struct { EnableProxyExtension bool } +// HTTPMetricsRegistry exposes operations to update http metrics in the Argo CD +// API server. +type HTTPMetricsRegistry interface { + // IncExtensionRequestCounter will increase the request counter for the given + // extension with the given status. + IncExtensionRequestCounter(extension string, status int) + // ObserveExtensionRequestDuration will register the request roundtrip duration + // between Argo CD API Server and the extension backend service for the given + // extension. + ObserveExtensionRequestDuration(extension string, duration time.Duration) +} + // initializeDefaultProject creates the default project if it does not already exist func initializeDefaultProject(opts ArgoCDServerOpts) error { defaultProj := &v1alpha1.AppProject{ @@ -484,6 +496,12 @@ func (a *ArgoCDServer) Init(ctx context.Context) { // golang/protobuf). func (a *ArgoCDServer) Run(ctx context.Context, listeners *Listeners) { a.userStateStorage.Init(ctx) + + metricsServ := metrics.NewMetricsServer(a.MetricsHost, a.MetricsPort) + if a.RedisClient != nil { + cacheutil.CollectMetrics(a.RedisClient, metricsServ) + } + svcSet := newArgoCDServiceSet(a) a.serviceSet = svcSet grpcS, appResourceTreeFn := a.newGRPCServer() @@ -492,9 +510,9 @@ func (a *ArgoCDServer) Run(ctx context.Context, listeners *Listeners) { var httpsS *http.Server if a.useTLS() { httpS = newRedirectServer(a.ListenPort, a.RootPath) - httpsS = a.newHTTPServer(ctx, a.ListenPort, grpcWebS, appResourceTreeFn, listeners.GatewayConn) + httpsS = a.newHTTPServer(ctx, a.ListenPort, grpcWebS, appResourceTreeFn, listeners.GatewayConn, metricsServ) } else { - httpS = a.newHTTPServer(ctx, a.ListenPort, grpcWebS, appResourceTreeFn, listeners.GatewayConn) + httpS = a.newHTTPServer(ctx, a.ListenPort, grpcWebS, appResourceTreeFn, listeners.GatewayConn, metricsServ) } if a.RootPath != "" { httpS.Handler = withRootPath(httpS.Handler, a) @@ -508,11 +526,6 @@ func (a *ArgoCDServer) Run(ctx context.Context, listeners *Listeners) { httpsS.Handler = &bug21955Workaround{handler: httpsS.Handler} } - metricsServ := metrics.NewMetricsServer(a.MetricsHost, a.MetricsPort) - if a.RedisClient != nil { - cacheutil.CollectMetrics(a.RedisClient, metricsServ) - } - // CMux is used to support servicing gRPC and HTTP1.1+JSON on the same port tcpm := cmux.New(listeners.Main) var tlsm cmux.CMux @@ -960,7 +973,7 @@ func compressHandler(handler http.Handler) http.Handler { // newHTTPServer returns the HTTP server to serve HTTP/HTTPS requests. This is implemented // using grpc-gateway as a proxy to the gRPC server. -func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandler http.Handler, appResourceTreeFn application.AppResourceTreeFn, conn *grpc.ClientConn) *http.Server { +func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandler http.Handler, appResourceTreeFn application.AppResourceTreeFn, conn *grpc.ClientConn, metricsReg HTTPMetricsRegistry) *http.Server { endpoint := fmt.Sprintf("localhost:%d", port) mux := http.NewServeMux() httpS := http.Server{ @@ -1009,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) @@ -1079,13 +1092,15 @@ func enforceContentTypes(handler http.Handler, types []string) http.Handler { // 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) diff --git a/test/e2e/sync_waves_test.go b/test/e2e/sync_waves_test.go index ac5db15eee57d..8d0ee14e487d1 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/syncwaves-prune-order/README.md b/test/e2e/testdata/syncwaves-prune-order/README.md new file mode 100644 index 0000000000000..92a62fdfe109d --- /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 0000000000000..f801a3992aa37 --- /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 0000000000000..9512644b731db --- /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/ui/src/app/applications/components/application-details/application-resource-list.scss b/ui/src/app/applications/components/application-details/application-resource-list.scss new file mode 100644 index 0000000000000..9bc4b17bfe7ed --- /dev/null +++ b/ui/src/app/applications/components/application-details/application-resource-list.scss @@ -0,0 +1,13 @@ +.application-details__item { + display: flex; + + .application-details__item_text { + overflow: hidden; + text-overflow: ellipsis; + } + + .application-details__external_link { + flex: 0; + min-width: 13px; + } +} diff --git a/ui/src/app/applications/components/application-details/application-resource-list.tsx b/ui/src/app/applications/components/application-details/application-resource-list.tsx index c5519fc4b6ff9..d1e01adb52c04 100644 --- a/ui/src/app/applications/components/application-details/application-resource-list.tsx +++ b/ui/src/app/applications/components/application-details/application-resource-list.tsx @@ -10,6 +10,7 @@ import * as _ from 'lodash'; import Moment from 'react-moment'; import {format} from 'date-fns'; import {ResourceNode, ResourceRef} from '../../../shared/models'; +import './application-resource-list.scss'; export const ApplicationResourceList = ({ resources, @@ -89,8 +90,8 @@ export const ApplicationResourceList = ({
{ResourceLabel({kind: res.kind})}
-
- {res.name} +
+ {res.name} {res.kind === 'Application' && ( {ctx => ( 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 37e6cc62ff0e9..4f372ef8f55c0 100644 --- a/ui/src/app/applications/components/application-summary/application-summary.tsx +++ b/ui/src/app/applications/components/application-summary/application-summary.tsx @@ -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; @@ -239,7 +249,7 @@ export const ApplicationSummary = (props: ApplicationSummaryProps) => { title: 'PATH', view: ( - {source.path ?? ''} + {processPath(source.path)} ), edit: (formApi: FormApi) => diff --git a/ui/src/app/login/components/utils.ts b/ui/src/app/login/components/utils.ts index 90453ced77d4a..6c715077cc9cc 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/util/helm/cmd.go b/util/helm/cmd.go index 419c7daff5d3c..cc2a1388d65a2 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/oidc/provider.go b/util/oidc/provider.go index fcb1a95b60f4f..d75bcf97efecd 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} } }