Repository: argoproj-labs/argocd-operator
Pull request #986 (2023-08-31 23:15:33)

code-refactoring: Gitops 3126 implement notifications controller
**What type of PR is this?**
/kind code-refactoring


**What does this PR do / why we need it**:
Implemented notifications controller.

NOTE: this PR just adds the package files and tests, this code is not invoked at any point yet
**Have you updated the necessary documentation?**

* [ ] Documentation update is required by this PR.
* [ ] Documentation has been updated.

**Which issue(s) this PR fixes**:

Fixes #?
GITOPS-3126: https://issues.redhat.com/browse/GITOPS-3126

**How to test changes / Special notes to the reviewer**:
Unit tests are yet included. Coming soon.

jaideepr97: some general comments about the unit tests

1) We should really avoid using the test name as a variable for conditional checks, I don' think that is good coding practice. If needed lets split the test up into multiple tests

2) there is no need to try and preserve/restore `resourceName` and `resourceLabels`. These variables are re-initialized each cycle, and setting them to something in a test is not going to break the operator 

3) If the operator is not handling a case in the reconciliation cycle, there is no need to add a test for it. Let us simplify the test cases to create/update/delete scenarios 
jaideepr97: LGTM, thanks a lot @ciiay !!

Patch:
diff --git a/common/defaults.go b/common/defaults.go
index 1d206e40..eb41108c 100644
--- a/common/defaults.go
+++ b/common/defaults.go
@@ -155,12 +155,6 @@ const (
 	ArgoCDDefaultRepoServerPort = 8081
 )
 
