Repository: argoproj-labs/argocd-operator
Pull request #635 (2022-05-04 15:25:33)

feat: add support for creating argocd notifications workloads 
**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**:
This is 1/2 PRs for adding support for spinning up the notifications-controller within argocd-operator. This PR adds the `.spec.Notifications` section within the Argo CD CR, which users can use to enable/disable the creation of argocd-notifications workload. It also adds a field within the Argo CD `status` to reflect the live state of the notifications controller. This PR creates the required deployment, serviceAccount, role and roleBinding required to have the controller brought up on the cluster successfully, and adds the reconciliation logic for these resoureces along with cleaning up the resources when notifications is disabled 

**Have you updated the necessary documentation?**

* [x] Documentation update is required by this PR.
* [ ] Documentation has been updated.

**Which issue(s) this PR fixes**:
https://github.com/argoproj-labs/argocd-operator/issues/654

**How to test changes / Special notes to the reviewer**:
1. Start the operator locally and spin up an empty Argo CD instance
2. Notice that out of the box Argo CD instance has `.spec.notifications.enabled` set to false and `.status` doesn't reflect 
anything about the notifications controller since it is currently disabled
3. Set `.spec.notifications.enabled` to `true`
4. Verify that deployment, sa, role and rolebinding for notifications-controller are created
5. Verify that `.status.notificationsController` is `running` after deployment has come up
6. Disable notifications by setting `.spec.notifications.enabled` to false
7. Verify that deployment, sa, role and roleBinding are properly cleaned up, and `.status` once again does not mention notifications controller
 

ishitasequeira: Just a few nitpicks which could be ignored. It works the way it has been mentioned in the **How to test**. 
LGTM!! 
@jopit PTAL

Patch:
diff --git a/api/v1alpha1/argocd_types.go b/api/v1alpha1/argocd_types.go
index d3a051d1..8b05ef3c 100644
--- a/api/v1alpha1/argocd_types.go
+++ b/api/v1alpha1/argocd_types.go
@@ -270,6 +270,28 @@ type ArgoCDList struct {
 	Items           []ArgoCD `json:"items"`
 }
 
