Repository: argoproj-labs/argocd-operator
Pull request #870 (2023-03-01 21:31:51)

fix: propagate hpa changes from cr to hpa on cluster
**What type of PR is this?**
> Uncomment only one ` /kind` line, and delete the rest.
> For example, `> /kind bug` would simply become: `/kind bug`

 /kind bug
> /kind chore
> /kind cleanup
> /kind failing-test
> /kind enhancement
> /kind documentation
> /kind code-refactoring


**What does this PR do / why we need it**:
Presently, if `.spec.server.autoscale.enabled` is set, the operator just creates/deletes an hpa. If hpa configuration parameters are changed in `.spec.server.autoscale.hpa` the created hpa is not reconcilied by the operator so the changes don't take effect

**Have you updated the necessary documentation?**

* [ ] Documentation update is required by this PR.
* [ ] Documentation has been updated.

**Which issue(s) this PR fixes**:

Fixes #?

**How to test changes / Special notes to the reviewer**:
1. Deploy operator
2. create Argo CD CR
3. enable server autoscale and set hpa requirements
4. confirm that hpa is created 
5. make changes to fields in `.spec.server.autoscale.hpa`
6. confirm that created hpa is updated with new settings


Patch:
diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml
index 73c8d732..4bc96f98 100644
--- a/.github/workflows/lint.yaml
+++ b/.github/workflows/lint.yaml
@@ -16,7 +16,7 @@ jobs:
       - name: Run golangci-lint
         uses: golangci/golangci-lint-action@v3
         with:
-          version: v1.45.2
+          version: v1.52.2
           args: --timeout 5m --exclude SA5011
           only-new-issues: true
 
diff --git a/.golangci.yml b/.golangci.yml
index 2ac64cf1..74323c10 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -18,4 +18,4 @@ linters-settings:
   goimports:
     local-prefixes: github.com/argoproj-labs/argocd-operator
 service:
-  golangci-lint-version: 1.38.0
+  golangci-lint-version: 1.52.2
diff --git a/controllers/argocd/deployment.go b/controllers/argocd/deployment.go
index 2684ac03..69ba39c0 100644
--- a/controllers/argocd/deployment.go
+++ b/controllers/argocd/deployment.go
@@ -53,7 +53,6 @@ func getArgoCDServerReplicas(cr *argoprojv1a1.ArgoCD) *int32 {
 	if !cr.Spec.Server.Autoscale.Enabled && cr.Spec.Server.Replicas != nil && *cr.Spec.Server.Replicas >= 0 {
 		return cr.Spec.Server.Replicas
 	}
-
 	return nil
 }
 