-// notifications-controller
-const (
-	// ArgoCDNotificationsControllerComponent is the name of the Notifications controller control plane component
-	ArgoCDNotificationsControllerComponent = "argocd-notifications-controller"
-)
-
 // grafana
 const (
 	// ArgoCDOperatorGrafanaComponent is the name of the Grafana control plane component
diff --git a/common/values.go b/common/values.go
index 9a0ca543..7ee54468 100644
--- a/common/values.go
+++ b/common/values.go
@@ -36,6 +36,9 @@ const (
 	// ArgoCDStatusCompleted is the completed status value.
 	ArgoCDStatusCompleted = "Completed"
 
+	// ArgoCDRepoServerTLS is the argocd repo server tls value.
+	ArgoCDRepoServerTLS = "argocd-repo-server-tls"
+
 	// K8sOSLinux is the value for kubernetes.io/os key for linux pods
 	K8sOSLinux = "linux"
 
@@ -49,7 +52,23 @@ const (
 
 	TLSCerts = "tls-certs"
 
+	CapabilityDropAll = "ALL"
+
 	VolumeMountPathTLS           = "/app/config/tls"
 	VolumeMountPathRepoServerTLS = "/app/config/reposerver/tls"
 	WorkingDirApp                = "/app"
 )
+
+// API group versions and resource kinds
+const (
+	APIVersionV1          = "v1"
+	APIGroupVersionAppsV1 = "apps/v1"
+	APIGroupVersionRbacV1 = "rbac.authorization.k8s.io/v1"
+
+	DeploymentKind     = "Deployment"
+	RoleKind           = "Role"
+	RoleBindingKind    = "RoleBinding"
+	ConfigMapKind      = "ConfigMap"
+	SecretKind         = "Secret"
+	ServiceAccountKind = "ServiceAccount"
+)
diff --git a/controllers/argocd/argocd_controller.go b/controllers/argocd/argocd_controller.go
index b0444fac..7e179fd7 100644
--- a/controllers/argocd/argocd_controller.go
+++ b/controllers/argocd/argocd_controller.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 
 	"github.com/argoproj-labs/argocd-operator/api/v1alpha1"
-	argoproj "github.com/argoproj-labs/argocd-operator/api/v1alpha1"
 	"github.com/argoproj-labs/argocd-operator/common"
 	"github.com/argoproj-labs/argocd-operator/controllers/argocd/appcontroller"
 	"github.com/argoproj-labs/argocd-operator/controllers/argocd/applicationset"
@@ -98,7 +97,7 @@ var log = ctrl.Log.WithName("controller_argocd")
 func (r *ArgoCDReconciler) Reconcile(ctx context.Context, request ctrl.Request) (ctrl.Result, error) {
 	argocdControllerLog := ctrl.Log.WithName("argocd-controller")
 
-	argocd := &argoproj.ArgoCD{}
+	argocd := &v1alpha1.ArgoCD{}
 	err := r.Client.Get(ctx, request.NamespacedName, argocd)
 	if err != nil {
 		if errors.IsNotFound(err) {
@@ -163,6 +162,8 @@ func (r *ArgoCDReconciler) Reconcile(ctx context.Context, request ctrl.Request)
 	// 	return reconcile.Result{}, err
 	// }
 
+	r.InitializeControllerReconcilers()
+
 	if err = r.reconcileControllers(); err != nil {
 		return reconcile.Result{}, err
 	}
@@ -216,8 +217,14 @@ func (r *ArgoCDReconciler) reconcileControllers() error {
 		r.Logger.Error(err, "failed to reconcile applicationset controller")
 	}
 
-	if err := r.NotificationsController.Reconcile(); err != nil {
-		r.Logger.Error(err, "failed to reconcile notifications controller")
+	if r.Instance.Spec.Notifications.Enabled {
+		if err := r.NotificationsController.Reconcile(); err != nil {
+			r.Logger.Error(err, "failed to reconcile notifications controller")
+		}
+	} else {
+		if err := r.NotificationsController.DeleteResources(); err != nil {
+			r.Logger.Error(err, "failed to delete notifications resources")
+		}
 	}
 
 	if err := r.SSOController.Reconcile(); err != nil {
@@ -264,7 +271,7 @@ func (r *ArgoCDReconciler) InitializeControllerReconcilers() {
 	}
 
 	r.NotificationsController = &notifications.NotificationsReconciler{
-		Client:   &r.Client,
+		Client:   r.Client,
 		Scheme:   r.Scheme,
 		Instance: r.Instance,
 	}
diff --git a/controllers/argocd/notifications/configmap.go b/controllers/argocd/notifications/configmap.go
new file mode 100644
index 00000000..7d6673d3
--- /dev/null
+++ b/controllers/argocd/notifications/configmap.go
@@ -0,0 +1,75 @@
+package notifications
+
+import (
+	"github.com/argoproj-labs/argocd-operator/pkg/cluster"
+	"github.com/argoproj-labs/argocd-operator/pkg/workloads"
+
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func (nr *NotificationsReconciler) reconcileConfigMap() error {
+
+	nr.Logger.Info("reconciling configMaps")
+
+	configMapRequest := workloads.ConfigMapRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        NotificationsConfigMapName,
+			Namespace:   nr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: nr.Instance.Annotations,
+		},
+		Data: GetDefaultNotificationsConfig(),
+	}
+
+	desiredConfigMap, err := workloads.RequestConfigMap(configMapRequest)
+
+	if err != nil {
+		nr.Logger.Error(err, "reconcileConfigMap: failed to request configMap", "name", desiredConfigMap.Name, "namespace", desiredConfigMap.Namespace)
+		nr.Logger.V(1).Info("reconcileConfigMap: one or more mutations could not be applied")
+		return err
+	}
+
+	namespace, err := cluster.GetNamespace(nr.Instance.Namespace, nr.Client)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileConfigMap: failed to retrieve namespace", "name", nr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := nr.DeleteConfigMap(desiredConfigMap.Namespace); err != nil {
+			nr.Logger.Error(err, "reconcileConfigMap: failed to delete configMap", "name", desiredConfigMap.Name, "namespace", desiredConfigMap.Namespace)
+		}
+		return err
+	}
+
+	existingConfigMap, err := workloads.GetConfigMap(desiredConfigMap.Name, desiredConfigMap.Namespace, nr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			nr.Logger.Error(err, "reconcileConfigMap: failed to retrieve configMap", "name", existingConfigMap.Name, "namespace", existingConfigMap.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(nr.Instance, desiredConfigMap, nr.Scheme); err != nil {
+			nr.Logger.Error(err, "reconcileConfigMap: failed to set owner reference for configMap", "name", desiredConfigMap.Name, "namespace", desiredConfigMap.Namespace)
+		}
+
+		if err = workloads.CreateConfigMap(desiredConfigMap, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileConfigMap: failed to create configMap", "name", desiredConfigMap.Name, "namespace", desiredConfigMap.Namespace)
+			return err
+		}
+		nr.Logger.V(0).Info("reconcileConfigMap: configMap created", "name", desiredConfigMap.Name, "namespace", desiredConfigMap.Namespace)
+		return nil
+	}
+
+	return nil
+}
+
+func (nr *NotificationsReconciler) DeleteConfigMap(namespace string) error {
+	if err := workloads.DeleteConfigMap(NotificationsConfigMapName, namespace, nr.Client); err != nil {
+		nr.Logger.Error(err, "DeleteConfigMap: failed to delete configMap", "name", NotificationsConfigMapName, "namespace", namespace)
+		return err
+	}
+	nr.Logger.V(0).Info("DeleteConfigMap: configMap deleted", "name", NotificationsConfigMapName, "namespace", namespace)
+	return nil
+}
diff --git a/controllers/argocd/notifications/constants.go b/controllers/argocd/notifications/constants.go
new file mode 100644
index 00000000..c4718d69
--- /dev/null
+++ b/controllers/argocd/notifications/constants.go
@@ -0,0 +1,8 @@
+package notifications
+
+// Values
+const (
+	ArgoCDNotificationsControllerComponent = "notifications-controller"
+	NotificationsSecretName                = "argocd-notifications-secret"
+	NotificationsConfigMapName             = "argocd-notifications-cm"
+)
diff --git a/controllers/argocd/notifications/deployment.go b/controllers/argocd/notifications/deployment.go
new file mode 100644
index 00000000..46ad3a27
--- /dev/null
+++ b/controllers/argocd/notifications/deployment.go
@@ -0,0 +1,222 @@
+package notifications
+
+import (
+	"time"
+
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/argoproj-labs/argocd-operator/pkg/cluster"
+	"github.com/argoproj-labs/argocd-operator/pkg/mutation"
+	"github.com/argoproj-labs/argocd-operator/pkg/util"
+	"github.com/argoproj-labs/argocd-operator/pkg/workloads"
+
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func (nr *NotificationsReconciler) getDesiredDeployment() *appsv1.Deployment {
+	desiredDeployment := &appsv1.Deployment{}
+
+	notificationEnv := nr.Instance.Spec.Notifications.Env
+	notificationEnv = util.EnvMerge(notificationEnv, util.ProxyEnvVars(), false)
+
+	objMeta := metav1.ObjectMeta{
+		Name:      resourceName,
+		Namespace: nr.Instance.Namespace,
+		Labels:    resourceLabels,
+	}
+	podSpec := corev1.PodSpec{
+		Volumes: []corev1.Volume{
+			{
+				Name: common.TLSCerts,
+				VolumeSource: corev1.VolumeSource{
+					ConfigMap: &corev1.ConfigMapVolumeSource{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: common.ArgoCDTLSCertsConfigMapName,
+						},
+					},
+				},
+			},
+			{
+				Name: common.ArgoCDRepoServerTLS,
+				VolumeSource: corev1.VolumeSource{
+					Secret: &corev1.SecretVolumeSource{
+						SecretName: common.ArgoCDRepoServerTLSSecretName,
+						Optional:   util.BoolPtr(true),
+					},
+				},
+			},
+		},
+		Containers: []corev1.Container{{
+			Command:         nr.GetNotificationsCommand(),
+			Image:           argocdcommon.GetArgoContainerImage(nr.Instance),
+			ImagePullPolicy: corev1.PullAlways,
+			Name:            ArgoCDNotificationsControllerComponent,
+			Env:             notificationEnv,
+			Resources:       nr.GetNotificationsResources(),
+			LivenessProbe: &corev1.Probe{
+				ProbeHandler: corev1.ProbeHandler{
+					TCPSocket: &corev1.TCPSocketAction{
+						Port: intstr.IntOrString{
+							IntVal: int32(9001),
+						},
+					},
+				},
+			},
+			SecurityContext: &corev1.SecurityContext{
+				AllowPrivilegeEscalation: util.BoolPtr(false),
+				Capabilities: &corev1.Capabilities{
+					Drop: []corev1.Capability{
+						common.CapabilityDropAll,
+					},
+				},
+			},
+			VolumeMounts: []corev1.VolumeMount{
+				{
+					Name:      common.TLSCerts,
+					MountPath: common.VolumeMountPathTLS,
+				},
+				{
+					Name:      common.ArgoCDRepoServerTLS,
+					MountPath: common.VolumeMountPathRepoServerTLS,
+				},
+			},
+			WorkingDir: common.WorkingDirApp,
+		}},
+		SecurityContext: &corev1.PodSecurityContext{
+			RunAsNonRoot: util.BoolPtr(true),
+		},
+	}
+
+	deploymentSpec := appsv1.DeploymentSpec{
+		Strategy: appsv1.DeploymentStrategy{
+			Type: appsv1.RecreateDeploymentStrategyType,
+		},
+		Template: corev1.PodTemplateSpec{
+			Spec: podSpec,
+			ObjectMeta: metav1.ObjectMeta{
+				Labels: map[string]string{
+					common.AppK8sKeyName: resourceName,
+				},
+			},
+		},
+		Selector: &metav1.LabelSelector{
+			MatchLabels: map[string]string{
+				common.AppK8sKeyName: resourceName,
+			},
+		},
+		Replicas: nr.GetArgoCDNotificationsControllerReplicas(),
+	}
+
+	desiredDeployment.ObjectMeta = objMeta
+	desiredDeployment.Spec = deploymentSpec
+	return desiredDeployment
+}
+
+func (nr *NotificationsReconciler) getDeploymentRequest(dep appsv1.Deployment) workloads.DeploymentRequest {
+	deploymentReq := workloads.DeploymentRequest{
+		ObjectMeta: dep.ObjectMeta,
+		Spec:       dep.Spec,
+		Client:     nr.Client,
+		Mutations:  []mutation.MutateFunc{mutation.ApplyReconcilerMutation},
+	}
+
+	return deploymentReq
+}
+
+func (nr *NotificationsReconciler) reconcileDeployment() error {
+
+	nr.Logger.Info("reconciling deployment")
+
+	desiredDeployment := nr.getDesiredDeployment()
+	deploymentRequest := nr.getDeploymentRequest(*desiredDeployment)
+
+	desiredDeployment, err := workloads.RequestDeployment(deploymentRequest)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileDeployment: failed to request deployment", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+		nr.Logger.V(1).Info("reconcileDeployment: one or more mutations could not be applied")
+		return err
+	}
+
+	namespace, err := cluster.GetNamespace(nr.Instance.Namespace, nr.Client)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileDeployment: failed to retrieve namespace", "name", nr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := nr.DeleteDeployment(desiredDeployment.Name, desiredDeployment.Namespace); err != nil {
+			nr.Logger.Error(err, "reconcileDeployment: failed to delete deployment", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+		}
+		return err
+	}
+
+	existingDeployment, err := workloads.GetDeployment(desiredDeployment.Name, desiredDeployment.Namespace, nr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			nr.Logger.Error(err, "reconcileDeployment: failed to retrieve deployment", "name", existingDeployment.Name, "namespace", existingDeployment.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(nr.Instance, desiredDeployment, nr.Scheme); err != nil {
+			nr.Logger.Error(err, "reconcileDeployment: failed to set owner reference for deployment", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+		}
+
+		if err = workloads.CreateDeployment(desiredDeployment, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileDeployment: failed to create deployment", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+			return err
+		}
+		nr.Logger.V(0).Info("reconcileDeployment: deployment created", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+		return nil
+	}
+	deploymentChanged := false
+
+	fieldsToCompare := []struct {
+		existing, desired interface{}
+		extraAction       func()
+	}{
+		{&existingDeployment.Spec.Template.Spec.Containers[0].Image, &desiredDeployment.Spec.Template.Spec.Containers[0].Image,
+			func() {
+				existingDeployment.Spec.Template.ObjectMeta.Labels[common.ImageUpgradedKey] = time.Now().UTC().Format(common.TimeFormatMST)
+			},
+		},
+		{&existingDeployment.Spec.Template.Spec.Containers[0].Command, &desiredDeployment.Spec.Template.Spec.Containers[0].Command, nil},
+		{&existingDeployment.Spec.Template.Spec.Containers[0].Env, &desiredDeployment.Spec.Template.Spec.Containers[0].Env, nil},
+		{&existingDeployment.Spec.Template.Spec.Containers[0].Resources, &desiredDeployment.Spec.Template.Spec.Containers[0].Resources, nil},
+		{&existingDeployment.Spec.Template.Spec.Volumes, &desiredDeployment.Spec.Template.Spec.Volumes, nil},
+		{&existingDeployment.Spec.Template.Spec.NodeSelector, &desiredDeployment.Spec.Template.Spec.NodeSelector, nil},
+		{&existingDeployment.Spec.Template.Spec.Tolerations, &desiredDeployment.Spec.Template.Spec.Tolerations, nil},
+		{&existingDeployment.Spec.Template.Spec.ServiceAccountName, &desiredDeployment.Spec.Template.Spec.ServiceAccountName, nil},
+		{&existingDeployment.Spec.Template.Labels, &desiredDeployment.Spec.Template.Labels, nil},
+		{&existingDeployment.Spec.Replicas, &desiredDeployment.Spec.Replicas, nil},
+		{&existingDeployment.Spec.Selector, &desiredDeployment.Spec.Selector, nil},
+		{&existingDeployment.Labels, &desiredDeployment.Labels, nil},
+	}
+
+	for _, field := range fieldsToCompare {
+		argocdcommon.UpdateIfChanged(field.existing, field.desired, field.extraAction, &deploymentChanged)
+	}
+
+	if deploymentChanged {
+
+		if err = workloads.UpdateDeployment(existingDeployment, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileDeployment: failed to update deployment", "name", existingDeployment.Name, "namespace", existingDeployment.Namespace)
+			return err
+		}
+	}
+
+	nr.Logger.V(0).Info("reconcileDeployment: deployment updated", "name", existingDeployment.Name, "namespace", existingDeployment.Namespace)
+	return nil
+}
+
+func (nr *NotificationsReconciler) DeleteDeployment(name, namespace string) error {
+	if err := workloads.DeleteDeployment(name, namespace, nr.Client); err != nil {
+		nr.Logger.Error(err, "DeleteDeployment: failed to delete deployment", "name", name, "namespace", namespace)
+		return err
+	}
+	nr.Logger.V(0).Info("DeleteDeployment: deployment deleted", "name", name, "namespace", namespace)
+	return nil
+}
diff --git a/controllers/argocd/notifications/notifications.go b/controllers/argocd/notifications/notifications.go
index c06908c9..8ddeba36 100644
--- a/controllers/argocd/notifications/notifications.go
+++ b/controllers/argocd/notifications/notifications.go
@@ -2,20 +2,99 @@ package notifications
 
 import (
 	"github.com/argoproj-labs/argocd-operator/api/v1alpha1"
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/pkg/util"
 	"github.com/go-logr/logr"
 	"k8s.io/apimachinery/pkg/runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
 type NotificationsReconciler struct {
-	Client   *client.Client
+	Client   client.Client
 	Scheme   *runtime.Scheme
 	Instance *v1alpha1.ArgoCD
 	Logger   logr.Logger
 }
 
+var (
+	resourceName   string
+	resourceLabels map[string]string
+)
+
 func (nr *NotificationsReconciler) Reconcile() error {
 
-	// controller logic goes here
+	nr.Logger = ctrl.Log.WithName(ArgoCDNotificationsControllerComponent).WithValues("instance", nr.Instance.Name, "instance-namespace", nr.Instance.Namespace)
+
+	resourceName = util.GenerateUniqueResourceName(nr.Instance.Name, nr.Instance.Namespace, ArgoCDNotificationsControllerComponent)
+	resourceLabels = common.DefaultLabels(resourceName, nr.Instance.Name, ArgoCDNotificationsControllerComponent)
+
+	if err := nr.reconcileServiceAccount(); err != nil {
+		nr.Logger.Info("reconciling notifications serviceaccount")
+		return err
+	}
+
+	if err := nr.reconcileRole(); err != nil {
+		nr.Logger.Info("reconciling notifications role")
+		return err
+	}
+
+	if err := nr.reconcileRoleBinding(); err != nil {
+		nr.Logger.Info("reconciling notifications roleBinding")
+		return err
+	}
+
+	if err := nr.reconcileConfigMap(); err != nil {
+		nr.Logger.Info("reconciling notifications configmap")
+		return err
+	}
+
+	if err := nr.reconcileSecret(); err != nil {
+		nr.Logger.Info("reconciling notifications secret")
+		return err
+	}
+
+	if err := nr.reconcileDeployment(); err != nil {
+		nr.Logger.Info("reconciling notifications deployment")
+		return err
+	}
+
 	return nil
 }
+
+func (nr *NotificationsReconciler) DeleteResources() error {
+
+	var deletionError error = nil
+
+	if err := nr.DeleteDeployment(resourceName, nr.Instance.Namespace); err != nil {
+		nr.Logger.Error(err, "DeleteResources: failed to delete deployment")
+		deletionError = err
+	}
+
+	if err := nr.DeleteSecret(nr.Instance.Namespace); err != nil {
+		nr.Logger.Error(err, "DeleteResources: failed to delete secret")
+		deletionError = err
+	}
+
+	if err := nr.DeleteConfigMap(nr.Instance.Namespace); err != nil {
+		nr.Logger.Error(err, "DeleteResources: failed to delete configmap")
+		deletionError = err
+	}
+
+	if err := nr.DeleteRoleBinding(resourceName, nr.Instance.Namespace); err != nil {
+		nr.Logger.Error(err, "DeleteResources: failed to delete roleBinding")
+		deletionError = err
+	}
+
+	if err := nr.DeleteRole(resourceName, nr.Instance.Namespace); err != nil {
+		nr.Logger.Error(err, "DeleteResources: failed to delete role")
+		deletionError = err
+	}
+
+	if err := nr.DeleteServiceAccount(resourceName, nr.Instance.Namespace); err != nil {
+		nr.Logger.Error(err, "DeleteResources: failed to delete serviceaccount")
+		deletionError = err
+	}
+
+	return deletionError
+}
diff --git a/controllers/argocd/notifications/role.go b/controllers/argocd/notifications/role.go
new file mode 100644
index 00000000..88b3535d
--- /dev/null
+++ b/controllers/argocd/notifications/role.go
@@ -0,0 +1,150 @@
+package notifications
+
+import (
+	"reflect"
+
+	"github.com/argoproj-labs/argocd-operator/pkg/cluster"
+	"github.com/argoproj-labs/argocd-operator/pkg/mutation"
+	"github.com/argoproj-labs/argocd-operator/pkg/permissions"
+
+	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func (nr *NotificationsReconciler) reconcileRole() error {
+
+	nr.Logger.Info("reconciling roles")
+
+	roleRequest := permissions.RoleRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        resourceName,
+			Namespace:   nr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: nr.Instance.Annotations,
+		},
+		Rules:     getPolicyRules(),
+		Client:    nr.Client,
+		Mutations: []mutation.MutateFunc{mutation.ApplyReconcilerMutation},
+	}
+
+	desiredRole, err := permissions.RequestRole(roleRequest)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileRole: failed to request role", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+		nr.Logger.V(1).Info("reconcileRole: one or more mutations could not be applied")
+		return err
+	}
+
+	namespace, err := cluster.GetNamespace(nr.Instance.Namespace, nr.Client)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileRole: failed to retrieve namespace", "name", nr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := nr.DeleteRole(desiredRole.Name, desiredRole.Namespace); err != nil {
+			nr.Logger.Error(err, "reconcileRole: failed to delete role", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+		}
+		return err
+	}
+
+	existingRole, err := permissions.GetRole(desiredRole.Name, desiredRole.Namespace, nr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			nr.Logger.Error(err, "reconcileRole: failed to retrieve role", "name", existingRole.Name, "namespace", existingRole.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(nr.Instance, desiredRole, nr.Scheme); err != nil {
+			nr.Logger.Error(err, "reconcileRole: failed to set owner reference for role", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+		}
+
+		if err = permissions.CreateRole(desiredRole, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileRole: failed to create role", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+			return err
+		}
+		nr.Logger.V(0).Info("reconcileRole: role created", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+		return nil
+	}
+
+	if !reflect.DeepEqual(existingRole.Rules, desiredRole.Rules) {
+		existingRole.Rules = desiredRole.Rules
+		if err = permissions.UpdateRole(existingRole, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileRole: failed to update role", "name", existingRole.Name, "namespace", existingRole.Namespace)
+			return err
+		}
+	}
+	nr.Logger.V(0).Info("reconcileRole: role updated", "name", existingRole.Name, "namespace", existingRole.Namespace)
+	return nil
+}
+
+func (nr *NotificationsReconciler) DeleteRole(name, namespace string) error {
+	if err := permissions.DeleteRole(name, namespace, nr.Client); err != nil {
+		nr.Logger.Error(err, "DeleteRole: failed to delete role", "name", name, "namespace", namespace)
+		return err
+	}
+	nr.Logger.V(0).Info("DeleteRole: role deleted", "name", name, "namespace", namespace)
+	return nil
+}
+
+func getPolicyRules() []rbacv1.PolicyRule {
+	return []rbacv1.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",
+			},
+		},
+	}
+}
diff --git a/controllers/argocd/notifications/rolebinding.go b/controllers/argocd/notifications/rolebinding.go
new file mode 100644
index 00000000..a2ebdd6f
--- /dev/null
+++ b/controllers/argocd/notifications/rolebinding.go
@@ -0,0 +1,117 @@
+package notifications
+
+import (
+	. "github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/argoproj-labs/argocd-operator/pkg/cluster"
+	"github.com/argoproj-labs/argocd-operator/pkg/permissions"
+
+	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func (nr *NotificationsReconciler) reconcileRoleBinding() error {
+
+	nr.Logger.Info("reconciling roleBindings")
+
+	sa, err := permissions.GetServiceAccount(resourceName, nr.Instance.Namespace, nr.Client)
+
+	if err != nil {
+		nr.Logger.Error(err, "reconsileRoleBinding: failed to get serviceaccount", "name", resourceName, "namespace", nr.Instance.Namespace)
+		return err
+	}
+
+	roleBindingRequest := permissions.RoleBindingRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        resourceName,
+			Namespace:   nr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: nr.Instance.Annotations,
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: rbacv1.GroupName,
+			Kind:     RoleKind,
+			Name:     resourceName,
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				Kind:      rbacv1.ServiceAccountKind,
+				Name:      sa.Name,
+				Namespace: sa.Namespace,
+			},
+		},
+	}
+
+	desiredRoleBinding := permissions.RequestRoleBinding(roleBindingRequest)
+
+	namespace, err := cluster.GetNamespace(nr.Instance.Namespace, nr.Client)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileRole: failed to retrieve namespace", "name", nr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := nr.DeleteRole(desiredRoleBinding.Name, desiredRoleBinding.Namespace); err != nil {
+			nr.Logger.Error(err, "reconcileRoleBinding: failed to delete roleBinding", "name", desiredRoleBinding.Name, "namespace", desiredRoleBinding.Namespace)
+		}
+		return err
+	}
+
+	existingRoleBinding, err := permissions.GetRoleBinding(desiredRoleBinding.Name, desiredRoleBinding.Namespace, nr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			nr.Logger.Error(err, "reconcileRoleBinding: failed to retrieve roleBinding", "name", existingRoleBinding.Name, "namespace", existingRoleBinding.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(nr.Instance, desiredRoleBinding, nr.Scheme); err != nil {
+			nr.Logger.Error(err, "reconcileRole: failed to set owner reference for role", "name", desiredRoleBinding.Name, "namespace", desiredRoleBinding.Namespace)
+		}
+
+		if err = permissions.CreateRoleBinding(desiredRoleBinding, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileRoleBinding: failed to create roleBinding", "name", desiredRoleBinding.Name, "namespace", desiredRoleBinding.Namespace)
+			return err
+		}
+		nr.Logger.V(0).Info("reconcileRoleBinding: roleBinding created", "name", desiredRoleBinding.Name, "namespace", desiredRoleBinding.Namespace)
+		return nil
+	}
+
+	roleBindingChanged := false
+	fieldsToCompare := []struct {
+		existing, desired interface{}
+	}{
+		{
+			&existingRoleBinding.RoleRef,
+			&desiredRoleBinding.RoleRef,
+		},
+		{
+			&existingRoleBinding.Subjects,
+			&desiredRoleBinding.Subjects,
+		},
+	}
+
+	for _, field := range fieldsToCompare {
+		argocdcommon.UpdateIfChanged(field.existing, field.desired, nil, &roleBindingChanged)
+	}
+
+	if roleBindingChanged {
+		if err = permissions.UpdateRoleBinding(existingRoleBinding, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileRoleBinding: failed to update roleBinding", "name", existingRoleBinding.Name, "namespace", existingRoleBinding.Namespace)
+			return err
+		}
+	}
+
+	nr.Logger.V(0).Info("reconcileRoleBinding: roleBinding updated", "name", existingRoleBinding.Name, "namespace", existingRoleBinding.Namespace)
+
+	return nil
+}
+
+func (nr *NotificationsReconciler) DeleteRoleBinding(name, namespace string) error {
+	if err := permissions.DeleteRoleBinding(name, namespace, nr.Client); err != nil {
+		nr.Logger.Error(err, "DeleteRole: failed to delete roleBinding", "name", name, "namespace", namespace)
+		return err
+	}
+	nr.Logger.V(0).Info("DeleteRoleBinding: roleBinding deleted", "name", name, "namespace", namespace)
+	return nil
+}
diff --git a/controllers/argocd/notifications/secret.go b/controllers/argocd/notifications/secret.go
new file mode 100644
index 00000000..e46a7874
--- /dev/null
+++ b/controllers/argocd/notifications/secret.go
@@ -0,0 +1,76 @@
+package notifications
+
+import (
+	"github.com/argoproj-labs/argocd-operator/pkg/cluster"
+	"github.com/argoproj-labs/argocd-operator/pkg/mutation"
+	"github.com/argoproj-labs/argocd-operator/pkg/workloads"
+
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func (nr *NotificationsReconciler) reconcileSecret() error {
+
+	nr.Logger.Info("reconciling secrets")
+
+	secretRequest := workloads.SecretRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      NotificationsSecretName,
+			Namespace: nr.Instance.Namespace,
+			Labels:    resourceLabels,
+		},
+
+		Client:    nr.Client,
+		Mutations: []mutation.MutateFunc{mutation.ApplyReconcilerMutation},
+	}
+
+	desiredSecret, err := workloads.RequestSecret(secretRequest)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileSecret: failed to request secret", "name", desiredSecret.Name, "namespace", desiredSecret.Namespace)
+		nr.Logger.V(1).Info("reconcileSecret: one or more mutations could not be applied")
+		return err
+	}
+
+	namespace, err := cluster.GetNamespace(nr.Instance.Namespace, nr.Client)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileSecret: failed to retrieve namespace", "name", nr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := nr.DeleteSecret(desiredSecret.Namespace); err != nil {
+			nr.Logger.Error(err, "reconcileSecret: failed to delete secret", "name", desiredSecret.Name, "namespace", desiredSecret.Namespace)
+		}
+		return err
+	}
+
+	existingSecret, err := workloads.GetSecret(desiredSecret.Name, desiredSecret.Namespace, nr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			nr.Logger.Error(err, "reconcileSecret: failed to retrieve secret", "name", existingSecret.Name, "namespace", existingSecret.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(nr.Instance, desiredSecret, nr.Scheme); err != nil {
+			nr.Logger.Error(err, "reconcileSecret: failed to set owner reference for secret", "name", desiredSecret.Name, "namespace", desiredSecret.Namespace)
+		}
+
+		if err = workloads.CreateSecret(desiredSecret, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileSecret: failed to create secret", "name", desiredSecret.Name, "namespace", desiredSecret.Namespace)
+			return err
+		}
+		nr.Logger.V(0).Info("reconcileSecret: secret created", "name", desiredSecret.Name, "namespace", desiredSecret.Namespace)
+		return nil
+	}
+
+	return nil
+}
+
+func (nr *NotificationsReconciler) DeleteSecret(namespace string) error {
+	if err := workloads.DeleteSecret(NotificationsSecretName, namespace, nr.Client); err != nil {
+		nr.Logger.Error(err, "DeleteSecret: failed to delete secret", "name", NotificationsSecretName, "namespace", namespace)
+		return err
+	}
+	nr.Logger.V(0).Info("DeleteSecret: secret deleted", "name", NotificationsSecretName, "namespace", namespace)
+	return nil
+}
diff --git a/controllers/argocd/notifications/serviceaccount.go b/controllers/argocd/notifications/serviceaccount.go
new file mode 100644
index 00000000..62c8d503
--- /dev/null
+++ b/controllers/argocd/notifications/serviceaccount.go
@@ -0,0 +1,68 @@
+package notifications
+
+import (
+	"github.com/argoproj-labs/argocd-operator/pkg/cluster"
+	"github.com/argoproj-labs/argocd-operator/pkg/permissions"
+
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func (nr *NotificationsReconciler) reconcileServiceAccount() error {
+
+	nr.Logger.Info("reconciling serviceAccounts")
+
+	serviceAccountRequest := permissions.ServiceAccountRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        resourceName,
+			Namespace:   nr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: nr.Instance.Annotations,
+		},
+	}
+
+	desiredServiceAccount := permissions.RequestServiceAccount(serviceAccountRequest)
+
+	namespace, err := cluster.GetNamespace(nr.Instance.Namespace, nr.Client)
+	if err != nil {
+		nr.Logger.Error(err, "reconcileServiceAccount: failed to retrieve namespace", "name", nr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := nr.DeleteServiceAccount(desiredServiceAccount.Name, desiredServiceAccount.Namespace); err != nil {
+			nr.Logger.Error(err, "reconcileServiceAccount: failed to delete serviceAccount", "name", desiredServiceAccount.Name, "namespace", desiredServiceAccount.Namespace)
+		}
+		return err
+	}
+
+	existingServiceAccount, err := permissions.GetServiceAccount(desiredServiceAccount.Name, desiredServiceAccount.Namespace, nr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			nr.Logger.Error(err, "reconcileServiceAccount: failed to retrieve serviceAccount", "name", existingServiceAccount.Name, "namespace", existingServiceAccount.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(nr.Instance, desiredServiceAccount, nr.Scheme); err != nil {
+			nr.Logger.Error(err, "reconcileServiceAccount: failed to set owner reference for serviceAccount", "name", desiredServiceAccount.Name, "namespace", desiredServiceAccount.Namespace)
+		}
+
+		if err = permissions.CreateServiceAccount(desiredServiceAccount, nr.Client); err != nil {
+			nr.Logger.Error(err, "reconcileServiceAccount: failed to create serviceAccount", "name", desiredServiceAccount.Name, "namespace", desiredServiceAccount.Namespace)
+			return err
+		}
+		nr.Logger.V(0).Info("reconcileServiceAccount: serviceAccount created", "name", desiredServiceAccount.Name, "namespace", desiredServiceAccount.Namespace)
+		return nil
+	}
+
+	return nil
+}
+
+func (nr *NotificationsReconciler) DeleteServiceAccount(name, namespace string) error {
+	if err := permissions.DeleteServiceAccount(name, namespace, nr.Client); err != nil {
+		nr.Logger.Error(err, "DeleteServiceAccount: failed to delete serviceAccount", "name", name, "namespace", namespace)
+		return err
+	}
+	nr.Logger.V(0).Info("DeleteServiceAccount: serviceAccount deleted", "name", name, "namespace", namespace)
+	return nil
+}
diff --git a/controllers/argocd/notifications/util.go b/controllers/argocd/notifications/util.go
new file mode 100644
index 00000000..1c1f87cf
--- /dev/null
+++ b/controllers/argocd/notifications/util.go
@@ -0,0 +1,578 @@
+package notifications
+
+import (
+	"github.com/argoproj-labs/argocd-operator/pkg/util"
+	corev1 "k8s.io/api/core/v1"
+)
+
+// GetDefaultNotificationsConfig returns a map that contains default triggers and template configurations for argocd-notifications-cm
+func GetDefaultNotificationsConfig() map[string]string {
+
+	notificationsConfig := make(map[string]string)
+
+	// configure default notifications templates
+
+	notificationsConfig["template.app-created"] = `email:
+  subject: Application {{.app.metadata.name}} has been created.
+message: Application {{.app.metadata.name}} has been created.
+teams:
+  title: Application {{.app.metadata.name}} has been created.`
+
+	notificationsConfig["template.app-deleted"] = `email:
+  subject: Application {{.app.metadata.name}} has been deleted.
+message: Application {{.app.metadata.name}} has been deleted.
+teams:
+  title: Application {{.app.metadata.name}} has been deleted.`
+
+	notificationsConfig["template.app-deployed"] = `email:
+  subject: New version of an application {{.app.metadata.name}} is up and running.
+message: |
+  {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} is now running new version of deployments manifests.
+slack:
+  attachments: |
+    [{
+      "title": "{{ .app.metadata.name}}",
+      "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
+      "color": "#18be52",
+      "fields": [
+      {
+        "title": "Sync Status",
+        "value": "{{.app.status.sync.status}}",
+        "short": true
+      },
+      {
+        "title": "Repository",
+        "value": "{{.app.spec.source.repoURL}}",
+        "short": true
+      },
+      {
+        "title": "Revision",
+        "value": "{{.app.status.sync.revision}}",
+        "short": true
+      }
+      {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "title": "{{$c.type}}",
+        "value": "{{$c.message}}",
+        "short": true
+      }
+      {{end}}
+      ]
+    }]
+  deliveryPolicy: Post
+  groupingKey: ""
+  notifyBroadcast: false
+teams:
+  facts: |
+    [{
+      "name": "Sync Status",
+      "value": "{{.app.status.sync.status}}"
+    },
+    {
+      "name": "Repository",
+      "value": "{{.app.spec.source.repoURL}}"
+    },
+    {
+      "name": "Revision",
+      "value": "{{.app.status.sync.revision}}"
+    }
+    {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "name": "{{$c.type}}",
+        "value": "{{$c.message}}"
+      }
+    {{end}}
+    ]
+  potentialAction: |-
+    [{
+      "@type":"OpenUri",
+      "name":"Operation Application",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}"
+      }]
+    },
+    {
+      "@type":"OpenUri",
+      "name":"Open Repository",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
+      }]
+    }]
+  themeColor: '#000080'
+  title: New version of an application {{.app.metadata.name}} is up and running.`
+
+	notificationsConfig["template.app-health-degraded"] = `email:
+  subject: Application {{.app.metadata.name}} has degraded.
+message: |
+  {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} has degraded.
+  Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
+slack:
+  attachments: |
+    [{
+      "title": "{{ .app.metadata.name}}",
+      "title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
+      "color": "#f4c030",
+      "fields": [
+      {
+        "title": "Health Status",
+        "value": "{{.app.status.health.status}}",
+        "short": true
+      },
+      {
+        "title": "Repository",
+        "value": "{{.app.spec.source.repoURL}}",
+        "short": true
+      }
+      {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "title": "{{$c.type}}",
+        "value": "{{$c.message}}",
+        "short": true
+      }
+      {{end}}
+      ]
+    }]
+  deliveryPolicy: Post
+  groupingKey: ""
+  notifyBroadcast: false
+teams:
+  facts: |
+    [{
+      "name": "Health Status",
+      "value": "{{.app.status.health.status}}"
+    },
+    {
+      "name": "Repository",
+      "value": "{{.app.spec.source.repoURL}}"
+    }
+    {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "name": "{{$c.type}}",
+        "value": "{{$c.message}}"
+      }
+    {{end}}
+    ]
+  potentialAction: |
+    [{
+      "@type":"OpenUri",
+      "name":"Open Application",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}"
+      }]
+    },
+    {
+      "@type":"OpenUri",
+      "name":"Open Repository",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
+      }]
+    }]
+  themeColor: '#FF0000'
+  title: Application {{.app.metadata.name}} has degraded.`
+
+	notificationsConfig["template.app-sync-failed"] = `email:
+  subject: Failed to sync application {{.app.metadata.name}}.
+message: |
+  {{if eq .serviceType "slack"}}:exclamation:{{end}}  The sync operation of application {{.app.metadata.name}} has failed at {{.app.status.operationState.finishedAt}} with the following error: {{.app.status.operationState.message}}
+  Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
+slack:
+  attachments: |
+    [{
+      "title": "{{ .app.metadata.name}}",
+      "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
+      "color": "#E96D76",
+      "fields": [
+      {
+        "title": "Sync Status",
+        "value": "{{.app.status.sync.status}}",
+        "short": true
+      },
+      {
+        "title": "Repository",
+        "value": "{{.app.spec.source.repoURL}}",
+        "short": true
+      }
+      {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "title": "{{$c.type}}",
+        "value": "{{$c.message}}",
+        "short": true
+      }
+      {{end}}
+      ]
+    }]
+  deliveryPolicy: Post
+  groupingKey: ""
+  notifyBroadcast: false
+teams:
+  facts: |
+    [{
+      "name": "Sync Status",
+      "value": "{{.app.status.sync.status}}"
+    },
+    {
+      "name": "Failed at",
+      "value": "{{.app.status.operationState.finishedAt}}"
+    },
+    {
+      "name": "Repository",
+      "value": "{{.app.spec.source.repoURL}}"
+    }
+    {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "name": "{{$c.type}}",
+        "value": "{{$c.message}}"
+      }
+    {{end}}
+    ]
+  potentialAction: |-
+    [{
+      "@type":"OpenUri",
+      "name":"Open Operation",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
+      }]
+    },
+    {
+      "@type":"OpenUri",
+      "name":"Open Repository",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
+      }]
+    }]
+  themeColor: '#FF0000'
+  title: Failed to sync application {{.app.metadata.name}}.`
+
+	notificationsConfig["template.app-sync-running"] = `email:
+  subject: Start syncing application {{.app.metadata.name}}.
+message: |
+  The sync operation of application {{.app.metadata.name}} has started at {{.app.status.operationState.startedAt}}.
+  Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
+slack:
+  attachments: |
+    [{
+      "title": "{{ .app.metadata.name}}",
+      "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
+      "color": "#0DADEA",
+      "fields": [
+      {
+        "title": "Sync Status",
+        "value": "{{.app.status.sync.status}}",
+        "short": true
+      },
+      {
+        "title": "Repository",
+        "value": "{{.app.spec.source.repoURL}}",
+        "short": true
+      }
+      {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "title": "{{$c.type}}",
+        "value": "{{$c.message}}",
+        "short": true
+      }
+      {{end}}
+      ]
+    }]
+  deliveryPolicy: Post
+  groupingKey: ""
+  notifyBroadcast: false
+teams:
+  facts: |
+    [{
+      "name": "Sync Status",
+      "value": "{{.app.status.sync.status}}"
+    },
+    {
+      "name": "Started at",
+      "value": "{{.app.status.operationState.startedAt}}"
+    },
+    {
+      "name": "Repository",
+      "value": "{{.app.spec.source.repoURL}}"
+    }
+    {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "name": "{{$c.type}}",
+        "value": "{{$c.message}}"
+      }
+    {{end}}
+    ]
+  potentialAction: |-
+    [{
+      "@type":"OpenUri",
+      "name":"Open Operation",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
+      }]
+    },
+    {
+      "@type":"OpenUri",
+      "name":"Open Repository",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
+      }]
+    }]
+  title: Start syncing application {{.app.metadata.name}}.`
+
+	notificationsConfig["template.app-sync-status-unknown"] = `email:
+  subject: Application {{.app.metadata.name}} sync status is 'Unknown'
+message: |
+  {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} sync is 'Unknown'.
+  Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
+  {{if ne .serviceType "slack"}}
+  {{range $c := .app.status.conditions}}
+      * {{$c.message}}
+  {{end}}
+  {{end}}
+slack:
+  attachments: |
+    [{
+      "title": "{{ .app.metadata.name}}",
+      "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
+      "color": "#E96D76",
+      "fields": [
+      {
+        "title": "Sync Status",
+        "value": "{{.app.status.sync.status}}",
+        "short": true
+      },
+      {
+        "title": "Repository",
+        "value": "{{.app.spec.source.repoURL}}",
+        "short": true
+      }
+      {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "title": "{{$c.type}}",
+        "value": "{{$c.message}}",
+        "short": true
+      }
+      {{end}}
+      ]
+    }]
+  deliveryPolicy: Post
+  groupingKey: ""
+  notifyBroadcast: false
+teams:
+  facts: |
+    [{
+      "name": "Sync Status",
+      "value": "{{.app.status.sync.status}}"
+    },
+    {
+      "name": "Repository",
+      "value": "{{.app.spec.source.repoURL}}"
+    }
+    {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "name": "{{$c.type}}",
+        "value": "{{$c.message}}"
+      }
+    {{end}}
+    ]
+  potentialAction: |-
+    [{
+      "@type":"OpenUri",
+      "name":"Open Application",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}"
+      }]
+    },
+    {
+      "@type":"OpenUri",
+      "name":"Open Repository",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
+      }]
+    }]
+  title: Application {{.app.metadata.name}} sync status is 'Unknown'`
+
+	notificationsConfig["template.app-sync-succeeded"] = `email:
+  subject: Application {{.app.metadata.name}} has been successfully synced.
+message: |
+  {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}.
+  Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
+slack:
+  attachments: |
+    [{
+      "title": "{{ .app.metadata.name}}",
+      "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
+      "color": "#18be52",
+      "fields": [
+      {
+        "title": "Sync Status",
+        "value": "{{.app.status.sync.status}}",
+        "short": true
+      },
+      {
+        "title": "Repository",
+        "value": "{{.app.spec.source.repoURL}}",
+        "short": true
+      }
+      {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "title": "{{$c.type}}",
+        "value": "{{$c.message}}",
+        "short": true
+      }
+      {{end}}
+      ]
+    }]
+  deliveryPolicy: Post
+  groupingKey: ""
+  notifyBroadcast: false
+teams:
+  facts: |
+    [{
+      "name": "Sync Status",
+      "value": "{{.app.status.sync.status}}"
+    },
+    {
+      "name": "Synced at",
+      "value": "{{.app.status.operationState.finishedAt}}"
+    },
+    {
+      "name": "Repository",
+      "value": "{{.app.spec.source.repoURL}}"
+    }
+    {{range $index, $c := .app.status.conditions}}
+      {{if not $index}},{{end}}
+      {{if $index}},{{end}}
+      {
+        "name": "{{$c.type}}",
+        "value": "{{$c.message}}"
+      }
+    {{end}}
+    ]
+  potentialAction: |-
+    [{
+      "@type":"OpenUri",
+      "name":"Operation Details",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
+      }]
+    },
+    {
+      "@type":"OpenUri",
+      "name":"Open Repository",
+      "targets":[{
+        "os":"default",
+        "uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
+      }]
+    }]
+  themeColor: '#000080'
+  title: Application {{.app.metadata.name}} has been successfully synced`
+
+	// configure default notifications triggers
+
+	notificationsConfig["trigger.on-created"] = `- description: Application is created.
+  oncePer: app.metadata.name
+  send:
+  - app-created
+  when: "true"`
+
+	notificationsConfig["trigger.on-deleted"] = `- description: Application is deleted.
+  oncePer: app.metadata.name
+  send:
+  - app-deleted
+  when: app.metadata.deletionTimestamp != nil`
+
+	notificationsConfig["trigger.on-deployed"] = `- description: Application is synced and healthy. Triggered once per commit.
+  oncePer: app.status.operationState.syncResult.revision
+  send:
+  - app-deployed
+  when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status
+      == 'Healthy'`
+
+	notificationsConfig["trigger.on-health-degraded"] = `- description: Application has degraded
+  send:
+  - app-health-degraded
+  when: app.status.health.status == 'Degraded'`
+
+	notificationsConfig["trigger.on-sync-failed"] = `- description: Application syncing has failed
+  send:
+  - app-sync-failed
+  when: app.status.operationState.phase in ['Error', 'Failed']`
+
+	notificationsConfig["trigger.on-sync-running"] = `- description: Application is being synced
+  send:
+  - app-sync-running
+  when: app.status.operationState.phase in ['Running']`
+
+	notificationsConfig["trigger.on-sync-status-unknown"] = `- description: Application status is 'Unknown'
+  send:
+  - app-sync-status-unknown
+  when: app.status.sync.status == 'Unknown'`
+
+	notificationsConfig["trigger.on-sync-succeeded"] = `- description: Application syncing has succeeded
+  send:
+  - app-sync-succeeded
+  when: app.status.operationState.phase in ['Succeeded']`
+
+	return notificationsConfig
+}
+
+func (nr *NotificationsReconciler) GetNotificationsCommand() []string {
+
+	cmd := make([]string, 0)
+	cmd = append(cmd, "argocd-notifications")
+
+	cmd = append(cmd, "--loglevel")
+	cmd = append(cmd, util.GetLogLevel(nr.Instance.Spec.Notifications.LogLevel))
+
+	return cmd
+}
+
+// GetNotificationsResources will return the ResourceRequirements for the Notifications container.
+func (nr *NotificationsReconciler) GetNotificationsResources() corev1.ResourceRequirements {
+	resources := corev1.ResourceRequirements{}
+
+	// Allow override of resource requirements from CR
+	if nr.Instance.Spec.Notifications.Resources != nil {
+		resources = *nr.Instance.Spec.Notifications.Resources
+	}
+
+	return resources
+}
+
+func (nr *NotificationsReconciler) GetArgoCDNotificationsControllerReplicas() *int32 {
+	if nr.Instance.Spec.Notifications.Replicas != nil && *nr.Instance.Spec.Notifications.Replicas >= 0 {
+		return nr.Instance.Spec.Notifications.Replicas
+	}
+
+	return nil
+}


Test patch:
diff --git a/controllers/argocd/argocdcommon/testing.go b/controllers/argocd/argocdcommon/testing.go
new file mode 100644
index 00000000..52b94fc1
--- /dev/null
+++ b/controllers/argocd/argocdcommon/testing.go
@@ -0,0 +1,44 @@
+package argocdcommon
+
+import (
+	"github.com/argoproj-labs/argocd-operator/api/v1alpha1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	TestNamespace  = "argocd"
+	TestArgoCDName = "argocd"
+)
+
+func MakeTestNamespace() *corev1.Namespace {
+	return &corev1.Namespace{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: TestNamespace,
+		},
+	}
+}
+func MakeTestServiceAccount() *corev1.ServiceAccount {
+	return &corev1.ServiceAccount{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      TestArgoCDName,
+			Namespace: TestNamespace,
+		},
+		Secrets: []corev1.ObjectReference{},
+	}
+}
+
+type argoCDOpt func(*v1alpha1.ArgoCD)
+
+func MakeTestArgoCD(opts ...argoCDOpt) *v1alpha1.ArgoCD {
+	a := &v1alpha1.ArgoCD{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      TestArgoCDName,
+			Namespace: TestNamespace,
+		},
+	}
+	for _, o := range opts {
+		o(a)
+	}
+	return a
+}
diff --git a/controllers/argocd/notifications/configmap_test.go b/controllers/argocd/notifications/configmap_test.go
new file mode 100644
index 00000000..1366c948
--- /dev/null
+++ b/controllers/argocd/notifications/configmap_test.go
@@ -0,0 +1,79 @@
+package notifications
+
+import (
+	"context"
+	"testing"
+
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/stretchr/testify/assert"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestNotificationsReconciler_reconcileConfigMap(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a configMap",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			err := nr.reconcileConfigMap()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+
+			currentConfigMap := &corev1.ConfigMap{}
+			err = nr.Client.Get(context.TODO(), types.NamespacedName{Name: NotificationsConfigMapName, Namespace: argocdcommon.TestNamespace}, currentConfigMap)
+			if err != nil {
+				t.Fatalf("Could not get current ConfigMap: %v", err)
+			}
+			assert.Equal(t, GetDefaultNotificationsConfig(), currentConfigMap.Data)
+		})
+	}
+}
+
+func TestNotificationsReconciler_DeleteConfigMap(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			if err := nr.DeleteConfigMap(ns.Name); (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+		})
+	}
+}
diff --git a/controllers/argocd/notifications/deployment_test.go b/controllers/argocd/notifications/deployment_test.go
new file mode 100644
index 00000000..cd093558
--- /dev/null
+++ b/controllers/argocd/notifications/deployment_test.go
@@ -0,0 +1,94 @@
+package notifications
+
+import (
+	"context"
+	"testing"
+
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/stretchr/testify/assert"
+
+	appsv1 "k8s.io/api/apps/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestNotificationsReconciler_reconcileDeployment(t *testing.T) {
+	resourceName = argocdcommon.TestArgoCDName
+	resourceLabels = testExpectedLabels
+	ns := argocdcommon.MakeTestNamespace()
+	nr := makeTestNotificationsReconciler(t, ns)
+
+	existingDeployment := nr.getDesiredDeployment()
+
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a deployment",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+		{
+			name: "update a deployment",
+			setupClient: func() *NotificationsReconciler {
+				outdatedDeployment := existingDeployment
+				outdatedDeployment.ObjectMeta.Labels = testKVP
+				return makeTestNotificationsReconciler(t, outdatedDeployment, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			err := nr.reconcileDeployment()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+
+			updatedDeployment := &appsv1.Deployment{}
+			err = nr.Client.Get(context.TODO(), types.NamespacedName{Name: resourceName, Namespace: argocdcommon.TestNamespace}, updatedDeployment)
+			if err != nil {
+				t.Fatalf("Could not get updated Deployment: %v", err)
+			}
+			assert.Equal(t, testExpectedLabels, updatedDeployment.ObjectMeta.Labels)
+		})
+	}
+}
+
+func TestNotificationsReconciler_DeleteDeployment(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			if err := nr.DeleteDeployment(resourceName, ns.Name); (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+		})
+	}
+}
diff --git a/controllers/argocd/notifications/notifications_test.go b/controllers/argocd/notifications/notifications_test.go
new file mode 100644
index 00000000..2caffdf1
--- /dev/null
+++ b/controllers/argocd/notifications/notifications_test.go
@@ -0,0 +1,117 @@
+package notifications
+
+import (
+	"testing"
+
+	"github.com/argoproj-labs/argocd-operator/api/v1alpha1"
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/stretchr/testify/assert"
+	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/client-go/kubernetes/scheme"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+)
+
+var (
+	testKey     = "test"
+	testVal     = "test"
+	testRoleRef = rbacv1.RoleRef{
+		Kind:     common.RoleKind,
+		Name:     argocdcommon.TestArgoCDName,
+		APIGroup: rbacv1.GroupName,
+	}
+
+	testSubjects = []rbacv1.Subject{
+		{
+			Kind:      rbacv1.ServiceAccountKind,
+			Name:      argocdcommon.TestArgoCDName,
+			Namespace: argocdcommon.TestNamespace,
+		},
+	}
+
+	testKVP = map[string]string{
+		testKey: testVal,
+	}
+
+	testExpectedLabels = common.DefaultLabels(argocdcommon.TestArgoCDName, argocdcommon.TestNamespace, ArgoCDNotificationsControllerComponent)
+)
+
+func makeTestNotificationsReconciler(t *testing.T, objs ...runtime.Object) *NotificationsReconciler {
+	s := scheme.Scheme
+	assert.NoError(t, v1alpha1.AddToScheme(s))
+
+	cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
+	logger := ctrl.Log.WithName(ArgoCDNotificationsControllerComponent)
+
+	return &NotificationsReconciler{
+		Client:   cl,
+		Scheme:   s,
+		Instance: argocdcommon.MakeTestArgoCD(),
+		Logger:   logger,
+	}
+}
+
+func TestNotificationsReconciler_Reconcile(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name         string
+		resourceName string
+		setupClient  func() *NotificationsReconciler
+		wantErr      bool
+	}{
+		{
+			name:         "successful reconcile",
+			resourceName: argocdcommon.TestArgoCDName,
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		nr := tt.setupClient()
+		err := nr.Reconcile()
+		assert.NoError(t, err)
+		if (err != nil) != tt.wantErr {
+			if tt.wantErr {
+				t.Errorf("Expected error but did not get one")
+			} else {
+				t.Errorf("Unexpected error: %v", err)
+			}
+		}
+	}
+}
+
+func TestNotificationsReconciler_DeleteResources(t *testing.T) {
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name         string
+		resourceName string
+		setupClient  func() *NotificationsReconciler
+		wantErr      bool
+	}{
+		{
+			name:         "successful delete",
+			resourceName: argocdcommon.TestArgoCDName,
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			if err := nr.DeleteResources(); (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+		})
+	}
+}
diff --git a/controllers/argocd/notifications/role_test.go b/controllers/argocd/notifications/role_test.go
new file mode 100644
index 00000000..5c052c93
--- /dev/null
+++ b/controllers/argocd/notifications/role_test.go
@@ -0,0 +1,102 @@
+package notifications
+
+import (
+	"context"
+	"testing"
+
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/stretchr/testify/assert"
+	rbacv1 "k8s.io/api/rbac/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestNotificationsReconciler_reconcileRole(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	existingRole := &rbacv1.Role{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       common.RoleKind,
+			APIVersion: common.APIGroupVersionRbacV1,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      argocdcommon.TestArgoCDName,
+			Namespace: argocdcommon.TestNamespace,
+		},
+		Rules: getPolicyRules(),
+	}
+
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a role",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+		{
+			name: "Update a role",
+			setupClient: func() *NotificationsReconciler {
+				outdatedRole := existingRole
+				outdatedRole.Rules = []rbacv1.PolicyRule{}
+				return makeTestNotificationsReconciler(t, outdatedRole, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			err := nr.reconcileRole()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+
+			updatedRole := &rbacv1.Role{}
+			err = nr.Client.Get(context.TODO(), types.NamespacedName{Name: argocdcommon.TestArgoCDName, Namespace: argocdcommon.TestNamespace}, updatedRole)
+			if err != nil {
+				t.Fatalf("Could not get updated Role: %v", err)
+			}
+			assert.Equal(t, getPolicyRules(), updatedRole.Rules)
+		})
+	}
+}
+
+func TestNotificationsReconciler_DeleteRole(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			if err := nr.DeleteRole(resourceName, ns.Name); (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+		})
+	}
+}
diff --git a/controllers/argocd/notifications/rolebinding_test.go b/controllers/argocd/notifications/rolebinding_test.go
new file mode 100644
index 00000000..02f43c0d
--- /dev/null
+++ b/controllers/argocd/notifications/rolebinding_test.go
@@ -0,0 +1,103 @@
+package notifications
+
+import (
+	"context"
+	"testing"
+
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/stretchr/testify/assert"
+	rbacv1 "k8s.io/api/rbac/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestNotificationsReconciler_reconcileRoleBinding(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	sa := argocdcommon.MakeTestServiceAccount()
+	resourceName = argocdcommon.TestArgoCDName
+
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a rolebinding",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns, sa)
+			},
+			wantErr: false,
+		},
+		{
+			name: "update a rolebinding",
+			setupClient: func() *NotificationsReconciler {
+				outdatedRoleBinding := &rbacv1.RoleBinding{
+					TypeMeta: metav1.TypeMeta{
+						Kind:       common.RoleBindingKind,
+						APIVersion: common.APIGroupVersionRbacV1,
+					},
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      argocdcommon.TestArgoCDName,
+						Namespace: argocdcommon.TestNamespace,
+					},
+					RoleRef:  rbacv1.RoleRef{},
+					Subjects: []rbacv1.Subject{},
+				}
+				return makeTestNotificationsReconciler(t, outdatedRoleBinding, ns, sa)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			err := nr.reconcileRoleBinding()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+			updatedRoleBinding := &rbacv1.RoleBinding{}
+			err = nr.Client.Get(context.TODO(), types.NamespacedName{Name: argocdcommon.TestArgoCDName, Namespace: argocdcommon.TestNamespace}, updatedRoleBinding)
+			if err != nil {
+				t.Fatalf("Could not get updated RoleBinding: %v", err)
+			}
+			assert.Equal(t, testRoleRef, updatedRoleBinding.RoleRef)
+			assert.Equal(t, testSubjects, updatedRoleBinding.Subjects)
+		})
+	}
+}
+
+func TestNotificationsReconciler_DeleteRoleBinding(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	sa := argocdcommon.MakeTestServiceAccount()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns, sa)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			if err := nr.DeleteRoleBinding(resourceName, ns.Name); (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+		})
+	}
+}
diff --git a/controllers/argocd/notifications/secret_test.go b/controllers/argocd/notifications/secret_test.go
new file mode 100644
index 00000000..743f4782
--- /dev/null
+++ b/controllers/argocd/notifications/secret_test.go
@@ -0,0 +1,79 @@
+package notifications
+
+import (
+	"context"
+	"testing"
+
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/stretchr/testify/assert"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestNotificationsReconciler_reconcileSecret(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceLabels = testExpectedLabels
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a secret",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			err := nr.reconcileSecret()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+
+			currentSecret := &corev1.Secret{}
+			err = nr.Client.Get(context.TODO(), types.NamespacedName{Name: NotificationsSecretName, Namespace: argocdcommon.TestNamespace}, currentSecret)
+			if err != nil {
+				t.Fatalf("Could not get current Secret: %v", err)
+			}
+			assert.Equal(t, testExpectedLabels, currentSecret.ObjectMeta.Labels)
+		})
+	}
+}
+
+func TestNotificationsReconciler_DeleteSecret(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			if err := nr.DeleteSecret(ns.Name); (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+		})
+	}
+}
diff --git a/controllers/argocd/notifications/serviceaccount_test.go b/controllers/argocd/notifications/serviceaccount_test.go
new file mode 100644
index 00000000..426c1d71
--- /dev/null
+++ b/controllers/argocd/notifications/serviceaccount_test.go
@@ -0,0 +1,102 @@
+package notifications
+
+import (
+	"context"
+	"testing"
+
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/stretchr/testify/assert"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestNotificationsReconciler_reconcileServiceAccount(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	resourceLabels = testExpectedLabels
+	existingServiceAccount := &corev1.ServiceAccount{
+		TypeMeta: metav1.TypeMeta{
+			Kind:       common.ServiceAccountKind,
+			APIVersion: common.APIVersionV1,
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        argocdcommon.TestArgoCDName,
+			Namespace:   argocdcommon.TestNamespace,
+			Labels:      resourceLabels,
+			Annotations: map[string]string{},
+		},
+	}
+
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a serviceAccount",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+		{
+			name: "update a serviceAccount",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, existingServiceAccount, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			err := nr.reconcileServiceAccount()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+
+			currentServiceAccount := &corev1.ServiceAccount{}
+			err = nr.Client.Get(context.TODO(), types.NamespacedName{Name: argocdcommon.TestArgoCDName, Namespace: argocdcommon.TestNamespace}, currentServiceAccount)
+			if err != nil {
+				t.Fatalf("Could not get current ServiceAccount: %v", err)
+			}
+			assert.Equal(t, testExpectedLabels, currentServiceAccount.Labels)
+		})
+	}
+}
+
+func TestNotificationsReconciler_DeleteServiceAccount(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *NotificationsReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *NotificationsReconciler {
+				return makeTestNotificationsReconciler(t, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			if err := nr.DeleteServiceAccount(resourceName, ns.Name); (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+		})
+	}
+}
diff --git a/controllers/argocd/status_test.go b/controllers/argocd/status_test.go
index f146dc83..87a82263 100644
--- a/controllers/argocd/status_test.go
+++ b/controllers/argocd/status_test.go
@@ -270,7 +270,7 @@ func TestArgoCDReconciler_reconcileStatusNotificationsController(t *testing.T) {
 	assert.Equal(t, "Pending", a.Status.NotificationsController)
 
 	a.Spec.Notifications.Enabled = false
-	assert.NoError(t, r.NotificationsController.Reconcile())
+	assert.NoError(t, r.NotificationsController.DeleteResources())
 	assert.NoError(t, r.reconcileStatusNotifications(a))
 	assert.Equal(t, "", a.Status.NotificationsController)
 }