+// ArgoCDNotifications defines whether the Argo CD Notifications controller should be installed.
+type ArgoCDNotifications struct {
+
+	// Replicas defines the number of replicas to run for notifications-controller
+	Replicas *int32 `json:"replicas,omitempty"`
+
+	// Enabled defines whether argocd-notifications controller should be deployed or not
+	Enabled bool `json:"enabled"`
+
+	// Image is the Argo CD Notifications image (optional)
+	Image string `json:"image,omitempty"`
+
+	// Version is the Argo CD Notifications image tag. (optional)
+	Version string `json:"version,omitempty"`
+
+	// Resources defines the Compute Resources required by the container for Argo CD Notifications.
+	Resources *corev1.ResourceRequirements `json:"resources,omitempty"`
+
+	// LogLevel describes the log level that should be used by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel if not set.  Valid options are debug,info, error, and warn.
+	LogLevel string `json:"logLevel,omitempty"`
+}
+
 // ArgoCDPrometheusSpec defines the desired state for the Prometheus component.
 type ArgoCDPrometheusSpec struct {
 	// Enabled will toggle Prometheus support globally for ArgoCD.
@@ -327,6 +349,14 @@ type ArgoCDRedisSpec struct {
 	// Version is the Redis container image tag.
 	//+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Version",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:fieldGroup:Redis","urn:alm:descriptor:com.tectonic.ui:text"}
 	Version string `json:"version,omitempty"`
+
+	// DisableTLSVerification defines whether redis server API should be accessed using strict TLS validation
+	DisableTLSVerification bool `json:"disableTLSVerification,omitempty"`
+
+	// AutoTLS specifies the method to use for automatic TLS configuration for the redis server
+	// The value specified here can currently be:
+	// - openshift - Use the OpenShift service CA to request TLS config
+	AutoTLS string `json:"autotls,omitempty"`
 }
 
 // ArgoCDRepoSpec defines the desired state for the Argo CD repo server component.
@@ -544,6 +574,14 @@ type ArgoCDSpec struct {
 	// DisableAdmin will disable the admin user.
 	DisableAdmin bool `json:"disableAdmin,omitempty"`
 
+	// ExtraConfig can be used to add fields to Argo CD configmap that are not supported by Argo CD CRD.
+	//
+	// Note: ExtraConfig takes precedence over Argo CD CRD.
+	// For example, A user sets `argocd.Spec.DisableAdmin` = true and also
+	// `a.Spec.ExtraConfig["admin.enabled"]` = true. In this case, operator updates
+	// Argo CD Configmap as follows -> argocd-cm.Data["admin.enabled"] = true.
+	ExtraConfig map[string]string `json:"extraConfig,omitempty"`
+
 	// GATrackingID is the google analytics tracking ID to use.
 	//+operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Google Analytics Tracking ID'",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:text","urn:alm:descriptor:com.tectonic.ui:advanced"}
 	GATrackingID string `json:"gaTrackingID,omitempty"`
@@ -594,6 +632,9 @@ type ArgoCDSpec struct {
 	// NodePlacement defines NodeSelectors and Taints for Argo CD workloads
 	NodePlacement *ArgoCDNodePlacementSpec `json:"nodePlacement,omitempty"`
 
+	// Notifications defines whether the Argo CD Notifications controller should be installed.
+	Notifications ArgoCDNotifications `json:"notifications,omitempty"`
+
 	// Prometheus defines the Prometheus server options for ArgoCD.
 	Prometheus ArgoCDPrometheusSpec `json:"prometheus,omitempty"`
 
@@ -672,6 +713,15 @@ type ArgoCDStatus struct {
 	//+operator-sdk:csv:customresourcedefinitions:type=status,displayName="Dex",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:text"}
 	Dex string `json:"dex,omitempty"`
 
+	// NotificationsController is a simple, high-level summary of where the Argo CD notifications controller component is in its lifecycle.
+	// There are five possible NotificationsController values:
+	// Pending: The Argo CD notifications controller component has been accepted by the Kubernetes system, but one or more of the required resources have not been created.
+	// Running: All of the required Pods for the Argo CD notifications controller component are in a Ready state.
+	// Failed: At least one of the  Argo CD notifications controller component Pods had a failure.
+	// Unknown: For some reason the state of the Argo CD notifications controller component could not be obtained.
+	//+operator-sdk:csv:customresourcedefinitions:type=status,displayName="NotificationsController",xDescriptors={"urn:alm:descriptor:com.tectonic.ui:text"}
+	NotificationsController string `json:"notificationsController,omitempty"`
+
 	// SSOConfig defines the status of SSO configuration.
 	// Success: Only one SSO provider is configured in CR.
 	// Failed: More than one SSO providers are configure in CR.
@@ -718,6 +768,9 @@ type ArgoCDStatus struct {
 	// RepoTLSChecksum contains the SHA256 checksum of the latest known state of tls.crt and tls.key in the argocd-repo-server-tls secret.
 	RepoTLSChecksum string `json:"repoTLSChecksum,omitempty"`
 
+	// RedisTLSChecksum contains the SHA256 checksum of the latest known state of tls.crt and tls.key in the argocd-operator-redis-tls secret.
+	RedisTLSChecksum string `json:"redisTLSChecksum,omitempty"`
+
 	// Host is the hostname of the Ingress.
 	Host string `json:"host,omitempty"`
 }
@@ -772,6 +825,12 @@ func (r *ArgoCDRepoSpec) WantsAutoTLS() bool {
 	return r.AutoTLS == "openshift"
 }
 
+// WantsAutoTLS returns true if the redis server configuration has set
+// the autoTLS toggle to a supported provider.
+func (r *ArgoCDRedisSpec) WantsAutoTLS() bool {
+	return r.AutoTLS == "openshift"
+}
+
 // ApplicationInstanceLabelKey returns either the custom application instance
 // label key if set, or the default value.
 func (a *ArgoCD) ApplicationInstanceLabelKey() string {
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index fc169483..6aad6213 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -487,6 +487,31 @@ func (in *ArgoCDNodePlacementSpec) DeepCopy() *ArgoCDNodePlacementSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArgoCDNotifications) DeepCopyInto(out *ArgoCDNotifications) {
+	*out = *in
+	if in.Replicas != nil {
+		in, out := &in.Replicas, &out.Replicas
+		*out = new(int32)
+		**out = **in
+	}
+	if in.Resources != nil {
+		in, out := &in.Resources, &out.Resources
+		*out = new(v1.ResourceRequirements)
+		(*in).DeepCopyInto(*out)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArgoCDNotifications.
+func (in *ArgoCDNotifications) DeepCopy() *ArgoCDNotifications {
+	if in == nil {
+		return nil
+	}
+	out := new(ArgoCDNotifications)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ArgoCDPrometheusSpec) DeepCopyInto(out *ArgoCDPrometheusSpec) {
 	*out = *in
@@ -791,6 +816,13 @@ func (in *ArgoCDSpec) DeepCopyInto(out *ArgoCDSpec) {
 	}
 	in.Controller.DeepCopyInto(&out.Controller)
 	in.Dex.DeepCopyInto(&out.Dex)
+	if in.ExtraConfig != nil {
+		in, out := &in.ExtraConfig, &out.ExtraConfig
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	in.Grafana.DeepCopyInto(&out.Grafana)
 	in.HA.DeepCopyInto(&out.HA)
 	if in.Import != nil {
@@ -809,6 +841,7 @@ func (in *ArgoCDSpec) DeepCopyInto(out *ArgoCDSpec) {
 		*out = new(ArgoCDNodePlacementSpec)
 		(*in).DeepCopyInto(*out)
 	}
+	in.Notifications.DeepCopyInto(&out.Notifications)
 	in.Prometheus.DeepCopyInto(&out.Prometheus)
 	in.RBAC.DeepCopyInto(&out.RBAC)
 	in.Redis.DeepCopyInto(&out.Redis)
diff --git a/build/redis/haproxy.cfg.tpl b/build/redis/haproxy.cfg.tpl
index db813c52..6fbdeb6c 100644
--- a/build/redis/haproxy.cfg.tpl
+++ b/build/redis/haproxy.cfg.tpl
@@ -1,3 +1,8 @@
+{{- if eq .UseTLS "true"}}
+global
+    ca-base /app/config/redis/tls
+{{- end}}
+
 defaults REDIS
     mode tcp
     timeout connect 4s
@@ -14,44 +19,74 @@ listen health_check_http_url
 backend check_if_redis_is_master_0
     mode tcp
     option tcp-check
+{{- if eq .UseTLS "false"}}
     tcp-check connect
+{{- else}}
+    tcp-check connect ssl
+{{- end}}
     tcp-check send PING\r\n
     tcp-check expect string +PONG
     tcp-check send SENTINEL\ get-master-addr-by-name\ argocd\r\n
     tcp-check expect string REPLACE_ANNOUNCE0
     tcp-check send QUIT\r\n
     tcp-check expect string +OK
+{{- if eq .UseTLS "false"}}
     server R0 {{.ServiceName}}-announce-0:26379 check inter 3s
     server R1 {{.ServiceName}}-announce-1:26379 check inter 3s
     server R2 {{.ServiceName}}-announce-2:26379 check inter 3s
+{{- else}}
+    server R0 {{.ServiceName}}-announce-0:26379 verify required ca-file tls.crt check inter 3s
+    server R1 {{.ServiceName}}-announce-1:26379 verify required ca-file tls.crt check inter 3s
+    server R2 {{.ServiceName}}-announce-2:26379 verify required ca-file tls.crt check inter 3s
+{{- end}}
 # Check Sentinel and whether they are nominated master
 backend check_if_redis_is_master_1
     mode tcp
     option tcp-check
+{{- if eq .UseTLS "false"}}
     tcp-check connect
+{{- else}}
+    tcp-check connect ssl
+{{- end}}
     tcp-check send PING\r\n
     tcp-check expect string +PONG
     tcp-check send SENTINEL\ get-master-addr-by-name\ argocd\r\n
     tcp-check expect string REPLACE_ANNOUNCE1
     tcp-check send QUIT\r\n
     tcp-check expect string +OK
+{{- if eq .UseTLS "false"}}
     server R0 {{.ServiceName}}-announce-0:26379 check inter 3s
     server R1 {{.ServiceName}}-announce-1:26379 check inter 3s
     server R2 {{.ServiceName}}-announce-2:26379 check inter 3s
+{{- else}}
+    server R0 {{.ServiceName}}-announce-0:26379 verify required ca-file tls.crt check inter 3s
+    server R1 {{.ServiceName}}-announce-1:26379 verify required ca-file tls.crt check inter 3s
+    server R2 {{.ServiceName}}-announce-2:26379 verify required ca-file tls.crt check inter 3s
+{{- end}}
 # Check Sentinel and whether they are nominated master
 backend check_if_redis_is_master_2
     mode tcp
     option tcp-check
+{{- if eq .UseTLS "false"}}
     tcp-check connect
+{{- else}}
+    tcp-check connect ssl
+{{- end}}
     tcp-check send PING\r\n
     tcp-check expect string +PONG
     tcp-check send SENTINEL\ get-master-addr-by-name\ argocd\r\n
     tcp-check expect string REPLACE_ANNOUNCE2
     tcp-check send QUIT\r\n
     tcp-check expect string +OK
+{{- if eq .UseTLS "false"}}
     server R0 {{.ServiceName}}-announce-0:26379 check inter 3s
     server R1 {{.ServiceName}}-announce-1:26379 check inter 3s
     server R2 {{.ServiceName}}-announce-2:26379 check inter 3s
+{{- else}}
+    server R0 {{.ServiceName}}-announce-0:26379 verify required ca-file tls.crt check inter 3s
+    server R1 {{.ServiceName}}-announce-1:26379 verify required ca-file tls.crt check inter 3s
+    server R2 {{.ServiceName}}-announce-2:26379 verify required ca-file tls.crt check inter 3s
+{{- end}}
 
 # decide redis backend to use
 #master
@@ -62,16 +97,29 @@ frontend ft_redis_master
 backend bk_redis_master
     mode tcp
     option tcp-check
+{{- if eq .UseTLS "false"}}
     tcp-check connect
+{{- else}}
+    tcp-check connect ssl
+{{- end}}
     tcp-check send PING\r\n
     tcp-check expect string +PONG
     tcp-check send info\ replication\r\n
     tcp-check expect string role:master
     tcp-check send QUIT\r\n
     tcp-check expect string +OK
+{{- if eq .UseTLS "false"}}
     use-server R0 if { srv_is_up(R0) } { nbsrv(check_if_redis_is_master_0) ge 2 }
     server R0 {{.ServiceName}}-announce-0:6379 check inter 3s fall 1 rise 1
     use-server R1 if { srv_is_up(R1) } { nbsrv(check_if_redis_is_master_1) ge 2 }
     server R1 {{.ServiceName}}-announce-1:6379 check inter 3s fall 1 rise 1
     use-server R2 if { srv_is_up(R2) } { nbsrv(check_if_redis_is_master_2) ge 2 }
     server R2 {{.ServiceName}}-announce-2:6379 check inter 3s fall 1 rise 1
+{{- else}}
+    use-server R0 if { srv_is_up(R0) } { nbsrv(check_if_redis_is_master_0) ge 2 }
+    server R0 {{.ServiceName}}-announce-0:6379 verify required ca-file tls.crt check inter 3s fall 1 rise 1
+    use-server R1 if { srv_is_up(R1) } { nbsrv(check_if_redis_is_master_1) ge 2 }
+    server R1 {{.ServiceName}}-announce-1:6379 verify required ca-file tls.crt check inter 3s fall 1 rise 1
+    use-server R2 if { srv_is_up(R2) } { nbsrv(check_if_redis_is_master_2) ge 2 }
+    server R2 {{.ServiceName}}-announce-2:6379 verify required ca-file tls.crt check inter 3s fall 1 rise 1
+{{- end}}
diff --git a/build/redis/init.sh.tpl b/build/redis/init.sh.tpl
index 4d38152a..9e08adad 100644
--- a/build/redis/init.sh.tpl
+++ b/build/redis/init.sh.tpl
@@ -1,24 +1,29 @@
 echo "$(date) Start..."
 HOSTNAME="$(cat /proc/sys/kernel/hostname)"
 INDEX="${HOSTNAME##*-}"
-SENTINEL_PORT=26379
+SENTINEL_PORT={{- if eq .UseTLS "false" -}}26379{{- else -}}0{{- end }}
 MASTER=''
 MASTER_GROUP="argocd"
 QUORUM="2"
 REDIS_CONF=/data/conf/redis.conf
+{{- if eq .UseTLS "false"}}
 REDIS_PORT=6379
 REDIS_TLS_PORT=
+{{- else}}
+REDIS_PORT=0
+REDIS_TLS_PORT=6379
+{{- end}}
 SENTINEL_CONF=/data/conf/sentinel.conf
-SENTINEL_TLS_PORT=
+SENTINEL_TLS_PORT={{- if eq .UseTLS "true" -}}26379{{- end }}
 SERVICE={{.ServiceName}}
-SENTINEL_TLS_REPLICATION_ENABLED=false
-REDIS_TLS_REPLICATION_ENABLED=false
+SENTINEL_TLS_REPLICATION_ENABLED={{.UseTLS}}
+REDIS_TLS_REPLICATION_ENABLED={{.UseTLS}}
 set -eu
 
 sentinel_get_master() {
 set +e
     if [ "$SENTINEL_PORT" -eq 0 ]; then
-        redis-cli -h "${SERVICE}" -p "${SENTINEL_TLS_PORT}"   --tls --cacert /tls-certs/ca.crt --cert /tls-certs/redis.crt --key /tls-certs/redis.key sentinel get-master-addr-by-name "${MASTER_GROUP}" |\
+        redis-cli -h "${SERVICE}" -p "${SENTINEL_TLS_PORT}"   --tls --cacert /app/config/redis/tls/tls.crt sentinel get-master-addr-by-name "${MASTER_GROUP}" |\
         grep -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'
     else
         redis-cli -h "${SERVICE}" -p "${SENTINEL_PORT}"  sentinel get-master-addr-by-name "${MASTER_GROUP}" |\
@@ -128,7 +133,7 @@ setup_defaults() {
 redis_ping() {
 set +e
     if [ "$REDIS_PORT" -eq 0 ]; then
-        redis-cli -h "${MASTER}" -p "${REDIS_TLS_PORT}"  --tls --cacert /tls-certs/ca.crt --cert /tls-certs/redis.crt --key /tls-certs/redis.key ping
+        redis-cli -h "${MASTER}" -p "${REDIS_TLS_PORT}"  --tls --cacert /app/config/redis/tls/tls.crt ping
     else
         redis-cli -h "${MASTER}" -p "${REDIS_PORT}" ping
     fi
@@ -165,7 +170,7 @@ find_master() {
         if [ "$SENTINEL_PORT" -eq 0 ]; then
             echo "  on sentinel (${SERVICE}:${SENTINEL_TLS_PORT}), sentinel grp (${MASTER_GROUP})"
             echo "  $(date).."
-            if redis-cli -h "${SERVICE}" -p "${SENTINEL_TLS_PORT}"   --tls --cacert /tls-certs/ca.crt --cert /tls-certs/redis.crt --key /tls-certs/redis.key sentinel failover "${MASTER_GROUP}" | grep -q 'NOGOODSLAVE' ; then
+            if redis-cli -h "${SERVICE}" -p "${SENTINEL_TLS_PORT}"   --tls --cacert /app/config/redis/tls/tls.crt sentinel failover "${MASTER_GROUP}" | grep -q 'NOGOODSLAVE' ; then
                 echo "  $(date) Failover returned with 'NOGOODSLAVE'"
                 echo "Setting defaults for this pod.."
                 setup_defaults
diff --git a/build/redis/redis.conf.tpl b/build/redis/redis.conf.tpl
index 55a878fc..1adee815 100644
--- a/build/redis/redis.conf.tpl
+++ b/build/redis/redis.conf.tpl
@@ -1,5 +1,15 @@
 dir "/data"
+{{- if eq .UseTLS "false"}}
 port 6379
+{{- else}}
+port 0
+tls-port 6379
+tls-cert-file /app/config/redis/tls/tls.crt
+tls-ca-cert-file /app/config/redis/tls/tls.crt
+tls-key-file /app/config/redis/tls/tls.key
+tls-replication yes
+tls-auth-clients no
+{{- end}}
 bind 0.0.0.0
 maxmemory 0
 maxmemory-policy volatile-lru
diff --git a/build/redis/redis_liveness.sh.tpl b/build/redis/redis_liveness.sh.tpl
index d59ef061..b6480718 100644
--- a/build/redis/redis_liveness.sh.tpl
+++ b/build/redis/redis_liveness.sh.tpl
@@ -2,6 +2,10 @@ response=$(
   redis-cli \
     -h localhost \
     -p 6379 \
+{{- if eq .UseTLS "true"}}
+    --tls \
+    --cacert /app/config/redis/tls/tls.crt \
+{{- end}}
     ping
 )
 if [ "$response" != "PONG" ] && [ "${response:0:7}" != "LOADING" ] ; then
diff --git a/build/redis/redis_readiness.sh.tpl b/build/redis/redis_readiness.sh.tpl
index 9e012458..c6caf15c 100644
--- a/build/redis/redis_readiness.sh.tpl
+++ b/build/redis/redis_readiness.sh.tpl
@@ -2,6 +2,10 @@ response=$(
   redis-cli \
     -h localhost \
     -p 6379 \
+{{- if eq .UseTLS "true"}}
+    --tls \
+    --cacert /app/config/redis/tls/tls.crt \
+{{- end}}
     ping
 )
 if [ "$response" != "PONG" ] ; then
diff --git a/build/redis/sentinel.conf.tpl b/build/redis/sentinel.conf.tpl
index 5be91286..5466fd5e 100644
--- a/build/redis/sentinel.conf.tpl
+++ b/build/redis/sentinel.conf.tpl
@@ -1,5 +1,15 @@
 dir "/data"
+{{- if eq .UseTLS "false"}}
 port 26379
+{{- else}}
+port 0
+tls-port 26379
+tls-cert-file /app/config/redis/tls/tls.crt
+tls-ca-cert-file /app/config/redis/tls/tls.crt
+tls-key-file /app/config/redis/tls/tls.key
+tls-replication yes
+tls-auth-clients no
+{{- end}}
 bind 0.0.0.0
     sentinel down-after-milliseconds argocd 10000
     sentinel failover-timeout argocd 180000
diff --git a/build/redis/sentinel_liveness.sh.tpl b/build/redis/sentinel_liveness.sh.tpl
index b5799cc7..5629c6f3 100644
--- a/build/redis/sentinel_liveness.sh.tpl
+++ b/build/redis/sentinel_liveness.sh.tpl
@@ -2,6 +2,10 @@ response=$(
   redis-cli \
     -h localhost \
     -p 26379 \
+{{- if eq .UseTLS "true"}}
+    --tls \
+    --cacert /app/config/redis/tls/tls.crt \
+{{- end}}
     ping
 )
 if [ "$response" != "PONG" ]; then
diff --git a/bundle/manifests/argocd-operator.clusterserviceversion.yaml b/bundle/manifests/argocd-operator.clusterserviceversion.yaml
index 55231e18..c31e9d9c 100644
--- a/bundle/manifests/argocd-operator.clusterserviceversion.yaml
+++ b/bundle/manifests/argocd-operator.clusterserviceversion.yaml
@@ -701,6 +701,19 @@ spec:
         path: dex
         x-descriptors:
         - urn:alm:descriptor:com.tectonic.ui:text
+      - description: 'NotificationsController is a simple, high-level summary of where
+          the Argo CD notifications controller component is in its lifecycle. There
+          are five possible NotificationsController values: Pending: The Argo CD notifications
+          controller component has been accepted by the Kubernetes system, but one
+          or more of the required resources have not been created. Running: All of
+          the required Pods for the Argo CD notifications controller component are
+          in a Ready state. Failed: At least one of the  Argo CD notifications controller
+          component Pods had a failure. Unknown: For some reason the state of the
+          Argo CD notifications controller component could not be obtained.'
+        displayName: NotificationsController
+        path: notificationsController
+        x-descriptors:
+        - urn:alm:descriptor:com.tectonic.ui:text
       - description: 'Phase is a simple, high-level summary of where the ArgoCD is
           in its lifecycle. There are five possible phase values: Pending: The ArgoCD
           has been accepted by the Kubernetes system, but one or more of the required
diff --git a/bundle/manifests/argoproj.io_argocds.yaml b/bundle/manifests/argoproj.io_argocds.yaml
index ff3b95fb..610ff8f1 100644
--- a/bundle/manifests/argoproj.io_argocds.yaml
+++ b/bundle/manifests/argoproj.io_argocds.yaml
@@ -345,6 +345,16 @@ spec:
               disableAdmin:
                 description: DisableAdmin will disable the admin user.
                 type: boolean
+              extraConfig:
+                additionalProperties:
+                  type: string
+                description: "ExtraConfig can be used to add fields to Argo CD configmap
+                  that are not supported by Argo CD CRD. \n Note: ExtraConfig takes
+                  precedence over Argo CD CRD. For example, A user sets `argocd.Spec.DisableAdmin`
+                  = true and also `a.Spec.ExtraConfig[\"admin.enabled\"]` = true.
+                  In this case, operator updates Argo CD Configmap as follows -> argocd-cm.Data[\"admin.enabled\"]
+                  = true."
+                type: object
               gaAnonymizeUsers:
                 description: GAAnonymizeUsers toggles user IDs being hashed before
                   sending to google analytics.
@@ -686,6 +696,60 @@ spec:
                       type: object
                     type: array
                 type: object
+              notifications:
+                description: Notifications defines whether the Argo CD Notifications
+                  controller should be installed.
+                properties:
+                  enabled:
+                    description: Enabled defines whether argocd-notifications controller
+                      should be deployed or not
+                    type: boolean
+                  image:
+                    description: Image is the Argo CD Notifications image (optional)
+                    type: string
+                  logLevel:
+                    description: LogLevel describes the log level that should be used
+                      by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel
+                      if not set.  Valid options are debug,info, error, and warn.
+                    type: string
+                  replicas:
+                    description: Replicas defines the number of replicas to run for
+                      notifications-controller
+                    format: int32
+                    type: integer
+                  resources:
+                    description: Resources defines the Compute Resources required
+                      by the container for Argo CD Notifications.
+                    properties:
+                      limits:
+                        additionalProperties:
+                          anyOf:
+                          - type: integer
+                          - type: string
+                          pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                          x-kubernetes-int-or-string: true
+                        description: 'Limits describes the maximum amount of compute
+                          resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
+                        type: object
+                      requests:
+                        additionalProperties:
+                          anyOf:
+                          - type: integer
+                          - type: string
+                          pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                          x-kubernetes-int-or-string: true
+                        description: 'Requests describes the minimum amount of compute
+                          resources required. If Requests is omitted for a container,
+                          it defaults to Limits if that is explicitly specified, otherwise
+                          to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
+                        type: object
+                    type: object
+                  version:
+                    description: Version is the Argo CD Notifications image tag. (optional)
+                    type: string
+                required:
+                - enabled
+                type: object
               oidcConfig:
                 description: OIDCConfig is the OIDC configuration as an alternative
                   to dex.
@@ -856,6 +920,16 @@ spec:
               redis:
                 description: Redis defines the Redis server options for ArgoCD.
                 properties:
+                  autotls:
+                    description: 'AutoTLS specifies the method to use for automatic
+                      TLS configuration for the redis server The value specified here
+                      can currently be: - openshift - Use the OpenShift service CA
+                      to request TLS config'
+                    type: string
+                  disableTLSVerification:
+                    description: DisableTLSVerification defines whether redis server
+                      API should be accessed using strict TLS validation
+                    type: boolean
                   image:
                     description: Image is the Redis container image.
                     type: string
@@ -5696,6 +5770,18 @@ spec:
               host:
                 description: Host is the hostname of the Ingress.
                 type: string
+              notificationsController:
+                description: 'NotificationsController is a simple, high-level summary
+                  of where the Argo CD notifications controller component is in its
+                  lifecycle. There are five possible NotificationsController values:
+                  Pending: The Argo CD notifications controller component has been
+                  accepted by the Kubernetes system, but one or more of the required
+                  resources have not been created. Running: All of the required Pods
+                  for the Argo CD notifications controller component are in a Ready
+                  state. Failed: At least one of the  Argo CD notifications controller
+                  component Pods had a failure. Unknown: For some reason the state
+                  of the Argo CD notifications controller component could not be obtained.'
+                type: string
               phase:
                 description: 'Phase is a simple, high-level summary of where the ArgoCD
                   is in its lifecycle. There are five possible phase values: Pending:
@@ -5716,6 +5802,11 @@ spec:
                   some reason the state of the Argo CD Redis component could not be
                   obtained.'
                 type: string
+              redisTLSChecksum:
+                description: RedisTLSChecksum contains the SHA256 checksum of the
+                  latest known state of tls.crt and tls.key in the argocd-operator-redis-tls
+                  secret.
+                type: string
               repo:
                 description: 'Repo is a simple, high-level summary of where the Argo
                   CD Repo component is in its lifecycle. There are five possible repo
diff --git a/common/defaults.go b/common/defaults.go
index a01875a0..7cebc838 100644
--- a/common/defaults.go
+++ b/common/defaults.go
@@ -15,7 +15,7 @@
 package common
 
 const (
-	// ArgoCDApplicationControllerComponent is the name of the application controller  control plane component
+	// ArgoCDApplicationControllerComponent is the name of the application controller control plane component
 	ArgoCDApplicationControllerComponent = "argocd-application-controller"
 
 	// ArgoCDApplicationControllerDefaultShardReplicas is the default number of replicas that the ArgoCD Application Controller Should Use
@@ -36,6 +36,9 @@ const (
 	// ArgoCDDexServerComponent is the name of the Dex server control plane component
 	ArgoCDDexServerComponent = "argocd-dex-server"
 
+	// ArgoCDNotificationsControllerComponent is the name of the Notifications controller control plane component
+	ArgoCDNotificationsControllerComponent = "argocd-notifications-controller"
+
 	// ArgoCDDefaultAdminPasswordLength is the length of the generated default admin password.
 	ArgoCDDefaultAdminPasswordLength = 32
 
diff --git a/common/values.go b/common/values.go
index 2fe13c94..1613c255 100644
--- a/common/values.go
+++ b/common/values.go
@@ -77,6 +77,9 @@ const (
 	// ArgoCDTLSCertsConfigMapName is the upstream hard-coded TLS certificate data ConfigMap name.
 	ArgoCDTLSCertsConfigMapName = "argocd-tls-certs-cm"
 
+	// ArgoCDRedisServerTLSSecretName is the name of the TLS secret for the redis-server
+	ArgoCDRedisServerTLSSecretName = "argocd-operator-redis-tls"
+
 	// ArgoCDRepoServerTLSSecretName is the name of the TLS secret for the repo-server
 	ArgoCDRepoServerTLSSecretName = "argocd-repo-server-tls"
 
diff --git a/config/crd/bases/argoproj.io_argocds.yaml b/config/crd/bases/argoproj.io_argocds.yaml
index 3ed48750..c69c9fbc 100644
--- a/config/crd/bases/argoproj.io_argocds.yaml
+++ b/config/crd/bases/argoproj.io_argocds.yaml
@@ -347,6 +347,16 @@ spec:
               disableAdmin:
                 description: DisableAdmin will disable the admin user.
                 type: boolean
+              extraConfig:
+                additionalProperties:
+                  type: string
+                description: "ExtraConfig can be used to add fields to Argo CD configmap
+                  that are not supported by Argo CD CRD. \n Note: ExtraConfig takes
+                  precedence over Argo CD CRD. For example, A user sets `argocd.Spec.DisableAdmin`
+                  = true and also `a.Spec.ExtraConfig[\"admin.enabled\"]` = true.
+                  In this case, operator updates Argo CD Configmap as follows -> argocd-cm.Data[\"admin.enabled\"]
+                  = true."
+                type: object
               gaAnonymizeUsers:
                 description: GAAnonymizeUsers toggles user IDs being hashed before
                   sending to google analytics.
@@ -688,6 +698,60 @@ spec:
                       type: object
                     type: array
                 type: object
+              notifications:
+                description: Notifications defines whether the Argo CD Notifications
+                  controller should be installed.
+                properties:
+                  enabled:
+                    description: Enabled defines whether argocd-notifications controller
+                      should be deployed or not
+                    type: boolean
+                  image:
+                    description: Image is the Argo CD Notifications image (optional)
+                    type: string
+                  logLevel:
+                    description: LogLevel describes the log level that should be used
+                      by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel
+                      if not set.  Valid options are debug,info, error, and warn.
+                    type: string
+                  replicas:
+                    description: Replicas defines the number of replicas to run for
+                      notifications-controller
+                    format: int32
+                    type: integer
+                  resources:
+                    description: Resources defines the Compute Resources required
+                      by the container for Argo CD Notifications.
+                    properties:
+                      limits:
+                        additionalProperties:
+                          anyOf:
+                          - type: integer
+                          - type: string
+                          pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                          x-kubernetes-int-or-string: true
+                        description: 'Limits describes the maximum amount of compute
+                          resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
+                        type: object
+                      requests:
+                        additionalProperties:
+                          anyOf:
+                          - type: integer
+                          - type: string
+                          pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                          x-kubernetes-int-or-string: true
+                        description: 'Requests describes the minimum amount of compute
+                          resources required. If Requests is omitted for a container,
+                          it defaults to Limits if that is explicitly specified, otherwise
+                          to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
+                        type: object
+                    type: object
+                  version:
+                    description: Version is the Argo CD Notifications image tag. (optional)
+                    type: string
+                required:
+                - enabled
+                type: object
               oidcConfig:
                 description: OIDCConfig is the OIDC configuration as an alternative
                   to dex.
@@ -858,6 +922,16 @@ spec:
               redis:
                 description: Redis defines the Redis server options for ArgoCD.
                 properties:
+                  autotls:
+                    description: 'AutoTLS specifies the method to use for automatic
+                      TLS configuration for the redis server The value specified here
+                      can currently be: - openshift - Use the OpenShift service CA
+                      to request TLS config'
+                    type: string
+                  disableTLSVerification:
+                    description: DisableTLSVerification defines whether redis server
+                      API should be accessed using strict TLS validation
+                    type: boolean
                   image:
                     description: Image is the Redis container image.
                     type: string
@@ -5698,6 +5772,18 @@ spec:
               host:
                 description: Host is the hostname of the Ingress.
                 type: string
+              notificationsController:
+                description: 'NotificationsController is a simple, high-level summary
+                  of where the Argo CD notifications controller component is in its
+                  lifecycle. There are five possible NotificationsController values:
+                  Pending: The Argo CD notifications controller component has been
+                  accepted by the Kubernetes system, but one or more of the required
+                  resources have not been created. Running: All of the required Pods
+                  for the Argo CD notifications controller component are in a Ready
+                  state. Failed: At least one of the  Argo CD notifications controller
+                  component Pods had a failure. Unknown: For some reason the state
+                  of the Argo CD notifications controller component could not be obtained.'
+                type: string
               phase:
                 description: 'Phase is a simple, high-level summary of where the ArgoCD
                   is in its lifecycle. There are five possible phase values: Pending:
@@ -5718,6 +5804,11 @@ spec:
                   some reason the state of the Argo CD Redis component could not be
                   obtained.'
                 type: string
+              redisTLSChecksum:
+                description: RedisTLSChecksum contains the SHA256 checksum of the
+                  latest known state of tls.crt and tls.key in the argocd-operator-redis-tls
+                  secret.
+                type: string
               repo:
                 description: 'Repo is a simple, high-level summary of where the Argo
                   CD Repo component is in its lifecycle. There are five possible repo
diff --git a/config/manifests/bases/argocd-operator.clusterserviceversion.yaml b/config/manifests/bases/argocd-operator.clusterserviceversion.yaml
index 72570fc5..c2f2eea8 100644
--- a/config/manifests/bases/argocd-operator.clusterserviceversion.yaml
+++ b/config/manifests/bases/argocd-operator.clusterserviceversion.yaml
@@ -578,6 +578,19 @@ spec:
         path: dex
         x-descriptors:
         - urn:alm:descriptor:com.tectonic.ui:text
+      - description: 'NotificationsController is a simple, high-level summary of where
+          the Argo CD notifications controller component is in its lifecycle. There
+          are five possible NotificationsController values: Pending: The Argo CD notifications
+          controller component has been accepted by the Kubernetes system, but one
+          or more of the required resources have not been created. Running: All of
+          the required Pods for the Argo CD notifications controller component are
+          in a Ready state. Failed: At least one of the  Argo CD notifications controller
+          component Pods had a failure. Unknown: For some reason the state of the
+          Argo CD notifications controller component could not be obtained.'
+        displayName: NotificationsController
+        path: notificationsController
+        x-descriptors:
+        - urn:alm:descriptor:com.tectonic.ui:text
       - description: 'Phase is a simple, high-level summary of where the ArgoCD is
           in its lifecycle. There are five possible phase values: Pending: The ArgoCD
           has been accepted by the Kubernetes system, but one or more of the required
diff --git a/controllers/argocd/argocd_controller.go b/controllers/argocd/argocd_controller.go
index 3f5d4511..490d0141 100644
--- a/controllers/argocd/argocd_controller.go
+++ b/controllers/argocd/argocd_controller.go
@@ -93,8 +93,10 @@ func (r *ReconcileArgoCD) Reconcile(ctx context.Context, request ctrl.Request) (
 				return reconcile.Result{}, fmt.Errorf("failed to delete ClusterResources: %w", err)
 			}
 
-			if err := r.removeManagedByLabelFromNamespaces(argocd.Namespace); err != nil {
-				return reconcile.Result{}, fmt.Errorf("failed to remove label from namespace[%v], error: %w", argocd.Namespace, err)
+			if isRemoveManagedByLabelOnArgoCDDeletion() {
+				if err := r.removeManagedByLabelFromNamespaces(argocd.Namespace); err != nil {
+					return reconcile.Result{}, fmt.Errorf("failed to remove label from namespace[%v], error: %w", argocd.Namespace, err)
+				}
 			}
 
 			if err := r.removeDeletionFinalizer(argocd); err != nil {
@@ -131,6 +133,6 @@ func (r *ReconcileArgoCD) Reconcile(ctx context.Context, request ctrl.Request) (
 // SetupWithManager sets up the controller with the Manager.
 func (r *ReconcileArgoCD) SetupWithManager(mgr ctrl.Manager) error {
 	bldr := ctrl.NewControllerManagedBy(mgr)
-	setResourceWatches(bldr, r.clusterResourceMapper, r.tlsSecretMapper, r.namespaceResourceMapper)
+	r.setResourceWatches(bldr, r.clusterResourceMapper, r.tlsSecretMapper, r.namespaceResourceMapper)
 	return bldr.Complete(r)
 }
diff --git a/controllers/argocd/configmap.go b/controllers/argocd/configmap.go
index 06ca06b5..3a790aef 100644
--- a/controllers/argocd/configmap.go
+++ b/controllers/argocd/configmap.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"path/filepath"
+	"reflect"
 	"strings"
 	"time"
 
@@ -74,7 +75,11 @@ func getConfigManagementPlugins(cr *argoprojv1a1.ArgoCD) string {
 func getDexConfig(cr *argoprojv1a1.ArgoCD) string {
 	config := common.ArgoCDDefaultDexConfig
 	if len(cr.Spec.Dex.Config) > 0 {
-		config = cr.Spec.Dex.Config
+		if cr.Spec.ExtraConfig["dex.config"] != "" {
+			config = cr.Spec.ExtraConfig["dex.config"]
+		} else {
+			config = cr.Spec.Dex.Config
+		}
 	}
 	return config
 }
@@ -260,12 +265,12 @@ func newConfigMapWithSuffix(suffix string, cr *argoprojv1a1.ArgoCD) *corev1.Conf
 }
 
 // reconcileConfigMaps will ensure that all ArgoCD ConfigMaps are present.
-func (r *ReconcileArgoCD) reconcileConfigMaps(cr *argoprojv1a1.ArgoCD) error {
+func (r *ReconcileArgoCD) reconcileConfigMaps(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
 	if err := r.reconcileArgoConfigMap(cr); err != nil {
 		return err
 	}
 
-	if err := r.reconcileRedisConfiguration(cr); err != nil {
+	if err := r.reconcileRedisConfiguration(cr, useTLSForRedis); err != nil {
 		return err
 	}
 
@@ -319,18 +324,8 @@ func (r *ReconcileArgoCD) reconcileCAConfigMap(cr *argoprojv1a1.ArgoCD) error {
 // reconcileConfiguration will ensure that the main ConfigMap for ArgoCD is present.
 func (r *ReconcileArgoCD) reconcileArgoConfigMap(cr *argoprojv1a1.ArgoCD) error {
 	cm := newConfigMapWithName(common.ArgoCDConfigMapName, cr)
-	if argoutil.IsObjectFound(r.Client, cr.Namespace, cm.Name, cm) {
-		if cr.Spec.SSO == nil {
-			if err := r.reconcileDexConfiguration(cm, cr); err != nil {
-				return err
-			}
-		}
-		return r.reconcileExistingArgoConfigMap(cm, cr)
-	}
 
-	if cm.Data == nil {
-		cm.Data = make(map[string]string)
-	}
+	cm.Data = make(map[string]string)
 
 	cm.Data[common.ArgoCDKeyApplicationInstanceLabelKey] = getApplicationInstanceLabelKey(cr)
 	cm.Data[common.ArgoCDKeyConfigManagementPlugins] = getConfigManagementPlugins(cr)
@@ -383,10 +378,32 @@ func (r *ReconcileArgoCD) reconcileArgoConfigMap(cr *argoprojv1a1.ArgoCD) error
 		}
 	}
 
+	if len(cr.Spec.ExtraConfig) > 0 {
+		for k, v := range cr.Spec.ExtraConfig {
+			cm.Data[k] = v
+		}
+	}
+
 	if err := controllerutil.SetControllerReference(cr, cm, r.Scheme); err != nil {
 		return err
 	}
+
+	existingCM := &corev1.ConfigMap{}
+	if argoutil.IsObjectFound(r.Client, cr.Namespace, cm.Name, existingCM) {
+		if cr.Spec.SSO == nil {
+			if err := r.reconcileDexConfiguration(existingCM, cr); err != nil {
+				return err
+			}
+		}
+
+		if !reflect.DeepEqual(cm.Data, existingCM.Data) {
+			existingCM.Data = cm.Data
+			return r.Client.Update(context.TODO(), existingCM)
+		}
+		return nil // Do nothing as there is no change in the configmap.
+	}
 	return r.Client.Create(context.TODO(), cm)
+
 }
 
 // reconcileDexConfiguration will ensure that Dex is configured properly.
@@ -421,137 +438,6 @@ func (r *ReconcileArgoCD) reconcileDexConfiguration(cm *corev1.ConfigMap, cr *ar
 	return nil
 }
 
-func (r *ReconcileArgoCD) reconcileExistingArgoConfigMap(cm *corev1.ConfigMap, cr *argoprojv1a1.ArgoCD) error {
-	changed := false
-
-	if cm.Data == nil {
-		cm.Data = make(map[string]string)
-	}
-
-	if cm.Data[common.ArgoCDKeyAdminEnabled] == fmt.Sprintf("%t", cr.Spec.DisableAdmin) {
-		cm.Data[common.ArgoCDKeyAdminEnabled] = fmt.Sprintf("%t", !cr.Spec.DisableAdmin)
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyApplicationInstanceLabelKey] != cr.Spec.ApplicationInstanceLabelKey {
-		cm.Data[common.ArgoCDKeyApplicationInstanceLabelKey] = cr.Spec.ApplicationInstanceLabelKey
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyConfigManagementPlugins] != cr.Spec.ConfigManagementPlugins {
-		cm.Data[common.ArgoCDKeyConfigManagementPlugins] = cr.Spec.ConfigManagementPlugins
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyGATrackingID] != cr.Spec.GATrackingID {
-		cm.Data[common.ArgoCDKeyGATrackingID] = cr.Spec.GATrackingID
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyGAAnonymizeUsers] != fmt.Sprint(cr.Spec.GAAnonymizeUsers) {
-		cm.Data[common.ArgoCDKeyGAAnonymizeUsers] = fmt.Sprint(cr.Spec.GAAnonymizeUsers)
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyHelpChatURL] != cr.Spec.HelpChatURL {
-		cm.Data[common.ArgoCDKeyHelpChatURL] = cr.Spec.HelpChatURL
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyHelpChatText] != cr.Spec.HelpChatText {
-		cm.Data[common.ArgoCDKeyHelpChatText] = cr.Spec.HelpChatText
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyKustomizeBuildOptions] != cr.Spec.KustomizeBuildOptions {
-		cm.Data[common.ArgoCDKeyKustomizeBuildOptions] = cr.Spec.KustomizeBuildOptions
-		changed = true
-	}
-
-	if len(cr.Spec.KustomizeVersions) > 0 {
-		for _, kv := range cr.Spec.KustomizeVersions {
-			if cm.Data["kustomize.version"+kv.Version] != kv.Path {
-				cm.Data["kustomize.version."+kv.Version] = kv.Path
-				changed = true
-			}
-		}
-	}
-
-	if cr.Spec.SSO == nil {
-		if cm.Data[common.ArgoCDKeyOIDCConfig] != cr.Spec.OIDCConfig {
-			cm.Data[common.ArgoCDKeyOIDCConfig] = cr.Spec.OIDCConfig
-			changed = true
-		}
-	}
-
-	if cm.Data[common.ArgoCDKeyResourceCustomizations] != cr.Spec.ResourceCustomizations {
-		cm.Data[common.ArgoCDKeyResourceCustomizations] = cr.Spec.ResourceCustomizations
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyResourceExclusions] != cr.Spec.ResourceExclusions {
-		cm.Data[common.ArgoCDKeyResourceExclusions] = cr.Spec.ResourceExclusions
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyResourceInclusions] != cr.Spec.ResourceInclusions {
-		cm.Data[common.ArgoCDKeyResourceInclusions] = cr.Spec.ResourceInclusions
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyResourceTrackingMethod] != cr.Spec.ResourceTrackingMethod {
-		cm.Data[common.ArgoCDKeyResourceTrackingMethod] = getResourceTrackingMethod(cr)
-		changed = true
-	}
-
-	uri := r.getArgoServerURI(cr)
-	if cm.Data[common.ArgoCDKeyServerURL] != uri {
-		cm.Data[common.ArgoCDKeyServerURL] = uri
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyStatusBadgeEnabled] != fmt.Sprint(cr.Spec.StatusBadgeEnabled) {
-		cm.Data[common.ArgoCDKeyStatusBadgeEnabled] = fmt.Sprint(cr.Spec.StatusBadgeEnabled)
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyUsersAnonymousEnabled] != fmt.Sprint(cr.Spec.UsersAnonymousEnabled) {
-		cm.Data[common.ArgoCDKeyUsersAnonymousEnabled] = fmt.Sprint(cr.Spec.UsersAnonymousEnabled)
-		changed = true
-	}
-
-	if cm.Data[common.ArgoCDKeyRepositoryCredentials] != cr.Spec.RepositoryCredentials {
-		cm.Data[common.ArgoCDKeyRepositoryCredentials] = cr.Spec.RepositoryCredentials
-		changed = true
-	}
-
-	if cr.Spec.Banner != nil {
-		if cm.Data[common.ArgoCDKeyBannerContent] != fmt.Sprint(cr.Spec.Banner.Content) {
-			cm.Data[common.ArgoCDKeyBannerContent] = fmt.Sprint(cr.Spec.Banner.Content)
-			changed = true
-		}
-		if cm.Data[common.ArgoCDKeyBannerURL] != fmt.Sprint(cr.Spec.Banner.URL) {
-			cm.Data[common.ArgoCDKeyBannerURL] = fmt.Sprint(cr.Spec.Banner.URL)
-			changed = true
-		}
-	} else {
-		if _, ok := cm.Data[common.ArgoCDKeyBannerContent]; ok {
-			delete(cm.Data, common.ArgoCDKeyBannerContent)
-			changed = true
-		}
-		if _, ok := cm.Data[common.ArgoCDKeyBannerURL]; ok {
-			delete(cm.Data, common.ArgoCDKeyBannerURL)
-			changed = true
-		}
-	}
-
-	if changed {
-		return r.Client.Update(context.TODO(), cm) // TODO: Reload Argo CD server after ConfigMap change (which properties)?
-	}
-
-	return nil // Nothing changed, no update needed...
-}
-
 // reconcileGrafanaConfiguration will ensure that the Grafana configuration ConfigMap is present.
 func (r *ReconcileArgoCD) reconcileGrafanaConfiguration(cr *argoprojv1a1.ArgoCD) error {
 	if !cr.Spec.Grafana.Enabled {
@@ -672,18 +558,18 @@ func (r *ReconcileArgoCD) reconcileRBACConfigMap(cm *corev1.ConfigMap, cr *argop
 }
 
 // reconcileRedisConfiguration will ensure that all of the Redis ConfigMaps are present for the given ArgoCD.
-func (r *ReconcileArgoCD) reconcileRedisConfiguration(cr *argoprojv1a1.ArgoCD) error {
-	if err := r.reconcileRedisHAConfigMap(cr); err != nil {
+func (r *ReconcileArgoCD) reconcileRedisConfiguration(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
+	if err := r.reconcileRedisHAConfigMap(cr, useTLSForRedis); err != nil {
 		return err
 	}
-	if err := r.reconcileRedisHAHealthConfigMap(cr); err != nil {
+	if err := r.reconcileRedisHAHealthConfigMap(cr, useTLSForRedis); err != nil {
 		return err
 	}
 	return nil
 }
 
 // reconcileRedisHAConfigMap will ensure that the Redis HA Health ConfigMap is present for the given ArgoCD.
-func (r *ReconcileArgoCD) reconcileRedisHAHealthConfigMap(cr *argoprojv1a1.ArgoCD) error {
+func (r *ReconcileArgoCD) reconcileRedisHAHealthConfigMap(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
 	cm := newConfigMapWithName(common.ArgoCDRedisHAHealthConfigMapName, cr)
 	if argoutil.IsObjectFound(r.Client, cr.Namespace, cm.Name, cm) {
 		if !cr.Spec.HA.Enabled {
@@ -698,9 +584,9 @@ func (r *ReconcileArgoCD) reconcileRedisHAHealthConfigMap(cr *argoprojv1a1.ArgoC
 	}
 
 	cm.Data = map[string]string{
-		"redis_liveness.sh":    getRedisLivenessScript(),
-		"redis_readiness.sh":   getRedisReadinessScript(),
-		"sentinel_liveness.sh": getSentinelLivenessScript(),
+		"redis_liveness.sh":    getRedisLivenessScript(useTLSForRedis),
+		"redis_readiness.sh":   getRedisReadinessScript(useTLSForRedis),
+		"sentinel_liveness.sh": getSentinelLivenessScript(useTLSForRedis),
 	}
 
 	if err := controllerutil.SetControllerReference(cr, cm, r.Scheme); err != nil {
@@ -710,7 +596,7 @@ func (r *ReconcileArgoCD) reconcileRedisHAHealthConfigMap(cr *argoprojv1a1.ArgoC
 }
 
 // reconcileRedisHAConfigMap will ensure that the Redis HA ConfigMap is present for the given ArgoCD.
-func (r *ReconcileArgoCD) reconcileRedisHAConfigMap(cr *argoprojv1a1.ArgoCD) error {
+func (r *ReconcileArgoCD) reconcileRedisHAConfigMap(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
 	cm := newConfigMapWithName(common.ArgoCDRedisHAConfigMapName, cr)
 	if argoutil.IsObjectFound(r.Client, cr.Namespace, cm.Name, cm) {
 		if !cr.Spec.HA.Enabled {
@@ -725,11 +611,11 @@ func (r *ReconcileArgoCD) reconcileRedisHAConfigMap(cr *argoprojv1a1.ArgoCD) err
 	}
 
 	cm.Data = map[string]string{
-		"haproxy.cfg":     getRedisHAProxyConfig(cr),
+		"haproxy.cfg":     getRedisHAProxyConfig(cr, useTLSForRedis),
 		"haproxy_init.sh": getRedisHAProxyScript(cr),
-		"init.sh":         getRedisInitScript(cr),
-		"redis.conf":      getRedisConf(),
-		"sentinel.conf":   getRedisSentinelConf(),
+		"init.sh":         getRedisInitScript(cr, useTLSForRedis),
+		"redis.conf":      getRedisConf(useTLSForRedis),
+		"sentinel.conf":   getRedisSentinelConf(useTLSForRedis),
 	}
 
 	if err := controllerutil.SetControllerReference(cr, cm, r.Scheme); err != nil {
@@ -738,6 +624,26 @@ func (r *ReconcileArgoCD) reconcileRedisHAConfigMap(cr *argoprojv1a1.ArgoCD) err
 	return r.Client.Create(context.TODO(), cm)
 }
 
+func (r *ReconcileArgoCD) recreateRedisHAConfigMap(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
+	cm := newConfigMapWithName(common.ArgoCDRedisHAConfigMapName, cr)
+	if argoutil.IsObjectFound(r.Client, cr.Namespace, cm.Name, cm) {
+		if err := r.Client.Delete(context.TODO(), cm); err != nil {
+			return err
+		}
+	}
+	return r.reconcileRedisHAConfigMap(cr, useTLSForRedis)
+}
+
+func (r *ReconcileArgoCD) recreateRedisHAHealthConfigMap(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
+	cm := newConfigMapWithName(common.ArgoCDRedisHAHealthConfigMapName, cr)
+	if argoutil.IsObjectFound(r.Client, cr.Namespace, cm.Name, cm) {
+		if err := r.Client.Delete(context.TODO(), cm); err != nil {
+			return err
+		}
+	}
+	return r.reconcileRedisHAHealthConfigMap(cr, useTLSForRedis)
+}
+
 // reconcileSSHKnownHosts will ensure that the ArgoCD SSH Known Hosts ConfigMap is present.
 func (r *ReconcileArgoCD) reconcileSSHKnownHosts(cr *argoprojv1a1.ArgoCD) error {
 	cm := newConfigMapWithName(common.ArgoCDKnownHostsConfigMapName, cr)
diff --git a/controllers/argocd/custommapper.go b/controllers/argocd/custommapper.go
index 3dc0729a..eba5a335 100644
--- a/controllers/argocd/custommapper.go
+++ b/controllers/argocd/custommapper.go
@@ -9,6 +9,7 @@ import (
 	"github.com/argoproj-labs/argocd-operator/common"
 
 	corev1 "k8s.io/api/core/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
@@ -34,13 +35,40 @@ func (r *ReconcileArgoCD) clusterResourceMapper(o client.Object) []reconcile.Req
 	return result
 }
 
+// isSecretOfInterest returns true if the name of the given secret matches one of the
+// well-known tls secrets used to secure communication amongst the Argo CD components.
+func isSecretOfInterest(o client.Object) bool {
+	if strings.HasSuffix(o.GetName(), "-repo-server-tls") {
+		return true
+	}
+	if o.GetName() == common.ArgoCDRedisServerTLSSecretName {
+		return true
+	}
+	return false
+}
+
+// isOwnerOfInterest returns true if the given owner is one of the Argo CD services that
+// may have been made the owner of the tls secret created by the OpenShift service CA, used
+// to secure communication amongst the Argo CD components.
+func isOwnerOfInterest(owner v1.OwnerReference) bool {
+	if owner.Kind != "Service" {
+		return false
+	}
+	if strings.HasSuffix(owner.Name, "-repo-server") {
+		return true
+	}
+	if strings.HasSuffix(owner.Name, "-redis") {
+		return true
+	}
+	return false
+}
+
 // tlsSecretMapper maps a watch event on a secret of type TLS back to the
 // ArgoCD object that we want to reconcile.
 func (r *ReconcileArgoCD) tlsSecretMapper(o client.Object) []reconcile.Request {
 	var result = []reconcile.Request{}
 
-	// The secret must end with '-repo-server-tls'
-	if !strings.HasSuffix(o.GetName(), "-repo-server-tls") {
+	if !isSecretOfInterest(o) {
 		return result
 	}
 	namespacedArgoCDObject := client.ObjectKey{}
@@ -51,7 +79,7 @@ func (r *ReconcileArgoCD) tlsSecretMapper(o client.Object) []reconcile.Request {
 		// service, which in turn is owned by the controller. This method performs
 		// a lookup of the controller through the intermediate owning service.
 		for _, secretOwner := range secretOwnerRefs {
-			if secretOwner.Kind == "Service" && strings.HasSuffix(secretOwner.Name, "-repo-server") {
+			if isOwnerOfInterest(secretOwner) {
 				key := client.ObjectKey{Name: secretOwner.Name, Namespace: o.GetNamespace()}
 				svc := &corev1.Service{}
 
diff --git a/controllers/argocd/deployment.go b/controllers/argocd/deployment.go
index 9f855e0a..7ccb6c16 100644
--- a/controllers/argocd/deployment.go
+++ b/controllers/argocd/deployment.go
@@ -207,8 +207,26 @@ func getArgoImportVolumes(cr *argoprojv1a1.ArgoCDExport) []corev1.Volume {
 	return volumes
 }
 
+func getArgoRedisArgs(useTLS bool) []string {
+	args := make([]string, 0)
+
+	args = append(args, "--save", "")
+	args = append(args, "--appendonly", "no")
+
+	if useTLS {
+		args = append(args, "--tls-port", "6379")
+		args = append(args, "--port", "0")
+
+		args = append(args, "--tls-cert-file", "/app/config/redis/tls/tls.crt")
+		args = append(args, "--tls-key-file", "/app/config/redis/tls/tls.key")
+		args = append(args, "--tls-auth-clients", "no")
+	}
+
+	return args
+}
+
 // getArgoRepoCommand will return the command for the ArgoCD Repo component.
-func getArgoRepoCommand(cr *argoprojv1a1.ArgoCD) []string {
+func getArgoRepoCommand(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) []string {
 	cmd := make([]string, 0)
 
 	cmd = append(cmd, "uid_entrypoint.sh")
@@ -217,6 +235,15 @@ func getArgoRepoCommand(cr *argoprojv1a1.ArgoCD) []string {
 	cmd = append(cmd, "--redis")
 	cmd = append(cmd, getRedisServerAddress(cr))
 
+	if useTLSForRedis {
+		cmd = append(cmd, "--redis-use-tls")
+		if isRedisTLSVerificationDisabled(cr) {
+			cmd = append(cmd, "--redis-insecure-skip-tls-verify")
+		} else {
+			cmd = append(cmd, "--redis-ca-certificate", "/app/config/reposerver/tls/redis/tls.crt")
+		}
+	}
+
 	cmd = append(cmd, "--loglevel")
 	cmd = append(cmd, getLogLevel(cr.Spec.Repo.LogLevel))
 
@@ -237,7 +264,7 @@ func getArgoCmpServerInitCommand() []string {
 }
 
 // getArgoServerCommand will return the command for the ArgoCD server component.
-func getArgoServerCommand(cr *argoprojv1a1.ArgoCD) []string {
+func getArgoServerCommand(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) []string {
 	cmd := make([]string, 0)
 	cmd = append(cmd, "argocd-server")
 
@@ -261,6 +288,15 @@ func getArgoServerCommand(cr *argoprojv1a1.ArgoCD) []string {
 	cmd = append(cmd, "--redis")
 	cmd = append(cmd, getRedisServerAddress(cr))
 
+	if useTLSForRedis {
+		cmd = append(cmd, "--redis-use-tls")
+		if isRedisTLSVerificationDisabled(cr) {
+			cmd = append(cmd, "--redis-insecure-skip-tls-verify")
+		} else {
+			cmd = append(cmd, "--redis-ca-certificate", "/app/config/server/tls/redis/tls.crt")
+		}
+	}
+
 	cmd = append(cmd, "--loglevel")
 	cmd = append(cmd, getLogLevel(cr.Spec.Server.LogLevel))
 
@@ -352,13 +388,13 @@ func newDeploymentWithSuffix(suffix string, component string, cr *argoprojv1a1.A
 }
 
 // reconcileDeployments will ensure that all Deployment resources are present for the given ArgoCD.
-func (r *ReconcileArgoCD) reconcileDeployments(cr *argoprojv1a1.ArgoCD) error {
+func (r *ReconcileArgoCD) reconcileDeployments(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
 	err := r.reconcileDexDeployment(cr)
 	if err != nil {
 		return err
 	}
 
-	err = r.reconcileRedisDeployment(cr)
+	err = r.reconcileRedisDeployment(cr, useTLSForRedis)
 	if err != nil {
 		return err
 	}
@@ -368,12 +404,12 @@ func (r *ReconcileArgoCD) reconcileDeployments(cr *argoprojv1a1.ArgoCD) error {
 		return err
 	}
 
-	err = r.reconcileRepoDeployment(cr)
+	err = r.reconcileRepoDeployment(cr, useTLSForRedis)
 	if err != nil {
 		return err
 	}
 
-	err = r.reconcileServerDeployment(cr)
+	err = r.reconcileServerDeployment(cr, useTLSForRedis)
 	if err != nil {
 		return err
 	}
@@ -636,15 +672,10 @@ func (r *ReconcileArgoCD) reconcileGrafanaDeployment(cr *argoprojv1a1.ArgoCD) er
 }
 
 // reconcileRedisDeployment will ensure the Deployment resource is present for the ArgoCD Redis component.
-func (r *ReconcileArgoCD) reconcileRedisDeployment(cr *argoprojv1a1.ArgoCD) error {
+func (r *ReconcileArgoCD) reconcileRedisDeployment(cr *argoprojv1a1.ArgoCD, useTLS bool) error {
 	deploy := newDeploymentWithSuffix("redis", "redis", cr)
 	deploy.Spec.Template.Spec.Containers = []corev1.Container{{
-		Args: []string{
-			"--save",
-			"",
-			"--appendonly",
-			"no",
-		},
+		Args:            getArgoRedisArgs(useTLS),
 		Image:           getRedisContainerImage(cr),
 		ImagePullPolicy: corev1.PullAlways,
 		Name:            "redis",
@@ -655,8 +686,26 @@ func (r *ReconcileArgoCD) reconcileRedisDeployment(cr *argoprojv1a1.ArgoCD) erro
 		},
 		Resources: getRedisResources(cr),
 		Env:       proxyEnvVars(),
+		VolumeMounts: []corev1.VolumeMount{
+			{
+				Name:      common.ArgoCDRedisServerTLSSecretName,
+				MountPath: "/app/config/redis/tls",
+			},
+		},
 	}}
 
+	deploy.Spec.Template.Spec.Volumes = []corev1.Volume{
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
+	}
+
 	if err := applyReconcilerHook(cr, deploy, ""); err != nil {
 		return err
 	}
@@ -676,6 +725,12 @@ func (r *ReconcileArgoCD) reconcileRedisDeployment(cr *argoprojv1a1.ArgoCD) erro
 			changed = true
 		}
 		updateNodePlacement(existing, deploy, &changed)
+
+		if !reflect.DeepEqual(deploy.Spec.Template.Spec.Containers[0].Args, existing.Spec.Template.Spec.Containers[0].Args) {
+			existing.Spec.Template.Spec.Containers[0].Args = deploy.Spec.Template.Spec.Containers[0].Args
+			changed = true
+		}
+
 		if !reflect.DeepEqual(existing.Spec.Template.Spec.Containers[0].Env,
 			deploy.Spec.Template.Spec.Containers[0].Env) {
 			existing.Spec.Template.Spec.Containers[0].Env = deploy.Spec.Template.Spec.Containers[0].Env
@@ -791,6 +846,10 @@ func (r *ReconcileArgoCD) reconcileRedisHAProxyDeployment(cr *argoprojv1a1.ArgoC
 				Name:      "shared-socket",
 				MountPath: "/run/haproxy",
 			},
+			{
+				Name:      common.ArgoCDRedisServerTLSSecretName,
+				MountPath: "/app/config/redis/tls",
+			},
 		},
 	}}
 
@@ -842,6 +901,15 @@ func (r *ReconcileArgoCD) reconcileRedisHAProxyDeployment(cr *argoprojv1a1.ArgoC
 				EmptyDir: &corev1.EmptyDirVolumeSource{},
 			},
 		},
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
 	}
 
 	deploy.Spec.Template.Spec.ServiceAccountName = fmt.Sprintf("%s-%s", cr.Name, "argocd-redis-ha")
@@ -857,7 +925,7 @@ func (r *ReconcileArgoCD) reconcileRedisHAProxyDeployment(cr *argoprojv1a1.ArgoC
 }
 
 // reconcileRepoDeployment will ensure the Deployment resource is present for the ArgoCD Repo component.
-func (r *ReconcileArgoCD) reconcileRepoDeployment(cr *argoprojv1a1.ArgoCD) error {
+func (r *ReconcileArgoCD) reconcileRepoDeployment(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
 	deploy := newDeploymentWithSuffix("repo-server", "repo-server", cr)
 	automountToken := false
 	if cr.Spec.Repo.MountSAToken {
@@ -922,6 +990,10 @@ func (r *ReconcileArgoCD) reconcileRepoDeployment(cr *argoprojv1a1.ArgoCD) error
 			Name:      "argocd-repo-server-tls",
 			MountPath: "/app/config/reposerver/tls",
 		},
+		{
+			Name:      common.ArgoCDRedisServerTLSSecretName,
+			MountPath: "/app/config/reposerver/tls/redis",
+		},
 		{
 			Name:      "plugins",
 			MountPath: "/home/argocd/cmp-server/plugins",
@@ -933,7 +1005,7 @@ func (r *ReconcileArgoCD) reconcileRepoDeployment(cr *argoprojv1a1.ArgoCD) error
 	}
 
 	deploy.Spec.Template.Spec.Containers = []corev1.Container{{
-		Command:         getArgoRepoCommand(cr),
+		Command:         getArgoRepoCommand(cr, useTLSForRedis),
 		Image:           getRepoServerContainerImage(cr),
 		ImagePullPolicy: corev1.PullAlways,
 		LivenessProbe: &corev1.Probe{
@@ -1025,6 +1097,15 @@ func (r *ReconcileArgoCD) reconcileRepoDeployment(cr *argoprojv1a1.ArgoCD) error
 				},
 			},
 		},
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
 		{
 			Name: "var-files",
 			VolumeSource: corev1.VolumeSource{
@@ -1114,12 +1195,12 @@ func (r *ReconcileArgoCD) reconcileRepoDeployment(cr *argoprojv1a1.ArgoCD) error
 }
 
 // reconcileServerDeployment will ensure the Deployment resource is present for the ArgoCD Server component.
-func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoprojv1a1.ArgoCD) error {
+func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
 	deploy := newDeploymentWithSuffix("server", "server", cr)
 	serverEnv := cr.Spec.Server.Env
 	serverEnv = argoutil.EnvMerge(serverEnv, proxyEnvVars(), false)
 	deploy.Spec.Template.Spec.Containers = []corev1.Container{{
-		Command:         getArgoServerCommand(cr),
+		Command:         getArgoServerCommand(cr, useTLSForRedis),
 		Image:           getArgoContainerImage(cr),
 		ImagePullPolicy: corev1.PullAlways,
 		Env:             serverEnv,
@@ -1164,6 +1245,10 @@ func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoprojv1a1.ArgoCD) err
 				Name:      "argocd-repo-server-tls",
 				MountPath: "/app/config/server/tls",
 			},
+			{
+				Name:      common.ArgoCDRedisServerTLSSecretName,
+				MountPath: "/app/config/server/tls/redis",
+			},
 		},
 	}}
 	deploy.Spec.Template.Spec.ServiceAccountName = fmt.Sprintf("%s-%s", cr.Name, "argocd-server")
@@ -1177,7 +1262,8 @@ func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoprojv1a1.ArgoCD) err
 					},
 				},
 			},
-		}, {
+		},
+		{
 			Name: "tls-certs",
 			VolumeSource: corev1.VolumeSource{
 				ConfigMap: &corev1.ConfigMapVolumeSource{
@@ -1186,7 +1272,8 @@ func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoprojv1a1.ArgoCD) err
 					},
 				},
 			},
-		}, {
+		},
+		{
 			Name: "argocd-repo-server-tls",
 			VolumeSource: corev1.VolumeSource{
 				Secret: &corev1.SecretVolumeSource{
@@ -1195,6 +1282,15 @@ func (r *ReconcileArgoCD) reconcileServerDeployment(cr *argoprojv1a1.ArgoCD) err
 				},
 			},
 		},
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
 	}
 
 	if replicas := getArgoCDServerReplicas(cr); replicas != nil {
@@ -1293,6 +1389,13 @@ func isDexDisabled() bool {
 	return false
 }
 
+func isRemoveManagedByLabelOnArgoCDDeletion() bool {
+	if v := os.Getenv("REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION"); v != "" {
+		return strings.ToLower(v) == "true"
+	}
+	return false
+}
+
 // to update nodeSelector and tolerations in reconciler
 func updateNodePlacement(existing *appsv1.Deployment, deploy *appsv1.Deployment, changed *bool) {
 	if !reflect.DeepEqual(existing.Spec.Template.Spec.NodeSelector, deploy.Spec.Template.Spec.NodeSelector) {
diff --git a/controllers/argocd/notifications.go b/controllers/argocd/notifications.go
new file mode 100644
index 00000000..e2583779
--- /dev/null
+++ b/controllers/argocd/notifications.go
@@ -0,0 +1,425 @@
+package argocd
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"time"
+
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+
+	argoprojv1a1 "github.com/argoproj-labs/argocd-operator/api/v1alpha1"
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argoutil"
+)
+
+func (r *ReconcileArgoCD) reconcileNotificationsController(cr *argoprojv1a1.ArgoCD) error {
+
+	log.Info("reconciling notifications serviceaccount")
+	sa, err := r.reconcileNotificationsServiceAccount(cr)
+	if err != nil {
+		return err
+	}
+
+	log.Info("reconciling notifications role")
+	role, err := r.reconcileNotificationsRole(cr)
+	if err != nil {
+		return err
+	}
+
+	log.Info("reconciling notifications role binding")
+	if err := r.reconcileNotificationsRoleBinding(cr, role, sa); err != nil {
+		return err
+	}
+
+	log.Info("reconciling notifications deployment")
+	if err := r.reconcileNotificationsDeployment(cr, sa); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// The code to create/delete notifications resources is written within the reconciliation logic itself. However, these functions must be called
+// in the right order depending on whether resources are getting created or deleted. During creation we must create the role and sa first.
+// RoleBinding and deployment are dependent on these resouces. During deletion the order is reversed.
+// Deployment and RoleBinding must be deleted before the role and sa. deleteNotificationsResources will only be called during
+// delete events, so we don't need to worry about duplicate, recurring reconciliation calls
+func (r *ReconcileArgoCD) deleteNotificationsResources(cr *argoprojv1a1.ArgoCD) error {
+
+	sa := &corev1.ServiceAccount{}
+	role := &rbacv1.Role{}
+
+	if err := argoutil.FetchObject(r.Client, cr.Namespace, fmt.Sprintf("%s-%s", cr.Name, common.ArgoCDNotificationsControllerComponent), sa); err != nil {
+		if !errors.IsNotFound(err) {
+			return err
+		}
+	}
+	if err := argoutil.FetchObject(r.Client, cr.Namespace, fmt.Sprintf("%s-%s", cr.Name, common.ArgoCDNotificationsControllerComponent), role); err != nil {
+		if !errors.IsNotFound(err) {
+			return err
+		}
+	}
+
+	log.Info("reconciling notifications deployment")
+	if err := r.reconcileNotificationsDeployment(cr, sa); err != nil {
+		return err
+	}
+
+	log.Info("reconciling notifications role binding")
+	if err := r.reconcileNotificationsRoleBinding(cr, role, sa); err != nil {
+		return err
+	}
+
+	log.Info("reconciling notifications serviceaccount")
+	_, err := r.reconcileNotificationsServiceAccount(cr)
+	if err != nil {
+		return err
+	}
+
+	log.Info("reconciling notifications role")
+	_, err = r.reconcileNotificationsRole(cr)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (r *ReconcileArgoCD) reconcileNotificationsServiceAccount(cr *argoprojv1a1.ArgoCD) (*corev1.ServiceAccount, error) {
+
+	sa := newServiceAccountWithName(common.ArgoCDNotificationsControllerComponent, cr)
+
+	if err := argoutil.FetchObject(r.Client, cr.Namespace, sa.Name, sa); err != nil {
+		if !errors.IsNotFound(err) {
+			return nil, fmt.Errorf("failed to get the serviceAccount associated with %s : %s", sa.Name, err)
+		}
+
+		// SA doesn't exist and shouldn't, nothing to do here
+		if !cr.Spec.Notifications.Enabled {
+			return nil, nil
+		}
+
+		// SA doesn't exist but should, so it should be created
+		if err := controllerutil.SetControllerReference(cr, sa, r.Scheme); err != nil {
+			return nil, err
+		}
+
+		log.Info(fmt.Sprintf("Creating serviceaccount %s", sa.Name))
+		err := r.Client.Create(context.TODO(), sa)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// SA exists but shouldn't, so it should be deleted
+	if !cr.Spec.Notifications.Enabled {
+		log.Info(fmt.Sprintf("Deleting serviceaccount %s as notifications is disabled", sa.Name))
+		return nil, r.Client.Delete(context.TODO(), sa)
+	}
+
+	return sa, nil
+}
+
+func (r *ReconcileArgoCD) reconcileNotificationsRole(cr *argoprojv1a1.ArgoCD) (*rbacv1.Role, error) {
+
+	policyRules := policyRuleForNotificationsController()
+	desiredRole := newRole(common.ArgoCDNotificationsControllerComponent, policyRules, cr)
+
+	existingRole := &rbacv1.Role{}
+	if err := argoutil.FetchObject(r.Client, cr.Namespace, desiredRole.Name, existingRole); err != nil {
+		if !errors.IsNotFound(err) {
+			return nil, fmt.Errorf("failed to get the role associated with %s : %s", desiredRole.Name, err)
+		}
+
+		// role does not exist and shouldn't, nothing to do here
+		if !cr.Spec.Notifications.Enabled {
+			return nil, nil
+		}
+
+		// role does not exist but should, so it should be created
+		if err := controllerutil.SetControllerReference(cr, desiredRole, r.Scheme); err != nil {
+			return nil, err
+		}
+
+		log.Info(fmt.Sprintf("Creating role %s", desiredRole.Name))
+		err := r.Client.Create(context.TODO(), desiredRole)
+		if err != nil {
+			return nil, err
+		}
+		return desiredRole, nil
+	}
+
+	// role exists but shouldn't, so it should be deleted
+	if !cr.Spec.Notifications.Enabled {
+		log.Info(fmt.Sprintf("Deleting role %s as notifications is disabled", existingRole.Name))
+		return nil, r.Client.Delete(context.TODO(), existingRole)
+	}
+
+	// role exists and should. Reconcile role if changed
+	if !reflect.DeepEqual(existingRole.Rules, desiredRole.Rules) {
+		existingRole.Rules = desiredRole.Rules
+		if err := controllerutil.SetControllerReference(cr, existingRole, r.Scheme); err != nil {
+			return nil, err
+		}
+		return existingRole, r.Client.Update(context.TODO(), existingRole)
+	}
+
+	return desiredRole, nil
+}
+
+func (r *ReconcileArgoCD) reconcileNotificationsRoleBinding(cr *argoprojv1a1.ArgoCD, role *rbacv1.Role, sa *corev1.ServiceAccount) error {
+
+	desiredRoleBinding := newRoleBindingWithname(common.ArgoCDNotificationsControllerComponent, cr)
+	desiredRoleBinding.RoleRef = rbacv1.RoleRef{
+		APIGroup: rbacv1.GroupName,
+		Kind:     "Role",
+		Name:     role.Name,
+	}
+
+	desiredRoleBinding.Subjects = []rbacv1.Subject{
+		{
+			Kind:      rbacv1.ServiceAccountKind,
+			Name:      sa.Name,
+			Namespace: sa.Namespace,
+		},
+	}
+
+	// fetch existing rolebinding by name
+	existingRoleBinding := &rbacv1.RoleBinding{}
+	if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: desiredRoleBinding.Name, Namespace: cr.Namespace}, existingRoleBinding); err != nil {
+		if !errors.IsNotFound(err) {
+			return fmt.Errorf("failed to get the rolebinding associated with %s : %s", desiredRoleBinding.Name, err)
+		}
+
+		// roleBinding does not exist and shouldn't, nothing to do here
+		if !cr.Spec.Notifications.Enabled {
+			return nil
+		}
+
+		// roleBinding does not exist but should, so it should be created
+		if err := controllerutil.SetControllerReference(cr, desiredRoleBinding, r.Scheme); err != nil {
+			return err
+		}
+
+		log.Info(fmt.Sprintf("Creating role %s", desiredRoleBinding.Name))
+		return r.Client.Create(context.TODO(), desiredRoleBinding)
+	}
+
+	// roleBinding exists but shouldn't, so it should be deleted
+	if !cr.Spec.Notifications.Enabled {
+		log.Info(fmt.Sprintf("Deleting roleBinding %s as notifications is disabled", existingRoleBinding.Name))
+		return r.Client.Delete(context.TODO(), existingRoleBinding)
+	}
+
+	// roleBinding exists and should. Reconcile roleBinding if changed
+	if !reflect.DeepEqual(existingRoleBinding.RoleRef, desiredRoleBinding.RoleRef) {
+		// if the RoleRef changes, delete the existing role binding and create a new one
+		if err := r.Client.Delete(context.TODO(), existingRoleBinding); err != nil {
+			return err
+		}
+	} else if !reflect.DeepEqual(existingRoleBinding.Subjects, desiredRoleBinding.Subjects) {
+		existingRoleBinding.Subjects = desiredRoleBinding.Subjects
+		if err := controllerutil.SetControllerReference(cr, existingRoleBinding, r.Scheme); err != nil {
+			return err
+		}
+		return r.Client.Update(context.TODO(), existingRoleBinding)
+	}
+
+	return nil
+}
+
+func (r *ReconcileArgoCD) reconcileNotificationsDeployment(cr *argoprojv1a1.ArgoCD, sa *corev1.ServiceAccount) error {
+
+	var runAsNonRoot bool = true
+	desiredDeployment := newDeploymentWithSuffix("notifications-controller", "controller", cr)
+
+	desiredDeployment.Spec.Strategy = appsv1.DeploymentStrategy{
+		Type: appsv1.RecreateDeploymentStrategyType,
+	}
+
+	if replicas := getArgoCDNotificationsControllerReplicas(cr); replicas != nil {
+		desiredDeployment.Spec.Replicas = replicas
+	}
+
+	podSpec := &desiredDeployment.Spec.Template.Spec
+	podSpec.SecurityContext = &corev1.PodSecurityContext{
+		RunAsNonRoot: &runAsNonRoot,
+	}
+	podSpec.ServiceAccountName = sa.ObjectMeta.Name
+	podSpec.Volumes = []corev1.Volume{
+		{
+			Name: "tls-certs",
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: common.ArgoCDTLSCertsConfigMapName,
+					},
+				},
+			},
+		},
+		{
+			Name: "argocd-repo-server-tls",
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRepoServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
+	}
+
+	podSpec.Containers = []corev1.Container{{
+		Command:         getNotificationsCommand(),
+		Image:           getArgoContainerImage(cr),
+		ImagePullPolicy: corev1.PullAlways,
+		Name:            common.ArgoCDNotificationsControllerComponent,
+		Resources:       getNotificationsResources(cr),
+		LivenessProbe: &corev1.Probe{
+			ProbeHandler: corev1.ProbeHandler{
+				TCPSocket: &corev1.TCPSocketAction{
+					Port: intstr.IntOrString{
+						IntVal: int32(9001),
+					},
+				},
+			},
+		},
+		VolumeMounts: []corev1.VolumeMount{
+			{
+				Name:      "tls-certs",
+				MountPath: "/app/config/tls",
+			},
+			{
+				Name:      "argocd-repo-server-tls",
+				MountPath: "/app/config/reposerver/tls",
+			},
+		},
+		WorkingDir: "/app",
+	}}
+
+	// fetch existing deployment by name
+	deploymentChanged := false
+	existingDeployment := &appsv1.Deployment{}
+	if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: desiredDeployment.Name, Namespace: cr.Namespace}, existingDeployment); err != nil {
+		if !errors.IsNotFound(err) {
+			return fmt.Errorf("failed to get the deployment associated with %s : %s", existingDeployment.Name, err)
+		}
+
+		// deployment does not exist and shouldn't, nothing to do here
+		if !cr.Spec.Notifications.Enabled {
+			return nil
+		}
+
+		// deployment does not exist but should, so it should be created
+		if err := controllerutil.SetControllerReference(cr, desiredDeployment, r.Scheme); err != nil {
+			return err
+		}
+
+		log.Info(fmt.Sprintf("Creating deployment %s", desiredDeployment.Name))
+		return r.Client.Create(context.TODO(), desiredDeployment)
+	}
+
+	// deployment exists but shouldn't, so it should be deleted
+	if !cr.Spec.Notifications.Enabled {
+		log.Info(fmt.Sprintf("Deleting deployment %s as notifications is disabled", existingDeployment.Name))
+		return r.Client.Delete(context.TODO(), existingDeployment)
+	}
+
+	// deployment exists and should. Reconcile deployment if changed
+	updateNodePlacement(existingDeployment, desiredDeployment, &deploymentChanged)
+
+	if existingDeployment.Spec.Template.Spec.Containers[0].Image != desiredDeployment.Spec.Template.Spec.Containers[0].Image {
+		existingDeployment.Spec.Template.Spec.Containers[0].Image = desiredDeployment.Spec.Template.Spec.Containers[0].Image
+		existingDeployment.Spec.Template.ObjectMeta.Labels["image.upgraded"] = time.Now().UTC().Format("01022006-150406-MST")
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[0].Command, desiredDeployment.Spec.Template.Spec.Containers[0].Command) {
+		existingDeployment.Spec.Template.Spec.Containers[0].Command = desiredDeployment.Spec.Template.Spec.Containers[0].Command
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Volumes, desiredDeployment.Spec.Template.Spec.Volumes) {
+		existingDeployment.Spec.Template.Spec.Volumes = desiredDeployment.Spec.Template.Spec.Volumes
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Spec.Replicas, desiredDeployment.Spec.Replicas) {
+		existingDeployment.Spec.Replicas = desiredDeployment.Spec.Replicas
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[0].VolumeMounts, desiredDeployment.Spec.Template.Spec.Containers[0].VolumeMounts) {
+		existingDeployment.Spec.Template.Spec.Containers[0].VolumeMounts = desiredDeployment.Spec.Template.Spec.Containers[0].VolumeMounts
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.Containers[0].Resources, desiredDeployment.Spec.Template.Spec.Containers[0].Resources) {
+		existingDeployment.Spec.Template.Spec.Containers[0].Resources = desiredDeployment.Spec.Template.Spec.Containers[0].Resources
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Spec.Template.Spec.ServiceAccountName, desiredDeployment.Spec.Template.Spec.ServiceAccountName) {
+		existingDeployment.Spec.Template.Spec.ServiceAccountName = desiredDeployment.Spec.Template.Spec.ServiceAccountName
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Labels, desiredDeployment.Labels) {
+		existingDeployment.Labels = desiredDeployment.Labels
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Spec.Template.Labels, desiredDeployment.Spec.Template.Labels) {
+		existingDeployment.Spec.Template.Labels = desiredDeployment.Spec.Template.Labels
+		deploymentChanged = true
+	}
+
+	if !reflect.DeepEqual(existingDeployment.Spec.Selector, desiredDeployment.Spec.Selector) {
+		existingDeployment.Spec.Selector = desiredDeployment.Spec.Selector
+		deploymentChanged = true
+	}
+
+	if deploymentChanged {
+		return r.Client.Update(context.TODO(), existingDeployment)
+	}
+
+	return nil
+
+}
+
+func getNotificationsCommand() []string {
+
+	cmd := make([]string, 0)
+	cmd = append(cmd, "argocd-notifications")
+
+	return cmd
+}
+
+// getNotificationsResources will return the ResourceRequirements for the Notifications container.
+func getNotificationsResources(cr *argoprojv1a1.ArgoCD) corev1.ResourceRequirements {
+	resources := corev1.ResourceRequirements{}
+
+	// Allow override of resource requirements from CR
+	if cr.Spec.Notifications.Resources != nil {
+		resources = *cr.Spec.Notifications.Resources
+	}
+
+	return resources
+}
+
+// getArgoCDNotificationsControllerReplicas will return the size value for the argocd-notifications-controller replica count if it
+// has been set in argocd CR. Otherwise, nil is returned if the replicas is not set in the argocd CR or
+// replicas value is < 0.
+func getArgoCDNotificationsControllerReplicas(cr *argoprojv1a1.ArgoCD) *int32 {
+	if cr.Spec.Notifications.Replicas != nil && *cr.Spec.Notifications.Replicas >= 0 {
+		return cr.Spec.Notifications.Replicas
+	}
+
+	return nil
+}
diff --git a/controllers/argocd/policyrule.go b/controllers/argocd/policyrule.go
index bc8735df..daa4428d 100644
--- a/controllers/argocd/policyrule.go
+++ b/controllers/argocd/policyrule.go
@@ -147,6 +147,69 @@ func policyRuleForServer() []v1.PolicyRule {
 	}
 }
 
+func policyRuleForNotificationsController() []v1.PolicyRule {
+	return []v1.PolicyRule{
+
+		{
+			APIGroups: []string{
+				"argoproj.io",
+			},
+			Resources: []string{
+				"applications",
+				"appprojects",
+			},
+			Verbs: []string{
+				"get",
+				"list",
+				"patch",
+				"update",
+				"watch",
+			},
+		},
+		{
+			APIGroups: []string{
+				"",
+			},
+			Resources: []string{
+				"configmaps",
+				"secrets",
+			},
+			Verbs: []string{
+				"list",
+				"watch",
+			},
+		},
+		{
+			APIGroups: []string{
+				"",
+			},
+			ResourceNames: []string{
+				"argocd-notifications-cm",
+			},
+			Resources: []string{
+				"configmaps",
+			},
+			Verbs: []string{
+				"get",
+			},
+		},
+		{
+			APIGroups: []string{
+				"",
+			},
+			ResourceNames: []string{
+				"argocd-notifications-secret",
+			},
+			Resources: []string{
+				"secrets",
+			},
+			Verbs: []string{
+				"get",
+			},
+		},
+	}
+}
+
 func policyRuleForServerClusterRole() []v1.PolicyRule {
 	return []v1.PolicyRule{
 		{
diff --git a/controllers/argocd/secret.go b/controllers/argocd/secret.go
index e78ca848..c98c95d9 100644
--- a/controllers/argocd/secret.go
+++ b/controllers/argocd/secret.go
@@ -541,6 +541,108 @@ func (r *ReconcileArgoCD) reconcileRepoServerTLSSecret(cr *argoprojv1a1.ArgoCD)
 	return nil
 }
 
+// reconcileRedisTLSSecret checks whether the argocd-operator-redis-tls secret
+// has changed since our last reconciliation loop. It does so by comparing the
+// checksum of tls.crt and tls.key in the status of the ArgoCD CR against the
+// values calculated from the live state in the cluster.
+func (r *ReconcileArgoCD) reconcileRedisTLSSecret(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
+	var tlsSecretObj corev1.Secret
+	var sha256sum string
+
+	log.Info("reconciling redis-server TLS secret")
+
+	tlsSecretName := types.NamespacedName{Namespace: cr.Namespace, Name: common.ArgoCDRedisServerTLSSecretName}
+	err := r.Client.Get(context.TODO(), tlsSecretName, &tlsSecretObj)
+	if err != nil {
+		if !apierrors.IsNotFound(err) {
+			return err
+		}
+	} else if tlsSecretObj.Type != corev1.SecretTypeTLS {
+		// We only process secrets of type kubernetes.io/tls
+		return nil
+	} else {
+		// We do the checksum over a concatenated byte stream of cert + key
+		crt, crtOk := tlsSecretObj.Data[corev1.TLSCertKey]
+		key, keyOk := tlsSecretObj.Data[corev1.TLSPrivateKeyKey]
+		if crtOk && keyOk {
+			var sumBytes []byte
+			sumBytes = append(sumBytes, crt...)
+			sumBytes = append(sumBytes, key...)
+			sha256sum = fmt.Sprintf("%x", sha256.Sum256(sumBytes))
+		}
+	}
+
+	// The content of the TLS secret has changed since we last looked if the
+	// calculated checksum doesn't match the one stored in the status.
+	if cr.Status.RedisTLSChecksum != sha256sum {
+		// We store the value early to prevent a possible restart loop, for the
+		// cost of a possibly missed restart when we cannot update the status
+		// field of the resource.
+		cr.Status.RedisTLSChecksum = sha256sum
+		err = r.Client.Status().Update(context.TODO(), cr)
+		if err != nil {
+			return err
+		}
+
+		// Trigger rollout of redis
+		if cr.Spec.HA.Enabled {
+			err = r.recreateRedisHAConfigMap(cr, useTLSForRedis)
+			if err != nil {
+				return err
+			}
+			err = r.recreateRedisHAHealthConfigMap(cr, useTLSForRedis)
+			if err != nil {
+				return err
+			}
+			haProxyDepl := newDeploymentWithSuffix("redis-ha-haproxy", "redis", cr)
+			err = r.triggerRollout(haProxyDepl, "redis.tls.cert.changed")
+			if err != nil {
+				return err
+			}
+			// If we use triggerRollout on the redis stateful set, kubernetes will attempt to restart the  pods
+			// one at a time, and the first one to restart (which will be using tls) will hang as it tries to
+			// communicate with the existing pods (which are not using tls) to establish which is the master.
+			// So instead we delete the stateful set, which will delete all the pods.
+			redisSts := newStatefulSetWithSuffix("redis-ha-server", "redis", cr)
+			if argoutil.IsObjectFound(r.Client, redisSts.Namespace, redisSts.Name, redisSts) {
+				err = r.Client.Delete(context.TODO(), redisSts)
+				if err != nil {
+					return err
+				}
+			}
+		} else {
+			redisDepl := newDeploymentWithSuffix("redis", "redis", cr)
+			err = r.triggerRollout(redisDepl, "redis.tls.cert.changed")
+			if err != nil {
+				return err
+			}
+		}
+
+		// Trigger rollout of API server
+		apiDepl := newDeploymentWithSuffix("server", "server", cr)
+		err = r.triggerRollout(apiDepl, "redis.tls.cert.changed")
+		if err != nil {
+			return err
+		}
+
+		// Trigger rollout of repository server
+		repoDepl := newDeploymentWithSuffix("repo-server", "repo-server", cr)
+		err = r.triggerRollout(repoDepl, "redis.tls.cert.changed")
+		if err != nil {
+			return err
+		}
+
+		// Trigger rollout of application controller
+		controllerSts := newStatefulSetWithSuffix("application-controller", "application-controller", cr)
+		err = r.triggerRollout(controllerSts, "redis.tls.cert.changed")
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 // reconcileSecrets will reconcile all ArgoCD Secret resources.
 func (r *ReconcileArgoCD) reconcileSecrets(cr *argoprojv1a1.ArgoCD) error {
 	if err := r.reconcileClusterSecrets(cr); err != nil {
diff --git a/controllers/argocd/service.go b/controllers/argocd/service.go
index 9eb0715d..18f244d8 100644
--- a/controllers/argocd/service.go
+++ b/controllers/argocd/service.go
@@ -285,10 +285,16 @@ func (r *ReconcileArgoCD) reconcileRedisHAServices(cr *argoprojv1a1.ArgoCD) erro
 // reconcileRedisService will ensure that the Service for Redis is present.
 func (r *ReconcileArgoCD) reconcileRedisService(cr *argoprojv1a1.ArgoCD) error {
 	svc := newServiceWithSuffix("redis", "redis", cr)
+
 	if argoutil.IsObjectFound(r.Client, cr.Namespace, svc.Name, svc) {
+		if ensureAutoTLSAnnotation(svc, common.ArgoCDRedisServerTLSSecretName, cr.Spec.Redis.WantsAutoTLS()) {
+			return r.Client.Update(context.TODO(), svc)
+		}
 		return nil // Service found, do nothing
 	}
 
+	ensureAutoTLSAnnotation(svc, common.ArgoCDRedisServerTLSSecretName, cr.Spec.Redis.WantsAutoTLS())
+
 	svc.Spec.Selector = map[string]string{
 		common.ArgoCDKeyName: nameWithSuffix("redis", cr),
 	}
diff --git a/controllers/argocd/statefulset.go b/controllers/argocd/statefulset.go
index 0673b8c7..ca7b43b5 100644
--- a/controllers/argocd/statefulset.go
+++ b/controllers/argocd/statefulset.go
@@ -211,6 +211,10 @@ func (r *ReconcileArgoCD) reconcileRedisStatefulSet(cr *argoprojv1a1.ArgoCD) err
 					MountPath: "/health",
 					Name:      "health",
 				},
+				{
+					Name:      common.ArgoCDRedisServerTLSSecretName,
+					MountPath: "/app/config/redis/tls",
+				},
 			},
 		},
 		{
@@ -269,6 +273,10 @@ func (r *ReconcileArgoCD) reconcileRedisStatefulSet(cr *argoprojv1a1.ArgoCD) err
 					MountPath: "/health",
 					Name:      "health",
 				},
+				{
+					Name:      common.ArgoCDRedisServerTLSSecretName,
+					MountPath: "/app/config/redis/tls",
+				},
 			},
 		},
 	}
@@ -308,6 +316,10 @@ func (r *ReconcileArgoCD) reconcileRedisStatefulSet(cr *argoprojv1a1.ArgoCD) err
 				MountPath: "/data",
 				Name:      "data",
 			},
+			{
+				Name:      common.ArgoCDRedisServerTLSSecretName,
+				MountPath: "/app/config/redis/tls",
+			},
 		},
 	}}
 
@@ -355,6 +367,15 @@ func (r *ReconcileArgoCD) reconcileRedisStatefulSet(cr *argoprojv1a1.ArgoCD) err
 				EmptyDir: &corev1.EmptyDirVolumeSource{},
 			},
 		},
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
 	}
 
 	ss.Spec.UpdateStrategy = appsv1.StatefulSetUpdateStrategy{
@@ -384,7 +405,7 @@ func getArgoControllerContainerEnv(cr *argoprojv1a1.ArgoCD) []corev1.EnvVar {
 	return env
 }
 
-func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoprojv1a1.ArgoCD) error {
+func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
 	var replicas int32 = common.ArgocdApplicationControllerDefaultReplicas
 
 	if cr.Spec.Controller.Sharding.Replicas != 0 && cr.Spec.Controller.Sharding.Enabled {
@@ -400,7 +421,7 @@ func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoproj
 	controllerEnv = argoutil.EnvMerge(controllerEnv, proxyEnvVars(), false)
 	podSpec := &ss.Spec.Template.Spec
 	podSpec.Containers = []corev1.Container{{
-		Command:         getArgoApplicationControllerCommand(cr),
+		Command:         getArgoApplicationControllerCommand(cr, useTLSForRedis),
 		Image:           getArgoContainerImage(cr),
 		ImagePullPolicy: corev1.PullAlways,
 		Name:            "argocd-application-controller",
@@ -436,6 +457,10 @@ func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoproj
 				Name:      "argocd-repo-server-tls",
 				MountPath: "/app/config/controller/tls",
 			},
+			{
+				Name:      common.ArgoCDRedisServerTLSSecretName,
+				MountPath: "/app/config/controller/tls/redis",
+			},
 		},
 	}}
 	podSpec.ServiceAccountName = nameWithSuffix("argocd-application-controller", cr)
@@ -449,6 +474,15 @@ func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoproj
 				},
 			},
 		},
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
 	}
 
 	ss.Spec.Template.Spec.Affinity = &corev1.Affinity{
@@ -513,7 +547,7 @@ func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoproj
 			existing.Spec.Template.ObjectMeta.Labels["image.upgraded"] = time.Now().UTC().Format("01022006-150406-MST")
 			changed = true
 		}
-		desiredCommand := getArgoApplicationControllerCommand(cr)
+		desiredCommand := getArgoApplicationControllerCommand(cr, useTLSForRedis)
 		if isRepoServerTLSVerificationRequested(cr) {
 			desiredCommand = append(desiredCommand, "--repo-server-strict-tls")
 		}
@@ -567,8 +601,8 @@ func (r *ReconcileArgoCD) reconcileApplicationControllerStatefulSet(cr *argoproj
 }
 
 // reconcileStatefulSets will ensure that all StatefulSets are present for the given ArgoCD.
-func (r *ReconcileArgoCD) reconcileStatefulSets(cr *argoprojv1a1.ArgoCD) error {
-	if err := r.reconcileApplicationControllerStatefulSet(cr); err != nil {
+func (r *ReconcileArgoCD) reconcileStatefulSets(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) error {
+	if err := r.reconcileApplicationControllerStatefulSet(cr, useTLSForRedis); err != nil {
 		return err
 	}
 	if err := r.reconcileRedisStatefulSet(cr); err != nil {
diff --git a/controllers/argocd/status.go b/controllers/argocd/status.go
index 11023c9d..215debef 100644
--- a/controllers/argocd/status.go
+++ b/controllers/argocd/status.go
@@ -60,6 +60,10 @@ func (r *ReconcileArgoCD) reconcileStatus(cr *argoprojv1a1.ArgoCD) error {
 		return err
 	}
 
+	if err := r.reconcileStatusNotifications(cr); err != nil {
+		return err
+	}
+
 	return nil
 }
 
@@ -222,6 +226,32 @@ func (r *ReconcileArgoCD) reconcileStatusServer(cr *argoprojv1a1.ArgoCD) error {
 	return nil
 }
 
+// reconcileStatusNotifications will ensure that the Notifications status is updated for the given ArgoCD.
+func (r *ReconcileArgoCD) reconcileStatusNotifications(cr *argoprojv1a1.ArgoCD) error {
+	status := "Unknown"
+
+	deploy := newDeploymentWithSuffix("notifications-controller", "controller", cr)
+	if argoutil.IsObjectFound(r.Client, cr.Namespace, deploy.Name, deploy) {
+		status = "Pending"
+
+		if deploy.Spec.Replicas != nil {
+			if deploy.Status.ReadyReplicas == *deploy.Spec.Replicas {
+				status = "Running"
+			}
+		}
+	}
+
+	if cr.Status.NotificationsController != status {
+		if !cr.Spec.Notifications.Enabled {
+			cr.Status.NotificationsController = ""
+		} else {
+			cr.Status.NotificationsController = status
+		}
+		return r.Client.Status().Update(context.TODO(), cr)
+	}
+	return nil
+}
+
 // reconcileStatusHost will ensure that the host status is updated for the given ArgoCD.
 func (r *ReconcileArgoCD) reconcileStatusHost(cr *argoprojv1a1.ArgoCD) error {
 	cr.Status.Host = ""
diff --git a/controllers/argocd/util.go b/controllers/argocd/util.go
index f714614d..8c7459ad 100644
--- a/controllers/argocd/util.go
+++ b/controllers/argocd/util.go
@@ -46,6 +46,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
 	v1 "k8s.io/api/rbac/v1"
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -103,15 +104,26 @@ func getArgoApplicationControllerResources(cr *argoprojv1a1.ArgoCD) corev1.Resou
 }
 
 // getArgoApplicationControllerCommand will return the command for the ArgoCD Application Controller component.
-func getArgoApplicationControllerCommand(cr *argoprojv1a1.ArgoCD) []string {
+func getArgoApplicationControllerCommand(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) []string {
 	cmd := []string{
 		"argocd-application-controller",
 		"--operation-processors", fmt.Sprint(getArgoServerOperationProcessors(cr)),
 		"--redis", getRedisServerAddress(cr),
-		"--repo-server", getRepoServerAddress(cr),
-		"--status-processors", fmt.Sprint(getArgoServerStatusProcessors(cr)),
-		"--kubectl-parallelism-limit", fmt.Sprint(getArgoControllerParellismLimit(cr)),
 	}
+
+	if useTLSForRedis {
+		cmd = append(cmd, "--redis-use-tls")
+		if isRedisTLSVerificationDisabled(cr) {
+			cmd = append(cmd, "--redis-insecure-skip-tls-verify")
+		} else {
+			cmd = append(cmd, "--redis-ca-certificate", "/app/config/controller/tls/redis/tls.crt")
+		}
+	}
+
+	cmd = append(cmd, "--repo-server", getRepoServerAddress(cr))
+	cmd = append(cmd, "--status-processors", fmt.Sprint(getArgoServerStatusProcessors(cr)))
+	cmd = append(cmd, "--kubectl-parallelism-limit", fmt.Sprint(getArgoControllerParellismLimit(cr)))
+
 	if cr.Spec.Controller.AppSync != nil {
 		cmd = append(cmd, "--app-resync", strconv.FormatInt(int64(cr.Spec.Controller.AppSync.Seconds()), 10))
 	}
@@ -197,6 +209,10 @@ func isRepoServerTLSVerificationRequested(cr *argoprojv1a1.ArgoCD) bool {
 	return cr.Spec.Repo.VerifyTLS
 }
 
+func isRedisTLSVerificationDisabled(cr *argoprojv1a1.ArgoCD) bool {
+	return cr.Spec.Redis.DisableTLSVerification
+}
+
 // getArgoServerGRPCHost will return the GRPC host for the given ArgoCD.
 func getArgoServerGRPCHost(cr *argoprojv1a1.ArgoCD) string {
 	host := nameWithSuffix("grpc", cr)
@@ -447,9 +463,12 @@ func getRedisConfigPath() string {
 
 // getRedisInitScript will load the redis configuration from a template on disk for the given ArgoCD.
 // If an error occurs, an empty string value will be returned.
-func getRedisConf() string {
+func getRedisConf(useTLSForRedis bool) string {
 	path := fmt.Sprintf("%s/redis.conf.tpl", getRedisConfigPath())
-	conf, err := loadTemplateFile(path, map[string]string{})
+	params := map[string]string{
+		"UseTLS": strconv.FormatBool(useTLSForRedis),
+	}
+	conf, err := loadTemplateFile(path, params)
 	if err != nil {
 		log.Error(err, "unable to load redis configuration")
 		return ""
@@ -524,10 +543,11 @@ func getRedisHAProxyContainerImage(cr *argoprojv1a1.ArgoCD) string {
 
 // getRedisInitScript will load the redis init script from a template on disk for the given ArgoCD.
 // If an error occurs, an empty string value will be returned.
-func getRedisInitScript(cr *argoprojv1a1.ArgoCD) string {
+func getRedisInitScript(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) string {
 	path := fmt.Sprintf("%s/init.sh.tpl", getRedisConfigPath())
 	vars := map[string]string{
 		"ServiceName": nameWithSuffix("redis-ha", cr),
+		"UseTLS":      strconv.FormatBool(useTLSForRedis),
 	}
 
 	script, err := loadTemplateFile(path, vars)
@@ -540,10 +560,11 @@ func getRedisInitScript(cr *argoprojv1a1.ArgoCD) string {
 
 // getRedisHAProxySConfig will load the Redis HA Proxy configuration from a template on disk for the given ArgoCD.
 // If an error occurs, an empty string value will be returned.
-func getRedisHAProxyConfig(cr *argoprojv1a1.ArgoCD) string {
+func getRedisHAProxyConfig(cr *argoprojv1a1.ArgoCD, useTLSForRedis bool) string {
 	path := fmt.Sprintf("%s/haproxy.cfg.tpl", getRedisConfigPath())
 	vars := map[string]string{
 		"ServiceName": nameWithSuffix("redis-ha", cr),
+		"UseTLS":      strconv.FormatBool(useTLSForRedis),
 	}
 
 	script, err := loadTemplateFile(path, vars)
@@ -596,9 +617,12 @@ func getRedisHAProxyResources(cr *argoprojv1a1.ArgoCD) corev1.ResourceRequiremen
 
 // getRedisSentinelConf will load the redis sentinel configuration from a template on disk for the given ArgoCD.
 // If an error occurs, an empty string value will be returned.
-func getRedisSentinelConf() string {
+func getRedisSentinelConf(useTLSForRedis bool) string {
 	path := fmt.Sprintf("%s/sentinel.conf.tpl", getRedisConfigPath())
-	conf, err := loadTemplateFile(path, map[string]string{})
+	params := map[string]string{
+		"UseTLS": strconv.FormatBool(useTLSForRedis),
+	}
+	conf, err := loadTemplateFile(path, params)
 	if err != nil {
 		log.Error(err, "unable to load redis sentinel configuration")
 		return ""
@@ -608,9 +632,12 @@ func getRedisSentinelConf() string {
 
 // getRedisLivenessScript will load the redis liveness script from a template on disk for the given ArgoCD.
 // If an error occurs, an empty string value will be returned.
-func getRedisLivenessScript() string {
+func getRedisLivenessScript(useTLSForRedis bool) string {
 	path := fmt.Sprintf("%s/redis_liveness.sh.tpl", getRedisConfigPath())
-	conf, err := loadTemplateFile(path, map[string]string{})
+	params := map[string]string{
+		"UseTLS": strconv.FormatBool(useTLSForRedis),
+	}
+	conf, err := loadTemplateFile(path, params)
 	if err != nil {
 		log.Error(err, "unable to load redis liveness script")
 		return ""
@@ -620,9 +647,12 @@ func getRedisLivenessScript() string {
 
 // getRedisReadinessScript will load the redis readiness script from a template on disk for the given ArgoCD.
 // If an error occurs, an empty string value will be returned.
-func getRedisReadinessScript() string {
+func getRedisReadinessScript(useTLSForRedis bool) string {
 	path := fmt.Sprintf("%s/redis_readiness.sh.tpl", getRedisConfigPath())
-	conf, err := loadTemplateFile(path, map[string]string{})
+	params := map[string]string{
+		"UseTLS": strconv.FormatBool(useTLSForRedis),
+	}
+	conf, err := loadTemplateFile(path, params)
 	if err != nil {
 		log.Error(err, "unable to load redis readiness script")
 		return ""
@@ -632,9 +662,12 @@ func getRedisReadinessScript() string {
 
 // getSentinelLivenessScript will load the redis liveness script from a template on disk for the given ArgoCD.
 // If an error occurs, an empty string value will be returned.
-func getSentinelLivenessScript() string {
+func getSentinelLivenessScript(useTLSForRedis bool) string {
 	path := fmt.Sprintf("%s/sentinel_liveness.sh.tpl", getRedisConfigPath())
-	conf, err := loadTemplateFile(path, map[string]string{})
+	params := map[string]string{
+		"UseTLS": strconv.FormatBool(useTLSForRedis),
+	}
+	conf, err := loadTemplateFile(path, params)
 	if err != nil {
 		log.Error(err, "unable to load sentinel liveness script")
 		return ""
@@ -711,6 +744,55 @@ func (r *ReconcileArgoCD) reconcileCertificateAuthority(cr *argoprojv1a1.ArgoCD)
 	return nil
 }
 
+func (r *ReconcileArgoCD) redisShouldUseTLS(cr *argoprojv1a1.ArgoCD) bool {
+	var tlsSecretObj corev1.Secret
+	tlsSecretName := types.NamespacedName{Namespace: cr.Namespace, Name: common.ArgoCDRedisServerTLSSecretName}
+	err := r.Client.Get(context.TODO(), tlsSecretName, &tlsSecretObj)
+	if err != nil {
+		if !apierrors.IsNotFound(err) {
+			log.Error(err, "error looking up redis tls secret")
+		}
+		return false
+	}
+
+	secretOwnerRefs := tlsSecretObj.GetOwnerReferences()
+	if len(secretOwnerRefs) > 0 {
+		// OpenShift service CA makes the owner reference for the TLS secret to the
+		// service, which in turn is owned by the controller. This method performs
+		// a lookup of the controller through the intermediate owning service.
+		for _, secretOwner := range secretOwnerRefs {
+			if isOwnerOfInterest(secretOwner) {
+				key := client.ObjectKey{Name: secretOwner.Name, Namespace: tlsSecretObj.GetNamespace()}
+				svc := &corev1.Service{}
+
+				// Get the owning object of the secret
+				err := r.Client.Get(context.TODO(), key, svc)
+				if err != nil {
+					log.Error(err, fmt.Sprintf("could not get owner of secret %s", tlsSecretObj.GetName()))
+					return false
+				}
+
+				// If there's an object of kind ArgoCD in the owner's list,
+				// this will be our reconciled object.
+				serviceOwnerRefs := svc.GetOwnerReferences()
+				for _, serviceOwner := range serviceOwnerRefs {
+					if serviceOwner.Kind == "ArgoCD" {
+						return true
+					}
+				}
+			}
+		}
+	} else {
+		// For secrets without owner (i.e. manually created), we apply some
+		// heuristics. This may not be as accurate (e.g. if the user made a
+		// typo in the resource's name), but should be good enough for now.
+		if _, ok := tlsSecretObj.Annotations[common.AnnotationName]; ok {
+			return true
+		}
+	}
+	return false
+}
+
 // reconcileResources will reconcile common ArgoCD resources.
 func (r *ReconcileArgoCD) reconcileResources(cr *argoprojv1a1.ArgoCD) error {
 	log.Info("reconciling status")
@@ -743,8 +825,10 @@ func (r *ReconcileArgoCD) reconcileResources(cr *argoprojv1a1.ArgoCD) error {
 		return err
 	}
 
+	useTLSForRedis := r.redisShouldUseTLS(cr)
+
 	log.Info("reconciling config maps")
-	if err := r.reconcileConfigMaps(cr); err != nil {
+	if err := r.reconcileConfigMaps(cr, useTLSForRedis); err != nil {
 		return err
 	}
 
@@ -754,12 +838,12 @@ func (r *ReconcileArgoCD) reconcileResources(cr *argoprojv1a1.ArgoCD) error {
 	}
 
 	log.Info("reconciling deployments")
-	if err := r.reconcileDeployments(cr); err != nil {
+	if err := r.reconcileDeployments(cr, useTLSForRedis); err != nil {
 		return err
 	}
 
 	log.Info("reconciling statefulsets")
-	if err := r.reconcileStatefulSets(cr); err != nil {
+	if err := r.reconcileStatefulSets(cr, useTLSForRedis); err != nil {
 		return err
 	}
 
@@ -806,10 +890,21 @@ func (r *ReconcileArgoCD) reconcileResources(cr *argoprojv1a1.ArgoCD) error {
 		}
 	}
 
+	if cr.Spec.Notifications.Enabled {
+		log.Info("reconciling Notifications controller")
+		if err := r.reconcileNotificationsController(cr); err != nil {
+			return err
+		}
+	}
+
 	if err := r.reconcileRepoServerTLSSecret(cr); err != nil {
 		return err
 	}
 
+	if err := r.reconcileRedisTLSSecret(cr, useTLSForRedis); err != nil {
+		return err
+	}
+
 	if cr.Spec.SSO != nil {
 		log.Info("reconciling SSO")
 		if err := r.reconcileSSO(cr); err != nil {
@@ -919,7 +1014,7 @@ func removeString(slice []string, s string) []string {
 }
 
 // setResourceWatches will register Watches for each of the supported Resources.
-func setResourceWatches(bldr *builder.Builder, clusterResourceMapper, tlsSecretMapper, namespaceResourceMapper handler.MapFunc) *builder.Builder {
+func (r *ReconcileArgoCD) setResourceWatches(bldr *builder.Builder, clusterResourceMapper, tlsSecretMapper, namespaceResourceMapper handler.MapFunc) *builder.Builder {
 
 	deploymentConfigPred := predicate.Funcs{
 		UpdateFunc: func(e event.UpdateEvent) bool {
@@ -974,8 +1069,31 @@ func setResourceWatches(bldr *builder.Builder, clusterResourceMapper, tlsSecretM
 		},
 	}
 
+	// Add new predicate to delete Notifications Resources. The predicate watches the Argo CD CR for changes to the `.spec.Notifications.Enabled`
+	// field. When a change is detected that results in notifications being disabled, we trigger deletion of notifications resources
+	deleteNotificationsPred := predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			newCR, ok := e.ObjectNew.(*argoprojv1a1.ArgoCD)
+			if !ok {
+				return false
+			}
+			oldCR, ok := e.ObjectOld.(*argoprojv1a1.ArgoCD)
+			if !ok {
+				return false
+			}
+			if oldCR.Spec.Notifications.Enabled && !newCR.Spec.Notifications.Enabled {
+				err := r.deleteNotificationsResources(newCR)
+				if err != nil {
+					log.Error(err, fmt.Sprintf("Failed to delete notifications controller resources for ArgoCD %s in namespace %s",
+						newCR.Name, newCR.Namespace))
+				}
+			}
+			return true
+		},
+	}
+
 	// Watch for changes to primary resource ArgoCD
-	bldr.For(&argoprojv1a1.ArgoCD{}, builder.WithPredicates(deleteSSOPred))
+	bldr.For(&argoprojv1a1.ArgoCD{}, builder.WithPredicates(deleteSSOPred, deleteNotificationsPred))
 
 	// Watch for changes to ConfigMap sub-resources owned by ArgoCD instances.
 	bldr.Owns(&corev1.ConfigMap{})
diff --git a/deploy/olm-catalog/argocd-operator/0.4.0/argocd-operator.v0.4.0.clusterserviceversion.yaml b/deploy/olm-catalog/argocd-operator/0.4.0/argocd-operator.v0.4.0.clusterserviceversion.yaml
index 55231e18..c31e9d9c 100644
--- a/deploy/olm-catalog/argocd-operator/0.4.0/argocd-operator.v0.4.0.clusterserviceversion.yaml
+++ b/deploy/olm-catalog/argocd-operator/0.4.0/argocd-operator.v0.4.0.clusterserviceversion.yaml
@@ -701,6 +701,19 @@ spec:
         path: dex
         x-descriptors:
         - urn:alm:descriptor:com.tectonic.ui:text
+      - description: 'NotificationsController is a simple, high-level summary of where
+          the Argo CD notifications controller component is in its lifecycle. There
+          are five possible NotificationsController values: Pending: The Argo CD notifications
+          controller component has been accepted by the Kubernetes system, but one
+          or more of the required resources have not been created. Running: All of
+          the required Pods for the Argo CD notifications controller component are
+          in a Ready state. Failed: At least one of the  Argo CD notifications controller
+          component Pods had a failure. Unknown: For some reason the state of the
+          Argo CD notifications controller component could not be obtained.'
+        displayName: NotificationsController
+        path: notificationsController
+        x-descriptors:
+        - urn:alm:descriptor:com.tectonic.ui:text
       - description: 'Phase is a simple, high-level summary of where the ArgoCD is
           in its lifecycle. There are five possible phase values: Pending: The ArgoCD
           has been accepted by the Kubernetes system, but one or more of the required
diff --git a/deploy/olm-catalog/argocd-operator/0.4.0/argoproj.io_argocds.yaml b/deploy/olm-catalog/argocd-operator/0.4.0/argoproj.io_argocds.yaml
index ff3b95fb..610ff8f1 100644
--- a/deploy/olm-catalog/argocd-operator/0.4.0/argoproj.io_argocds.yaml
+++ b/deploy/olm-catalog/argocd-operator/0.4.0/argoproj.io_argocds.yaml
@@ -345,6 +345,16 @@ spec:
               disableAdmin:
                 description: DisableAdmin will disable the admin user.
                 type: boolean
+              extraConfig:
+                additionalProperties:
+                  type: string
+                description: "ExtraConfig can be used to add fields to Argo CD configmap
+                  that are not supported by Argo CD CRD. \n Note: ExtraConfig takes
+                  precedence over Argo CD CRD. For example, A user sets `argocd.Spec.DisableAdmin`
+                  = true and also `a.Spec.ExtraConfig[\"admin.enabled\"]` = true.
+                  In this case, operator updates Argo CD Configmap as follows -> argocd-cm.Data[\"admin.enabled\"]
+                  = true."
+                type: object
               gaAnonymizeUsers:
                 description: GAAnonymizeUsers toggles user IDs being hashed before
                   sending to google analytics.
@@ -686,6 +696,60 @@ spec:
                       type: object
                     type: array
                 type: object
+              notifications:
+                description: Notifications defines whether the Argo CD Notifications
+                  controller should be installed.
+                properties:
+                  enabled:
+                    description: Enabled defines whether argocd-notifications controller
+                      should be deployed or not
+                    type: boolean
+                  image:
+                    description: Image is the Argo CD Notifications image (optional)
+                    type: string
+                  logLevel:
+                    description: LogLevel describes the log level that should be used
+                      by the argocd-notifications. Defaults to ArgoCDDefaultLogLevel
+                      if not set.  Valid options are debug,info, error, and warn.
+                    type: string
+                  replicas:
+                    description: Replicas defines the number of replicas to run for
+                      notifications-controller
+                    format: int32
+                    type: integer
+                  resources:
+                    description: Resources defines the Compute Resources required
+                      by the container for Argo CD Notifications.
+                    properties:
+                      limits:
+                        additionalProperties:
+                          anyOf:
+                          - type: integer
+                          - type: string
+                          pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                          x-kubernetes-int-or-string: true
+                        description: 'Limits describes the maximum amount of compute
+                          resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
+                        type: object
+                      requests:
+                        additionalProperties:
+                          anyOf:
+                          - type: integer
+                          - type: string
+                          pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                          x-kubernetes-int-or-string: true
+                        description: 'Requests describes the minimum amount of compute
+                          resources required. If Requests is omitted for a container,
+                          it defaults to Limits if that is explicitly specified, otherwise
+                          to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
+                        type: object
+                    type: object
+                  version:
+                    description: Version is the Argo CD Notifications image tag. (optional)
+                    type: string
+                required:
+                - enabled
+                type: object
               oidcConfig:
                 description: OIDCConfig is the OIDC configuration as an alternative
                   to dex.
@@ -856,6 +920,16 @@ spec:
               redis:
                 description: Redis defines the Redis server options for ArgoCD.
                 properties:
+                  autotls:
+                    description: 'AutoTLS specifies the method to use for automatic
+                      TLS configuration for the redis server The value specified here
+                      can currently be: - openshift - Use the OpenShift service CA
+                      to request TLS config'
+                    type: string
+                  disableTLSVerification:
+                    description: DisableTLSVerification defines whether redis server
+                      API should be accessed using strict TLS validation
+                    type: boolean
                   image:
                     description: Image is the Redis container image.
                     type: string
@@ -5696,6 +5770,18 @@ spec:
               host:
                 description: Host is the hostname of the Ingress.
                 type: string
+              notificationsController:
+                description: 'NotificationsController is a simple, high-level summary
+                  of where the Argo CD notifications controller component is in its
+                  lifecycle. There are five possible NotificationsController values:
+                  Pending: The Argo CD notifications controller component has been
+                  accepted by the Kubernetes system, but one or more of the required
+                  resources have not been created. Running: All of the required Pods
+                  for the Argo CD notifications controller component are in a Ready
+                  state. Failed: At least one of the  Argo CD notifications controller
+                  component Pods had a failure. Unknown: For some reason the state
+                  of the Argo CD notifications controller component could not be obtained.'
+                type: string
               phase:
                 description: 'Phase is a simple, high-level summary of where the ArgoCD
                   is in its lifecycle. There are five possible phase values: Pending:
@@ -5716,6 +5802,11 @@ spec:
                   some reason the state of the Argo CD Redis component could not be
                   obtained.'
                 type: string
+              redisTLSChecksum:
+                description: RedisTLSChecksum contains the SHA256 checksum of the
+                  latest known state of tls.crt and tls.key in the argocd-operator-redis-tls
+                  secret.
+                type: string
               repo:
                 description: 'Repo is a simple, high-level summary of where the Argo
                   CD Repo component is in its lifecycle. There are five possible repo
diff --git a/docs/reference/argocd.md b/docs/reference/argocd.md
index 4f217598..9fc6a5a6 100644
--- a/docs/reference/argocd.md
+++ b/docs/reference/argocd.md
@@ -784,6 +784,8 @@ The following properties are available for configuring the Redis component.
 
 Name | Default | Description
 --- | --- | ---
+AutoTLS | "" | Provider to use for creating the redis server's TLS certificate (one of: `openshift`). Currently only available for OpenShift.
+DisableTLSVerification | false | defines whether the redis server should be accessed using strict TLS validation
 Image | `redis` | The container image for Redis. This overrides the `ARGOCD_REDIS_IMAGE` environment variable.
 Resources | [Empty] | The container compute resources.
 Version | 5.0.3 (SHA) | The tag to use with the Redis container image.
@@ -804,6 +806,8 @@ spec:
     image: redis
     resources: {}
     version: "5.0.3"
+    disableTLSVerification: false
+    autotls: ""
 ```
 
 ## Repo Options
diff --git a/docs/usage/custom_roles.md b/docs/usage/custom_roles.md
index 94254294..04633217 100644
--- a/docs/usage/custom_roles.md
+++ b/docs/usage/custom_roles.md
@@ -38,3 +38,19 @@ spec:
           - 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/extra-config.md b/docs/usage/extra-config.md
new file mode 100644
index 00000000..17a5ea46
--- /dev/null
+++ b/docs/usage/extra-config.md
@@ -0,0 +1,30 @@
+# Configure Argo CD ConfigMap for features not supported by Argo CD CRD
+
+## Motivation
+
+As the Argo CD upstream project evolves, new features are continuously added and new Argo CD config map configurations are constantly introduced. It is very difficult to keep Argocd Operator up to date with Argo CD’s new configurations. Argocd Operator is lagging behind to support new Argo CD features in months or more.
+
+But, oftentimes to support a new feature, it is as simple as reconciling a configmap entry.  However, the code to make this a first class configuration by making it as a field in Argocd CR requires many changes. It takes quite a bit of development effort to reconcile just one new configmap entry. Also, since we made a new field in Argo CD CR, User will have to read the Argocd Operator’s manual in order to figure out the name of the field even though the user may already know the configmap entry key.
+
+## Using ExtraConfig
+
+Users can add dynamic entries to Argo CD configmap using `ExtraConfig` in Argocd CR. It is completely optional and has no default value.
+
+When `ExtraConfig` is set, the entries specified are reconciled to the live Argo CD configmap. Users can specify arbitrary configmap entries with this `ExtraConfig`. This allows users to specify a new configuration even though the configuration is not supported by Argo CD CRD.
+
+**Note**: `ExtraConfig` takes precedence over Argo CD CRD.
+
+## Example
+
+```yaml
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  extraConfig: 
+    "ping": "pong" // The same entry is reflected in Argo CD Configmap.
+  server:
+    ingress:
+      enabled: true
+```
diff --git a/mkdocs.yml b/mkdocs.yml
index 93a5b35d..a742f288 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -28,6 +28,7 @@ nav:
   - Usage: 
     - Basics: usage/basics.md
     - Export: usage/export.md
+    - ExtraConfig: usage/extra-config.md
     - High Availability: usage/ha.md
     - Ingress: usage/ingress.md
     - Insights: usage/insights.md


Test patch:
diff --git a/controllers/argocd/applicationset_test.go b/controllers/argocd/applicationset_test.go
index 6b89fb67..cf669c13 100644
--- a/controllers/argocd/applicationset_test.go
+++ b/controllers/argocd/applicationset_test.go
@@ -38,8 +38,9 @@ import (
 func applicationSetDefaultVolumeMounts() []corev1.VolumeMount {
 	repoMounts := repoServerDefaultVolumeMounts()
 	ignoredMounts := map[string]bool{
-		"plugins":                true,
-		"argocd-repo-server-tls": true,
+		"plugins":                             true,
+		"argocd-repo-server-tls":              true,
+		common.ArgoCDRedisServerTLSSecretName: true,
 	}
 	mounts := make([]corev1.VolumeMount, len(repoMounts)-len(ignoredMounts), len(repoMounts)-len(ignoredMounts))
 	j := 0
@@ -55,9 +56,10 @@ func applicationSetDefaultVolumeMounts() []corev1.VolumeMount {
 func applicationSetDefaultVolumes() []corev1.Volume {
 	repoVolumes := repoServerDefaultVolumes()
 	ignoredVolumes := map[string]bool{
-		"var-files":              true,
-		"plugins":                true,
-		"argocd-repo-server-tls": true,
+		"var-files":                           true,
+		"plugins":                             true,
+		"argocd-repo-server-tls":              true,
+		common.ArgoCDRedisServerTLSSecretName: true,
 	}
 	volumes := make([]corev1.Volume, len(repoVolumes)-len(ignoredVolumes), len(repoVolumes)-len(ignoredVolumes))
 	j := 0
diff --git a/controllers/argocd/argocd_controller_test.go b/controllers/argocd/argocd_controller_test.go
index 9a58399e..afdcb3af 100644
--- a/controllers/argocd/argocd_controller_test.go
+++ b/controllers/argocd/argocd_controller_test.go
@@ -17,6 +17,7 @@ package argocd
 import (
 	"context"
 	"fmt"
+	"os"
 	"testing"
 	"time"
 
@@ -102,6 +103,75 @@ func TestReconcileArgoCD_Reconcile(t *testing.T) {
 	}
 }
 
+func TestReconcileArgoCD_Reconcile_RemoveManagedByLabelOnArgocdDeletion(t *testing.T) {
+	restoreEnv(t)
+	logf.SetLogger(ZapLogger(true))
+	
+	tests := []struct {
+		testName									string
+		nsName										string
+		isRemoveManagedByLabelOnArgoCDDeletionSet	bool
+		}{
+			{
+				testName: 									"Without REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION set",
+				nsName:										"newNamespaceTest1",
+				isRemoveManagedByLabelOnArgoCDDeletionSet: 	false,
+			},
+			{
+				testName: 									"With REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION set",
+				nsName:										"newNamespaceTest2",
+				isRemoveManagedByLabelOnArgoCDDeletionSet: 	true,
+			},
+		}
+
+	for _, test := range tests {
+		t.Run(test.testName, func(t *testing.T) {
+			a := makeTestArgoCD(deletedAt(time.Now()), addFinalizer(common.ArgoCDDeletionFinalizer))
+			r := makeTestReconciler(t, a)
+
+			nsArgocd := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
+				Name: a.Namespace,
+			}}
+			err := r.Client.Create(context.TODO(), nsArgocd)
+			assert.NoError(t, err)
+
+			if (test.isRemoveManagedByLabelOnArgoCDDeletionSet) {
+				os.Setenv("REMOVE_MANAGED_BY_LABEL_ON_ARGOCD_DELETION", "true")
+			}
+		
+			ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{
+				Name: test.nsName,
+				Labels: map[string]string{
+					common.ArgoCDManagedByLabel: a.Namespace,
+				}},
+			}
+			err = r.Client.Create(context.TODO(), ns)
+			assert.NoError(t, err)
+			
+			req := reconcile.Request{
+				NamespacedName: types.NamespacedName{
+					Name:      a.Name,
+					Namespace: a.Namespace,
+				},
+			}
+		
+			_, err = r.Reconcile(context.TODO(), req)
+			assert.NoError(t, err)
+		
+			assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: ns.Name}, ns))
+			if (test.isRemoveManagedByLabelOnArgoCDDeletionSet) {
+				// Check if the managed-by label gets removed from the new namespace
+				if _, ok := ns.Labels[common.ArgoCDManagedByLabel]; ok {
+					t.Errorf("Expected the label[%v] to be removed from the namespace[%v]", common.ArgoCDManagedByLabel, ns.Name)
+				}
+			} else {
+				// Check if the managed-by label still exists in the new namespace
+				assert.Equal(t, ns.Labels[common.ArgoCDManagedByLabel], a.Namespace)
+			}
+		})
+	}
+}
+
 func deletedAt(now time.Time) argoCDOpt {
 	return func(a *argov1alpha1.ArgoCD) {
 		wrapped := metav1.NewTime(now)
diff --git a/controllers/argocd/configmap_test.go b/controllers/argocd/configmap_test.go
index 097076fb..9fd2ac6f 100644
--- a/controllers/argocd/configmap_test.go
+++ b/controllers/argocd/configmap_test.go
@@ -571,3 +571,82 @@ func TestReconcileArgoCD_reconcileArgoConfigMap_withResourceCustomizations(t *te
 		t.Fatalf("reconcileArgoConfigMap failed got %q, want %q", c, customizations)
 	}
 }
+
+func TestReconcileArgoCD_reconcileArgoConfigMap_withExtraConfig(t *testing.T) {
+	a := makeTestArgoCD()
+	r := makeTestReconciler(t, a)
+
+	err := r.reconcileArgoConfigMap(a)
+	assert.NoError(t, err)
+
+	// Verify Argo CD configmap is created.
+	cm := &corev1.ConfigMap{}
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      common.ArgoCDConfigMapName,
+		Namespace: testNamespace,
+	}, cm)
+	assert.NoError(t, err)
+
+	// Verify that updates to the configmap are rejected(reconciled back to default) by the operator.
+	cm.Data["ping"] = "pong"
+	err = r.Client.Update(context.TODO(), cm)
+	assert.NoError(t, err)
+
+	err = r.reconcileArgoConfigMap(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      common.ArgoCDConfigMapName,
+		Namespace: testNamespace,
+	}, cm)
+	assert.NoError(t, err)
+
+	assert.Equal(t, cm.Data["ping"], "")
+
+	// Verify that operator updates argocd-cm according to ExtraConfig.
+	a.Spec.ExtraConfig = map[string]string{
+		"foo": "bar",
+	}
+
+	err = r.reconcileArgoConfigMap(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      common.ArgoCDConfigMapName,
+		Namespace: testNamespace,
+	}, cm)
+	assert.NoError(t, err)
+
+	assert.Equal(t, cm.Data["foo"], "bar")
+
+	// Verify that ExtraConfig overrides FirstClass entries
+	a.Spec.DisableAdmin = true
+	a.Spec.ExtraConfig["admin.enabled"] = "true"
+
+	err = r.reconcileArgoConfigMap(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      common.ArgoCDConfigMapName,
+		Namespace: testNamespace,
+	}, cm)
+
+	assert.NoError(t, err)
+	assert.Equal(t, cm.Data["admin.enabled"], "true")
+
+	// Verify that deletion of a field from ExtraConfig does not delete any existing configuration
+	// created by FirstClass citizens.
+	a.Spec.ExtraConfig = make(map[string]string, 0)
+
+	err = r.reconcileArgoConfigMap(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      common.ArgoCDConfigMapName,
+		Namespace: testNamespace,
+	}, cm)
+
+	assert.NoError(t, err)
+	assert.Equal(t, cm.Data["admin.enabled"], "false")
+
+}
diff --git a/controllers/argocd/custommapper_test.go b/controllers/argocd/custommapper_test.go
index 7f30fc82..95d43e1c 100644
--- a/controllers/argocd/custommapper_test.go
+++ b/controllers/argocd/custommapper_test.go
@@ -103,7 +103,7 @@ func TestReconcileArgoCD_clusterRoleBindingMapper(t *testing.T) {
 	}
 }
 
-func TestReconcileArgoCD_tlsSecretMapper(t *testing.T) {
+func TestReconcileArgoCD_tlsSecretMapperRepoServer(t *testing.T) {
 	argocd := &v1alpha1.ArgoCD{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "argocd",
@@ -320,6 +320,223 @@ func TestReconcileArgoCD_tlsSecretMapper(t *testing.T) {
 
 }
 
+func TestReconcileArgoCD_tlsSecretMapperRedis(t *testing.T) {
+	argocd := &v1alpha1.ArgoCD{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "argocd",
+			Namespace: "argocd-operator",
+			UID:       "abcd",
+		},
+	}
+
+	t.Run("Map with proper ownerReference", func(t *testing.T) {
+		service := &v1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-redis",
+				Namespace: "argocd-operator",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: "argoproj.io/v1alpha1",
+						Kind:       "ArgoCD",
+						Name:       "argocd",
+						UID:        argocd.GetUID(),
+					},
+				},
+				UID: "service-123",
+			},
+		}
+		secret := &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-operator-redis-tls",
+				Namespace: "argocd-operator",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: "v1",
+						Kind:       "Service",
+						Name:       "argocd-redis",
+						UID:        service.GetUID(),
+					},
+				},
+			},
+			Type: corev1.SecretTypeTLS,
+			Data: map[string][]byte{
+				corev1.TLSCertKey:       []byte("foo"),
+				corev1.TLSPrivateKeyKey: []byte("bar"),
+			},
+		}
+		objs := []runtime.Object{
+			argocd,
+			secret,
+			service,
+		}
+		r := makeReconciler(t, argocd, objs...)
+		want := []reconcile.Request{
+			{
+				NamespacedName: types.NamespacedName{
+					Name:      "argocd",
+					Namespace: "argocd-operator",
+				},
+			},
+		}
+		got := r.tlsSecretMapper(secret)
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("Reconciliation unsucessful: got: %v, want: %v", got, want)
+		}
+	})
+
+	t.Run("Map with ownerReference on non-existing owner", func(t *testing.T) {
+		service := &v1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-redis",
+				Namespace: "argocd-operator",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: "argoproj.io/v1alpha1",
+						Kind:       "ArgoCD",
+						Name:       "argocd",
+						UID:        argocd.GetUID(),
+					},
+				},
+				UID: "service-123",
+			},
+		}
+		secret := &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-operator-redis-tls",
+				Namespace: "argocd-operator",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: "v1",
+						Kind:       "Service",
+						Name:       "argocd-redis",
+						UID:        service.GetUID(),
+					},
+				},
+			},
+			Type: corev1.SecretTypeTLS,
+			Data: map[string][]byte{
+				corev1.TLSCertKey:       []byte("foo"),
+				corev1.TLSPrivateKeyKey: []byte("bar"),
+			},
+		}
+		objs := []runtime.Object{
+			argocd,
+			secret,
+		}
+		r := makeReconciler(t, argocd, objs...)
+		want := []reconcile.Request{}
+		got := r.tlsSecretMapper(secret)
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("Reconciliation unsucessful: got: %v, want: %v", got, want)
+		}
+	})
+
+	t.Run("Map with invalid owner", func(t *testing.T) {
+		service := &v1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-redis",
+				Namespace: "argocd-operator",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: "argoproj.io/v1alpha1",
+						Kind:       "ArgoCD",
+						Name:       "argocd",
+						UID:        argocd.GetUID(),
+					},
+				},
+				UID: "service-123",
+			},
+		}
+		secret := &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-operator-redis-tls",
+				Namespace: "argocd-operator",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: "v1",
+						Kind:       "Service",
+						Name:       "argocd-server",
+						UID:        service.GetUID(),
+					},
+				},
+			},
+			Type: corev1.SecretTypeTLS,
+			Data: map[string][]byte{
+				corev1.TLSCertKey:       []byte("foo"),
+				corev1.TLSPrivateKeyKey: []byte("bar"),
+			},
+		}
+		objs := []runtime.Object{
+			argocd,
+			secret,
+			service,
+		}
+		r := makeReconciler(t, argocd, objs...)
+		want := []reconcile.Request{}
+		got := r.tlsSecretMapper(secret)
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("Reconciliation unsucessful: got: %v, want: %v", got, want)
+		}
+	})
+
+	t.Run("Map with owner annotation", func(t *testing.T) {
+		secret := &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-operator-redis-tls",
+				Namespace: "argocd-operator",
+				Annotations: map[string]string{
+					common.AnnotationName: "argocd",
+				},
+			},
+			Type: corev1.SecretTypeTLS,
+			Data: map[string][]byte{
+				corev1.TLSCertKey:       []byte("foo"),
+				corev1.TLSPrivateKeyKey: []byte("bar"),
+			},
+		}
+		objs := []runtime.Object{
+			secret,
+		}
+		r := makeReconciler(t, argocd, objs...)
+		want := []reconcile.Request{
+			{
+				NamespacedName: types.NamespacedName{
+					Name:      "argocd",
+					Namespace: "argocd-operator",
+				},
+			},
+		}
+		got := r.tlsSecretMapper(secret)
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("Reconciliation unsucessful: got: %v, want: %v", got, want)
+		}
+	})
+
+	t.Run("Map without owner and without annotation", func(t *testing.T) {
+		secret := &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-operator-redis-tls",
+				Namespace: "argocd-operator",
+			},
+			Type: corev1.SecretTypeTLS,
+			Data: map[string][]byte{
+				corev1.TLSCertKey:       []byte("foo"),
+				corev1.TLSPrivateKeyKey: []byte("bar"),
+			},
+		}
+		objs := []runtime.Object{
+			secret,
+		}
+		r := makeReconciler(t, argocd, objs...)
+		want := []reconcile.Request{}
+		got := r.tlsSecretMapper(secret)
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("Reconciliation unsucessful: got: %v, want: %v", got, want)
+		}
+	})
+
+}
+
 func TestReconcileArgoCD_namespaceResourceMapper(t *testing.T) {
 	a := makeTestArgoCD()
 	r := makeTestReconciler(t, a)
diff --git a/controllers/argocd/deployment_test.go b/controllers/argocd/deployment_test.go
index 55da5db3..843c01b7 100644
--- a/controllers/argocd/deployment_test.go
+++ b/controllers/argocd/deployment_test.go
@@ -3,6 +3,7 @@ package argocd
 import (
 	"context"
 	"os"
+	"reflect"
 	"strings"
 	"testing"
 	"time"
@@ -65,7 +66,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_replicas(t *testing.T) {
 			})
 			r := makeTestReconciler(t, a)
 
-			err := r.reconcileRepoDeployment(a)
+			err := r.reconcileRepoDeployment(a, false)
 			assert.NoError(t, err)
 
 			deployment := &appsv1.Deployment{}
@@ -116,7 +117,7 @@ func TestReconcileArgoCD_reconcile_ServerDeployment_replicas(t *testing.T) {
 			})
 			r := makeTestReconciler(t, a)
 
-			err := r.reconcileServerDeployment(a)
+			err := r.reconcileServerDeployment(a, false)
 			assert.NoError(t, err)
 
 			deployment := &appsv1.Deployment{}
@@ -157,7 +158,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_loglevel(t *testing.T) {
 
 		r := makeTestReconciler(t, lglv)
 
-		err := r.reconcileRepoDeployment(lglv)
+		err := r.reconcileRepoDeployment(lglv, false)
 		assert.NoError(t, err)
 		deployment := &appsv1.Deployment{}
 		err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -191,7 +192,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_volumes(t *testing.T) {
 		a := makeTestArgoCD()
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileRepoDeployment(a)
+		err := r.reconcileRepoDeployment(a, false)
 		assert.NoError(t, err)
 		deployment := &appsv1.Deployment{}
 		err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -216,7 +217,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_volumes(t *testing.T) {
 		})
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileRepoDeployment(a)
+		err := r.reconcileRepoDeployment(a, false)
 		assert.NoError(t, err)
 		deployment := &appsv1.Deployment{}
 		err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -246,7 +247,7 @@ func TestReconcileArgoCD_reconcile_ServerDeployment_env(t *testing.T) {
 		a.Spec.Repo.ExecTimeout = &timeout
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileServerDeployment(a)
+		err := r.reconcileServerDeployment(a, false)
 		assert.NoError(t, err)
 		deployment := &appsv1.Deployment{}
 		err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -280,7 +281,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_env(t *testing.T) {
 		a.Spec.Repo.ExecTimeout = &timeout
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileRepoDeployment(a)
+		err := r.reconcileRepoDeployment(a, false)
 		assert.NoError(t, err)
 		deployment := &appsv1.Deployment{}
 		err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -302,7 +303,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_env(t *testing.T) {
 		a.Spec.Repo.ExecTimeout = &timeout
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileRepoDeployment(a)
+		err := r.reconcileRepoDeployment(a, false)
 		assert.NoError(t, err)
 		deployment := &appsv1.Deployment{}
 		err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -328,7 +329,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_env(t *testing.T) {
 		}
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileRepoDeployment(a)
+		err := r.reconcileRepoDeployment(a, false)
 		assert.NoError(t, err)
 		deployment := &appsv1.Deployment{}
 		err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -345,7 +346,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_env(t *testing.T) {
 		a := makeTestArgoCD()
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileRepoDeployment(a)
+		err := r.reconcileRepoDeployment(a, false)
 		assert.NoError(t, err)
 		deployment := &appsv1.Deployment{}
 		err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -365,7 +366,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_mounts(t *testing.T) {
 		a := makeTestArgoCD()
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileRepoDeployment(a)
+		err := r.reconcileRepoDeployment(a, false)
 		assert.NoError(t, err)
 
 		deployment := &appsv1.Deployment{}
@@ -389,7 +390,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_mounts(t *testing.T) {
 		})
 		r := makeTestReconciler(t, a)
 
-		err := r.reconcileRepoDeployment(a)
+		err := r.reconcileRepoDeployment(a, false)
 		assert.NoError(t, err)
 
 		deployment := &appsv1.Deployment{}
@@ -413,7 +414,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_initContainers(t *testing.T) {
 	})
 	r := makeTestReconciler(t, a)
 
-	err := r.reconcileRepoDeployment(a)
+	err := r.reconcileRepoDeployment(a, false)
 	assert.NoError(t, err)
 
 	deployment := &appsv1.Deployment{}
@@ -449,7 +450,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_missingInitContainers(t *testin
 	}
 	r := makeTestReconciler(t, a, d)
 
-	err := r.reconcileRepoDeployment(a)
+	err := r.reconcileRepoDeployment(a, false)
 	assert.NoError(t, err)
 	deployment := &appsv1.Deployment{}
 	err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -490,7 +491,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_unexpectedInitContainer(t *test
 	}
 	r := makeTestReconciler(t, a, d)
 
-	err := r.reconcileRepoDeployment(a)
+	err := r.reconcileRepoDeployment(a, false)
 	assert.NoError(t, err)
 	deployment := &appsv1.Deployment{}
 	err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -507,7 +508,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_command(t *testing.T) {
 	a := makeTestArgoCD()
 	r := makeTestReconciler(t, a)
 
-	err := r.reconcileRepoDeployment(a)
+	err := r.reconcileRepoDeployment(a, false)
 	assert.NoError(t, err)
 
 	deployment := &appsv1.Deployment{}
@@ -518,7 +519,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_command(t *testing.T) {
 	assert.NoError(t, err)
 
 	deployment.Spec.Template.Spec.Containers[0].Command[6] = "debug"
-	err = r.reconcileRepoDeployment(a)
+	err = r.reconcileRepoDeployment(a, false)
 
 	assert.Equal(t, "debug", deployment.Spec.Template.Spec.Containers[0].Command[6])
 }
@@ -606,7 +607,7 @@ func TestReconcileArgoCD_reconcileDeployments_proxy(t *testing.T) {
 	})
 	r := makeTestReconciler(t, a)
 
-	err := r.reconcileDeployments(a)
+	err := r.reconcileDeployments(a, false)
 	assert.NoError(t, err)
 
 	for _, v := range deploymentNames {
@@ -626,7 +627,7 @@ func TestReconcileArgoCD_reconcileDeployments_proxy_update_existing(t *testing.T
 		a.Spec.Grafana.Enabled = true
 	})
 	r := makeTestReconciler(t, a)
-	err := r.reconcileDeployments(a)
+	err := r.reconcileDeployments(a, false)
 	assert.NoError(t, err)
 	for _, v := range deploymentNames {
 		refuteDeploymentHasProxyVars(t, r.Client, v)
@@ -638,7 +639,7 @@ func TestReconcileArgoCD_reconcileDeployments_proxy_update_existing(t *testing.T
 
 	logf.SetLogger(ZapLogger(true))
 
-	err = r.reconcileDeployments(a)
+	err = r.reconcileDeployments(a, false)
 	assert.NoError(t, err)
 
 	for _, v := range deploymentNames {
@@ -659,7 +660,7 @@ func TestReconcileArgoCD_reconcileDeployments_HA_proxy(t *testing.T) {
 	})
 	r := makeTestReconciler(t, a)
 
-	err := r.reconcileDeployments(a)
+	err := r.reconcileDeployments(a, false)
 	assert.NoError(t, err)
 
 	assertDeploymentHasProxyVars(t, r.Client, "argocd-redis-ha-haproxy")
@@ -731,7 +732,7 @@ func TestReconcileArgoCD_reconcileRepoDeployment_updatesVolumeMounts(t *testing.
 	}
 	r := makeTestReconciler(t, a, d)
 
-	err := r.reconcileRepoDeployment(a)
+	err := r.reconcileRepoDeployment(a, false)
 	assert.NoError(t, err)
 
 	deployment := &appsv1.Deployment{}
@@ -741,8 +742,8 @@ func TestReconcileArgoCD_reconcileRepoDeployment_updatesVolumeMounts(t *testing.
 	}, deployment)
 	assert.NoError(t, err)
 
-	assert.Len(t, deployment.Spec.Template.Spec.Volumes, 8)
-	assert.Len(t, deployment.Spec.Template.Spec.Containers[0].VolumeMounts, 7)
+	assert.Len(t, deployment.Spec.Template.Spec.Volumes, 9)
+	assert.Len(t, deployment.Spec.Template.Spec.Containers[0].VolumeMounts, 8)
 }
 
 func Test_proxyEnvVars(t *testing.T) {
@@ -790,7 +791,7 @@ func TestReconcileArgoCD_reconcileDeployment_nodePlacement(t *testing.T) {
 		}
 	}))
 	r := makeTestReconciler(t, a)
-	err := r.reconcileRepoDeployment(a) //can use other deployments as well
+	err := r.reconcileRepoDeployment(a, false) //can use other deployments as well
 	assert.NoError(t, err)
 	deployment := &appsv1.Deployment{}
 	err = r.Client.Get(context.TODO(), types.NamespacedName{
@@ -1008,11 +1009,69 @@ func TestReconcileArgoCD_reconcileDexDeployment_withUpdate(t *testing.T) {
 	assert.Equal(t, want, deployment.Spec.Template.Spec)
 }
 
+func TestReconcileArgocd_reconcileRepoServerRedisTLS(t *testing.T) {
+	t.Run("with DisableTLSVerification = false (the default)", func(t *testing.T) {
+		logf.SetLogger(ZapLogger(true))
+		a := makeTestArgoCD()
+		r := makeTestReconciler(t, a)
+		assert.NoError(t, r.reconcileRepoDeployment(a, true))
+
+		deployment := &appsv1.Deployment{}
+		assert.NoError(t, r.Client.Get(
+			context.TODO(),
+			types.NamespacedName{
+				Name:      "argocd-repo-server",
+				Namespace: a.Namespace,
+			},
+			deployment))
+
+		wantCmd := []string{
+			"uid_entrypoint.sh",
+			"argocd-repo-server",
+			"--redis", "argocd-redis.argocd.svc.cluster.local:6379",
+			"--redis-use-tls",
+			"--redis-ca-certificate", "/app/config/reposerver/tls/redis/tls.crt",
+			"--loglevel", "info",
+			"--logformat", "text",
+		}
+		assert.Equal(t, wantCmd, deployment.Spec.Template.Spec.Containers[0].Command)
+	})
+
+	t.Run("with DisableTLSVerification = true", func(t *testing.T) {
+		logf.SetLogger(ZapLogger(true))
+		a := makeTestArgoCD(func(cd *argoprojv1alpha1.ArgoCD) {
+			cd.Spec.Redis.DisableTLSVerification = true
+		})
+		r := makeTestReconciler(t, a)
+		assert.NoError(t, r.reconcileRepoDeployment(a, true))
+
+		deployment := &appsv1.Deployment{}
+		assert.NoError(t, r.Client.Get(
+			context.TODO(),
+			types.NamespacedName{
+				Name:      "argocd-repo-server",
+				Namespace: a.Namespace,
+			},
+			deployment))
+
+		wantCmd := []string{
+			"uid_entrypoint.sh",
+			"argocd-repo-server",
+			"--redis", "argocd-redis.argocd.svc.cluster.local:6379",
+			"--redis-use-tls",
+			"--redis-insecure-skip-tls-verify",
+			"--loglevel", "info",
+			"--logformat", "text",
+		}
+		assert.Equal(t, wantCmd, deployment.Spec.Template.Spec.Containers[0].Command)
+	})
+}
+
 func TestReconcileArgoCD_reconcileServerDeployment(t *testing.T) {
 	logf.SetLogger(ZapLogger(true))
 	a := makeTestArgoCD()
 	r := makeTestReconciler(t, a)
-	assert.NoError(t, r.reconcileServerDeployment(a))
+	assert.NoError(t, r.reconcileServerDeployment(a, false))
 
 	deployment := &appsv1.Deployment{}
 	assert.NoError(t, r.Client.Get(
@@ -1075,6 +1134,35 @@ func TestReconcileArgoCD_reconcileServerDeployment(t *testing.T) {
 	}
 
 	assert.Equal(t, want, deployment.Spec.Template.Spec)
+
+	assert.NoError(t, r.reconcileServerDeployment(a, true))
+	deployment = &appsv1.Deployment{}
+	assert.NoError(t, r.Client.Get(
+		context.TODO(),
+		types.NamespacedName{
+			Name:      "argocd-server",
+			Namespace: a.Namespace,
+		},
+		deployment))
+	wantCmd := []string{
+		"argocd-server",
+		"--staticassets",
+		"/shared/app",
+		"--dex-server",
+		"http://argocd-dex-server.argocd.svc.cluster.local:5556",
+		"--repo-server",
+		"argocd-repo-server.argocd.svc.cluster.local:8081",
+		"--redis",
+		"argocd-redis.argocd.svc.cluster.local:6379",
+		"--redis-use-tls",
+		"--redis-ca-certificate",
+		"/app/config/server/tls/redis/tls.crt",
+		"--loglevel",
+		"info",
+		"--logformat",
+		"text",
+	}
+	assert.Equal(t, wantCmd, deployment.Spec.Template.Spec.Containers[0].Command)
 }
 
 func TestArgoCDServerDeploymentCommand(t *testing.T) {
@@ -1104,7 +1192,7 @@ func TestArgoCDServerDeploymentCommand(t *testing.T) {
 	}
 
 	deployment := &appsv1.Deployment{}
-	assert.NoError(t, r.reconcileServerDeployment(a))
+	assert.NoError(t, r.reconcileServerDeployment(a, false))
 
 	assert.NoError(t, r.Client.Get(
 		context.TODO(),
@@ -1126,7 +1214,7 @@ func TestArgoCDServerDeploymentCommand(t *testing.T) {
 		"test",
 	}
 
-	assert.NoError(t, r.reconcileServerDeployment(a))
+	assert.NoError(t, r.reconcileServerDeployment(a, false))
 	assert.NoError(t, r.Client.Get(
 		context.TODO(),
 		types.NamespacedName{
@@ -1144,7 +1232,7 @@ func TestArgoCDServerDeploymentCommand(t *testing.T) {
 		"foo.scv.cluster.local:6379",
 	}
 
-	assert.NoError(t, r.reconcileServerDeployment(a))
+	assert.NoError(t, r.reconcileServerDeployment(a, false))
 	assert.NoError(t, r.Client.Get(
 		context.TODO(),
 		types.NamespacedName{
@@ -1158,7 +1246,7 @@ func TestArgoCDServerDeploymentCommand(t *testing.T) {
 	// Remove all the command arguments that were added.
 	a.Spec.Server.ExtraCommandArgs = []string{}
 
-	assert.NoError(t, r.reconcileServerDeployment(a))
+	assert.NoError(t, r.reconcileServerDeployment(a, false))
 	assert.NoError(t, r.Client.Get(
 		context.TODO(),
 		types.NamespacedName{
@@ -1187,7 +1275,7 @@ func TestReconcileArgoCD_reconcileServerDeploymentWithInsecure(t *testing.T) {
 	})
 	r := makeTestReconciler(t, a)
 
-	assert.NoError(t, r.reconcileServerDeployment(a))
+	assert.NoError(t, r.reconcileServerDeployment(a, false))
 
 	deployment := &appsv1.Deployment{}
 	assert.NoError(t, r.Client.Get(
@@ -1258,12 +1346,12 @@ func TestReconcileArgoCD_reconcileServerDeploymentChangedToInsecure(t *testing.T
 	a := makeTestArgoCD()
 	r := makeTestReconciler(t, a)
 
-	assert.NoError(t, r.reconcileServerDeployment(a))
+	assert.NoError(t, r.reconcileServerDeployment(a, false))
 
 	a = makeTestArgoCD(func(a *argoprojv1alpha1.ArgoCD) {
 		a.Spec.Server.Insecure = true
 	})
-	assert.NoError(t, r.reconcileServerDeployment(a))
+	assert.NoError(t, r.reconcileServerDeployment(a, false))
 
 	deployment := &appsv1.Deployment{}
 	assert.NoError(t, r.Client.Get(
@@ -1329,6 +1417,48 @@ func TestReconcileArgoCD_reconcileServerDeploymentChangedToInsecure(t *testing.T
 	assert.Equal(t, want, deployment.Spec.Template.Spec)
 }
 
+func TestReconcileArgoCD_reconcileRedisDeploymentWithoutTLS(t *testing.T) {
+	cr := makeTestArgoCD()
+	r := makeTestReconciler(t, cr)
+
+	want := []string{
+		"--save",
+		"",
+		"--appendonly", "no",
+	}
+
+	assert.NoError(t, r.reconcileRedisDeployment(cr, false))
+	d := &appsv1.Deployment{}
+	assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: cr.Name + "-redis", Namespace: cr.Namespace}, d))
+	got := d.Spec.Template.Spec.Containers[0].Args
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("Reconciliation unsucessful: got: %v, want: %v", got, want)
+	}
+}
+
+func TestReconcileArgoCD_reconcileRedisDeploymentWithTLS(t *testing.T) {
+	cr := makeTestArgoCD()
+	r := makeTestReconciler(t, cr)
+
+	want := []string{
+		"--save", "",
+		"--appendonly", "no",
+		"--tls-port", "6379",
+		"--port", "0",
+		"--tls-cert-file", "/app/config/redis/tls/tls.crt",
+		"--tls-key-file", "/app/config/redis/tls/tls.key",
+		"--tls-auth-clients", "no",
+	}
+
+	assert.NoError(t, r.reconcileRedisDeployment(cr, true))
+	d := &appsv1.Deployment{}
+	assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: cr.Name + "-redis", Namespace: cr.Namespace}, d))
+	got := d.Spec.Template.Spec.Containers[0].Args
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("Reconciliation unsucessful: got: %v, want: %v", got, want)
+	}
+}
+
 func TestReconcileArgoCD_reconcileRedisDeployment(t *testing.T) {
 	// tests reconciler hook for redis deployment
 	cr := makeTestArgoCD()
@@ -1337,7 +1467,7 @@ func TestReconcileArgoCD_reconcileRedisDeployment(t *testing.T) {
 	defer resetHooks()()
 	Register(testDeploymentHook)
 
-	assert.NoError(t, r.reconcileRedisDeployment(cr))
+	assert.NoError(t, r.reconcileRedisDeployment(cr, false))
 	d := &appsv1.Deployment{}
 	assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Name: cr.Name + "-redis", Namespace: cr.Namespace}, d))
 	assert.Equal(t, int32(3), *d.Spec.Replicas)
@@ -1351,7 +1481,7 @@ func TestReconcileArgoCD_reconcileRedisDeployment_with_error(t *testing.T) {
 	defer resetHooks()()
 	Register(testErrorHook)
 
-	assert.Error(t, r.reconcileRedisDeployment(cr), "this is a test error")
+	assert.Error(t, r.reconcileRedisDeployment(cr, false), "this is a test error")
 }
 
 func restoreEnv(t *testing.T) {
@@ -1563,6 +1693,15 @@ func repoServerDefaultVolumes() []corev1.Volume {
 				},
 			},
 		},
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
 		{
 			Name: "var-files",
 			VolumeSource: corev1.VolumeSource{
@@ -1588,6 +1727,7 @@ func repoServerDefaultVolumeMounts() []corev1.VolumeMount {
 		{Name: "gpg-keyring", MountPath: "/app/config/gpg/keys"},
 		{Name: "tmp", MountPath: "/tmp"},
 		{Name: "argocd-repo-server-tls", MountPath: "/app/config/reposerver/tls"},
+		{Name: common.ArgoCDRedisServerTLSSecretName, MountPath: "/app/config/reposerver/tls/redis"},
 		{Name: "plugins", MountPath: "/home/argocd/cmp-server/plugins"},
 	}
 	return mounts
@@ -1604,7 +1744,8 @@ func serverDefaultVolumes() []corev1.Volume {
 					},
 				},
 			},
-		}, {
+		},
+		{
 			Name: "tls-certs",
 			VolumeSource: corev1.VolumeSource{
 				ConfigMap: &corev1.ConfigMapVolumeSource{
@@ -1613,7 +1754,8 @@ func serverDefaultVolumes() []corev1.Volume {
 					},
 				},
 			},
-		}, {
+		},
+		{
 			Name: "argocd-repo-server-tls",
 			VolumeSource: corev1.VolumeSource{
 				Secret: &corev1.SecretVolumeSource{
@@ -1622,6 +1764,15 @@ func serverDefaultVolumes() []corev1.Volume {
 				},
 			},
 		},
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
 	}
 	return volumes
 }
@@ -1637,6 +1788,9 @@ func serverDefaultVolumeMounts() []corev1.VolumeMount {
 		}, {
 			Name:      "argocd-repo-server-tls",
 			MountPath: "/app/config/server/tls",
+		}, {
+			Name:      common.ArgoCDRedisServerTLSSecretName,
+			MountPath: "/app/config/server/tls/redis",
 		},
 	}
 	return mounts
diff --git a/controllers/argocd/notifications_test.go b/controllers/argocd/notifications_test.go
new file mode 100644
index 00000000..73b5f90b
--- /dev/null
+++ b/controllers/argocd/notifications_test.go
@@ -0,0 +1,223 @@
+package argocd
+
+import (
+	"context"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/stretchr/testify/assert"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+	argoprojv1alpha1 "github.com/argoproj-labs/argocd-operator/api/v1alpha1"
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argoutil"
+)
+
+func TestReconcileNotifications_CreateRoles(t *testing.T) {
+	logf.SetLogger(ZapLogger(true))
+	a := makeTestArgoCD(func(a *argoprojv1alpha1.ArgoCD) {
+		a.Spec.Notifications.Enabled = true
+	})
+
+	r := makeTestReconciler(t, a)
+
+	_, err := r.reconcileNotificationsRole(a)
+	assert.NoError(t, err)
+
+	testRole := &rbacv1.Role{}
+	assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      generateResourceName(common.ArgoCDNotificationsControllerComponent, a),
+		Namespace: a.Namespace,
+	}, testRole))
+
+	desiredPolicyRules := policyRuleForNotificationsController()
+
+	assert.Equal(t, desiredPolicyRules, testRole.Rules)
+
+	a.Spec.Notifications.Enabled = false
+	_, err = r.reconcileNotificationsRole(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      generateResourceName(common.ArgoCDNotificationsControllerComponent, a),
+		Namespace: a.Namespace,
+	}, testRole)
+	assert.True(t, errors.IsNotFound(err))
+}
+
+func TestReconcileNotifications_CreateServiceAccount(t *testing.T) {
+	logf.SetLogger(ZapLogger(true))
+	a := makeTestArgoCD(func(a *argoprojv1alpha1.ArgoCD) {
+		a.Spec.Notifications.Enabled = true
+	})
+
+	r := makeTestReconciler(t, a)
+
+	desiredSa, err := r.reconcileNotificationsServiceAccount(a)
+	assert.NoError(t, err)
+
+	testSa := &corev1.ServiceAccount{}
+	assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      generateResourceName(common.ArgoCDNotificationsControllerComponent, a),
+		Namespace: a.Namespace,
+	}, testSa))
+
+	assert.Equal(t, testSa.Name, desiredSa.Name)
+
+	a.Spec.Notifications.Enabled = false
+	_, err = r.reconcileNotificationsServiceAccount(a)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      generateResourceName(common.ArgoCDNotificationsControllerComponent, a),
+		Namespace: a.Namespace,
+	}, testSa)
+	assert.True(t, errors.IsNotFound(err))
+
+}
+
+func TestReconcileNotifications_CreateRoleBinding(t *testing.T) {
+	logf.SetLogger(ZapLogger(true))
+	a := makeTestArgoCD(func(a *argoprojv1alpha1.ArgoCD) {
+		a.Spec.Notifications.Enabled = true
+	})
+	r := makeTestReconciler(t, a)
+
+	role := &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{Name: "role-name"}}
+	sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "sa-name"}}
+
+	err := r.reconcileNotificationsRoleBinding(a, role, sa)
+	assert.NoError(t, err)
+
+	roleBinding := &rbacv1.RoleBinding{}
+	assert.NoError(t, r.Client.Get(
+		context.TODO(),
+		types.NamespacedName{
+			Name:      generateResourceName(common.ArgoCDNotificationsControllerComponent, a),
+			Namespace: a.Namespace,
+		},
+		roleBinding))
+
+	assert.Equal(t, roleBinding.RoleRef.Name, role.Name)
+	assert.Equal(t, roleBinding.Subjects[0].Name, sa.Name)
+
+	a.Spec.Notifications.Enabled = false
+	err = r.reconcileNotificationsRoleBinding(a, role, sa)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      generateResourceName(common.ArgoCDNotificationsControllerComponent, a),
+		Namespace: a.Namespace,
+	}, roleBinding)
+	assert.True(t, errors.IsNotFound(err))
+}
+
+func TestReconcileNotifications_CreateDeployments(t *testing.T) {
+	logf.SetLogger(ZapLogger(true))
+	a := makeTestArgoCD(func(a *argoprojv1alpha1.ArgoCD) {
+		a.Spec.Notifications.Enabled = true
+	})
+
+	r := makeTestReconciler(t, a)
+
+	sa := corev1.ServiceAccount{}
+
+	assert.NoError(t, r.reconcileNotificationsDeployment(a, &sa))
+
+	deployment := &appsv1.Deployment{}
+	assert.NoError(t, r.Client.Get(
+		context.TODO(),
+		types.NamespacedName{
+			Name:      a.Name + "-notifications-controller",
+			Namespace: a.Namespace,
+		},
+		deployment))
+
+	// Ensure the created Deployment has the expected properties
+	assert.Equal(t, deployment.Spec.Template.Spec.ServiceAccountName, sa.ObjectMeta.Name)
+
+	want := []corev1.Container{{
+		Command:         []string{"argocd-notifications"},
+		Image:           argoutil.CombineImageTag(common.ArgoCDDefaultArgoImage, common.ArgoCDDefaultArgoVersion),
+		ImagePullPolicy: corev1.PullAlways,
+		Name:            "argocd-notifications-controller",
+		VolumeMounts: []corev1.VolumeMount{
+			{
+				Name:      "tls-certs",
+				MountPath: "/app/config/tls",
+			},
+			{
+				Name:      "argocd-repo-server-tls",
+				MountPath: "/app/config/reposerver/tls",
+			},
+		},
+		Resources:  corev1.ResourceRequirements{},
+		WorkingDir: "/app",
+		LivenessProbe: &corev1.Probe{
+			ProbeHandler: corev1.ProbeHandler{
+				TCPSocket: &corev1.TCPSocketAction{
+					Port: intstr.IntOrString{
+						IntVal: int32(9001),
+					},
+				},
+			},
+		},
+	}}
+
+	if diff := cmp.Diff(want, deployment.Spec.Template.Spec.Containers); diff != "" {
+		t.Fatalf("failed to reconcile notifications-controller deployment containers:\n%s", diff)
+	}
+
+	volumes := []corev1.Volume{
+		{
+			Name: "tls-certs",
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: "argocd-tls-certs-cm",
+					},
+				},
+			},
+		},
+		{
+			Name: "argocd-repo-server-tls",
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: "argocd-repo-server-tls",
+					Optional:   boolPtr(true),
+				},
+			},
+		},
+	}
+
+	if diff := cmp.Diff(volumes, deployment.Spec.Template.Spec.Volumes); diff != "" {
+		t.Fatalf("failed to reconcile notifications-controller deployment volumes:\n%s", diff)
+	}
+
+	expectedSelector := &metav1.LabelSelector{
+		MatchLabels: map[string]string{
+			common.ArgoCDKeyName: deployment.Name,
+		},
+	}
+
+	if diff := cmp.Diff(expectedSelector, deployment.Spec.Selector); diff != "" {
+		t.Fatalf("failed to reconcile notifications-controller label selector:\n%s", diff)
+	}
+
+	a.Spec.Notifications.Enabled = false
+	err := r.reconcileNotificationsDeployment(a, &sa)
+	assert.NoError(t, err)
+
+	err = r.Client.Get(context.TODO(), types.NamespacedName{
+		Name:      generateResourceName(common.ArgoCDNotificationsControllerComponent, a),
+		Namespace: a.Namespace,
+	}, deployment)
+	assert.True(t, errors.IsNotFound(err))
+}
diff --git a/controllers/argocd/rolebinding_test.go b/controllers/argocd/rolebinding_test.go
index a1343b04..45435567 100644
--- a/controllers/argocd/rolebinding_test.go
+++ b/controllers/argocd/rolebinding_test.go
@@ -6,12 +6,13 @@ import (
 	"os"
 	"testing"
 
-	"github.com/argoproj-labs/argocd-operator/common"
 	"github.com/stretchr/testify/assert"
 	rbacv1 "k8s.io/api/rbac/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/argoproj-labs/argocd-operator/common"
 )
 
 func TestReconcileArgoCD_reconcileRoleBinding(t *testing.T) {
diff --git a/controllers/argocd/secret_test.go b/controllers/argocd/secret_test.go
index 7ccb2a9c..991c459b 100644
--- a/controllers/argocd/secret_test.go
+++ b/controllers/argocd/secret_test.go
@@ -132,7 +132,7 @@ func Test_ReconcileArgoCD_ReconcileRepoTLSSecret(t *testing.T) {
 			t.Errorf("Expected rollout of argocd-server, but it didn't happen: %v", serverDepl.Spec.Template.ObjectMeta.Labels)
 		}
 		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-repo-server", Namespace: "argocd-operator"}, repoDepl)
-		repoRollout, ok := serverDepl.Spec.Template.ObjectMeta.Labels["repo.tls.cert.changed"]
+		repoRollout, ok := repoDepl.Spec.Template.ObjectMeta.Labels["repo.tls.cert.changed"]
 		if !ok {
 			t.Errorf("Expected rollout of argocd-repo-server, but it didn't happen: %v", repoDepl.Spec.Template.ObjectMeta.Labels)
 		}
@@ -158,14 +158,14 @@ func Test_ReconcileArgoCD_ReconcileRepoTLSSecret(t *testing.T) {
 			t.Errorf("Did not expect rollout of argocd-server, but it did happen: %v", serverDepl.Spec.Template.ObjectMeta.Labels)
 		}
 		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-repo-server", Namespace: "argocd-operator"}, repoDepl)
-		repoRolloutNew, ok := serverDepl.Spec.Template.ObjectMeta.Labels["repo.tls.cert.changed"]
+		repoRolloutNew, ok := repoDepl.Spec.Template.ObjectMeta.Labels["repo.tls.cert.changed"]
 		if !ok || repoRollout != repoRolloutNew {
-			t.Errorf("Did not expect rollout of argocd-repo-server, but it did happen: %v", serverDepl.Spec.Template.ObjectMeta.Labels)
+			t.Errorf("Did not expect rollout of argocd-repo-server, but it did happen: %v", repoDepl.Spec.Template.ObjectMeta.Labels)
 		}
 		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-application-controller", Namespace: "argocd-operator"}, ctrlSts)
 		ctrlRolloutNew, ok := ctrlSts.Spec.Template.ObjectMeta.Labels["repo.tls.cert.changed"]
 		if !ok || ctrlRollout != ctrlRolloutNew {
-			t.Errorf("Did not expect rollout of argocd-application-server, but it did happen: %v", serverDepl.Spec.Template.ObjectMeta.Labels)
+			t.Errorf("Did not expect rollout of argocd-application-server, but it did happen: %v", ctrlSts.Spec.Template.ObjectMeta.Labels)
 		}
 
 		// Update certificate in the secret must trigger new rollout
@@ -187,14 +187,14 @@ func Test_ReconcileArgoCD_ReconcileRepoTLSSecret(t *testing.T) {
 			t.Errorf("Error in SHA256 sum of secret, want=%s got=%s", shasum, argocd.Status.RepoTLSChecksum)
 		}
 
-		// This time, label should not have changed
+		// This time, label should have changed
 		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-server", Namespace: "argocd-operator"}, serverDepl)
 		deplRolloutNew, ok = serverDepl.Spec.Template.ObjectMeta.Labels["repo.tls.cert.changed"]
 		if !ok || deplRollout == deplRolloutNew {
 			t.Errorf("Expected rollout of argocd-server, but it didn't happen: %v", serverDepl.Spec.Template.ObjectMeta.Labels)
 		}
 		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-repo-server", Namespace: "argocd-operator"}, repoDepl)
-		repoRolloutNew, ok = serverDepl.Spec.Template.ObjectMeta.Labels["repo.tls.cert.changed"]
+		repoRolloutNew, ok = repoDepl.Spec.Template.ObjectMeta.Labels["repo.tls.cert.changed"]
 		if !ok || repoRollout == repoRolloutNew {
 			t.Errorf("Expected rollout of argocd-repo-server, but it didn't happen: %v", repoDepl.Spec.Template.ObjectMeta.Labels)
 		}
@@ -208,6 +208,177 @@ func Test_ReconcileArgoCD_ReconcileRepoTLSSecret(t *testing.T) {
 
 }
 
+func Test_ReconcileArgoCD_ReconcileRedisTLSSecret(t *testing.T) {
+	argocd := &v1alpha1.ArgoCD{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "argocd",
+			Namespace: "argocd-operator",
+			UID:       "abcd",
+		},
+	}
+	crt := []byte("foo")
+	key := []byte("bar")
+	t.Run("Reconcile TLS secret", func(t *testing.T) {
+		service := &v1.Service{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-redis",
+				Namespace: "argocd-operator",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: "argoproj.io/v1alpha1",
+						Kind:       "ArgoCD",
+						Name:       "argocd",
+						UID:        argocd.GetUID(),
+					},
+				},
+				UID: "service-123",
+			},
+		}
+		secret := &corev1.Secret{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "argocd-operator-redis-tls",
+				Namespace: "argocd-operator",
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion: "v1",
+						Kind:       "Service",
+						Name:       "argocd-redis",
+						UID:        service.GetUID(),
+					},
+				},
+			},
+			Type: corev1.SecretTypeTLS,
+			Data: map[string][]byte{
+				corev1.TLSCertKey:       crt,
+				corev1.TLSPrivateKeyKey: key,
+			},
+		}
+		var sumOver []byte
+		sumOver = append(sumOver, crt...)
+		sumOver = append(sumOver, key...)
+		shasum := fmt.Sprintf("%x", sha256.Sum256(sumOver))
+		serverDepl := newDeploymentWithSuffix("server", "server", argocd)
+		repoDepl := newDeploymentWithSuffix("repo-server", "repo-server", argocd)
+		redisDepl := newDeploymentWithSuffix("redis", "redis", argocd)
+		ctrlSts := newStatefulSetWithSuffix("application-controller", "application-controller", argocd)
+		objs := []runtime.Object{
+			argocd,
+			secret,
+			service,
+			serverDepl,
+			repoDepl,
+			redisDepl,
+			ctrlSts,
+		}
+
+		r := makeReconciler(t, argocd, objs...)
+
+		err := r.reconcileRedisTLSSecret(argocd, true)
+		if err != nil {
+			t.Errorf("Error should be nil, but is %v", err)
+		}
+		if shasum != argocd.Status.RedisTLSChecksum {
+			t.Errorf("Error in SHA256 sum of secret, want=%s got=%s", shasum, argocd.Status.RedisTLSChecksum)
+		}
+
+		certChangedLabel := "redis.tls.cert.changed"
+
+		// Workloads should have been requested to re-rollout on a change
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-server", Namespace: "argocd-operator"}, serverDepl)
+		deplRollout, ok := serverDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok {
+			t.Errorf("Expected rollout of argocd-server, but it didn't happen: %v", serverDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-repo-server", Namespace: "argocd-operator"}, repoDepl)
+		repoRollout, ok := repoDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok {
+			t.Errorf("Expected rollout of argocd-repo-server, but it didn't happen: %v", repoDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-redis", Namespace: "argocd-operator"}, redisDepl)
+		redisRollout, ok := redisDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok {
+			t.Errorf("Expected rollout of argocd-redis, but it didn't happen: %v", redisDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-application-controller", Namespace: "argocd-operator"}, ctrlSts)
+		ctrlRollout, ok := ctrlSts.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok {
+			t.Errorf("Expected rollout of argocd-application-server, but it didn't happen: %v", ctrlSts.Spec.Template.ObjectMeta.Labels)
+		}
+
+		// Second run - no change
+		err = r.reconcileRedisTLSSecret(argocd, true)
+		if err != nil {
+			t.Errorf("Error should be nil, but is %v", err)
+		}
+		if shasum != argocd.Status.RedisTLSChecksum {
+			t.Errorf("Error in SHA256 sum of secret, want=%s got=%s", shasum, argocd.Status.RepoTLSChecksum)
+		}
+
+		// This time, label should not have changed
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-server", Namespace: "argocd-operator"}, serverDepl)
+		deplRolloutNew, ok := serverDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok || deplRollout != deplRolloutNew {
+			t.Errorf("Did not expect rollout of argocd-server, but it did happen: %v", serverDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-repo-server", Namespace: "argocd-operator"}, repoDepl)
+		repoRolloutNew, ok := repoDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok || repoRollout != repoRolloutNew {
+			t.Errorf("Did not expect rollout of argocd-repo-server, but it did happen: %v", repoDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-redis", Namespace: "argocd-operator"}, redisDepl)
+		redisRolloutNew, ok := redisDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok || redisRollout != redisRolloutNew {
+			t.Errorf("Did not expect rollout of argocd-redis, but it did happen: %v", redisDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-application-controller", Namespace: "argocd-operator"}, ctrlSts)
+		ctrlRolloutNew, ok := ctrlSts.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok || ctrlRollout != ctrlRolloutNew {
+			t.Errorf("Did not expect rollout of argocd-application-server, but it did happen: %v", ctrlSts.Spec.Template.ObjectMeta.Labels)
+		}
+
+		// Update certificate in the secret must trigger new rollout
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-repo-server-tls", Namespace: "argocd-operator"}, secret)
+		secret.Data["tls.crt"] = []byte("bar")
+		r.Client.Update(context.TODO(), secret)
+
+		sumOver = []byte{}
+		sumOver = append(sumOver, []byte("bar")...)
+		sumOver = append(sumOver, key...)
+		shasum = fmt.Sprintf("%x", sha256.Sum256(sumOver))
+
+		// Second run - no change
+		err = r.reconcileRedisTLSSecret(argocd, true)
+		if err != nil {
+			t.Errorf("Error should be nil, but is %v", err)
+		}
+		if shasum != argocd.Status.RedisTLSChecksum {
+			t.Errorf("Error in SHA256 sum of secret, want=%s got=%s", shasum, argocd.Status.RedisTLSChecksum)
+		}
+
+		// This time, label should have changed
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-server", Namespace: "argocd-operator"}, serverDepl)
+		deplRolloutNew, ok = serverDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok || deplRollout == deplRolloutNew {
+			t.Errorf("Expected rollout of argocd-server, but it didn't happen: %v", serverDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-repo-server", Namespace: "argocd-operator"}, repoDepl)
+		repoRolloutNew, ok = repoDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok || repoRollout == repoRolloutNew {
+			t.Errorf("Expected rollout of argocd-repo-server, but it didn't happen: %v", repoDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-redis", Namespace: "argocd-operator"}, redisDepl)
+		redisRolloutNew, ok = repoDepl.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok || redisRollout == redisRolloutNew {
+			t.Errorf("Expected rollout of argocd-redis, but it didn't happen: %v", redisDepl.Spec.Template.ObjectMeta.Labels)
+		}
+		r.Client.Get(context.TODO(), types.NamespacedName{Name: "argocd-application-controller", Namespace: "argocd-operator"}, ctrlSts)
+		ctrlRolloutNew, ok = ctrlSts.Spec.Template.ObjectMeta.Labels[certChangedLabel]
+		if !ok || ctrlRollout == ctrlRolloutNew {
+			t.Errorf("Expected rollout of argocd-application-controller, but it didn't happen: %v", ctrlSts.Spec.Template.ObjectMeta.Labels)
+		}
+	})
+}
+
 func Test_ReconcileArgoCD_ClusterPermissionsSecret(t *testing.T) {
 	logf.SetLogger(ZapLogger(true))
 	a := makeTestArgoCD()
diff --git a/controllers/argocd/service_account_test.go b/controllers/argocd/service_account_test.go
index 0fe47ba4..c7a700ab 100644
--- a/controllers/argocd/service_account_test.go
+++ b/controllers/argocd/service_account_test.go
@@ -20,12 +20,13 @@ import (
 	"os"
 	"testing"
 
-	"github.com/argoproj-labs/argocd-operator/common"
 	"github.com/stretchr/testify/assert"
 	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/rbac/v1"
 	"k8s.io/apimachinery/pkg/types"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/argoproj-labs/argocd-operator/common"
 )
 
 func TestReconcileArgoCD_reconcileServiceAccountPermissions(t *testing.T) {
diff --git a/controllers/argocd/service_test.go b/controllers/argocd/service_test.go
index 45e90f28..7499c99b 100644
--- a/controllers/argocd/service_test.go
+++ b/controllers/argocd/service_test.go
@@ -5,10 +5,11 @@ import (
 	"os"
 	"testing"
 
-	"github.com/argoproj-labs/argocd-operator/common"
 	"github.com/stretchr/testify/assert"
 	"k8s.io/apimachinery/pkg/types"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+	"github.com/argoproj-labs/argocd-operator/common"
 )
 
 func TestReconcileArgoCD_reconcileDexService_Dex_Enabled(t *testing.T) {
diff --git a/controllers/argocd/sso_test.go b/controllers/argocd/sso_test.go
index 98934bb0..a6eff499 100644
--- a/controllers/argocd/sso_test.go
+++ b/controllers/argocd/sso_test.go
@@ -18,7 +18,6 @@ import (
 	"context"
 	"testing"
 
-	argov1alpha1 "github.com/argoproj-labs/argocd-operator/api/v1alpha1"
 	oappsv1 "github.com/openshift/api/apps/v1"
 	routev1 "github.com/openshift/api/route/v1"
 	templatev1 "github.com/openshift/api/template/v1"
@@ -32,6 +31,8 @@ import (
 	"k8s.io/client-go/kubernetes/scheme"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	logf "sigs.k8s.io/controller-runtime/pkg/log"
+
+	argov1alpha1 "github.com/argoproj-labs/argocd-operator/api/v1alpha1"
 )
 
 func makeFakeReconciler(t *testing.T, acd *argov1alpha1.ArgoCD, objs ...runtime.Object) *ReconcileArgoCD {
diff --git a/controllers/argocd/statefulset_test.go b/controllers/argocd/statefulset_test.go
index 846d917e..2394b32d 100644
--- a/controllers/argocd/statefulset_test.go
+++ b/controllers/argocd/statefulset_test.go
@@ -39,6 +39,15 @@ func controllerDefaultVolumes() []corev1.Volume {
 				},
 			},
 		},
+		{
+			Name: common.ArgoCDRedisServerTLSSecretName,
+			VolumeSource: corev1.VolumeSource{
+				Secret: &corev1.SecretVolumeSource{
+					SecretName: common.ArgoCDRedisServerTLSSecretName,
+					Optional:   boolPtr(true),
+				},
+			},
+		},
 	}
 	return volumes
 }
@@ -49,6 +58,10 @@ func controllerDefaultVolumeMounts() []corev1.VolumeMount {
 			Name:      "argocd-repo-server-tls",
 			MountPath: "/app/config/controller/tls",
 		},
+		{
+			Name:      common.ArgoCDRedisServerTLSSecretName,
+			MountPath: "/app/config/controller/tls/redis",
+		},
 	}
 	return mounts
 }
@@ -95,7 +108,7 @@ func TestReconcileArgoCD_reconcileApplicationController(t *testing.T) {
 	a := makeTestArgoCD()
 	r := makeTestReconciler(t, a)
 
-	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a))
+	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false))
 
 	ss := &appsv1.StatefulSet{}
 	assert.NoError(t, r.Client.Get(
@@ -128,15 +141,47 @@ func TestReconcileArgoCD_reconcileApplicationController(t *testing.T) {
 	}
 }
 
+func TestReconcileArgoCD_reconcileApplicationController_withRedisTLS(t *testing.T) {
+	logf.SetLogger(ZapLogger(true))
+	a := makeTestArgoCD()
+	r := makeTestReconciler(t, a)
+
+	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, true))
+
+	ss := &appsv1.StatefulSet{}
+	assert.NoError(t, r.Client.Get(
+		context.TODO(),
+		types.NamespacedName{
+			Name:      "argocd-application-controller",
+			Namespace: a.Namespace,
+		},
+		ss))
+	command := ss.Spec.Template.Spec.Containers[0].Command
+	want := []string{
+		"argocd-application-controller",
+		"--operation-processors", "10",
+		"--redis", "argocd-redis.argocd.svc.cluster.local:6379",
+		"--redis-use-tls",
+		"--redis-ca-certificate", "/app/config/controller/tls/redis/tls.crt",
+		"--repo-server", "argocd-repo-server.argocd.svc.cluster.local:8081",
+		"--status-processors", "20",
+		"--kubectl-parallelism-limit", "10",
+		"--loglevel", "info",
+		"--logformat", "text"}
+	if diff := cmp.Diff(want, command); diff != "" {
+		t.Fatalf("reconciliation failed:\n%s", diff)
+	}
+}
+
 func TestReconcileArgoCD_reconcileApplicationController_withUpdate(t *testing.T) {
 	logf.SetLogger(ZapLogger(true))
 	a := makeTestArgoCD()
 	r := makeTestReconciler(t, a)
 
-	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a))
+	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false))
 
 	a = makeTestArgoCD(controllerProcessors(30))
-	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a))
+	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false))
 
 	ss := &appsv1.StatefulSet{}
 	assert.NoError(t, r.Client.Get(
@@ -169,7 +214,7 @@ func TestReconcileArgoCD_reconcileApplicationController_withUpgrade(t *testing.T
 	deploy := newDeploymentWithSuffix("application-controller", "application-controller", a)
 	assert.NoError(t, r.Client.Create(context.TODO(), deploy))
 
-	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a))
+	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false))
 	err := r.Client.Get(context.TODO(), types.NamespacedName{Name: deploy.Name, Namespace: deploy.Namespace}, deploy)
 	assert.Errorf(t, err, "not found")
 }
@@ -192,7 +237,7 @@ func TestReconcileArgoCD_reconcileApplicationController_withResources(t *testing
 	}
 	r := makeTestReconciler(t, a, &ex)
 
-	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a))
+	assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false))
 
 	ss := &appsv1.StatefulSet{}
 	assert.NoError(t, r.Client.Get(
@@ -280,7 +325,7 @@ func TestReconcileArgoCD_reconcileApplicationController_withSharding(t *testing.
 		})
 		r := makeTestReconciler(t, a)
 
-		assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a))
+		assert.NoError(t, r.reconcileApplicationControllerStatefulSet(a, false))
 
 		ss := &appsv1.StatefulSet{}
 		assert.NoError(t, r.Client.Get(
diff --git a/controllers/argocd/status_test.go b/controllers/argocd/status_test.go
index 07842af2..fb5c15db 100644
--- a/controllers/argocd/status_test.go
+++ b/controllers/argocd/status_test.go
@@ -162,3 +162,23 @@ func TestReconcileArgoCD_reconcileStatusHost(t *testing.T) {
 		})
 	}
 }
+
+func TestReconcileArgoCD_reconcileStatusNotificationsController(t *testing.T) {
+
+	logf.SetLogger(ZapLogger(true))
+	a := makeTestArgoCD()
+	r := makeTestReconciler(t, a)
+
+	assert.NoError(t, r.reconcileStatusNotifications(a))
+	assert.Equal(t, "", a.Status.NotificationsController)
+
+	a.Spec.Notifications.Enabled = true
+	assert.NoError(t, r.reconcileNotificationsController(a))
+	assert.NoError(t, r.reconcileStatusNotifications(a))
+	assert.Equal(t, "Pending", a.Status.NotificationsController)
+
+	a.Spec.Notifications.Enabled = false
+	assert.NoError(t, r.deleteNotificationsResources(a))
+	assert.NoError(t, r.reconcileStatusNotifications(a))
+	assert.Equal(t, "", a.Status.NotificationsController)
+}
diff --git a/controllers/argocd/util_test.go b/controllers/argocd/util_test.go
index adf95370..0acfe5b7 100644
--- a/controllers/argocd/util_test.go
+++ b/controllers/argocd/util_test.go
@@ -427,7 +427,7 @@ func TestGetArgoApplicationControllerCommand(t *testing.T) {
 
 	for _, tt := range cmdTests {
 		cr := makeTestArgoCD(tt.opts...)
-		cmd := getArgoApplicationControllerCommand(cr)
+		cmd := getArgoApplicationControllerCommand(cr, false)
 
 		if !reflect.DeepEqual(cmd, tt.want) {
 			t.Fatalf("got %#v, want %#v", cmd, tt.want)
@@ -627,6 +627,34 @@ func TestSetManagedNamespaces(t *testing.T) {
 	}
 }
 
+func TestGetRedisConf(t *testing.T) {
+	s1 := getRedisConf(true)
+	//assert.Equal(t, "", s1)
+
+	s1 = getRedisConf(false)
+	//assert.Equal(t, "", s1)
+
+	s1 = getRedisSentinelConf(true)
+	//assert.Equal(t, "", s1)
+
+	s1 = getRedisSentinelConf(false)
+	//assert.Equal(t, "", s1)
+
+	a := makeTestArgoCD()
+
+	s1 = getRedisInitScript(a, true)
+	//assert.Equal(t, "", s1)
+
+	s1 = getRedisLivenessScript(false)
+	//assert.Equal(t, "", s1)
+
+	s1 = getRedisReadinessScript(true)
+	//assert.Equal(t, "", s1)
+
+	s1 = getSentinelLivenessScript(true)
+	assert.Equal(t, "", s1)
+}
+
 func TestGenerateRandomString(t *testing.T) {
 
 	// verify the creation of unique strings
diff --git a/tests/k8s/1-007_validate_volume_mounts/01-assert.yaml b/tests/k8s/1-007_validate_volume_mounts/01-assert.yaml
index 5dec0f03..0f1b3047 100644
--- a/tests/k8s/1-007_validate_volume_mounts/01-assert.yaml
+++ b/tests/k8s/1-007_validate_volume_mounts/01-assert.yaml
@@ -24,6 +24,8 @@ spec:
           name: tls-certs
         - mountPath: /app/config/server/tls
           name: argocd-repo-server-tls
+        - mountPath: /app/config/server/tls/redis
+          name: argocd-operator-redis-tls
       volumes:
       - configMap:
           defaultMode: 420
@@ -38,6 +40,11 @@ spec:
           defaultMode: 420
           optional: true
           secretName: argocd-repo-server-tls
+      - name: argocd-operator-redis-tls
+        secret:
+          defaultMode: 420
+          optional: true
+          secretName: argocd-operator-redis-tls
 ---
 apiVersion: apps/v1
 kind: Deployment
@@ -60,6 +67,8 @@ spec:
           name: tmp
         - mountPath: /app/config/reposerver/tls
           name: argocd-repo-server-tls
+        - mountPath: /app/config/reposerver/tls/redis
+          name: argocd-operator-redis-tls
         - mountPath: /home/argocd/cmp-server/plugins
           name: plugins
       volumes:
@@ -84,6 +93,11 @@ spec:
           defaultMode: 420
           optional: true
           secretName: argocd-repo-server-tls
+      - name: argocd-operator-redis-tls
+        secret:
+          defaultMode: 420
+          optional: true
+          secretName: argocd-operator-redis-tls
       - emptyDir: {}
         name: var-files
       - emptyDir: {}
@@ -100,10 +114,16 @@ spec:
       - volumeMounts: 
         - mountPath: /app/config/controller/tls
           name: argocd-repo-server-tls
+        - mountPath: /app/config/controller/tls/redis
+          name: argocd-operator-redis-tls
       volumes:
       - name: argocd-repo-server-tls
         secret:
           defaultMode: 420
           optional: true
           secretName: argocd-repo-server-tls
-    
\ No newline at end of file
+      - name: argocd-operator-redis-tls
+        secret:
+          defaultMode: 420
+          optional: true
+          secretName: argocd-operator-redis-tls
diff --git a/tests/k8s/1-018_validate_extra_config/01-argocd-with-extraconfig.yaml b/tests/k8s/1-018_validate_extra_config/01-argocd-with-extraconfig.yaml
new file mode 100644
index 00000000..8c10115f
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/01-argocd-with-extraconfig.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  extraConfig:
+    "admin.enabled": "true" # enable admin user through extraConfig
\ No newline at end of file
diff --git a/tests/k8s/1-018_validate_extra_config/01-assert.yaml b/tests/k8s/1-018_validate_extra_config/01-assert.yaml
new file mode 100644
index 00000000..78e6be9b
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/01-assert.yaml
@@ -0,0 +1,17 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestAssert
+timeout: 120
+---
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: argocd-cm
+data:
+  admin.enabled: "true"
\ No newline at end of file
diff --git a/tests/k8s/1-018_validate_extra_config/02-argocd-with-firstclass-and-extraconfig copy.yaml b/tests/k8s/1-018_validate_extra_config/02-argocd-with-firstclass-and-extraconfig copy.yaml
new file mode 100644
index 00000000..bbac50ba
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/02-argocd-with-firstclass-and-extraconfig copy.yaml
@@ -0,0 +1,8 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  disableAdmin: true
+  extraConfig:
+    "admin.enabled": "true" # override admin user through extraConfig
\ No newline at end of file
diff --git a/tests/k8s/1-018_validate_extra_config/02-assert.yaml b/tests/k8s/1-018_validate_extra_config/02-assert.yaml
new file mode 100644
index 00000000..e5470d42
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/02-assert.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: argocd-cm
+data:
+  admin.enabled: "true"
\ No newline at end of file
diff --git a/tests/k8s/1-018_validate_extra_config/03-assert.yaml b/tests/k8s/1-018_validate_extra_config/03-assert.yaml
new file mode 100644
index 00000000..3f172ffd
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/03-assert.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: argocd-cm
+data:
+  admin.enabled: "true" # operator should reject any manual updates to the configmap.
\ No newline at end of file
diff --git a/tests/k8s/1-018_validate_extra_config/03-update-configmap.yaml b/tests/k8s/1-018_validate_extra_config/03-update-configmap.yaml
new file mode 100644
index 00000000..aff583e2
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/03-update-configmap.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: argocd-cm
+data:
+  admin.enabled: "false"
\ No newline at end of file
diff --git a/tests/k8s/1-018_validate_extra_config/04-assert.yaml b/tests/k8s/1-018_validate_extra_config/04-assert.yaml
new file mode 100644
index 00000000..d005cd46
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/04-assert.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: argocd-cm
+data:
+  admin.enabled: "true"
+  dex.config: |
+    connectors:
+      - type: github
+        id: github
+        name: github-using-first-class
+        config:
+          clientID: first-class
+          clientSecret: $dex.github.clientSecret
+          orgs:
+          - name: first-class
\ No newline at end of file
diff --git a/tests/k8s/1-018_validate_extra_config/04-create-dex-without-extraconfig.yaml b/tests/k8s/1-018_validate_extra_config/04-create-dex-without-extraconfig.yaml
new file mode 100644
index 00000000..0ab7807a
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/04-create-dex-without-extraconfig.yaml
@@ -0,0 +1,18 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  disableAdmin: true
+  dex:
+   config: |
+    connectors:
+      - type: github
+        id: github
+        name: github-using-first-class
+        config:
+          clientID: first-class
+          clientSecret: $dex.github.clientSecret
+          orgs:
+          - name: first-class
+
diff --git a/tests/k8s/1-018_validate_extra_config/05-assert.yaml b/tests/k8s/1-018_validate_extra_config/05-assert.yaml
new file mode 100644
index 00000000..747283ca
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/05-assert.yaml
@@ -0,0 +1,17 @@
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: argocd-cm
+data:
+  admin.enabled: "true"
+  dex.config: |
+    connectors:
+      - type: github
+        id: github
+        name: github-using-extra-config
+        config:
+          clientID: extra-config
+          clientSecret: $dex.github.clientSecret
+          orgs:
+          - name: extra-config
\ No newline at end of file
diff --git a/tests/k8s/1-018_validate_extra_config/05-override-dex-using-extraconfig.yaml b/tests/k8s/1-018_validate_extra_config/05-override-dex-using-extraconfig.yaml
new file mode 100644
index 00000000..49cb7201
--- /dev/null
+++ b/tests/k8s/1-018_validate_extra_config/05-override-dex-using-extraconfig.yaml
@@ -0,0 +1,29 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  disableAdmin: true
+  dex:
+   config: |
+    connectors:
+      - type: github
+        id: github
+        name: github-using-first-class
+        config:
+          clientID: first-class
+          clientSecret: $dex.github.clientSecret
+          orgs:
+          - name: first-class
+  extraConfig:
+    "admin.enabled": "true"
+    "dex.config": |
+      connectors:
+        - type: github
+          id: github
+          name: github-using-extra-config
+          config:
+            clientID: extra-config
+            clientSecret: $dex.github.clientSecret
+            orgs:
+            - name: extra-config