@@ -1315,8 +1314,10 @@ func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoprojv1a1.ArgoCD, use
 			changed = true
 		}
 		if !reflect.DeepEqual(deploy.Spec.Replicas, existing.Spec.Replicas) {
-			existing.Spec.Replicas = deploy.Spec.Replicas
-			changed = true
+			if !cr.Spec.Server.Autoscale.Enabled {
+				existing.Spec.Replicas = deploy.Spec.Replicas
+				changed = true
+			}
 		}
 		if changed {
 			return r.Client.Update(context.TODO(), existing)
diff --git a/controllers/argocd/hpa.go b/controllers/argocd/hpa.go
index b289ca5e..0cee4a3c 100644
--- a/controllers/argocd/hpa.go
+++ b/controllers/argocd/hpa.go
@@ -16,6 +16,7 @@ package argocd
 
 import (
 	"context"
+	"reflect"
 
 	autoscaling "k8s.io/api/autoscaling/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -25,6 +26,12 @@ import (
 	"github.com/argoproj-labs/argocd-operator/controllers/argoutil"
 )
 
+var (
+	maxReplicas int32 = 3
+	minReplicas int32 = 1
+	tcup        int32 = 50
+)
+
 func newHorizontalPodAutoscaler(cr *argoprojv1a1.ArgoCD) *autoscaling.HorizontalPodAutoscaler {
 	return &autoscaling.HorizontalPodAutoscaler{
 		ObjectMeta: metav1.ObjectMeta{
@@ -50,39 +57,54 @@ func newHorizontalPodAutoscalerWithSuffix(suffix string, cr *argoprojv1a1.ArgoCD
 	return newHorizontalPodAutoscalerWithName(nameWithSuffix(suffix, cr), cr)
 }
 
-// reconcileServerHPA will ensure that the HorizontalPodAutoscaler is present for the Argo CD Server component.
+// reconcileServerHPA will ensure that the HorizontalPodAutoscaler is present for the Argo CD Server component, and reconcile any detected changes.
 func (r *ReconcileArgoCD) reconcileServerHPA(cr *argoprojv1a1.ArgoCD) error {
-	hpa := newHorizontalPodAutoscalerWithSuffix("server", cr)
-	if argoutil.IsObjectFound(r.Client, cr.Namespace, hpa.Name, hpa) {
+
+	defaultHPA := newHorizontalPodAutoscalerWithSuffix("server", cr)
+	defaultHPA.Spec = autoscaling.HorizontalPodAutoscalerSpec{
+		MaxReplicas:                    maxReplicas,
+		MinReplicas:                    &minReplicas,
+		TargetCPUUtilizationPercentage: &tcup,
+		ScaleTargetRef: autoscaling.CrossVersionObjectReference{
+			APIVersion: "apps/v1",
+			Kind:       "Deployment",
+			Name:       nameWithSuffix("server", cr),
+		},
+	}
+
+	existingHPA := newHorizontalPodAutoscalerWithSuffix("server", cr)
+	if argoutil.IsObjectFound(r.Client, cr.Namespace, existingHPA.Name, existingHPA) {
 		if !cr.Spec.Server.Autoscale.Enabled {
-			return r.Client.Delete(context.TODO(), hpa) // HorizontalPodAutoscaler found but globally disabled, delete it.
+			return r.Client.Delete(context.TODO(), existingHPA) // HorizontalPodAutoscaler found but globally disabled, delete it.
+		}
+
+		changed := false
+		// HorizontalPodAutoscaler found, reconcile if necessary changes detected
+		if cr.Spec.Server.Autoscale.HPA != nil {
+			if !reflect.DeepEqual(existingHPA.Spec, cr.Spec.Server.Autoscale.HPA) {
+				existingHPA.Spec = *cr.Spec.Server.Autoscale.HPA
+				changed = true
+			}
 		}
-		return nil // HorizontalPodAutoscaler found and configured, nothing do to, move along...
+
+		if changed {
+			return r.Client.Update(context.TODO(), existingHPA)
+		}
+
+		// HorizontalPodAutoscaler found, no changes detected
+		return nil
 	}
 
 	if !cr.Spec.Server.Autoscale.Enabled {
 		return nil // AutoScale not enabled, move along...
 	}
 
+	// AutoScale enabled, no existing HPA found, create
 	if cr.Spec.Server.Autoscale.HPA != nil {
-		hpa.Spec = *cr.Spec.Server.Autoscale.HPA
-	} else {
-		hpa.Spec.MaxReplicas = 3
-
-		var minrReplicas int32 = 1
-		hpa.Spec.MinReplicas = &minrReplicas
-
-		var tcup int32 = 50
-		hpa.Spec.TargetCPUUtilizationPercentage = &tcup
-
-		hpa.Spec.ScaleTargetRef = autoscaling.CrossVersionObjectReference{
-			APIVersion: "apps/v1",
-			Kind:       "Deployment",
-			Name:       nameWithSuffix("server", cr),
-		}
+		defaultHPA.Spec = *cr.Spec.Server.Autoscale.HPA
 	}
 
-	return r.Client.Create(context.TODO(), hpa)
+	return r.Client.Create(context.TODO(), defaultHPA)
 }
 
 // reconcileAutoscalers will ensure that all HorizontalPodAutoscalers are present for the given ArgoCD.
diff --git a/controllers/argocd/secret.go b/controllers/argocd/secret.go
index 7b577980..7c353c08 100644
--- a/controllers/argocd/secret.go
+++ b/controllers/argocd/secret.go
@@ -184,7 +184,7 @@ func (r *ReconcileArgoCD) reconcileArgoSecret(cr *argoprojv1a1.ArgoCD) error {
 		common.ArgoCDKeyTLSPrivateKey:      tlsSecret.Data[common.ArgoCDKeyTLSPrivateKey],
 	}
 
-	if cr.Spec.SSO != nil && cr.Spec.SSO.Provider == v1alpha1.SSOProviderTypeDex {
+	if cr.Spec.Dex != nil || (cr.Spec.SSO != nil && cr.Spec.SSO.Provider == v1alpha1.SSOProviderTypeDex) {
 		dexOIDCClientSecret, err := r.getDexOAuthClientSecret(cr)
 		if err != nil {
 			return nil
@@ -334,14 +334,18 @@ func (r *ReconcileArgoCD) reconcileExistingArgoSecret(cr *argoprojv1a1.ArgoCD, s
 		changed = true
 	}
 
-	if cr.Spec.SSO != nil && cr.Spec.SSO.Provider == v1alpha1.SSOProviderTypeDex {
-		if secret.Data[common.ArgoCDDexSecretKey] == nil {
-			dexOIDCClientSecret, err := r.getDexOAuthClientSecret(cr)
-			if err != nil {
-				return nil
+	if cr.Spec.Dex != nil || (cr.Spec.SSO != nil && cr.Spec.SSO.Provider == v1alpha1.SSOProviderTypeDex) {
+		dexOIDCClientSecret, err := r.getDexOAuthClientSecret(cr)
+		if err != nil {
+			return err
+		}
+		actual := string(secret.Data[common.ArgoCDDexSecretKey])
+		if dexOIDCClientSecret != nil {
+			expected := *dexOIDCClientSecret
+			if actual != expected {
+				secret.Data[common.ArgoCDDexSecretKey] = []byte(*dexOIDCClientSecret)
+				changed = true
 			}
-			secret.Data[common.ArgoCDDexSecretKey] = []byte(*dexOIDCClientSecret)
-			changed = true
 		}
 	}
 
diff --git a/docs/reference/argocd.md b/docs/reference/argocd.md
index 8d1a0427..3076b6af 100644
--- a/docs/reference/argocd.md
+++ b/docs/reference/argocd.md
@@ -1303,6 +1303,9 @@ Name | Default | Description
 Enabled | false | Toggle Autoscaling support globally for the Argo CD server component.
 HPA | [Object] | HorizontalPodAutoscaler options for the Argo CD Server component.
 
+!!! note
+    When `.spec.server.autoscale.enabled` is set to `true`, the number of required replicas (if set) in `.spec.server.replicas` will be ignored. The final replica count on the server deployment will be controlled by the Horizontal Pod Autoscaler instead. 
+
 ### Server Command Arguments
 
 Allows a user to pass arguments to Argo CD Server command.
@@ -1557,7 +1560,7 @@ The following properties are available for configuring a [UI banner message](htt
 Name | Default | Description
 --- | --- | ---
 Banner.Content | [Empty] | The banner message content (required if a banner should be displayed).
-BAnner.URL.SecretName | [Empty] | The banner message link URL (optional).
+Banner.URL | [Empty] | The banner message link URL (optional).
 
 ### Banner Example
 The following example enables a UI banner with message content and URL.
diff --git a/docs/usage/basics.md b/docs/usage/basics.md
index d5f97e0b..1ba43527 100644
--- a/docs/usage/basics.md
+++ b/docs/usage/basics.md
@@ -186,3 +186,112 @@ See the [routes][docs_routes] documentation for steps to configure the Route sup
 [docs_ingress]:./ingress.md
 [docs_routes]:./routes.md
 [argocd_reference]:../reference/argocd.md
+
+### Default Permissions provided to Argo CD instance
+
+By default Argo CD instance is provided the following permissions
+
+* Argo CD instance is provided with ADMIN privileges for the namespace it is installed in. For instance, if an Argo CD instance is deployed in **foo** namespace, it will have **ADMIN privileges** to manage resources for that namespace.
+
+* Argo CD is provided the following cluster scoped permissions because Argo CD requires cluster-wide read privileges on resources to function properly. (Please see [RBAC](https://argo-cd.readthedocs.io/en/stable/operator-manual/security/#cluster-rbac) section for more details.)
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  labels:
+    app.kubernetes.io/managed-by: <argocd-instance-name>
+    app.kubernetes.io/name: <argocd-instance-name>
+    app.kubernetes.io/part-of: argocd
+  name: <argocd-instance-name>-argocd-application-controller
+  namespace: <argocd-instance-namespace>
+rules:
+- apiGroups:
+  - '*'
+  resources:
+  - '*'
+  verbs:
+  - '*'
+```
+
+## Cluster Scoped Instance
+
+The Argo CD instance created above can also be used to manage the cluster scoped resources by adding the namespace of the Argo CD instance to the `ARGOCD_CLUSTER_CONFIG_NAMESPACES` environment variable of subscription resource as shown below.
+
+```yml
+apiVersion: operators.coreos.com/v1alpha1
+kind: Subscription
+metadata:
+  name: argocd-operator
+spec:
+  config:
+   env: 
+    - name: ARGOCD_CLUSTER_CONFIG_NAMESPACES
+      value: <list of namespaces of cluster-scoped Argo CD instances>
+  channel: alpha
+  name: argocd-operator
+  source: argocd-catalog
+  sourceNamespace: olm
+```
+
+### In-built permissions for cluster configuration
+
+Argo CD is granted the following permissions using a cluster role when it is configured as cluster-scoped instance. **Argo CD is not granted cluster-admin**.
+
+Please note that these permissions are in addition to the `admin` privileges that Argo CD has to the namespace in which it is installed.
+
+```yaml
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: <argocd-instance-name>-<namespace>-argocd-application-controller
+  labels:
+    app.kubernetes.io/managed-by: <argocd-instance-namespace>
+    app.kubernetes.io/name: <argocd-instance-namespace>
+    app.kubernetes.io/part-of: argocd
+rules:
+  - verbs:
+      - '*'
+    apiGroups:
+      - '*'
+    resources:
+      - '*'
+```
+
+### Additional permissions
+
+Users can extend the permissions granted to Argo CD application controller by creating cluster roles with additional permissions and then a new cluster role binding to associate them to the service account.
+
+For example, user can extend the permissions for an Argo CD instance to be able to list the secrets for all namespaces by creating the below resources.
+
+#### Cluster Role
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+  # "namespace" omitted since ClusterRoles are not namespaced
+  name: secrets-cluster-role
+rules:
+- apiGroups: [""] #specifies core api groups
+  resources: ["secrets"]
+  verbs: ["*"]
+```
+
+#### Cluster Role Binding
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+# This cluster role binding allows Service Account to read secrets in any namespace.
+kind: ClusterRoleBinding
+metadata:
+  name: read-secrets-global
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: secrets-cluster-role # Name of cluster role to be referenced
+subjects:
+- kind: ServiceAccount
+  name: <argocd-instance-service-account-name>
+  namespace: <argocd-instance-namespace>
+```
diff --git a/docs/usage/custom_roles.md b/docs/usage/custom_roles.md
index 04633217..e11d6c6c 100644
--- a/docs/usage/custom_roles.md
+++ b/docs/usage/custom_roles.md
@@ -37,20 +37,4 @@ spec:
             value: custom-controller-role
           - name: SERVER_CLUSTER_ROLE
             value: custom-server-role
-```
-
-
-When an Argo CD instance is deleted, namespaces managed by that instance (via the `argocd.argoproj.io/managed-by` label ) will retain the label by default. Users can change this behavior by setting the environment variable `REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION` to `true` in the Subscription.
-
-```yaml
-apiVersion: operators.coreos.com/v1alpha1
-kind: Subscription
-metadata:
-  name: argocd-operator
-  namespace: argocd
-spec:
-  config:
-    env:
-    - name: REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION
-      value: "true"
 ```
\ No newline at end of file
diff --git a/docs/usage/deploy-to-different-namespaces.md b/docs/usage/deploy-to-different-namespaces.md
new file mode 100644
index 00000000..89e1b2ba
--- /dev/null
+++ b/docs/usage/deploy-to-different-namespaces.md
@@ -0,0 +1,14 @@
+# Deploy resources to a different namespace
+
+To grant Argo CD the permissions to manage resources in multiple namespaces, we need to configure the namespace with a label `argocd.argoproj.io/managed-by` and the value being the namespace of the managing Argo CD instance.
+
+For example, If Argo CD instance deployed in the namespace `foo` wants to manage resources in namespace `bar`. Update the namespace `bar` as shown below.
+
+```yml
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: bar
+  labels:
+    argocd.argoproj.io/managed-by: foo // namespace of the Argo CD instance
+```
diff --git a/docs/usage/environment_variables.md b/docs/usage/environment_variables.md
new file mode 100644
index 00000000..2a2e3451
--- /dev/null
+++ b/docs/usage/environment_variables.md
@@ -0,0 +1,39 @@
+# Environment Variables
+
+The following environment variables are available in `argocd-operator`:
+
+| Environment Variable | Default Value | Description |
+| --- | --- | --- |
+| `CONTROLLER_CLUSTER_ROLE` | none | Administrators can configure a common cluster role for all the managed namespaces in role bindings for the Argo CD application controller with this environment variable. Note: If this environment variable contains custom roles, the Operator doesn't create the default admin role. Instead, it uses the existing custom role for all managed namespaces. |
+| `SERVER_CLUSTER_ROLE` | none | Administrators can configure a common cluster role for all the managed namespaces in role bindings for the Argo CD server with this environment variable. Note: If this environment variable contains custom roles, the Operator doesn’t create the default admin role. Instead, it uses the existing custom role for all managed namespaces. |
+| `REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION` | false | When an Argo CD instance is deleted, namespaces managed by that instance (via the `argocd.argoproj.io/managed-by` label ) will retain the label by default. Users can change this behavior by setting the environment variable `REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION` to `true` in the Subscription. |
+
+Custom Environment Variables are supported in `applicationSet`, `controller`, `notifications`, `repo` and `server` components. For example:
+
+```
+...
+kind: ArgoCD
+metadata:
+  name: argocd
+  labels:
+    example: controller
+spec:
+  controller:
+    resources: {}
+    env:
+      - name: FOO
+        value: bar
+...
+```
+
+The following default value of images could be overridden by setting the environment variables:
+| Environment Variable | Default Value |
+| --- | --- |
+| `ARGOCD_IMAGE` | [quay.io/argoproj/argocd](quay.io/argoproj/argocd) |
+| `ARGOCD_REPOSERVER_IMAGE` | [quay.io/argoproj/argocd](quay.io/argoproj/argocd) |
+| `ARGOCD_DEX_IMAGE` | [ghcr.io/dexidp/dex](ghcr.io/dexidp/dex) |
+| `ARGOCD_KEYCLOAK_IMAGE` | [quay.io/keycloak/keycloak](quay.io/keycloak/keycloak) |
+| `ARGOCD_REDIS_IMAGE` | redis |
+| `ARGOCD_REDIS_HA_IMAGE` | redis |
+| `ARGOCD_REDIS_HA_PROXY_IMAGE` | haproxy |
+| `ARGOCD_GRAFANA_IMAGE` | grafana/grafana |
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 7242a32c..70a39440 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/argoproj-labs/argocd-operator
 go 1.18
 
 require (
-	github.com/argoproj/argo-cd/v2 v2.6.3
+	github.com/argoproj/argo-cd/v2 v2.6.7
 	github.com/coreos/prometheus-operator v0.40.0
 	github.com/go-logr/logr v1.2.3
 	github.com/google/go-cmp v0.5.9
@@ -68,11 +68,11 @@ require (
 	go.uber.org/multierr v1.6.0 // indirect
 	go.uber.org/zap v1.19.1 // indirect
 	golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
-	golang.org/x/net v0.4.0 // indirect
+	golang.org/x/net v0.7.0 // indirect
 	golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb // indirect
-	golang.org/x/sys v0.3.0 // indirect
-	golang.org/x/term v0.3.0 // indirect
-	golang.org/x/text v0.5.0 // indirect
+	golang.org/x/sys v0.5.0 // indirect
+	golang.org/x/term v0.5.0 // indirect
+	golang.org/x/text v0.7.0 // indirect
 	golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
 	google.golang.org/appengine v1.6.7 // indirect
diff --git a/go.sum b/go.sum
index 3a0a204f..d46b0ae6 100644
--- a/go.sum
+++ b/go.sum
@@ -145,8 +145,8 @@ github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20210826220005-b48c857c3a0e/go.m
 github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db/go.mod h1:VTxUBvSJ3s3eHAg65PNgrsn5BtqCRPdmyXh6rAfdxN0=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
-github.com/argoproj/argo-cd/v2 v2.6.3 h1:XEvGDnGRroAw5mKKUTcfIbTmjmhL0hEsVh1+vkytrLo=
-github.com/argoproj/argo-cd/v2 v2.6.3/go.mod h1:wkQAcIEXpgP8F45T/9UwwHhhLz2KgITba0NOby4NTPQ=
+github.com/argoproj/argo-cd/v2 v2.6.7 h1:hBW8QNi6rAN5yERIiwLz3dzErxoJ1Y3BGWwlFsnxETM=
+github.com/argoproj/argo-cd/v2 v2.6.7/go.mod h1:Vqnr5UMfUt+01ycy1bVTARUVGuOUZmGAp52CC3spkVo=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
@@ -1425,8 +1425,8 @@ golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qx
 golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
 golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
-golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
+golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
+golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1571,13 +1571,13 @@ golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
-golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI=
-golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
+golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180805044716-cb6730876b98/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1588,8 +1588,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
-golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/mkdocs.yml b/mkdocs.yml
index e7157c4c..0ff6cb74 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -30,6 +30,7 @@ nav:
     - Basics: usage/basics.md
     - Config Management: usage/config_management_2.0.md
     - Custom Tooling: usage/customization.md
+    - Deploy Resources to Different Namespaces: usage/deploy-to-different-namespaces.md
     - Export: usage/export.md
     - ExtraConfig: usage/extra-config.md
     - High Availability: usage/ha.md


Test patch:
diff --git a/controllers/argocd/deployment_test.go b/controllers/argocd/deployment_test.go
index 7cb5b378..1d9c2051 100644
--- a/controllers/argocd/deployment_test.go
+++ b/controllers/argocd/deployment_test.go
@@ -87,34 +87,46 @@ func TestReconcileArgoCD_reconcileRepoDeployment_replicas(t *testing.T) {
 func TestReconcileArgoCD_reconcile_ServerDeployment_replicas(t *testing.T) {
 	logf.SetLogger(ZapLogger(true))
 
+	var (
+		initalReplicas  int32 = 4
+		updatedReplicas int32 = 5
+	)
+
 	tests := []struct {
-		name          string
-		replicas      int32
-		autoscale     bool
-		expectedNil   bool
-		expectedValue int32
+		name              string
+		initialReplicas   *int32
+		updatedReplicas   *int32
+		autoscale         bool
+		wantFinalReplicas *int32
 	}{
 		{
-			name:          "replicas field in the spec should reflect the number of replicas on the cluster",
-			replicas:      5,
-			autoscale:     false,
-			expectedNil:   false,
-			expectedValue: 5,
+			name:              "deployment spec replicas initially nil, updated by operator, no autoscale",
+			initialReplicas:   nil,
+			updatedReplicas:   &updatedReplicas,
+			autoscale:         false,
+			wantFinalReplicas: &updatedReplicas,
+		},
+		{
+			name:              "deployment spec replicas initially not nil, updated by operator, no autoscale",
+			initialReplicas:   &initalReplicas,
+			updatedReplicas:   &updatedReplicas,
+			autoscale:         false,
+			wantFinalReplicas: &updatedReplicas,
 		},
 		{
-			name:        "replicas should be overriden by autoscale",
-			replicas:    1,
-			autoscale:   true,
-			expectedNil: true,
+			name:              "deployment spec replicas initially nil, ignored by operator with autoscale",
+			initialReplicas:   nil,
+			updatedReplicas:   &updatedReplicas,
+			autoscale:         true,
+			wantFinalReplicas: nil,
 		},
 	}
 
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-
 			a := makeTestArgoCD(func(a *argoprojv1alpha1.ArgoCD) {
+				a.Spec.Server.Replicas = test.initialReplicas
 				a.Spec.Server.Autoscale.Enabled = test.autoscale
-				a.Spec.Server.Replicas = &test.replicas
 			})
 			r := makeTestReconciler(t, a)
 
@@ -127,10 +139,20 @@ func TestReconcileArgoCD_reconcile_ServerDeployment_replicas(t *testing.T) {
 				Namespace: testNamespace,
 			}, deployment)
 			assert.NoError(t, err)
-			assert.Equal(t, test.expectedNil, deployment.Spec.Replicas == nil)
-			if deployment.Spec.Replicas != nil {
-				assert.Equal(t, test.expectedValue, *deployment.Spec.Replicas)
-			}
+			assert.Equal(t, test.initialReplicas, deployment.Spec.Replicas)
+
+			a.Spec.Server.Replicas = test.updatedReplicas
+			err = r.reconcileServerDeployment(a, false)
+			assert.NoError(t, err)
+
+			deployment = &appsv1.Deployment{}
+			err = r.Client.Get(context.TODO(), types.NamespacedName{
+				Name:      "argocd-server",
+				Namespace: testNamespace,
+			}, deployment)
+			assert.NoError(t, err)
+			assert.Equal(t, test.wantFinalReplicas, deployment.Spec.Replicas)
+
 		})
 	}
 }
diff --git a/controllers/argocd/hpa_test.go b/controllers/argocd/hpa_test.go
new file mode 100644
index 00000000..1090bff8
--- /dev/null
+++ b/controllers/argocd/hpa_test.go
@@ -0,0 +1,91 @@
+package argocd
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	autoscaling "k8s.io/api/autoscaling/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/types"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+)
+
+var (
+	min     int32 = 5
+	max     int32 = 8
+	cpuUtil int32 = 45
+)
+
+func TestReconcileHPA(t *testing.T) {
+
+	logf.SetLogger(ZapLogger(true))
+	a := makeTestArgoCD()
+	r := makeTestReconciler(t, a)
+
+	existingHPA := newHorizontalPodAutoscalerWithSuffix("server", a)
+
+	defaultHPASpec := autoscaling.HorizontalPodAutoscalerSpec{
+		MaxReplicas:                    maxReplicas,
+		MinReplicas:                    &minReplicas,
+		TargetCPUUtilizationPercentage: &tcup,
+		ScaleTargetRef: autoscaling.CrossVersionObjectReference{
+			APIVersion: "apps/v1",
+			Kind:       "Deployment",
+			Name:       nameWithSuffix("server", a),
+		},
+	}
+
+	updatedHPASpec := autoscaling.HorizontalPodAutoscalerSpec{
+		MaxReplicas:                    max,
+		MinReplicas:                    &min,
+		TargetCPUUtilizationPercentage: &cpuUtil,
+		ScaleTargetRef: autoscaling.CrossVersionObjectReference{
+			APIVersion: "apps/v1",
+			Kind:       "Deployment",
+			Name:       nameWithSuffix("server", a),
+		},
+	}
+
+	err := r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      "argocd-server",
+		Namespace: testNamespace,
+	}, existingHPA)
+	assert.True(t, errors.IsNotFound(err))
+
+	a.Spec.Server.Autoscale.Enabled = true
+
+	err = r.reconcileServerHPA(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      "argocd-server",
+		Namespace: testNamespace,
+	}, existingHPA)
+	assert.NoError(t, err)
+	assert.Equal(t, defaultHPASpec, existingHPA.Spec)
+
+	a.Spec.Server.Autoscale.HPA = &updatedHPASpec
+
+	err = r.reconcileServerHPA(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      "argocd-server",
+		Namespace: testNamespace,
+	}, existingHPA)
+	assert.NoError(t, err)
+	assert.Equal(t, updatedHPASpec, existingHPA.Spec)
+
+	a.Spec.Server.Autoscale.Enabled = false
+
+	err = r.reconcileServerHPA(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      "argocd-server",
+		Namespace: testNamespace,
+	}, existingHPA)
+	assert.True(t, errors.IsNotFound(err))
+
+}
diff --git a/tests/ocp/1-004_validate_dex_clientsecret/01-assert.yaml b/tests/ocp/1-004_validate_dex_clientsecret/01-assert.yaml
new file mode 100644
index 00000000..6f0ec542
--- /dev/null
+++ b/tests/ocp/1-004_validate_dex_clientsecret/01-assert.yaml
@@ -0,0 +1,15 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+timeout: 120
+---
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: example-argocd-argocd-dex-server
\ No newline at end of file
diff --git a/tests/ocp/1-004_validate_dex_clientsecret/01-install.yaml b/tests/ocp/1-004_validate_dex_clientsecret/01-install.yaml
new file mode 100644
index 00000000..afa54859
--- /dev/null
+++ b/tests/ocp/1-004_validate_dex_clientsecret/01-install.yaml
@@ -0,0 +1,9 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  sso:
+    provider: dex
+    dex:
+     openShiftOAuth: true
\ No newline at end of file
diff --git a/tests/ocp/1-004_validate_dex_clientsecret/02-verify-clientsecret.yaml b/tests/ocp/1-004_validate_dex_clientsecret/02-verify-clientsecret.yaml
new file mode 100644
index 00000000..25defd5b
--- /dev/null
+++ b/tests/ocp/1-004_validate_dex_clientsecret/02-verify-clientsecret.yaml
@@ -0,0 +1,19 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+- script: |
+    # This test validates the Dex Client Secret copied by the operator from dex serviceaccount token secret in to argocd-secret.
+    # To verify the behavior we should first get the token secret name of the dex service account.
+    secret=$(oc get -n $NAMESPACE sa example-argocd-argocd-dex-server -o json | jq -r '.secrets' | grep token | sed 's/    "name": "//g' | sed 's/"//g')
+    
+    # Extract the clientSecret 
+    expectedClientSecret=$(oc get secret $secret -n $NAMESPACE -o json | jq -r '.data.token')
+    
+    # actualClientSecret is the value of the secret in argocd-secret where argocd-operator should copy the secret from
+    actualClientSecret=$(oc get secret argocd-secret -o json -n $NAMESPACE | jq -r '.data."oidc.dex.clientSecret"')
+    
+    # Verify
+    if $expectedClientSecret != $actualClientSecret; then
+      echo "Error: Dex Client Secret for OIDC is not valid"
+      exit 1
+    fi
\ No newline at end of file
diff --git a/tests/ocp/1-005_validate_dex_clientsecret_deprecated/01-assert.yaml b/tests/ocp/1-005_validate_dex_clientsecret_deprecated/01-assert.yaml
new file mode 100644
index 00000000..6f0ec542
--- /dev/null
+++ b/tests/ocp/1-005_validate_dex_clientsecret_deprecated/01-assert.yaml
@@ -0,0 +1,15 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+timeout: 120
+---
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: example-argocd-argocd-dex-server
\ No newline at end of file
diff --git a/tests/ocp/1-005_validate_dex_clientsecret_deprecated/01-install.yaml b/tests/ocp/1-005_validate_dex_clientsecret_deprecated/01-install.yaml
new file mode 100644
index 00000000..b52696a5
--- /dev/null
+++ b/tests/ocp/1-005_validate_dex_clientsecret_deprecated/01-install.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  dex:
+    openShiftOAuth: true
\ No newline at end of file
diff --git a/tests/ocp/1-005_validate_dex_clientsecret_deprecated/02-verify-clientsecret.yaml b/tests/ocp/1-005_validate_dex_clientsecret_deprecated/02-verify-clientsecret.yaml
new file mode 100644
index 00000000..25defd5b
--- /dev/null
+++ b/tests/ocp/1-005_validate_dex_clientsecret_deprecated/02-verify-clientsecret.yaml
@@ -0,0 +1,19 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+- script: |
+    # This test validates the Dex Client Secret copied by the operator from dex serviceaccount token secret in to argocd-secret.
+    # To verify the behavior we should first get the token secret name of the dex service account.
+    secret=$(oc get -n $NAMESPACE sa example-argocd-argocd-dex-server -o json | jq -r '.secrets' | grep token | sed 's/    "name": "//g' | sed 's/"//g')
+    
+    # Extract the clientSecret 
+    expectedClientSecret=$(oc get secret $secret -n $NAMESPACE -o json | jq -r '.data.token')
+    
+    # actualClientSecret is the value of the secret in argocd-secret where argocd-operator should copy the secret from
+    actualClientSecret=$(oc get secret argocd-secret -o json -n $NAMESPACE | jq -r '.data."oidc.dex.clientSecret"')
+    
+    # Verify
+    if $expectedClientSecret != $actualClientSecret; then
+      echo "Error: Dex Client Secret for OIDC is not valid"
+      exit 1
+    fi
\ No newline at end of file