diff --git a/pkg/permissions/permissions_test.go b/pkg/permissions/permissions_test.go
index ddf34119..ccbdffaf 100644
--- a/pkg/permissions/permissions_test.go
+++ b/pkg/permissions/permissions_test.go
@@ -51,7 +51,7 @@ var (
 	}
 	testSubjects = []rbacv1.Subject{
 		{
-			Kind:      "ServiceAccount",
+			Kind:      rbacv1.ServiceAccountKind,
 			Name:      testName,
 			Namespace: testNamespace,
 		},
diff --git a/tests/controller/notifications/1-001_enable_notifications/00-assert.yaml b/tests/controller/notifications/1-001_enable_notifications/00-assert.yaml
new file mode 100644
index 00000000..8185706d
--- /dev/null
+++ b/tests/controller/notifications/1-001_enable_notifications/00-assert.yaml
@@ -0,0 +1,6 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
diff --git a/tests/controller/notifications/1-001_enable_notifications/00-errors.yaml b/tests/controller/notifications/1-001_enable_notifications/00-errors.yaml
new file mode 100644
index 00000000..edc10ee0
--- /dev/null
+++ b/tests/controller/notifications/1-001_enable_notifications/00-errors.yaml
@@ -0,0 +1,6 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  notificationsController:
diff --git a/tests/controller/notifications/1-001_enable_notifications/00-install.yaml b/tests/controller/notifications/1-001_enable_notifications/00-install.yaml
new file mode 100644
index 00000000..090703c4
--- /dev/null
+++ b/tests/controller/notifications/1-001_enable_notifications/00-install.yaml
@@ -0,0 +1,10 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  server:
+    route:
+      enabled: true
+  notifications:
+    enabled: false
diff --git a/tests/controller/notifications/1-001_enable_notifications/01-assert.yaml b/tests/controller/notifications/1-001_enable_notifications/01-assert.yaml
new file mode 100644
index 00000000..cc59369a
--- /dev/null
+++ b/tests/controller/notifications/1-001_enable_notifications/01-assert.yaml
@@ -0,0 +1,43 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+  notificationsController: Running
+---
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+kind: Secret
+apiVersion: v1
+metadata:
+  name: argocd-notifications-secret
+---
+kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: argocd-notifications-cm
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: example-argocd-notifications-controller
+status:
+  conditions:
+    - type: Available
+      status: 'True'
+    - type: Progressing
+      status: 'True'
diff --git a/tests/controller/notifications/1-001_enable_notifications/01-enable_notifications.yaml b/tests/controller/notifications/1-001_enable_notifications/01-enable_notifications.yaml
new file mode 100644
index 00000000..7ff25195
--- /dev/null
+++ b/tests/controller/notifications/1-001_enable_notifications/01-enable_notifications.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  notifications:
+    enabled: true
diff --git a/tests/controller/notifications/1-002_disable_notifications/00-assert-notifications-resources-generated.yaml b/tests/controller/notifications/1-002_disable_notifications/00-assert-notifications-resources-generated.yaml
new file mode 100644
index 00000000..cc59369a
--- /dev/null
+++ b/tests/controller/notifications/1-002_disable_notifications/00-assert-notifications-resources-generated.yaml
@@ -0,0 +1,43 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+  notificationsController: Running
+---
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+kind: Secret
+apiVersion: v1
+metadata:
+  name: argocd-notifications-secret
+---
+kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: argocd-notifications-cm
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: example-argocd-notifications-controller
+status:
+  conditions:
+    - type: Available
+      status: 'True'
+    - type: Progressing
+      status: 'True'
diff --git a/tests/controller/notifications/1-002_disable_notifications/00-install-argocd-with-notificationis-enabled.yaml b/tests/controller/notifications/1-002_disable_notifications/00-install-argocd-with-notificationis-enabled.yaml
new file mode 100644
index 00000000..7ff25195
--- /dev/null
+++ b/tests/controller/notifications/1-002_disable_notifications/00-install-argocd-with-notificationis-enabled.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  notifications:
+    enabled: true
diff --git a/tests/controller/notifications/1-002_disable_notifications/01-wait.yaml b/tests/controller/notifications/1-002_disable_notifications/01-wait.yaml
new file mode 100644
index 00000000..8df52df1
--- /dev/null
+++ b/tests/controller/notifications/1-002_disable_notifications/01-wait.yaml
@@ -0,0 +1,4 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+- script: kubectl wait --for=condition=Available deployment/example-argocd-notifications-controller -n $NAMESPACE --timeout=60s
diff --git a/tests/controller/notifications/1-002_disable_notifications/02-disable_notifications.yaml b/tests/controller/notifications/1-002_disable_notifications/02-disable_notifications.yaml
new file mode 100644
index 00000000..60a2e66f
--- /dev/null
+++ b/tests/controller/notifications/1-002_disable_notifications/02-disable_notifications.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  notifications:
+    enabled: false
\ No newline at end of file
diff --git a/tests/controller/notifications/1-002_disable_notifications/03-errors.yaml b/tests/controller/notifications/1-002_disable_notifications/03-errors.yaml
new file mode 100644
index 00000000..e0147f4b
--- /dev/null
+++ b/tests/controller/notifications/1-002_disable_notifications/03-errors.yaml
@@ -0,0 +1,36 @@
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+  name: example-argocd-notifications-controller
+---
+kind: Secret
+apiVersion: v1
+metadata:
+  name: argocd-notifications-secret
+---
+kind: ConfigMap
+apiVersion: v1
+metadata:
+  name: argocd-notifications-cm
+---
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+kind: Role
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+kind: RoleBinding
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: example-argocd-argocd-notifications-controller
+---
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  notificationsController:
diff --git a/tests/controller/notifications/1-003_validate_notifications/00-install-argocd-and-notification-service.yaml b/tests/controller/notifications/1-003_validate_notifications/00-install-argocd-and-notification-service.yaml
new file mode 100644
index 00000000..21bd2d10
--- /dev/null
+++ b/tests/controller/notifications/1-003_validate_notifications/00-install-argocd-and-notification-service.yaml
@@ -0,0 +1,56 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: argocd
+spec:
+  notifications:
+    enabled: true
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: smtp4dev
+spec:
+  selector:
+    app: smtp4dev
+  ports:
+    - name: smtp
+      protocol: TCP
+      port: 2525
+      targetPort: 2525
+    - name: http
+      protocol: TCP
+      port: 80
+      targetPort: 80
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: smtp4dev
+  labels:
+    app: smtp4dev
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: smtp4dev
+  template:
+    metadata:
+      labels:
+        app: smtp4dev
+    spec:
+      affinity:
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+              - matchExpressions:
+                  - key: kubernetes.io/os
+                    operator: In
+                    values:
+                      - linux
+      containers:
+        - name: smtp4dev
+          image: quay.io/argoprojlabs/argocd-notifications-e2e-smtplistener:multiarch
+          ports:
+            - containerPort: 80
+            - containerPort: 2525
\ No newline at end of file
diff --git a/tests/controller/notifications/1-003_validate_notifications/01-wait.yaml b/tests/controller/notifications/1-003_validate_notifications/01-wait.yaml
new file mode 100644
index 00000000..68ff472f
--- /dev/null
+++ b/tests/controller/notifications/1-003_validate_notifications/01-wait.yaml
@@ -0,0 +1,7 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+- script: |
+    set -e  # Exit if any command fails
+    kubectl wait --for=condition=Available deployment/argocd-notifications-controller --timeout=60s -n $NAMESPACE || { echo "argocd-notifications-controller is not available within 60s"; exit 1; }
+    kubectl wait --for=condition=Available deployment/smtp4dev --timeout=60s -n $NAMESPACE || { echo "smtp4dev is not available within 60s"; exit 1; }
diff --git a/tests/controller/notifications/1-003_validate_notifications/02-assert-patched-cm.yaml b/tests/controller/notifications/1-003_validate_notifications/02-assert-patched-cm.yaml
new file mode 100644
index 00000000..e3d017ba
--- /dev/null
+++ b/tests/controller/notifications/1-003_validate_notifications/02-assert-patched-cm.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: argocd-notifications-cm
+data:
+  service.email.gmail: "{host: smtp4dev, port: 2525, from: fake@email.com }"
diff --git a/tests/controller/notifications/1-003_validate_notifications/02-update-notifications-cm.yaml b/tests/controller/notifications/1-003_validate_notifications/02-update-notifications-cm.yaml
new file mode 100644
index 00000000..0d94ebbc
--- /dev/null
+++ b/tests/controller/notifications/1-003_validate_notifications/02-update-notifications-cm.yaml
@@ -0,0 +1,7 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+- script: |
+    set -e 
+
+    kubectl patch cm argocd-notifications-cm -n $NAMESPACE --type merge -p '{"data": {"service.email.gmail": "{host: smtp4dev, port: 2525, from: fake@email.com }" }}'
diff --git a/tests/controller/notifications/1-003_validate_notifications/03-create-an-app.yaml b/tests/controller/notifications/1-003_validate_notifications/03-create-an-app.yaml
new file mode 100644
index 00000000..15774862
--- /dev/null
+++ b/tests/controller/notifications/1-003_validate_notifications/03-create-an-app.yaml
@@ -0,0 +1,30 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+- script: |
+    set -e
+
+    cat << EOF | kubectl apply -f -
+    apiVersion: argoproj.io/v1alpha1
+    kind: Application
+    metadata:
+      name: my-app-3
+      namespace: $NAMESPACE
+      annotations:
+        "notifications.argoproj.io/subscribe.on-created.gmail": "jdfake@email.com"
+    spec:
+      destination:
+        namespace: $NAMESPACE
+        server: https://kubernetes.default.svc
+      project: default
+      source:
+        repoURL: https://github.com/redhat-developer/gitops-operator
+        path: test/examples/nginx
+        targetRevision: HEAD
+    EOF
+
+    # Loop to wait for Application to be synced and healthy
+    while [[ "$(kubectl get application my-app-3 -n $NAMESPACE -o jsonpath='{.status.sync.status}')" != "Synced" ]] || [[ "$(kubectl get application my-app-3 -n $NAMESPACE -o jsonpath='{.status.health.status}')" != "Healthy" ]]; do
+      echo "Waiting for Application to be Synced and Healthy..."
+      sleep 5
+    done
\ No newline at end of file
diff --git a/tests/controller/notifications/1-003_validate_notifications/04-delete-the-app.yaml b/tests/controller/notifications/1-003_validate_notifications/04-delete-the-app.yaml
new file mode 100644
index 00000000..6bfad73d
--- /dev/null
+++ b/tests/controller/notifications/1-003_validate_notifications/04-delete-the-app.yaml
@@ -0,0 +1,13 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+- script: | 
+    set -e 
+  
+    kubectl delete -n $NAMESPACE application.argoproj.io my-app-3
+
+    # Loop to wait for Application to be deleted
+    while kubectl get application my-app-3 -n $NAMESPACE --ignore-not-found; do
+      echo "Waiting for Application to be deleted..."
+      sleep 5
+    done
diff --git a/tests/controller/notifications/1-003_validate_notifications/05-verify-email.yaml b/tests/controller/notifications/1-003_validate_notifications/05-verify-email.yaml
new file mode 100644
index 00000000..239621b0
--- /dev/null
+++ b/tests/controller/notifications/1-003_validate_notifications/05-verify-email.yaml
@@ -0,0 +1,17 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+- script: | 
+    set -e
+    smtp4dev_pod=$(kubectl get pod -l=app=smtp4dev -o NAME -n $NAMESPACE)
+    exit_code=$(kubectl -n $NAMESPACE exec -i --stdin "${smtp4dev_pod}" -- /bin/bash \
+    -c 'if [[ $(grep -rnw /tmp -e "Subject: Application my-app-3 has been created.") ]]; then
+    exit 0; else
+    exit 1;
+    fi')
+
+    if [ $exit_code=0 ]; then
+        exit 0
+    else
+        exit 1
+    fi
\ No newline at end of file