Repository: argoproj-labs/argocd-operator
Pull request #1011 (2023-09-29 19:10:58)

code-refactoring: Gitops 3127 implement applicationset controller package
**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-3127: https://issues.redhat.com/browse/GITOPS-3127

**How to test changes / Special notes to the reviewer**:
Unit tests and kuttl tests uploaded.

jaideepr97: @ciiay if all the old code has been accounted for, please remove those files 
`controllers/argocd/applicationset.go` and `controllers/argocd/applicationset_test.go`

Patch:
diff --git a/common/defaults.go b/common/defaults.go
index 45136803..bfb710e9 100644
--- a/common/defaults.go
+++ b/common/defaults.go
@@ -249,7 +249,7 @@ const (
 	ArgoCDDefaultArgoImage = "quay.io/argoproj/argocd"
 
 	// ArgoCDDefaultArgoVersion is the Argo CD container image digest to use when version not specified.
-	ArgoCDDefaultArgoVersion = "sha256:7daba5f38b23f4f091951b727db6f87dc04ad396fd21044401502438d633836e" // v2.7.6
+	ArgoCDDefaultArgoVersion = "sha256:d40da8f5747415eb7f9b5c2d9b645aecd423888cad9b36e4f986bff8ecf0a786" // v2.8.3
 
 	// ArgoCDDefaultBackupKeyLength is the length of the generated default backup key.
 	ArgoCDDefaultBackupKeyLength = 32
diff --git a/common/values.go b/common/values.go
index 758cb654..fc7c9706 100644
--- a/common/values.go
+++ b/common/values.go
@@ -39,11 +39,12 @@ const (
 	// 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"
 	// ArgoCDAppSetGitlabSCMTLSCertsConfigMapName is the hard-coded ApplicationSet Gitlab SCM TLS certificate data ConfigMap name.
 	ArgoCDAppSetGitlabSCMTLSCertsConfigMapName = "argocd-appset-gitlab-scm-tls-certs-cm"
 
+	// K8sOSLinux is the value for kubernetes.io/os key for linux pods
+	K8sOSLinux = "linux"
+
 	// ArgoCDRedisServerTLSSecretName is the name of the TLS secret for the redis-server
 	ArgoCDRedisServerTLSSecretName = "argocd-operator-redis-tls"
 
@@ -62,6 +63,15 @@ const (
 	VolumeMountPathTLS           = "/app/config/tls"
 	VolumeMountPathRepoServerTLS = "/app/config/reposerver/tls"
 	WorkingDirApp                = "/app"
+	Webhook                      = "webhook"
+	SSHKnownHosts                = "ssh-known-hosts"
+	VolumeMountPathSSH           = "/app/config/ssh"
+	GPGKeys                      = "gpg-keys"
+	VolumeMountPathGPG           = "/app/config/gpg/source"
+	GPGKeyRing                   = "gpg-keyring"
+	VolumeMountPathGPGKeyring    = "/app/config/gpg/keys"
+	VolumeTmp                    = "tmp"
+	VolumeMountPathTmp           = "/tmp"
 )
 
 // API group versions and resource kinds
@@ -75,5 +85,11 @@ const (
 	RoleBindingKind    = "RoleBinding"
 	ConfigMapKind      = "ConfigMap"
 	SecretKind         = "Secret"
+	ServiceKind        = "Service"
 	ServiceAccountKind = "ServiceAccount"
 )
+
+// Commnds
+const (
+	LogLevel = "--loglevel"
+)
diff --git a/controllers/argocd/applicationset.go b/controllers/argocd/applicationset.go
deleted file mode 100644
index 82e30fd0..00000000
--- a/controllers/argocd/applicationset.go
+++ /dev/null
@@ -1,557 +0,0 @@
-// Copyright 2021 ArgoCD Operator Developers
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// 	http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package argocd
-
-import (
-	"context"
-	"fmt"
-	"os"
-	"reflect"
-
-	corev1 "k8s.io/api/core/v1"
-	v1 "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"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-
-	argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1"
-	"github.com/argoproj-labs/argocd-operator/common"
-	"github.com/argoproj-labs/argocd-operator/pkg/mutation/openshift"
-	"github.com/argoproj-labs/argocd-operator/pkg/util"
-)
-
-const (
-	ApplicationSetGitlabSCMTlsCertPath = "/app/tls/scm/cert"
-)
-
-// getArgoApplicationSetCommand will return the command for the ArgoCD ApplicationSet component.
-func getArgoApplicationSetCommand(cr *argoproj.ArgoCD) []string {
-	cmd := make([]string, 0)
-
-	cmd = append(cmd, "entrypoint.sh")
-	cmd = append(cmd, "argocd-applicationset-controller")
-
-	cmd = append(cmd, "--argocd-repo-server")
-	cmd = append(cmd, getRepoServerAddress(cr))
-
-	cmd = append(cmd, "--loglevel")
-	cmd = append(cmd, util.GetLogLevel(cr.Spec.ApplicationSet.LogLevel))
-
-	if cr.Spec.ApplicationSet.SCMRootCAConfigMap != "" {
-		cmd = append(cmd, "--scm-root-ca-path")
-		cmd = append(cmd, ApplicationSetGitlabSCMTlsCertPath)
-	}
-
-	// ApplicationSet command arguments provided by the user
-	extraArgs := cr.Spec.ApplicationSet.ExtraCommandArgs
-	err := isMergable(extraArgs, cmd)
-	if err != nil {
-		return cmd
-	}
-
-	cmd = append(cmd, extraArgs...)
-
-	return cmd
-}
-
-func (r *ArgoCDReconciler) reconcileApplicationSetController(cr *argoproj.ArgoCD) error {
-
-	log.Info("reconciling applicationset serviceaccounts")
-	sa, err := r.reconcileApplicationSetServiceAccount(cr)
-	if err != nil {
-		return err
-	}
-
-	log.Info("reconciling applicationset roles")
-	role, err := r.reconcileApplicationSetRole(cr)
-	if err != nil {
-		return err
-	}
-
-	log.Info("reconciling applicationset role bindings")
-	if err := r.reconcileApplicationSetRoleBinding(cr, role, sa); err != nil {
-		return err
-	}
-
-	log.Info("reconciling applicationset deployments")
-	if err := r.reconcileApplicationSetDeployment(cr, sa); err != nil {
-		return err
-	}
-
-	log.Info("reconciling applicationset service")
-	if err := r.reconcileApplicationSetService(cr); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// reconcileApplicationControllerDeployment will ensure the Deployment resource is present for the ArgoCD Application Controller component.
-func (r *ArgoCDReconciler) reconcileApplicationSetDeployment(cr *argoproj.ArgoCD, sa *corev1.ServiceAccount) error {
-	deploy := newDeploymentWithSuffix("applicationset-controller", "controller", cr)
-
-	setAppSetLabels(&deploy.ObjectMeta)
-
-	podSpec := &deploy.Spec.Template.Spec
-
-	podSpec.ServiceAccountName = sa.ObjectMeta.Name
-
-	podSpec.Volumes = []corev1.Volume{
-		{
-			Name: "ssh-known-hosts",
-			VolumeSource: corev1.VolumeSource{
-				ConfigMap: &corev1.ConfigMapVolumeSource{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: common.ArgoCDKnownHostsConfigMapName,
-					},
-				},
-			},
-		},
-		{
-			Name: "tls-certs",
-			VolumeSource: corev1.VolumeSource{
-				ConfigMap: &corev1.ConfigMapVolumeSource{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: common.ArgoCDTLSCertsConfigMapName,
-					},
-				},
-			},
-		},
-		{
-			Name: "gpg-keys",
-			VolumeSource: corev1.VolumeSource{
-				ConfigMap: &corev1.ConfigMapVolumeSource{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: common.ArgoCDGPGKeysConfigMapName,
-					},
-				},
-			},
-		},
-		{
-			Name: "gpg-keyring",
-			VolumeSource: corev1.VolumeSource{
-				EmptyDir: &corev1.EmptyDirVolumeSource{},
-			},
-		},
-		{
-			Name: "tmp",
-			VolumeSource: corev1.VolumeSource{
-				EmptyDir: &corev1.EmptyDirVolumeSource{},
-			},
-		},
-	}
-	addSCMGitlabVolumeMount := false
-	if scmRootCAConfigMapName := getSCMRootCAConfigMapName(cr); scmRootCAConfigMapName != "" {
-		cm := newConfigMapWithName(scmRootCAConfigMapName, cr)
-		if util.IsObjectFound(r.Client, cr.Namespace, cr.Spec.ApplicationSet.SCMRootCAConfigMap, cm) {
-			addSCMGitlabVolumeMount = true
-			podSpec.Volumes = append(podSpec.Volumes, corev1.Volume{
-				Name: "appset-gitlab-scm-tls-cert",
-				VolumeSource: corev1.VolumeSource{
-					ConfigMap: &corev1.ConfigMapVolumeSource{
-						LocalObjectReference: corev1.LocalObjectReference{
-							Name: common.ArgoCDAppSetGitlabSCMTLSCertsConfigMapName,
-						},
-					},
-				},
-			})
-		}
-	}
-
-	podSpec.Containers = []corev1.Container{
-		applicationSetContainer(cr, addSCMGitlabVolumeMount),
-	}
-	openshift.AddSeccompProfileForOpenShift(cr, podSpec, r.Client)
-
-	if existing := newDeploymentWithSuffix("applicationset-controller", "controller", cr); util.IsObjectFound(r.Client, cr.Namespace, existing.Name, existing) {
-
-		existingSpec := existing.Spec.Template.Spec
-
-		deploymentsDifferent := !reflect.DeepEqual(existingSpec.Containers[0], podSpec.Containers) ||
-			!reflect.DeepEqual(existingSpec.Volumes, podSpec.Volumes) ||
-			existingSpec.ServiceAccountName != podSpec.ServiceAccountName ||
-			!reflect.DeepEqual(existing.Labels, deploy.Labels) ||
-			!reflect.DeepEqual(existing.Spec.Template.Labels, deploy.Spec.Template.Labels) ||
-			!reflect.DeepEqual(existing.Spec.Selector, deploy.Spec.Selector) ||
-			!reflect.DeepEqual(existing.Spec.Template.Spec.NodeSelector, deploy.Spec.Template.Spec.NodeSelector) ||
-			!reflect.DeepEqual(existing.Spec.Template.Spec.Tolerations, deploy.Spec.Template.Spec.Tolerations)
-
-		// If the Deployment already exists, make sure the values we care about are up-to-date
-		if deploymentsDifferent {
-			existing.Spec.Template.Spec.Containers = podSpec.Containers
-			existing.Spec.Template.Spec.Volumes = podSpec.Volumes
-			existing.Spec.Template.Spec.ServiceAccountName = podSpec.ServiceAccountName
-			existing.Labels = deploy.Labels
-			existing.Spec.Template.Labels = deploy.Spec.Template.Labels
-			existing.Spec.Selector = deploy.Spec.Selector
-			existing.Spec.Template.Spec.NodeSelector = deploy.Spec.Template.Spec.NodeSelector
-			existing.Spec.Template.Spec.Tolerations = deploy.Spec.Template.Spec.Tolerations
-			return r.Client.Update(context.TODO(), existing)
-		}
-		return nil // Deployment found with nothing to do, move along...
-	}
-
-	if err := controllerutil.SetControllerReference(cr, deploy, r.Scheme); err != nil {
-		return err
-	}
-	return r.Client.Create(context.TODO(), deploy)
-
-}
-
-func applicationSetContainer(cr *argoproj.ArgoCD, addSCMGitlabVolumeMount bool) corev1.Container {
-	// Global proxy env vars go first
-	appSetEnv := []corev1.EnvVar{{
-		Name: "NAMESPACE",
-		ValueFrom: &corev1.EnvVarSource{
-			FieldRef: &corev1.ObjectFieldSelector{
-				FieldPath: "metadata.namespace",
-			},
-		},
-	}}
-
-	// Merge ApplicationSet env vars provided by the user
-	// User should be able to override the default NAMESPACE environmental variable
-	appSetEnv = util.EnvMerge(cr.Spec.ApplicationSet.Env, appSetEnv, true)
-	// Environment specified in the CR take precedence over everything else
-	appSetEnv = util.EnvMerge(appSetEnv, util.ProxyEnvVars(), false)
-
-	container := corev1.Container{
-		Command:         getArgoApplicationSetCommand(cr),
-		Env:             appSetEnv,
-		Image:           getApplicationSetContainerImage(cr),
-		ImagePullPolicy: corev1.PullAlways,
-		Name:            "argocd-applicationset-controller",
-		Resources:       getApplicationSetResources(cr),
-		VolumeMounts: []corev1.VolumeMount{
-			{
-				Name:      "ssh-known-hosts",
-				MountPath: "/app/config/ssh",
-			},
-			{
-				Name:      "tls-certs",
-				MountPath: "/app/config/tls",
-			},
-			{
-				Name:      "gpg-keys",
-				MountPath: "/app/config/gpg/source",
-			},
-			{
-				Name:      "gpg-keyring",
-				MountPath: "/app/config/gpg/keys",
-			},
-			{
-				Name:      "tmp",
-				MountPath: "/tmp",
-			},
-		},
-		Ports: []corev1.ContainerPort{
-			{
-				ContainerPort: 7000,
-				Name:          "webhook",
-			},
-			{
-				ContainerPort: 8080,
-				Name:          "metrics",
-			},
-		},
-		SecurityContext: &corev1.SecurityContext{
-			Capabilities: &corev1.Capabilities{
-				Drop: []corev1.Capability{
-					"ALL",
-				},
-			},
-			AllowPrivilegeEscalation: util.BoolPtr(false),
-			ReadOnlyRootFilesystem:   util.BoolPtr(true),
-			RunAsNonRoot:             util.BoolPtr(true),
-		},
-	}
-	if addSCMGitlabVolumeMount {
-		container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
-			Name:      "appset-gitlab-scm-tls-cert",
-			MountPath: ApplicationSetGitlabSCMTlsCertPath,
-		})
-	}
-	return container
-}
-
-func (r *ArgoCDReconciler) reconcileApplicationSetServiceAccount(cr *argoproj.ArgoCD) (*corev1.ServiceAccount, error) {
-
-	sa := newServiceAccountWithName("applicationset-controller", cr)
-	setAppSetLabels(&sa.ObjectMeta)
-
-	exists := true
-	if err := util.FetchObject(r.Client, cr.Namespace, sa.Name, sa); err != nil {
-		if !errors.IsNotFound(err) {
-			return nil, err
-		}
-		exists = false
-	}
-
-	if exists {
-		return sa, nil
-	}
-
-	if err := controllerutil.SetControllerReference(cr, sa, r.Scheme); err != nil {
-		return nil, err
-	}
-
-	err := r.Client.Create(context.TODO(), sa)
-	if err != nil {
-		return nil, err
-	}
-
-	return sa, err
-}
-
-func (r *ArgoCDReconciler) reconcileApplicationSetRole(cr *argoproj.ArgoCD) (*v1.Role, error) {
-
-	policyRules := []v1.PolicyRule{
-
-		// ApplicationSet
-		{
-			APIGroups: []string{"argoproj.io"},
-			Resources: []string{
-				"applications",
-				"applicationsets",
-				"appprojects",
-				"applicationsets/finalizers",
-			},
-			Verbs: []string{
-				"create",
-				"delete",
-				"get",
-				"list",
-				"patch",
-				"update",
-				"watch",
-			},
-		},
-		// ApplicationSet Status
-		{
-			APIGroups: []string{"argoproj.io"},
-			Resources: []string{
-				"applicationsets/status",
-			},
-			Verbs: []string{
-				"get",
-				"patch",
-				"update",
-			},
-		},
-
-		// Events
-		{
-			APIGroups: []string{""},
-			Resources: []string{
-				"events",
-			},
-			Verbs: []string{
-				"create",
-				"delete",
-				"get",
-				"list",
-				"patch",
-				"update",
-				"watch",
-			},
-		},
-
-		// Read Secrets/ConfigMaps
-		{
-			APIGroups: []string{""},
-			Resources: []string{
-				"secrets",
-				"configmaps",
-			},
-			Verbs: []string{
-				"get",
-				"list",
-				"watch",
-			},
-		},
-
-		// Read Deployments
-		{
-			APIGroups: []string{"apps", "extensions"},
-			Resources: []string{
-				"deployments",
-			},
-			Verbs: []string{
-				"get",
-				"list",
-				"watch",
-			},
-		},
-	}
-
-	role := newRole("applicationset-controller", policyRules, cr)
-	setAppSetLabels(&role.ObjectMeta)
-
-	err := r.Client.Get(context.TODO(), types.NamespacedName{Name: role.Name, Namespace: cr.Namespace}, role)
-	if err != nil {
-		if !errors.IsNotFound(err) {
-			return nil, fmt.Errorf("failed to reconcile the role for the service account associated with %s : %s", role.Name, err)
-		}
-		if err = controllerutil.SetControllerReference(cr, role, r.Scheme); err != nil {
-			return nil, err
-		}
-		return role, r.Client.Create(context.TODO(), role)
-	}
-
-	role.Rules = policyRules
-	if err = controllerutil.SetControllerReference(cr, role, r.Scheme); err != nil {
-		return nil, err
-	}
-	return role, r.Client.Update(context.TODO(), role)
-}
-
-func (r *ArgoCDReconciler) reconcileApplicationSetRoleBinding(cr *argoproj.ArgoCD, role *v1.Role, sa *corev1.ServiceAccount) error {
-
-	name := "applicationset-controller"
-
-	// get expected name
-	roleBinding := newRoleBindingWithname(name, cr)
-
-	// fetch existing rolebinding by name
-	roleBindingExists := true
-	if err := r.Client.Get(context.TODO(), types.NamespacedName{Name: roleBinding.Name, Namespace: cr.Namespace}, roleBinding); err != nil {
-		if !errors.IsNotFound(err) {
-			return fmt.Errorf("failed to get the rolebinding associated with %s : %s", name, err)
-		}
-		roleBindingExists = false
-	}
-
-	setAppSetLabels(&roleBinding.ObjectMeta)
-
-	roleBinding.RoleRef = v1.RoleRef{
-		APIGroup: v1.GroupName,
-		Kind:     "Role",
-		Name:     role.Name,
-	}
-
-	roleBinding.Subjects = []v1.Subject{
-		{
-			Kind:      v1.ServiceAccountKind,
-			Name:      sa.Name,
-			Namespace: sa.Namespace,
-		},
-	}
-
-	if err := controllerutil.SetControllerReference(cr, roleBinding, r.Scheme); err != nil {
-		return err
-	}
-
-	if roleBindingExists {
-		return r.Client.Update(context.TODO(), roleBinding)
-	}
-
-	return r.Client.Create(context.TODO(), roleBinding)
-}
-
-func getApplicationSetContainerImage(cr *argoproj.ArgoCD) string {
-	defaultImg, defaultTag := false, false
-
-	img := ""
-	tag := ""
-
-	// First pull from spec, if it exists
-	if cr.Spec.ApplicationSet != nil {
-		img = cr.Spec.ApplicationSet.Image
-		tag = cr.Spec.ApplicationSet.Version
-	}
-
-	// If spec is empty, use the defaults
-	if img == "" {
-		img = common.ArgoCDDefaultArgoImage
-		defaultImg = true
-	}
-	if tag == "" {
-		tag = common.ArgoCDDefaultArgoVersion
-		defaultTag = true
-	}
-
-	// If an env var is specified then use that, but don't override the spec values (if they are present)
-	if e := os.Getenv(common.ArgoCDImageEnvVar); e != "" && (defaultTag && defaultImg) {
-		return e
-	}
-	return util.CombineImageTag(img, tag)
-}
-
-// getApplicationSetResources will return the ResourceRequirements for the Application Sets container.
-func getApplicationSetResources(cr *argoproj.ArgoCD) corev1.ResourceRequirements {
-	resources := corev1.ResourceRequirements{}
-
-	// Allow override of resource requirements from CR
-	if cr.Spec.ApplicationSet.Resources != nil {
-		resources = *cr.Spec.ApplicationSet.Resources
-	}
-
-	return resources
-}
-
-func setAppSetLabels(obj *metav1.ObjectMeta) {
-	obj.Labels["app.kubernetes.io/name"] = "argocd-applicationset-controller"
-	obj.Labels["app.kubernetes.io/part-of"] = "argocd-applicationset"
-	obj.Labels["app.kubernetes.io/component"] = "controller"
-}
-
-// reconcileApplicationSetService will ensure that the Service is present for the ApplicationSet webhook and metrics component.
-func (r *ArgoCDReconciler) reconcileApplicationSetService(cr *argoproj.ArgoCD) error {
-	log.Info("reconciling applicationset service")
-
-	svc := newServiceWithSuffix(common.ApplicationSetServiceNameSuffix, common.ApplicationSetServiceNameSuffix, cr)
-	if cr.Spec.ApplicationSet == nil {
-
-		if util.IsObjectFound(r.Client, cr.Namespace, svc.Name, svc) {
-			err := util.FetchObject(r.Client, cr.Namespace, svc.Name, svc)
-			if err != nil {
-				return err
-			}
-			log.Info(fmt.Sprintf("Deleting applicationset controller service %s as applicationset is disabled", svc.Name))
-			err = r.Delete(context.TODO(), svc)
-			if err != nil {
-				return err
-			}
-		}
-	} else {
-		if util.IsObjectFound(r.Client, cr.Namespace, svc.Name, svc) {
-			return nil // Service found, do nothing
-		}
-	}
-	svc.Spec.Ports = []corev1.ServicePort{
-		{
-			Name:       "webhook",
-			Port:       7000,
-			Protocol:   corev1.ProtocolTCP,
-			TargetPort: intstr.FromInt(7000),
-		}, {
-			Name:       "metrics",
-			Port:       8080,
-			Protocol:   corev1.ProtocolTCP,
-			TargetPort: intstr.FromInt(8080),
-		},
-	}
-
-	svc.Spec.Selector = map[string]string{
-		common.AppK8sKeyName: util.NameWithSuffix(cr.Name, common.ApplicationSetServiceNameSuffix),
-	}
-
-	if err := controllerutil.SetControllerReference(cr, svc, r.Scheme); err != nil {
-		return err
-	}
-	return r.Client.Create(context.TODO(), svc)
-}
diff --git a/controllers/argocd/applicationset/applicationset.go b/controllers/argocd/applicationset/applicationset.go
index 5edda468..5b644a25 100644
--- a/controllers/argocd/applicationset/applicationset.go
+++ b/controllers/argocd/applicationset/applicationset.go
@@ -2,20 +2,106 @@ package applicationset
 
 import (
 	argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1"
+	"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 ApplicationSetReconciler struct {
-	Client   *client.Client
+	Client   client.Client
 	Scheme   *runtime.Scheme
 	Instance *argoproj.ArgoCD
 	Logger   logr.Logger
 }
 
+var (
+	resourceName   string
+	resourceLabels map[string]string
+)
+
 func (asr *ApplicationSetReconciler) Reconcile() error {
 
-	// controller logic goes here
+	asr.Logger = ctrl.Log.WithName(AppSetControllerComponent).WithValues("instance", asr.Instance.Name, "instance-namespace", asr.Instance.Namespace)
+
+	resourceName = util.GenerateUniqueResourceName(asr.Instance.Name, asr.Instance.Namespace, AppSetControllerComponent)
+	resourceLabels = common.DefaultLabels(resourceName, asr.Instance.Name, AppSetControllerComponent)
+
+	if err := asr.reconcileServiceAccount(); err != nil {
+		asr.Logger.Info("reconciling applicationSet serviceaccount")
+		return err
+	}
+
+	if err := asr.reconcileRole(); err != nil {
+		asr.Logger.Info("reconciling applicationSet role")
+		return err
+	}
+
+	if err := asr.reconcileRoleBinding(); err != nil {
+		asr.Logger.Info("reconciling applicationSet roleBinding")
+		return err
+	}
+
+	if asr.Instance.Spec.ApplicationSet.WebhookServer.Route.Enabled {
+		if err := asr.reconcileWebhookRoute(); err != nil {
+			asr.Logger.Info("reconciling applicationSet webhook route")
+			return err
+		}
+	} else {
+		if err := asr.deleteWebhookRoute(AppSetWebhookRouteName, asr.Instance.Namespace); err != nil {
+			asr.Logger.Error(err, "deleting applicationSet webhook route: failed to delete webhook route")
+			return err
+		}
+	}
+
+	if err := asr.reconcileService(); err != nil {
+		asr.Logger.Info("reconciling applicationSet service")
+		return err
+	}
+
+	if err := asr.reconcileDeployment(); err != nil {
+		asr.Logger.Info("reconciling applicationSet deployment")
+		return err
+	}
+
 	return nil
 }
+
+func (asr *ApplicationSetReconciler) DeleteResources() error {
+
+	var deletionError error = nil
+
+	if err := asr.deleteDeployment(resourceName, asr.Instance.Namespace); err != nil {
+		asr.Logger.Error(err, "DeleteResources: failed to delete deployment")
+		deletionError = err
+	}
+
+	if err := asr.deleteService(resourceName, asr.Instance.Namespace); err != nil {
+		asr.Logger.Error(err, "DeleteResources: failed to delete service")
+		deletionError = err
+	}
+
+	if err := asr.deleteWebhookRoute(resourceName, asr.Instance.Namespace); err != nil {
+		asr.Logger.Error(err, "DeleteResources: failed to delete webhook service")
+		deletionError = err
+	}
+
+	if err := asr.deleteRoleBinding(resourceName, asr.Instance.Namespace); err != nil {
+		asr.Logger.Error(err, "DeleteResources: failed to delete roleBinding")
+		deletionError = err
+	}
+
+	if err := asr.deleteRole(resourceName, asr.Instance.Namespace); err != nil {
+		asr.Logger.Error(err, "DeleteResources: failed to delete role")
+		deletionError = err
+	}
+
+	if err := asr.deleteServiceAccount(resourceName, asr.Instance.Namespace); err != nil {
+		asr.Logger.Error(err, "DeleteResources: failed to delete serviceaccount")
+		deletionError = err
+	}
+
+	return deletionError
+}
diff --git a/controllers/argocd/applicationset/constants.go b/controllers/argocd/applicationset/constants.go
new file mode 100644
index 00000000..29661ea0
--- /dev/null
+++ b/controllers/argocd/applicationset/constants.go
@@ -0,0 +1,14 @@
+package applicationset
+
+const (
+	// Values
+	AppSetControllerComponent  = "applicationset-controller"
+	AppSetController           = "argocd-applicationset-controller"
+	AppSetGitlabSCMTlsCert     = "appset-gitlab-scm-tls-cert"
+	AppSetGitlabSCMTlsCertPath = "/app/tls/scm/cert"
+	AppSetWebhookRouteName     = "applicationset-controller-webhook"
+
+	// Commands
+	EntryPointSh     = "entrypoint.sh"
+	ArgoCDRepoServer = "--argocd-repo-server"
+)
diff --git a/controllers/argocd/applicationset/deployment.go b/controllers/argocd/applicationset/deployment.go
new file mode 100644
index 00000000..59920cca
--- /dev/null
+++ b/controllers/argocd/applicationset/deployment.go
@@ -0,0 +1,322 @@
+package applicationset
+
+import (
+	"time"
+
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/reposerver"
+	"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"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+func (asr *ApplicationSetReconciler) reconcileDeployment() error {
+
+	asr.Logger.Info("reconciling deployment")
+
+	desiredDeployment := asr.getDesiredDeployment()
+	deploymentRequest := asr.getDeploymentRequest(*desiredDeployment)
+
+	desiredDeployment, err := workloads.RequestDeployment(deploymentRequest)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileDeployment: failed to request deployment", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+		asr.Logger.V(1).Info("reconcileDeployment: one or more mutations could not be applied")
+		return err
+	}
+
+	namespace, err := cluster.GetNamespace(asr.Instance.Namespace, asr.Client)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileDeployment: failed to retrieve namespace", "name", asr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := asr.deleteDeployment(desiredDeployment.Name, desiredDeployment.Namespace); err != nil {
+			asr.Logger.Error(err, "reconcileDeployment: failed to delete deployment", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+		}
+		return err
+	}
+
+	existingDeployment, err := workloads.GetDeployment(desiredDeployment.Name, desiredDeployment.Namespace, asr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			asr.Logger.Error(err, "reconcileDeployment: failed to retrieve deployment", "name", existingDeployment.Name, "namespace", existingDeployment.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(asr.Instance, desiredDeployment, asr.Scheme); err != nil {
+			asr.Logger.Error(err, "reconcileDeployment: failed to set owner reference for deployment", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+		}
+
+		if err = workloads.CreateDeployment(desiredDeployment, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileDeployment: failed to create deployment", "name", desiredDeployment.Name, "namespace", desiredDeployment.Namespace)
+			return err
+		}
+		asr.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, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileDeployment: failed to update deployment", "name", existingDeployment.Name, "namespace", existingDeployment.Namespace)
+			return err
+		}
+	}
+
+	asr.Logger.V(0).Info("reconcileDeployment: deployment updated", "name", existingDeployment.Name, "namespace", existingDeployment.Namespace)
+	return nil
+}
+
+func (asr *ApplicationSetReconciler) deleteDeployment(name, namespace string) error {
+	if err := workloads.DeleteDeployment(name, namespace, asr.Client); err != nil {
+		asr.Logger.Error(err, "DeleteDeployment: failed to delete deployment", "name", name, "namespace", namespace)
+		return err
+	}
+	asr.Logger.V(0).Info("DeleteDeployment: deployment deleted", "name", name, "namespace", namespace)
+	return nil
+}
+
+func (asr *ApplicationSetReconciler) getDesiredDeployment() *appsv1.Deployment {
+	desiredDeployment := &appsv1.Deployment{}
+
+	objMeta := metav1.ObjectMeta{
+		Name:      resourceName,
+		Namespace: asr.Instance.Namespace,
+		Labels:    resourceLabels,
+	}
+	addSCMGitlabVolumeMount := false
+	if scmRootCAConfigMapName := asr.getSCMRootCAConfigMapName(); scmRootCAConfigMapName != "" {
+		cm, err := workloads.GetConfigMap(scmRootCAConfigMapName, asr.Instance.Namespace, asr.Client)
+		if err != nil {
+			asr.Logger.Error(err, "failed to get SCM Root CA ConfigMap")
+		}
+		if util.IsObjectFound(asr.Client, asr.Instance.Namespace, asr.Instance.Spec.ApplicationSet.SCMRootCAConfigMap, cm) {
+			addSCMGitlabVolumeMount = true
+		}
+	}
+	podSpec := corev1.PodSpec{
+		ServiceAccountName: resourceName,
+		Volumes:            asr.getApplicationSetPodVolumes(addSCMGitlabVolumeMount),
+		Containers:         []corev1.Container{*asr.getApplicationSetContainer(addSCMGitlabVolumeMount)},
+		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,
+			},
+		},
+	}
+
+	desiredDeployment.ObjectMeta = objMeta
+	desiredDeployment.Spec = deploymentSpec
+	return desiredDeployment
+}
+
+func (asr *ApplicationSetReconciler) getDeploymentRequest(dep appsv1.Deployment) workloads.DeploymentRequest {
+	deploymentReq := workloads.DeploymentRequest{
+		ObjectMeta: dep.ObjectMeta,
+		Spec:       dep.Spec,
+		Client:     asr.Client,
+		Mutations:  []mutation.MutateFunc{mutation.ApplyReconcilerMutation},
+	}
+
+	return deploymentReq
+}
+
+func (asr *ApplicationSetReconciler) getArgoApplicationSetCommand() []string {
+	cmd := make([]string, 0)
+
+	cmd = append(cmd, EntryPointSh)
+	cmd = append(cmd, AppSetController)
+
+	cmd = append(cmd, ArgoCDRepoServer)
+	cmd = append(cmd, reposerver.GetRepoServerAddress(resourceName, asr.Instance.Namespace))
+
+	cmd = append(cmd, common.LogLevel)
+	cmd = append(cmd, util.GetLogLevel(asr.Instance.Spec.ApplicationSet.LogLevel))
+
+	// ApplicationSet command arguments provided by the user
+	extraArgs := asr.Instance.Spec.ApplicationSet.ExtraCommandArgs
+	err := util.IsMergable(extraArgs, cmd)
+	if err != nil {
+		return cmd
+	}
+
+	cmd = append(cmd, extraArgs...)
+
+	return cmd
+}
+
+func (asr *ApplicationSetReconciler) getApplicationSetContainer(addSCMGitlabVolumeMount bool) *corev1.Container {
+	appSetEnv := asr.Instance.Spec.ApplicationSet.Env
+	appSetEnv = util.EnvMerge(appSetEnv, util.ProxyEnvVars(), false)
+	container := &corev1.Container{
+		Command:         asr.getArgoApplicationSetCommand(),
+		Image:           argocdcommon.GetArgoContainerImage(asr.Instance),
+		ImagePullPolicy: corev1.PullAlways,
+		Name:            AppSetController,
+		Env:             appSetEnv,
+		Resources:       asr.getApplicationSetResources(),
+		SecurityContext: &corev1.SecurityContext{
+			AllowPrivilegeEscalation: util.BoolPtr(false),
+			ReadOnlyRootFilesystem:   util.BoolPtr(true),
+			RunAsNonRoot:             util.BoolPtr(true),
+			Capabilities: &corev1.Capabilities{
+				Drop: []corev1.Capability{
+					common.CapabilityDropAll,
+				},
+			},
+		},
+		VolumeMounts: []corev1.VolumeMount{
+			{
+				Name:      common.SSHKnownHosts,
+				MountPath: common.VolumeMountPathSSH,
+			},
+			{
+				Name:      common.TLSCerts,
+				MountPath: common.VolumeMountPathTLS,
+			},
+			{
+				Name:      common.GPGKeys,
+				MountPath: common.VolumeMountPathGPG,
+			},
+			{
+				Name:      common.GPGKeyRing,
+				MountPath: common.VolumeMountPathGPGKeyring,
+			},
+			{
+				Name:      common.VolumeTmp,
+				MountPath: common.VolumeMountPathTmp,
+			},
+		},
+		Ports: []corev1.ContainerPort{
+			{
+				ContainerPort: 7000,
+				Name:          common.Webhook,
+			},
+			{
+				ContainerPort: 8080,
+				Name:          common.ArgoCDMetrics,
+			},
+		},
+	}
+
+	if addSCMGitlabVolumeMount {
+		container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
+			Name:      AppSetGitlabSCMTlsCert,
+			MountPath: AppSetGitlabSCMTlsCertPath,
+		})
+	}
+
+	return container
+}
+
+func (asr *ApplicationSetReconciler) getApplicationSetPodVolumes(addSCMGitlabVolumeMount bool) []corev1.Volume {
+	volumes := []corev1.Volume{
+		{
+			Name: common.SSHKnownHosts,
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: common.ArgoCDKnownHostsConfigMapName,
+					},
+				},
+			},
+		},
+		{
+			Name: common.TLSCerts,
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: common.ArgoCDTLSCertsConfigMapName,
+					},
+				},
+			},
+		},
+		{
+			Name: common.GPGKeys,
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: common.ArgoCDGPGKeysConfigMapName,
+					},
+				},
+			},
+		},
+		{
+			Name: common.GPGKeyRing,
+			VolumeSource: corev1.VolumeSource{
+				EmptyDir: &corev1.EmptyDirVolumeSource{},
+			},
+		},
+		{
+			Name: common.VolumeTmp,
+			VolumeSource: corev1.VolumeSource{
+				EmptyDir: &corev1.EmptyDirVolumeSource{},
+			},
+		},
+	}
+	if addSCMGitlabVolumeMount {
+		volumes = append(volumes, corev1.Volume{
+			Name: AppSetGitlabSCMTlsCert,
+			VolumeSource: corev1.VolumeSource{
+				ConfigMap: &corev1.ConfigMapVolumeSource{
+					LocalObjectReference: corev1.LocalObjectReference{
+						Name: common.ArgoCDAppSetGitlabSCMTLSCertsConfigMapName,
+					},
+				},
+			},
+		})
+	}
+	return volumes
+}
diff --git a/controllers/argocd/applicationset/role.go b/controllers/argocd/applicationset/role.go
new file mode 100644
index 00000000..566643b4
--- /dev/null
+++ b/controllers/argocd/applicationset/role.go
@@ -0,0 +1,172 @@
+package applicationset
+
+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 (asr *ApplicationSetReconciler) reconcileRole() error {
+
+	asr.Logger.Info("reconciling roles")
+
+	roleRequest := permissions.RoleRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        resourceName,
+			Namespace:   asr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: asr.Instance.Annotations,
+		},
+		Rules:     getPolicyRules(),
+		Client:    asr.Client,
+		Mutations: []mutation.MutateFunc{mutation.ApplyReconcilerMutation},
+	}
+
+	desiredRole, err := permissions.RequestRole(roleRequest)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileRole: failed to request role", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+		asr.Logger.V(1).Info("reconcileRole: one or more mutations could not be applied")
+		return err
+	}
+
+	namespace, err := cluster.GetNamespace(asr.Instance.Namespace, asr.Client)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileRole: failed to retrieve namespace", "name", asr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := asr.deleteRole(desiredRole.Name, desiredRole.Namespace); err != nil {
+			asr.Logger.Error(err, "reconcileRole: failed to delete role", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+		}
+		return err
+	}
+
+	existingRole, err := permissions.GetRole(desiredRole.Name, desiredRole.Namespace, asr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			asr.Logger.Error(err, "reconcileRole: failed to retrieve role", "name", existingRole.Name, "namespace", existingRole.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(asr.Instance, desiredRole, asr.Scheme); err != nil {
+			asr.Logger.Error(err, "reconcileRole: failed to set owner reference for role", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+		}
+
+		if err = permissions.CreateRole(desiredRole, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileRole: failed to create role", "name", desiredRole.Name, "namespace", desiredRole.Namespace)
+			return err
+		}
+		asr.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, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileRole: failed to update role", "name", existingRole.Name, "namespace", existingRole.Namespace)
+			return err
+		}
+	}
+	asr.Logger.V(0).Info("reconcileRole: role updated", "name", existingRole.Name, "namespace", existingRole.Namespace)
+	return nil
+}
+
+func (asr *ApplicationSetReconciler) deleteRole(name, namespace string) error {
+	if err := permissions.DeleteRole(name, namespace, asr.Client); err != nil {
+		asr.Logger.Error(err, "DeleteRole: failed to delete role", "name", name, "namespace", namespace)
+		return err
+	}
+	asr.Logger.V(0).Info("DeleteRole: role deleted", "name", name, "namespace", namespace)
+	return nil
+}
+
+func getPolicyRules() []rbacv1.PolicyRule {
+	return []rbacv1.PolicyRule{
+		// ApplicationSet
+		{
+			APIGroups: []string{"argoproj.io"},
+			Resources: []string{
+				"applications",
+				"applicationsets",
+				"applicationsets/finalizers",
+			},
+			Verbs: []string{
+				"create",
+				"delete",
+				"get",
+				"list",
+				"patch",
+				"update",
+				"watch",
+			},
+		},
+		// Appprojects
+		{
+			APIGroups: []string{"argoproj.io"},
+			Resources: []string{
+				"appprojects",
+			},
+			Verbs: []string{
+				"get",
+			},
+		},
+		// ApplicationSet Status
+		{
+			APIGroups: []string{"argoproj.io"},
+			Resources: []string{
+				"applicationsets/status",
+			},
+			Verbs: []string{
+				"get",
+				"patch",
+				"update",
+			},
+		},
+		// Events
+		{
+			APIGroups: []string{""},
+			Resources: []string{
+				"events",
+			},
+			Verbs: []string{
+				"create",
+				"get",
+				"list",
+				"patch",
+				"watch",
+			},
+		},
+		// Read Secrets/ConfigMaps
+		{
+			APIGroups: []string{""},
+			Resources: []string{
+				"secrets",
+				"configmaps",
+			},
+			Verbs: []string{
+				"get",
+				"list",
+				"watch",
+			},
+		},
+		// Read Deployments
+		{
+			APIGroups: []string{"apps", "extensions"},
+			Resources: []string{
+				"deployments",
+			},
+			Verbs: []string{
+				"get",
+				"list",
+				"watch",
+			},
+		},
+	}
+}
diff --git a/controllers/argocd/applicationset/rolebinding.go b/controllers/argocd/applicationset/rolebinding.go
new file mode 100644
index 00000000..f17f9a5a
--- /dev/null
+++ b/controllers/argocd/applicationset/rolebinding.go
@@ -0,0 +1,117 @@
+package applicationset
+
+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 (asr *ApplicationSetReconciler) reconcileRoleBinding() error {
+
+	asr.Logger.Info("reconciling roleBinding")
+
+	sa, err := permissions.GetServiceAccount(resourceName, asr.Instance.Namespace, asr.Client)
+
+	if err != nil {
+		asr.Logger.Error(err, "reconcileRoleBinding: failed to get serviceaccount", "name", resourceName, "namespace", asr.Instance.Namespace)
+		return err
+	}
+
+	roleBindingRequest := permissions.RoleBindingRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        resourceName,
+			Namespace:   asr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: asr.Instance.Annotations,
+		},
+		RoleRef: rbacv1.RoleRef{
+			APIGroup: rbacv1.GroupName,
+			Kind:     common.RoleKind,
+			Name:     resourceName,
+		},
+		Subjects: []rbacv1.Subject{
+			{
+				Kind:      rbacv1.ServiceAccountKind,
+				Name:      sa.Name,
+				Namespace: sa.Namespace,
+			},
+		},
+	}
+
+	desiredRoleBinding := permissions.RequestRoleBinding(roleBindingRequest)
+
+	namespace, err := cluster.GetNamespace(asr.Instance.Namespace, asr.Client)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileRole: failed to retrieve namespace", "name", asr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := asr.deleteRole(desiredRoleBinding.Name, desiredRoleBinding.Namespace); err != nil {
+			asr.Logger.Error(err, "reconcileRoleBinding: failed to delete roleBinding", "name", desiredRoleBinding.Name, "namespace", desiredRoleBinding.Namespace)
+		}
+		return err
+	}
+
+	existingRoleBinding, err := permissions.GetRoleBinding(desiredRoleBinding.Name, desiredRoleBinding.Namespace, asr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			asr.Logger.Error(err, "reconcileRoleBinding: failed to retrieve roleBinding", "name", existingRoleBinding.Name, "namespace", existingRoleBinding.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(asr.Instance, desiredRoleBinding, asr.Scheme); err != nil {
+			asr.Logger.Error(err, "reconcileRole: failed to set owner reference for role", "name", desiredRoleBinding.Name, "namespace", desiredRoleBinding.Namespace)
+		}
+
+		if err = permissions.CreateRoleBinding(desiredRoleBinding, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileRoleBinding: failed to create roleBinding", "name", desiredRoleBinding.Name, "namespace", desiredRoleBinding.Namespace)
+			return err
+		}
+		asr.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, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileRoleBinding: failed to update roleBinding", "name", existingRoleBinding.Name, "namespace", existingRoleBinding.Namespace)
+			return err
+		}
+	}
+
+	asr.Logger.V(0).Info("reconcileRoleBinding: roleBinding updated", "name", existingRoleBinding.Name, "namespace", existingRoleBinding.Namespace)
+
+	return nil
+}
+
+func (asr *ApplicationSetReconciler) deleteRoleBinding(name, namespace string) error {
+	if err := permissions.DeleteRoleBinding(name, namespace, asr.Client); err != nil {
+		asr.Logger.Error(err, "DeleteRole: failed to delete roleBinding", "name", name, "namespace", namespace)
+		return err
+	}
+	asr.Logger.V(0).Info("DeleteRoleBinding: roleBinding deleted", "name", name, "namespace", namespace)
+	return nil
+}
diff --git a/controllers/argocd/applicationset/service.go b/controllers/argocd/applicationset/service.go
new file mode 100644
index 00000000..91b8bdb9
--- /dev/null
+++ b/controllers/argocd/applicationset/service.go
@@ -0,0 +1,101 @@
+package applicationset
+
+import (
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/pkg/cluster"
+	"github.com/argoproj-labs/argocd-operator/pkg/mutation"
+	"github.com/argoproj-labs/argocd-operator/pkg/networking"
+
+	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 (asr *ApplicationSetReconciler) reconcileService() error {
+
+	asr.Logger.Info("reconciling services")
+
+	serviceRequest := networking.ServiceRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        resourceName,
+			Namespace:   asr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: asr.Instance.Annotations,
+		},
+		Spec:      GetServiceSpec(),
+		Client:    asr.Client,
+		Mutations: []mutation.MutateFunc{mutation.ApplyReconcilerMutation},
+	}
+
+	desiredService, err := networking.RequestService(serviceRequest)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileService: failed to request service", "name", desiredService.Name, "namespace", desiredService.Namespace)
+		asr.Logger.V(1).Info("reconcileService: one or more mutations could not be applied")
+		return err
+	}
+
+	namespace, err := cluster.GetNamespace(asr.Instance.Namespace, asr.Client)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileService: failed to retrieve namespace", "name", asr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := asr.deleteService(desiredService.Name, desiredService.Namespace); err != nil {
+			asr.Logger.Error(err, "reconcileService: failed to delete service", "name", desiredService.Name, "namespace", desiredService.Namespace)
+		}
+		return err
+	}
+
+	existingService, err := networking.GetService(desiredService.Name, desiredService.Namespace, asr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			asr.Logger.Error(err, "reconcileService: failed to retrieve service", "name", existingService.Name, "namespace", existingService.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(asr.Instance, desiredService, asr.Scheme); err != nil {
+			asr.Logger.Error(err, "reconcileService: failed to set owner reference for service", "name", desiredService.Name, "namespace", desiredService.Namespace)
+		}
+
+		if err = networking.CreateService(desiredService, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileService: failed to create service", "name", desiredService.Name, "namespace", desiredService.Namespace)
+			return err
+		}
+		asr.Logger.V(0).Info("reconcileService: service created", "name", desiredService.Name, "namespace", desiredService.Namespace)
+		return nil
+	}
+
+	return nil
+}
+
+func (asr *ApplicationSetReconciler) deleteService(name, namespace string) error {
+	if err := networking.DeleteService(name, namespace, asr.Client); err != nil {
+		asr.Logger.Error(err, "DeleteService: failed to delete service", "name", name, "namespace", namespace)
+		return err
+	}
+	asr.Logger.V(0).Info("DeleteService: service deleted", "name", name, "namespace", namespace)
+	return nil
+}
+
+func GetServiceSpec() corev1.ServiceSpec {
+	return corev1.ServiceSpec{
+		Ports: []corev1.ServicePort{
+			{
+				Name:       common.Webhook,
+				Port:       7000,
+				Protocol:   corev1.ProtocolTCP,
+				TargetPort: intstr.FromInt(7000),
+			},
+			{
+				Name:       common.ArgoCDMetrics,
+				Port:       8080,
+				TargetPort: intstr.FromInt(8080),
+			},
+		},
+		Selector: map[string]string{
+			common.AppK8sKeyName: resourceName,
+		},
+	}
+}
diff --git a/controllers/argocd/applicationset/serviceaccount.go b/controllers/argocd/applicationset/serviceaccount.go
new file mode 100644
index 00000000..6eaf3e3d
--- /dev/null
+++ b/controllers/argocd/applicationset/serviceaccount.go
@@ -0,0 +1,68 @@
+package applicationset
+
+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 (asr *ApplicationSetReconciler) reconcileServiceAccount() error {
+
+	asr.Logger.Info("reconciling serviceAccounts")
+
+	serviceAccountRequest := permissions.ServiceAccountRequest{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        resourceName,
+			Namespace:   asr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: asr.Instance.Annotations,
+		},
+	}
+
+	desiredServiceAccount := permissions.RequestServiceAccount(serviceAccountRequest)
+
+	namespace, err := cluster.GetNamespace(asr.Instance.Namespace, asr.Client)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileServiceAccount: failed to retrieve namespace", "name", asr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := asr.deleteServiceAccount(desiredServiceAccount.Name, desiredServiceAccount.Namespace); err != nil {
+			asr.Logger.Error(err, "reconcileServiceAccount: failed to delete serviceAccount", "name", desiredServiceAccount.Name, "namespace", desiredServiceAccount.Namespace)
+		}
+		return err
+	}
+
+	existingServiceAccount, err := permissions.GetServiceAccount(desiredServiceAccount.Name, desiredServiceAccount.Namespace, asr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			asr.Logger.Error(err, "reconcileServiceAccount: failed to retrieve serviceAccount", "name", existingServiceAccount.Name, "namespace", existingServiceAccount.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(asr.Instance, desiredServiceAccount, asr.Scheme); err != nil {
+			asr.Logger.Error(err, "reconcileServiceAccount: failed to set owner reference for serviceAccount", "name", desiredServiceAccount.Name, "namespace", desiredServiceAccount.Namespace)
+		}
+
+		if err = permissions.CreateServiceAccount(desiredServiceAccount, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileServiceAccount: failed to create serviceAccount", "name", desiredServiceAccount.Name, "namespace", desiredServiceAccount.Namespace)
+			return err
+		}
+		asr.Logger.V(0).Info("reconcileServiceAccount: serviceAccount created", "name", desiredServiceAccount.Name, "namespace", desiredServiceAccount.Namespace)
+		return nil
+	}
+
+	return nil
+}
+
+func (nr *ApplicationSetReconciler) 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/applicationset/util.go b/controllers/argocd/applicationset/util.go
new file mode 100644
index 00000000..d11a0512
--- /dev/null
+++ b/controllers/argocd/applicationset/util.go
@@ -0,0 +1,25 @@
+package applicationset
+
+import (
+	corev1 "k8s.io/api/core/v1"
+)
+
+// getApplicationSetResources will return the ResourceRequirements for the Application Sets container.
+func (asr *ApplicationSetReconciler) getApplicationSetResources() corev1.ResourceRequirements {
+	resources := corev1.ResourceRequirements{}
+
+	// Allow override of resource requirements from CR
+	if asr.Instance.Spec.ApplicationSet.Resources != nil {
+		resources = *asr.Instance.Spec.ApplicationSet.Resources
+	}
+
+	return resources
+}
+
+// getSCMRootCAConfigMapName will return the SCMRootCA ConfigMap name for the given ArgoCD ApplicationSet Controller.
+func (asr *ApplicationSetReconciler) getSCMRootCAConfigMapName() string {
+	if asr.Instance.Spec.ApplicationSet.SCMRootCAConfigMap != "" && len(asr.Instance.Spec.ApplicationSet.SCMRootCAConfigMap) > 0 {
+		return asr.Instance.Spec.ApplicationSet.SCMRootCAConfigMap
+	}
+	return ""
+}
diff --git a/controllers/argocd/applicationset/webhookroute.go b/controllers/argocd/applicationset/webhookroute.go
new file mode 100644
index 00000000..8e6de8a9
--- /dev/null
+++ b/controllers/argocd/applicationset/webhookroute.go
@@ -0,0 +1,169 @@
+package applicationset
+
+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/mutation"
+	"github.com/argoproj-labs/argocd-operator/pkg/networking"
+	"github.com/argoproj-labs/argocd-operator/pkg/util"
+
+	routev1 "github.com/openshift/api/route/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 (asr *ApplicationSetReconciler) reconcileWebhookRoute() error {
+	asr.Logger.Info("reconciling webhookroutes")
+
+	desiredWebhookRoute := asr.getDesiredWebhookRoute()
+	webhookRouteRequest := asr.getWebhookRouteRequest(*desiredWebhookRoute)
+
+	desiredWebhookRoute, err := networking.RequestRoute(webhookRouteRequest)
+
+	if err != nil {
+		asr.Logger.Error(err, "reconcileRoute: failed to request route", "name", desiredWebhookRoute.Name, "namespace", desiredWebhookRoute.Namespace)
+		return err
+	}
+
+	namespace, err := cluster.GetNamespace(asr.Instance.Namespace, asr.Client)
+	if err != nil {
+		asr.Logger.Error(err, "reconcileRoute: failed to retrieve namespace", "name", asr.Instance.Namespace)
+		return err
+	}
+	if namespace.DeletionTimestamp != nil {
+		if err := asr.deleteWebhookRoute(desiredWebhookRoute.Name, desiredWebhookRoute.Namespace); err != nil {
+			asr.Logger.Error(err, "reconcileRoute: failed to delete route", "name", desiredWebhookRoute.Name, "namespace", desiredWebhookRoute.Namespace)
+		}
+		return err
+	}
+
+	existingRoute, err := networking.GetRoute(desiredWebhookRoute.Name, desiredWebhookRoute.Namespace, asr.Client)
+	if err != nil {
+		if !errors.IsNotFound(err) {
+			asr.Logger.Error(err, "reconcileRoute: failed to retrieve route", "name", desiredWebhookRoute.Name, "namespace", desiredWebhookRoute.Namespace)
+			return err
+		}
+
+		if err = controllerutil.SetControllerReference(asr.Instance, desiredWebhookRoute, asr.Scheme); err != nil {
+			asr.Logger.Error(err, "reconcileRoute: failed to set owner reference for route", "name", desiredWebhookRoute.Name, "namespace", desiredWebhookRoute.Namespace)
+		}
+
+		if err = networking.CreateRoute(desiredWebhookRoute, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileRoute: failed to create route", "name", desiredWebhookRoute.Name, "namespace", desiredWebhookRoute.Namespace)
+			return err
+		}
+		asr.Logger.V(0).Info("reconcileRoute: route created", "name", desiredWebhookRoute.Name, "namespace", desiredWebhookRoute.Namespace)
+		return nil
+	}
+
+	webhookRouteChanged := false
+
+	fieldsToCompare := []struct {
+		existing, desired interface{}
+		extraAction       func()
+	}{
+		{&existingRoute.Annotations, &desiredWebhookRoute.Annotations, nil},
+		{&existingRoute.Labels, &desiredWebhookRoute.Labels, nil},
+		{&existingRoute.Spec.WildcardPolicy, &desiredWebhookRoute.Spec.WildcardPolicy, nil},
+		{&existingRoute.Spec.Host, &desiredWebhookRoute.Spec.Host, nil},
+		{&existingRoute.Spec.Port, &desiredWebhookRoute.Spec.Port, nil},
+		{&existingRoute.Spec.TLS, &desiredWebhookRoute.Spec.TLS, nil},
+		{&existingRoute.Spec.To, &desiredWebhookRoute.Spec.To, nil},
+	}
+
+	for _, field := range fieldsToCompare {
+		argocdcommon.UpdateIfChanged(field.existing, field.desired, field.extraAction, &webhookRouteChanged)
+	}
+
+	if webhookRouteChanged {
+		if err = networking.UpdateRoute(existingRoute, asr.Client); err != nil {
+			asr.Logger.Error(err, "reconcileWebhookRoute: failed to update webhook route", "name", existingRoute.Name, "namespace", existingRoute.Namespace)
+			return err
+		}
+	}
+
+	asr.Logger.V(0).Info("reconcileRoute: webhook route updated", "name", desiredWebhookRoute.Name, "namespace", desiredWebhookRoute.Namespace)
+
+	return nil
+}
+
+func (asr *ApplicationSetReconciler) deleteWebhookRoute(name, namespace string) error {
+	if err := networking.DeleteRoute(name, namespace, asr.Client); err != nil {
+		asr.Logger.Error(err, "DeleteRoute: failed to delete route", "name", name, "namespace", namespace)
+		return err
+	}
+	asr.Logger.V(0).Info("DeleteRoute: route deleted", "name", name, "namespace", namespace)
+	return nil
+}
+
+func (asr *ApplicationSetReconciler) getWebhookRouteSpec() routev1.RouteSpec {
+	routeSpec := routev1.RouteSpec{
+		Port: &routev1.RoutePort{
+			TargetPort: intstr.FromString(common.Webhook),
+		},
+		TLS: &routev1.TLSConfig{
+			InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
+			Termination:                   routev1.TLSTerminationPassthrough,
+		},
+		To: routev1.RouteTargetReference{
+			Kind: common.ServiceKind,
+			Name: util.NameWithSuffix(asr.Instance.Name, AppSetControllerComponent),
+		},
+	}
+
+	if asr.Instance.Spec.Server.Insecure {
+		routeSpec.TLS.Termination = routev1.TLSTerminationEdge
+	}
+
+	if len(asr.Instance.Spec.Server.Host) > 0 {
+		routeSpec.Host = asr.Instance.Spec.Server.Host
+	}
+
+	// Allow override of the WildcardPolicy for the Route
+	if asr.Instance.Spec.Server.Route.WildcardPolicy != nil && len(*asr.Instance.Spec.Server.Route.WildcardPolicy) > 0 {
+		routeSpec.WildcardPolicy = *asr.Instance.Spec.Server.Route.WildcardPolicy
+	}
+
+	return routeSpec
+}
+
+func (asr *ApplicationSetReconciler) getWebhookRouteRequest(route routev1.Route) networking.RouteRequest {
+	webhookRouteReq := networking.RouteRequest{
+		ObjectMeta: route.ObjectMeta,
+		Spec:       route.Spec,
+		Client:     asr.Client,
+		Mutations:  []mutation.MutateFunc{mutation.ApplyReconcilerMutation},
+	}
+	return webhookRouteReq
+}
+
+func (asr *ApplicationSetReconciler) getDesiredWebhookRoute() *routev1.Route {
+	desiredWebhook := &routev1.Route{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:        AppSetWebhookRouteName,
+			Namespace:   asr.Instance.Namespace,
+			Labels:      resourceLabels,
+			Annotations: asr.Instance.Annotations,
+		},
+		Spec: asr.getWebhookRouteSpec(),
+	}
+
+	// Allow override of the Annotations for the Route.
+	if len(asr.Instance.Spec.ApplicationSet.WebhookServer.Route.Annotations) > 0 {
+		desiredWebhook.ObjectMeta.Annotations = asr.Instance.Spec.Server.Route.Annotations
+	}
+
+	// Allow override of the Labels for the Route.
+	if len(asr.Instance.Spec.ApplicationSet.WebhookServer.Route.Labels) > 0 {
+		labels := desiredWebhook.ObjectMeta.Labels
+		for key, val := range asr.Instance.Spec.Server.Route.Labels {
+			labels[key] = val
+		}
+		desiredWebhook.ObjectMeta.Labels = labels
+	}
+
+	return desiredWebhook
+}
diff --git a/controllers/argocd/argocd_controller.go b/controllers/argocd/argocd_controller.go
index b8e6f139..657da165 100644
--- a/controllers/argocd/argocd_controller.go
+++ b/controllers/argocd/argocd_controller.go
@@ -260,8 +260,14 @@ func (r *ArgoCDReconciler) reconcileControllers() error {
 	}
 
 	// non-core components, don't return reconciliation errors
-	if err := r.AppsetController.Reconcile(); err != nil {
-		r.Logger.Error(err, "failed to reconcile applicationset controller")
+	if r.Instance.Spec.ApplicationSet != nil {
+		if err := r.AppsetController.Reconcile(); err != nil {
+			r.Logger.Error(err, "failed to reconcile applicationset controller")
+		}
+	} else {
+		if err := r.AppsetController.DeleteResources(); err != nil {
+			r.Logger.Error(err, "failed to delete applicationset resources")
+		}
 	}
 
 	if r.Instance.Spec.Notifications.Enabled {
@@ -333,7 +339,7 @@ func (r *ArgoCDReconciler) InitializeControllerReconcilers() {
 	}
 
 	r.AppsetController = &applicationset.ApplicationSetReconciler{
-		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
index 7d6673d3..e1c2d1ca 100644
--- a/controllers/argocd/notifications/configmap.go
+++ b/controllers/argocd/notifications/configmap.go
@@ -37,7 +37,7 @@ func (nr *NotificationsReconciler) reconcileConfigMap() error {
 		return err
 	}
 	if namespace.DeletionTimestamp != nil {
-		if err := nr.DeleteConfigMap(desiredConfigMap.Namespace); err != 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
@@ -65,7 +65,7 @@ func (nr *NotificationsReconciler) reconcileConfigMap() error {
 	return nil
 }
 
-func (nr *NotificationsReconciler) DeleteConfigMap(namespace string) error {
+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
diff --git a/controllers/argocd/notifications/constants.go b/controllers/argocd/notifications/constants.go
index c4718d69..cd31f4a7 100644
--- a/controllers/argocd/notifications/constants.go
+++ b/controllers/argocd/notifications/constants.go
@@ -2,7 +2,7 @@ package notifications
 
 // Values
 const (
-	ArgoCDNotificationsControllerComponent = "notifications-controller"
-	NotificationsSecretName                = "argocd-notifications-secret"
-	NotificationsConfigMapName             = "argocd-notifications-cm"
+	NotificationsControllerComponent = "notifications-controller"
+	NotificationsSecretName          = "argocd-notifications-secret"
+	NotificationsConfigMapName       = "argocd-notifications-cm"
 )
diff --git a/controllers/argocd/notifications/deployment.go b/controllers/argocd/notifications/deployment.go
index 46ad3a27..d9336cd6 100644
--- a/controllers/argocd/notifications/deployment.go
+++ b/controllers/argocd/notifications/deployment.go
@@ -18,6 +18,99 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 )
 
+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
+}
+
 func (nr *NotificationsReconciler) getDesiredDeployment() *appsv1.Deployment {
 	desiredDeployment := &appsv1.Deployment{}
 
@@ -30,6 +123,7 @@ func (nr *NotificationsReconciler) getDesiredDeployment() *appsv1.Deployment {
 		Labels:    resourceLabels,
 	}
 	podSpec := corev1.PodSpec{
+		ServiceAccountName: resourceName,
 		Volumes: []corev1.Volume{
 			{
 				Name: common.TLSCerts,
@@ -55,7 +149,7 @@ func (nr *NotificationsReconciler) getDesiredDeployment() *appsv1.Deployment {
 			Command:         nr.GetNotificationsCommand(),
 			Image:           argocdcommon.GetArgoContainerImage(nr.Instance),
 			ImagePullPolicy: corev1.PullAlways,
-			Name:            ArgoCDNotificationsControllerComponent,
+			Name:            NotificationsControllerComponent,
 			Env:             notificationEnv,
 			Resources:       nr.GetNotificationsResources(),
 			LivenessProbe: &corev1.Probe{
@@ -127,96 +221,3 @@ func (nr *NotificationsReconciler) getDeploymentRequest(dep appsv1.Deployment) w
 
 	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 a9d8795d..40d4bb97 100644
--- a/controllers/argocd/notifications/notifications.go
+++ b/controllers/argocd/notifications/notifications.go
@@ -24,10 +24,10 @@ var (
 
 func (nr *NotificationsReconciler) Reconcile() error {
 
-	nr.Logger = ctrl.Log.WithName(ArgoCDNotificationsControllerComponent).WithValues("instance", nr.Instance.Name, "instance-namespace", nr.Instance.Namespace)
+	nr.Logger = ctrl.Log.WithName(NotificationsControllerComponent).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)
+	resourceName = util.GenerateUniqueResourceName(nr.Instance.Name, nr.Instance.Namespace, NotificationsControllerComponent)
+	resourceLabels = common.DefaultLabels(resourceName, nr.Instance.Name, NotificationsControllerComponent)
 
 	if err := nr.reconcileServiceAccount(); err != nil {
 		nr.Logger.Info("reconciling notifications serviceaccount")
@@ -66,32 +66,32 @@ func (nr *NotificationsReconciler) DeleteResources() error {
 
 	var deletionError error = nil
 
-	if err := nr.DeleteDeployment(resourceName, nr.Instance.Namespace); err != 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 {
+	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 {
+	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 {
+	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 {
+	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 {
+	if err := nr.deleteServiceAccount(resourceName, nr.Instance.Namespace); err != nil {
 		nr.Logger.Error(err, "DeleteResources: failed to delete serviceaccount")
 		deletionError = err
 	}
diff --git a/controllers/argocd/notifications/role.go b/controllers/argocd/notifications/role.go
index 88b3535d..d2c8145a 100644
--- a/controllers/argocd/notifications/role.go
+++ b/controllers/argocd/notifications/role.go
@@ -42,7 +42,7 @@ func (nr *NotificationsReconciler) reconcileRole() error {
 		return err
 	}
 	if namespace.DeletionTimestamp != nil {
-		if err := nr.DeleteRole(desiredRole.Name, desiredRole.Namespace); err != 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
@@ -78,7 +78,7 @@ func (nr *NotificationsReconciler) reconcileRole() error {
 	return nil
 }
 
-func (nr *NotificationsReconciler) DeleteRole(name, namespace string) error {
+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
diff --git a/controllers/argocd/notifications/rolebinding.go b/controllers/argocd/notifications/rolebinding.go
index a2ebdd6f..b4f2d10a 100644
--- a/controllers/argocd/notifications/rolebinding.go
+++ b/controllers/argocd/notifications/rolebinding.go
@@ -14,12 +14,12 @@ import (
 
 func (nr *NotificationsReconciler) reconcileRoleBinding() error {
 
-	nr.Logger.Info("reconciling roleBindings")
+	nr.Logger.Info("reconciling roleBinding")
 
 	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)
+		nr.Logger.Error(err, "reconcileRoleBinding: failed to get serviceaccount", "name", resourceName, "namespace", nr.Instance.Namespace)
 		return err
 	}
 
@@ -52,7 +52,7 @@ func (nr *NotificationsReconciler) reconcileRoleBinding() error {
 		return err
 	}
 	if namespace.DeletionTimestamp != nil {
-		if err := nr.DeleteRole(desiredRoleBinding.Name, desiredRoleBinding.Namespace); err != 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
@@ -107,7 +107,7 @@ func (nr *NotificationsReconciler) reconcileRoleBinding() error {
 	return nil
 }
 
-func (nr *NotificationsReconciler) DeleteRoleBinding(name, namespace string) error {
+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
diff --git a/controllers/argocd/notifications/secret.go b/controllers/argocd/notifications/secret.go
index e46a7874..38c2bb1b 100644
--- a/controllers/argocd/notifications/secret.go
+++ b/controllers/argocd/notifications/secret.go
@@ -38,7 +38,7 @@ func (nr *NotificationsReconciler) reconcileSecret() error {
 		return err
 	}
 	if namespace.DeletionTimestamp != nil {
-		if err := nr.DeleteSecret(desiredSecret.Namespace); err != 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
@@ -66,7 +66,7 @@ func (nr *NotificationsReconciler) reconcileSecret() error {
 	return nil
 }
 
-func (nr *NotificationsReconciler) DeleteSecret(namespace string) error {
+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
diff --git a/controllers/argocd/notifications/serviceaccount.go b/controllers/argocd/notifications/serviceaccount.go
index 62c8d503..f81fad58 100644
--- a/controllers/argocd/notifications/serviceaccount.go
+++ b/controllers/argocd/notifications/serviceaccount.go
@@ -30,7 +30,7 @@ func (nr *NotificationsReconciler) reconcileServiceAccount() error {
 		return err
 	}
 	if namespace.DeletionTimestamp != nil {
-		if err := nr.DeleteServiceAccount(desiredServiceAccount.Name, desiredServiceAccount.Namespace); err != 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
@@ -58,7 +58,7 @@ func (nr *NotificationsReconciler) reconcileServiceAccount() error {
 	return nil
 }
 
-func (nr *NotificationsReconciler) DeleteServiceAccount(name, namespace string) error {
+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
diff --git a/controllers/argocd/reposerver/constants.go b/controllers/argocd/reposerver/constants.go
new file mode 100644
index 00000000..0a891a65
--- /dev/null
+++ b/controllers/argocd/reposerver/constants.go
@@ -0,0 +1,5 @@
+package reposerver
+
+const (
+	ArgoCDRepoServerControllerComponent = "repo-server"
+)
diff --git a/controllers/argocd/reposerver/util.go b/controllers/argocd/reposerver/util.go
new file mode 100644
index 00000000..af52a052
--- /dev/null
+++ b/controllers/argocd/reposerver/util.go
@@ -0,0 +1,11 @@
+package reposerver
+
+import (
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/pkg/util"
+)
+
+// GetRepoServerAddress will return the Argo CD repo server address.
+func GetRepoServerAddress(name string, namespace string) string {
+	return util.FqdnServiceRef(util.NameWithSuffix(name, ArgoCDRepoServerControllerComponent), namespace, common.ArgoCDDefaultRepoServerPort)
+}
diff --git a/controllers/argocd/util.go b/controllers/argocd/util.go
index c51814b4..59a32644 100644
--- a/controllers/argocd/util.go
+++ b/controllers/argocd/util.go
@@ -733,7 +733,7 @@ func (r *ArgoCDReconciler) reconcileResources(cr *argoproj.ArgoCD) error {
 
 	if cr.Spec.ApplicationSet != nil {
 		log.Info("reconciling ApplicationSet controller")
-		if err := r.reconcileApplicationSetController(cr); err != nil {
+		if err := r.AppsetController.Reconcile(); err != nil {
 			return err
 		}
 	}
diff --git a/pkg/util/string.go b/pkg/util/string.go
index 06522637..a07f651c 100644
--- a/pkg/util/string.go
+++ b/pkg/util/string.go
@@ -2,6 +2,7 @@ package util
 
 import (
 	"encoding/base64"
+	"fmt"
 	"sort"
 	"strings"
 )
@@ -57,3 +58,13 @@ func GenerateRandomString(s int) (string, error) {
 	}
 	return base64.URLEncoding.EncodeToString(b), nil
 }
+
+// IsMergable returns error if any of the extraArgs is already part of the default command Arguments.
+func IsMergable(extraArgs []string, cmd []string) error {
+	for _, arg := range extraArgs {
+		if len(arg) > 2 && arg[:2] == "--" && ContainsString(cmd, arg) {
+			return fmt.Errorf("arg %s is already part of the default command arguments", arg)
+		}
+	}
+	return nil
+}


Test patch:
diff --git a/controllers/argocd/applicationset/applicationset_test.go b/controllers/argocd/applicationset/applicationset_test.go
new file mode 100644
index 00000000..7f60ee26
--- /dev/null
+++ b/controllers/argocd/applicationset/applicationset_test.go
@@ -0,0 +1,42 @@
+package applicationset
+
+import (
+	"testing"
+
+	argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1"
+	"github.com/argoproj-labs/argocd-operator/common"
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	routev1 "github.com/openshift/api/route/v1"
+	"github.com/stretchr/testify/assert"
+	"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 testExpectedLabels = common.DefaultLabels(argocdcommon.TestArgoCDName, argocdcommon.TestNamespace, AppSetControllerComponent)
+
+func makeTestApplicationSetReconciler(t *testing.T, webhookServerRouteEnabled bool, objs ...runtime.Object) *ApplicationSetReconciler {
+	s := scheme.Scheme
+
+	assert.NoError(t, routev1.Install(s))
+	assert.NoError(t, argoproj.AddToScheme(s))
+
+	cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
+	logger := ctrl.Log.WithName(AppSetControllerComponent)
+
+	return &ApplicationSetReconciler{
+		Client: cl,
+		Scheme: s,
+		Instance: argocdcommon.MakeTestArgoCD(func(a *argoproj.ArgoCD) {
+			a.Spec.ApplicationSet = &argoproj.ArgoCDApplicationSet{
+				WebhookServer: argoproj.WebhookServerSpec{
+					Route: argoproj.ArgoCDRouteSpec{
+						Enabled: webhookServerRouteEnabled,
+					},
+				},
+			}
+		}),
+		Logger: logger,
+	}
+}
diff --git a/controllers/argocd/applicationset/deployment_test.go b/controllers/argocd/applicationset/deployment_test.go
new file mode 100644
index 00000000..4f9de6e4
--- /dev/null
+++ b/controllers/argocd/applicationset/deployment_test.go
@@ -0,0 +1,94 @@
+package applicationset
+
+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 TestApplicationSetReconciler_reconcileDeployment(t *testing.T) {
+	resourceName = argocdcommon.TestArgoCDName
+	resourceLabels = testExpectedLabels
+	ns := argocdcommon.MakeTestNamespace()
+	asr := makeTestApplicationSetReconciler(t, false, ns)
+
+	existingDeployment := asr.getDesiredDeployment()
+
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a deployment",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, ns)
+			},
+			wantErr: false,
+		},
+		{
+			name: "update a deployment",
+			setupClient: func() *ApplicationSetReconciler {
+				outdatedDeployment := existingDeployment
+				outdatedDeployment.ObjectMeta.Labels = argocdcommon.TestKVP
+				return makeTestApplicationSetReconciler(t, false, outdatedDeployment, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			asr := tt.setupClient()
+			err := asr.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 = asr.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 TestApplicationSetReconciler_DeleteDeployment(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			asr := tt.setupClient()
+			if err := asr.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/applicationset/role_test.go b/controllers/argocd/applicationset/role_test.go
new file mode 100644
index 00000000..0707ac16
--- /dev/null
+++ b/controllers/argocd/applicationset/role_test.go
@@ -0,0 +1,102 @@
+package applicationset
+
+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 TestApplicationSetReconciler_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() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a role",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, ns)
+			},
+			wantErr: false,
+		},
+		{
+			name: "Update a role",
+			setupClient: func() *ApplicationSetReconciler {
+				outdatedRole := existingRole
+				outdatedRole.Rules = []rbacv1.PolicyRule{}
+				return makeTestApplicationSetReconciler(t, false, 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 TestApplicationSetReconciler_DeleteRole(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, 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/applicationset/rolebinding_test.go b/controllers/argocd/applicationset/rolebinding_test.go
new file mode 100644
index 00000000..dcf8e553
--- /dev/null
+++ b/controllers/argocd/applicationset/rolebinding_test.go
@@ -0,0 +1,103 @@
+package applicationset
+
+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 TestApplicationSetReconciler_reconcileRoleBinding(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	sa := argocdcommon.MakeTestServiceAccount()
+	resourceName = argocdcommon.TestArgoCDName
+
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a rolebinding",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, ns, sa)
+			},
+			wantErr: false,
+		},
+		{
+			name: "update a rolebinding",
+			setupClient: func() *ApplicationSetReconciler {
+				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 makeTestApplicationSetReconciler(t, false, 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, argocdcommon.TestRoleRef, updatedRoleBinding.RoleRef)
+			assert.Equal(t, argocdcommon.TestSubjects, updatedRoleBinding.Subjects)
+		})
+	}
+}
+
+func TestApplicationSetReconciler_DeleteRoleBinding(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	sa := argocdcommon.MakeTestServiceAccount()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, 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/applicationset/service_test.go b/controllers/argocd/applicationset/service_test.go
new file mode 100644
index 00000000..8b59b38e
--- /dev/null
+++ b/controllers/argocd/applicationset/service_test.go
@@ -0,0 +1,81 @@
+package applicationset
+
+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 TestApplicationSetReconciler_reconcileService(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	sa := argocdcommon.MakeTestServiceAccount()
+	resourceName = argocdcommon.TestArgoCDName
+
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a Service",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, ns, sa)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			err := nr.reconcileService()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+			currentService := &corev1.Service{}
+			err = nr.Client.Get(context.TODO(), types.NamespacedName{Name: argocdcommon.TestArgoCDName, Namespace: argocdcommon.TestNamespace}, currentService)
+			if err != nil {
+				t.Fatalf("Could not get current Service: %v", err)
+			}
+			assert.Equal(t, GetServiceSpec().Ports, currentService.Spec.Ports)
+		})
+	}
+}
+
+func TestApplicationSetReconciler_DeleteService(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	sa := argocdcommon.MakeTestServiceAccount()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, ns, sa)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			nr := tt.setupClient()
+			if err := nr.deleteService(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/applicationset/serviceaccount_test.go b/controllers/argocd/applicationset/serviceaccount_test.go
new file mode 100644
index 00000000..2aacc46e
--- /dev/null
+++ b/controllers/argocd/applicationset/serviceaccount_test.go
@@ -0,0 +1,81 @@
+package applicationset
+
+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 TestApplicationSetReconciler_reconcileServiceAccount(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	resourceLabels = testExpectedLabels
+
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "create a serviceAccount",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, 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 TestApplicationSetReconciler_DeleteServiceAccount(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	resourceName = argocdcommon.TestArgoCDName
+	tests := []struct {
+		name        string
+		setupClient func() *ApplicationSetReconciler
+		wantErr     bool
+	}{
+		{
+			name: "successful delete",
+			setupClient: func() *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, false, 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/applicationset/webhookroute_test.go b/controllers/argocd/applicationset/webhookroute_test.go
new file mode 100644
index 00000000..bea86ebc
--- /dev/null
+++ b/controllers/argocd/applicationset/webhookroute_test.go
@@ -0,0 +1,137 @@
+package applicationset
+
+import (
+	"context"
+	"testing"
+
+	"github.com/argoproj-labs/argocd-operator/controllers/argocd/argocdcommon"
+	"github.com/stretchr/testify/assert"
+
+	routev1 "github.com/openshift/api/route/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/types"
+)
+
+func TestApplicationSetReconciler_reconcileWebhookRoute(t *testing.T) {
+	resourceLabels = testExpectedLabels
+	ns := argocdcommon.MakeTestNamespace()
+	asr := makeTestApplicationSetReconciler(t, true, ns)
+
+	existingWebhookRoute := asr.getDesiredWebhookRoute()
+
+	tests := []struct {
+		name                      string
+		webhookServerRouteEnabled bool
+		setupClient               func(bool) *ApplicationSetReconciler
+		wantErr                   bool
+	}{
+		{
+			name:                      "create a webhookRoute",
+			webhookServerRouteEnabled: true,
+			setupClient: func(webhookServerRouteEnabled bool) *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, webhookServerRouteEnabled, ns)
+			},
+			wantErr: false,
+		},
+		{
+			name:                      "update a webhookRoute",
+			webhookServerRouteEnabled: true,
+			setupClient: func(webhookServerRouteEnabled bool) *ApplicationSetReconciler {
+				outdatedWebhookRoute := existingWebhookRoute
+				outdatedWebhookRoute.ObjectMeta.Labels = argocdcommon.TestKVP
+				return makeTestApplicationSetReconciler(t, webhookServerRouteEnabled, outdatedWebhookRoute, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			asr := tt.setupClient(tt.webhookServerRouteEnabled)
+			err := asr.reconcileWebhookRoute()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+
+			updatedWebhookRoute := &routev1.Route{}
+			err = asr.Client.Get(context.TODO(), types.NamespacedName{Name: AppSetWebhookRouteName, Namespace: argocdcommon.TestNamespace}, updatedWebhookRoute)
+			if err != nil {
+				t.Fatalf("Could not get updated WebhookRoute: %v", err)
+			}
+			assert.Equal(t, testExpectedLabels, updatedWebhookRoute.ObjectMeta.Labels)
+		})
+	}
+}
+
+func TestApplicationSetReconciler_reconcileWebhookRoute_WebhookServerRouteDisabled(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+
+	tests := []struct {
+		name                      string
+		webhookServerRouteEnabled bool
+		setupClient               func(bool) *ApplicationSetReconciler
+		wantErr                   bool
+	}{
+		{
+			name:                      "clear a webhookRoute",
+			webhookServerRouteEnabled: false,
+			setupClient: func(webhookServerRouteEnabled bool) *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, webhookServerRouteEnabled, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			asr := tt.setupClient(tt.webhookServerRouteEnabled)
+			err := asr.reconcileWebhookRoute()
+			if (err != nil) != tt.wantErr {
+				if tt.wantErr {
+					t.Errorf("Expected error but did not get one")
+				} else {
+					t.Errorf("Unexpected error: %v", err)
+				}
+			}
+
+			webhookRoute := &routev1.Route{}
+			err = asr.Client.Get(context.TODO(), types.NamespacedName{Name: AppSetWebhookRouteName, Namespace: argocdcommon.TestNamespace}, webhookRoute)
+			if err != nil {
+				assert.Equal(t, errors.IsNotFound(err), true)
+			}
+		})
+	}
+}
+
+func TestApplicationSetReconciler_deleteWebhookRoute(t *testing.T) {
+	ns := argocdcommon.MakeTestNamespace()
+	tests := []struct {
+		name                      string
+		webhookServerRouteEnabled bool
+		setupClient               func(bool) *ApplicationSetReconciler
+		wantErr                   bool
+	}{
+		{
+			name:                      "successful delete",
+			webhookServerRouteEnabled: true,
+			setupClient: func(webhookServerRouteEnabled bool) *ApplicationSetReconciler {
+				return makeTestApplicationSetReconciler(t, webhookServerRouteEnabled, ns)
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			asr := tt.setupClient(tt.webhookServerRouteEnabled)
+			if err := asr.deleteWebhookRoute(AppSetWebhookRouteName, 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/applicationset_test.go b/controllers/argocd/applicationset_test.go
deleted file mode 100644
index ad3ba108..00000000
--- a/controllers/argocd/applicationset_test.go
+++ /dev/null
@@ -1,665 +0,0 @@
-// Copyright 2021 ArgoCD Operator Developers
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// 	http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package argocd
-
-import (
-	"context"
-	"sort"
-	"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"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/controller-runtime/pkg/client"
-	logf "sigs.k8s.io/controller-runtime/pkg/log"
-
-	argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1"
-	"github.com/argoproj-labs/argocd-operator/common"
-	"github.com/argoproj-labs/argocd-operator/pkg/util"
-)
-
-func applicationSetDefaultVolumeMounts() []corev1.VolumeMount {
-	repoMounts := repoServerDefaultVolumeMounts()
-	ignoredMounts := map[string]bool{
-		"plugins":                             true,
-		"argocd-repo-server-tls":              true,
-		common.ArgoCDRedisServerTLSSecretName: true,
-	}
-	mounts := make([]corev1.VolumeMount, len(repoMounts)-len(ignoredMounts), len(repoMounts)-len(ignoredMounts))
-	j := 0
-	for _, mount := range repoMounts {
-		if !ignoredMounts[mount.Name] {
-			mounts[j] = mount
-			j += 1
-		}
-	}
-	return mounts
-}
-
-func applicationSetDefaultVolumes() []corev1.Volume {
-	repoVolumes := repoServerDefaultVolumes()
-	ignoredVolumes := map[string]bool{
-		"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
-	for _, volume := range repoVolumes {
-		if !ignoredVolumes[volume.Name] {
-			volumes[j] = volume
-			j += 1
-		}
-	}
-	return volumes
-}
-
-func TestReconcileApplicationSet_CreateDeployments(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-	a := makeTestArgoCD()
-	a.Spec.ApplicationSet = &argoproj.ArgoCDApplicationSet{}
-
-	r := makeTestReconciler(t, a)
-
-	sa := corev1.ServiceAccount{}
-
-	assert.NoError(t, r.reconcileApplicationSetDeployment(a, &sa))
-
-	deployment := &appsv1.Deployment{}
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	// Ensure the created Deployment has the expected properties
-	checkExpectedDeploymentValues(t, r, deployment, &sa, a)
-}
-
-func checkExpectedDeploymentValues(t *testing.T, r *ArgoCDReconciler, deployment *appsv1.Deployment, sa *corev1.ServiceAccount, a *argoproj.ArgoCD) {
-	assert.Equal(t, deployment.Spec.Template.Spec.ServiceAccountName, sa.ObjectMeta.Name)
-	appsetAssertExpectedLabels(t, &deployment.ObjectMeta)
-
-	want := []corev1.Container{applicationSetContainer(a, false)}
-
-	if diff := cmp.Diff(want, deployment.Spec.Template.Spec.Containers); diff != "" {
-		t.Fatalf("failed to reconcile applicationset-controller deployment containers:\n%s", diff)
-	}
-
-	volumes := []corev1.Volume{
-		{
-			Name: "ssh-known-hosts",
-			VolumeSource: corev1.VolumeSource{
-				ConfigMap: &corev1.ConfigMapVolumeSource{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: common.ArgoCDKnownHostsConfigMapName,
-					},
-				},
-			},
-		},
-		{
-			Name: "tls-certs",
-			VolumeSource: corev1.VolumeSource{
-				ConfigMap: &corev1.ConfigMapVolumeSource{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: common.ArgoCDTLSCertsConfigMapName,
-					},
-				},
-			},
-		},
-		{
-			Name: "gpg-keys",
-			VolumeSource: corev1.VolumeSource{
-				ConfigMap: &corev1.ConfigMapVolumeSource{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: common.ArgoCDGPGKeysConfigMapName,
-					},
-				},
-			},
-		},
-		{
-			Name: "gpg-keyring",
-			VolumeSource: corev1.VolumeSource{
-				EmptyDir: &corev1.EmptyDirVolumeSource{},
-			},
-		},
-		{
-			Name: "tmp",
-			VolumeSource: corev1.VolumeSource{
-				EmptyDir: &corev1.EmptyDirVolumeSource{},
-			},
-		},
-	}
-
-	if a.Spec.ApplicationSet.SCMRootCAConfigMap != "" && util.IsObjectFound(r.Client, a.Namespace, common.ArgoCDAppSetGitlabSCMTLSCertsConfigMapName, a) {
-		volumes = append(volumes, corev1.Volume{
-			Name: "appset-gitlab-scm-tls-cert",
-			VolumeSource: corev1.VolumeSource{
-				ConfigMap: &corev1.ConfigMapVolumeSource{
-					LocalObjectReference: corev1.LocalObjectReference{
-						Name: common.ArgoCDAppSetGitlabSCMTLSCertsConfigMapName,
-					},
-				},
-			},
-		})
-	}
-
-	if diff := cmp.Diff(volumes, deployment.Spec.Template.Spec.Volumes); diff != "" {
-		t.Fatalf("failed to reconcile applicationset-controller deployment volumes:\n%s", diff)
-	}
-
-	expectedSelector := &metav1.LabelSelector{
-		MatchLabels: map[string]string{
-			common.AppK8sKeyName: deployment.Name,
-		},
-	}
-
-	if diff := cmp.Diff(expectedSelector, deployment.Spec.Selector); diff != "" {
-		t.Fatalf("failed to reconcile applicationset-controller label selector:\n%s", diff)
-	}
-}
-
-func TestReconcileApplicationSetProxyConfiguration(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-
-	// Proxy Env vars
-	setProxyEnvVars(t)
-
-	a := makeTestArgoCD()
-	a.Spec.ApplicationSet = &argoproj.ArgoCDApplicationSet{}
-
-	r := makeTestReconciler(t, a)
-
-	sa := corev1.ServiceAccount{}
-
-	r.reconcileApplicationSetDeployment(a, &sa)
-
-	want := []corev1.EnvVar{
-		{
-			Name:  "HTTPS_PROXY",
-			Value: "https://example.com",
-		},
-		{
-			Name:  "HTTP_PROXY",
-			Value: "http://example.com",
-		},
-		{
-			Name: "NAMESPACE",
-			ValueFrom: &corev1.EnvVarSource{
-				FieldRef: &corev1.ObjectFieldSelector{
-					FieldPath: "metadata.namespace",
-				},
-			},
-		},
-		{
-			Name:  "NO_PROXY",
-			Value: ".cluster.local",
-		},
-	}
-
-	deployment := &appsv1.Deployment{}
-
-	// reconcile ApplicationSets
-	r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment)
-
-	if diff := cmp.Diff(want, deployment.Spec.Template.Spec.Containers[0].Env); diff != "" {
-		t.Fatalf("failed to reconcile applicationset-controller deployment containers:\n%s", diff)
-	}
-
-}
-
-func TestReconcileApplicationSet_UpdateExistingDeployments(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-	a := makeTestArgoCD()
-
-	a.Spec.ApplicationSet = &argoproj.ArgoCDApplicationSet{}
-
-	existingDeployment := &appsv1.Deployment{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      a.Name + "-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		Spec: appsv1.DeploymentSpec{
-			Template: corev1.PodTemplateSpec{
-				Spec: corev1.PodSpec{
-					Containers: []corev1.Container{
-						{
-							Name: "fake-container",
-						},
-					},
-				},
-			},
-		},
-	}
-
-	runtimeObjs := []runtime.Object{a, existingDeployment}
-	r := makeTestReconciler(t, runtimeObjs...)
-
-	sa := corev1.ServiceAccount{}
-
-	assert.NoError(t, r.reconcileApplicationSetDeployment(a, &sa))
-
-	deployment := &appsv1.Deployment{}
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	// Ensure the updated Deployment has the expected properties
-	checkExpectedDeploymentValues(t, r, deployment, &sa, a)
-
-}
-
-func TestReconcileApplicationSet_Deployments_resourceRequirements(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-	a := makeTestArgoCDWithResources()
-
-	r := makeTestReconciler(t, a)
-
-	sa := corev1.ServiceAccount{}
-
-	assert.NoError(t, r.reconcileApplicationSetDeployment(a, &sa))
-
-	deployment := &appsv1.Deployment{}
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	assert.Equal(t, deployment.Spec.Template.Spec.ServiceAccountName, sa.ObjectMeta.Name)
-	appsetAssertExpectedLabels(t, &deployment.ObjectMeta)
-
-	containerWant := []corev1.Container{applicationSetContainer(a, false)}
-
-	if diff := cmp.Diff(containerWant, deployment.Spec.Template.Spec.Containers); diff != "" {
-		t.Fatalf("failed to reconcile argocd-server deployment:\n%s", diff)
-	}
-
-	volumesWant := applicationSetDefaultVolumes()
-
-	if diff := cmp.Diff(volumesWant, deployment.Spec.Template.Spec.Volumes); diff != "" {
-		t.Fatalf("failed to reconcile argocd-server deployment:\n%s", diff)
-	}
-}
-
-func TestReconcileApplicationSet_Deployments_SpecOverride(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-
-	tests := []struct {
-		name                   string
-		appSetField            *argoproj.ArgoCDApplicationSet
-		envVars                map[string]string
-		expectedContainerImage string
-	}{
-		{
-			name:                   "unspecified fields should use default",
-			appSetField:            &argoproj.ArgoCDApplicationSet{},
-			expectedContainerImage: util.CombineImageTag(common.ArgoCDDefaultArgoImage, common.ArgoCDDefaultArgoVersion),
-		},
-		{
-			name: "ensure that sha hashes are formatted correctly",
-			appSetField: &argoproj.ArgoCDApplicationSet{
-				Image:   "custom-image",
-				Version: "sha256:b835999eb5cf75d01a2678cd971095926d9c2566c9ffe746d04b83a6a0a2849f",
-			},
-			expectedContainerImage: "custom-image@sha256:b835999eb5cf75d01a2678cd971095926d9c2566c9ffe746d04b83a6a0a2849f",
-		},
-		{
-			name: "custom image should properly substitute",
-			appSetField: &argoproj.ArgoCDApplicationSet{
-				Image:   "custom-image",
-				Version: "custom-version",
-			},
-			expectedContainerImage: "custom-image:custom-version",
-		},
-		{
-			name:                   "verify env var substitution overrides default",
-			appSetField:            &argoproj.ArgoCDApplicationSet{},
-			envVars:                map[string]string{common.ArgoCDImageEnvVar: "custom-env-image"},
-			expectedContainerImage: "custom-env-image",
-		},
-
-		{
-			name: "env var should not override spec fields",
-			appSetField: &argoproj.ArgoCDApplicationSet{
-				Image:   "custom-image",
-				Version: "custom-version",
-			},
-			envVars:                map[string]string{common.ArgoCDImageEnvVar: "custom-env-image"},
-			expectedContainerImage: "custom-image:custom-version",
-		},
-		{
-			name: "ensure scm tls cert mount is present",
-			appSetField: &argoproj.ArgoCDApplicationSet{
-				SCMRootCAConfigMap: "test-scm-tls-mount",
-			},
-			envVars:                map[string]string{common.ArgoCDImageEnvVar: "custom-env-image"},
-			expectedContainerImage: "custom-env-image",
-		},
-	}
-
-	for _, test := range tests {
-		t.Run(test.name, func(t *testing.T) {
-
-			for testEnvVar, testEnvValue := range test.envVars {
-				t.Setenv(testEnvVar, testEnvValue)
-			}
-
-			a := makeTestArgoCD()
-			r := makeTestReconciler(t, a)
-			cm := newConfigMapWithName(getCAConfigMapName(a), a)
-			r.Client.Create(context.Background(), cm, &client.CreateOptions{})
-
-			a.Spec.ApplicationSet = test.appSetField
-
-			sa := corev1.ServiceAccount{}
-			assert.NoError(t, r.reconcileApplicationSetDeployment(a, &sa))
-
-			deployment := &appsv1.Deployment{}
-			assert.NoError(t, r.Client.Get(
-				context.TODO(),
-				types.NamespacedName{
-					Name:      "argocd-applicationset-controller",
-					Namespace: a.Namespace,
-				},
-				deployment))
-
-			specImage := deployment.Spec.Template.Spec.Containers[0].Image
-			assert.Equal(t, test.expectedContainerImage, specImage)
-			checkExpectedDeploymentValues(t, r, deployment, &sa, a)
-		})
-	}
-
-}
-
-func TestReconcileApplicationSet_ServiceAccount(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-	a := makeTestArgoCD()
-	r := makeTestReconciler(t, a)
-
-	retSa, err := r.reconcileApplicationSetServiceAccount(a)
-	assert.NoError(t, err)
-
-	sa := &corev1.ServiceAccount{}
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		sa))
-
-	assert.Equal(t, sa.Name, retSa.Name)
-
-	appsetAssertExpectedLabels(t, &sa.ObjectMeta)
-}
-
-func TestReconcileApplicationSet_Role(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-	a := makeTestArgoCD()
-	r := makeTestReconciler(t, a)
-
-	roleRet, err := r.reconcileApplicationSetRole(a)
-	assert.NoError(t, err)
-
-	role := &rbacv1.Role{}
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		role))
-
-	assert.Equal(t, roleRet.Name, role.Name)
-	appsetAssertExpectedLabels(t, &role.ObjectMeta)
-
-	expectedResources := []string{
-		"deployments",
-		"secrets",
-		"configmaps",
-		"events",
-		"applicationsets/status",
-		"applications",
-		"applicationsets",
-		"appprojects",
-		"applicationsets/finalizers",
-	}
-
-	foundResources := []string{}
-
-	for _, rule := range role.Rules {
-		for _, resource := range rule.Resources {
-			foundResources = append(foundResources, resource)
-		}
-	}
-
-	sort.Strings(expectedResources)
-	sort.Strings(foundResources)
-
-	assert.Equal(t, expectedResources, foundResources)
-}
-
-func TestReconcileApplicationSet_RoleBinding(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-	a := makeTestArgoCD()
-	r := makeTestReconciler(t, a)
-
-	role := &rbacv1.Role{ObjectMeta: metav1.ObjectMeta{Name: "role-name"}}
-	sa := &corev1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Name: "sa-name"}}
-
-	err := r.reconcileApplicationSetRoleBinding(a, role, sa)
-	assert.NoError(t, err)
-
-	roleBinding := &rbacv1.RoleBinding{}
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		roleBinding))
-
-	appsetAssertExpectedLabels(t, &roleBinding.ObjectMeta)
-
-	assert.Equal(t, roleBinding.RoleRef.Name, role.Name)
-	assert.Equal(t, roleBinding.Subjects[0].Name, sa.Name)
-
-}
-
-func appsetAssertExpectedLabels(t *testing.T, meta *metav1.ObjectMeta) {
-	assert.Equal(t, meta.Labels["app.kubernetes.io/name"], "argocd-applicationset-controller")
-	assert.Equal(t, meta.Labels["app.kubernetes.io/part-of"], "argocd-applicationset")
-	assert.Equal(t, meta.Labels["app.kubernetes.io/component"], "controller")
-}
-
-func setProxyEnvVars(t *testing.T) {
-	t.Setenv("HTTPS_PROXY", "https://example.com")
-	t.Setenv("HTTP_PROXY", "http://example.com")
-	t.Setenv("NO_PROXY", ".cluster.local")
-}
-
-func TestReconcileApplicationSet_Service(t *testing.T) {
-	logf.SetLogger(ZapLogger(true))
-	a := makeTestArgoCD()
-	r := makeTestReconciler(t, a)
-
-	s := newServiceWithSuffix(common.ApplicationSetServiceNameSuffix, common.ApplicationSetServiceNameSuffix, a)
-
-	assert.NoError(t, r.reconcileApplicationSetService(a))
-	assert.NoError(t, r.Client.Get(context.TODO(), types.NamespacedName{Namespace: s.Namespace, Name: s.Name}, s))
-}
-
-func TestArgoCDApplicationSetCommand(t *testing.T) {
-	a := makeTestArgoCD()
-	a.Spec.ApplicationSet = &argoproj.ArgoCDApplicationSet{}
-	r := makeTestReconciler(t, a)
-
-	baseCommand := []string{
-		"entrypoint.sh",
-		"argocd-applicationset-controller",
-		"--argocd-repo-server",
-		"argocd-repo-server.argocd.svc.cluster.local:8081",
-		"--loglevel",
-		"info",
-	}
-
-	// When a single command argument is passed
-	a.Spec.ApplicationSet.ExtraCommandArgs = []string{
-		"--foo",
-		"bar",
-	}
-
-	deployment := &appsv1.Deployment{}
-	assert.NoError(t, r.reconcileApplicationSetController(a))
-
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	cmd := append(baseCommand, "--foo", "bar")
-	assert.Equal(t, cmd, deployment.Spec.Template.Spec.Containers[0].Command)
-
-	// When multiple command arguments are passed
-	a.Spec.ApplicationSet.ExtraCommandArgs = []string{
-		"--foo",
-		"bar",
-		"--ping",
-		"pong",
-		"test",
-	}
-
-	assert.NoError(t, r.reconcileApplicationSetController(a))
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	cmd = append(cmd, "--ping", "pong", "test")
-	assert.Equal(t, cmd, deployment.Spec.Template.Spec.Containers[0].Command)
-
-	// When one of the ExtraCommandArgs already exists in cmd with same or different value
-	a.Spec.ApplicationSet.ExtraCommandArgs = []string{
-		"--argocd-repo-server",
-		"foo.scv.cluster.local:6379",
-	}
-
-	assert.NoError(t, r.reconcileApplicationSetController(a))
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	assert.Equal(t, baseCommand, deployment.Spec.Template.Spec.Containers[0].Command)
-
-	// Remove all the command arguments that were added.
-	a.Spec.ApplicationSet.ExtraCommandArgs = []string{}
-
-	assert.NoError(t, r.reconcileApplicationSetController(a))
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	assert.Equal(t, baseCommand, deployment.Spec.Template.Spec.Containers[0].Command)
-}
-
-func TestArgoCDApplicationSetEnv(t *testing.T) {
-	a := makeTestArgoCD()
-	a.Spec.ApplicationSet = &argoproj.ArgoCDApplicationSet{}
-	r := makeTestReconciler(t, a)
-
-	defaultEnv := []corev1.EnvVar{
-		{
-			Name: "NAMESPACE",
-			ValueFrom: &corev1.EnvVarSource{
-				FieldRef: &corev1.ObjectFieldSelector{
-					APIVersion: "",
-					FieldPath:  "metadata.namespace",
-				},
-			},
-		},
-	}
-
-	// Pass an environment variable using Argo CD CR.
-	customEnv := []corev1.EnvVar{
-		{
-			Name:  "foo",
-			Value: "bar",
-		},
-	}
-	a.Spec.ApplicationSet.Env = customEnv
-
-	deployment := &appsv1.Deployment{}
-	assert.NoError(t, r.reconcileApplicationSetController(a))
-
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	expectedEnv := append(defaultEnv, customEnv...)
-	assert.Equal(t, expectedEnv, deployment.Spec.Template.Spec.Containers[0].Env)
-
-	// Remove all the env vars that were added.
-	a.Spec.ApplicationSet.Env = []corev1.EnvVar{}
-
-	assert.NoError(t, r.reconcileApplicationSetController(a))
-	assert.NoError(t, r.Client.Get(
-		context.TODO(),
-		types.NamespacedName{
-			Name:      "argocd-applicationset-controller",
-			Namespace: a.Namespace,
-		},
-		deployment))
-
-	assert.Equal(t, defaultEnv, deployment.Spec.Template.Spec.Containers[0].Env)
-}
diff --git a/controllers/argocd/argocdcommon/testing.go b/controllers/argocd/argocdcommon/testing.go
index f954af33..24deed41 100644
--- a/controllers/argocd/argocdcommon/testing.go
+++ b/controllers/argocd/argocdcommon/testing.go
@@ -2,7 +2,9 @@ package argocdcommon
 
 import (
 	argoproj "github.com/argoproj-labs/argocd-operator/api/v1beta1"
+	"github.com/argoproj-labs/argocd-operator/common"
 	corev1 "k8s.io/api/core/v1"
+	rbacv1 "k8s.io/api/rbac/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -11,6 +13,28 @@ const (
 	TestArgoCDName = "argocd"
 )
 
+var (
+	TestKey     = "test"
+	TestVal     = "test"
+	TestRoleRef = rbacv1.RoleRef{
+		Kind:     common.RoleKind,
+		Name:     TestArgoCDName,
+		APIGroup: rbacv1.GroupName,
+	}
+
+	TestSubjects = []rbacv1.Subject{
+		{
+			Kind:      rbacv1.ServiceAccountKind,
+			Name:      TestArgoCDName,
+			Namespace: TestNamespace,
+		},
+	}
+
+	TestKVP = map[string]string{
+		TestKey: TestVal,
+	}
+)
+
 func MakeTestNamespace() *corev1.Namespace {
 	return &corev1.Namespace{
 		ObjectMeta: metav1.ObjectMeta{
diff --git a/controllers/argocd/notifications/configmap_test.go b/controllers/argocd/notifications/configmap_test.go
index 1366c948..94df5a5b 100644
--- a/controllers/argocd/notifications/configmap_test.go
+++ b/controllers/argocd/notifications/configmap_test.go
@@ -67,7 +67,7 @@ func TestNotificationsReconciler_DeleteConfigMap(t *testing.T) {
 	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 err := nr.deleteConfigMap(ns.Name); (err != nil) != tt.wantErr {
 				if tt.wantErr {
 					t.Errorf("Expected error but did not get one")
 				} else {
diff --git a/controllers/argocd/notifications/deployment_test.go b/controllers/argocd/notifications/deployment_test.go
index cd093558..884a6bcb 100644
--- a/controllers/argocd/notifications/deployment_test.go
+++ b/controllers/argocd/notifications/deployment_test.go
@@ -35,7 +35,7 @@ func TestNotificationsReconciler_reconcileDeployment(t *testing.T) {
 			name: "update a deployment",
 			setupClient: func() *NotificationsReconciler {
 				outdatedDeployment := existingDeployment
-				outdatedDeployment.ObjectMeta.Labels = testKVP
+				outdatedDeployment.ObjectMeta.Labels = argocdcommon.TestKVP
 				return makeTestNotificationsReconciler(t, outdatedDeployment, ns)
 			},
 			wantErr: false,
@@ -82,7 +82,7 @@ func TestNotificationsReconciler_DeleteDeployment(t *testing.T) {
 	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 err := nr.deleteDeployment(resourceName, ns.Name); (err != nil) != tt.wantErr {
 				if tt.wantErr {
 					t.Errorf("Expected error but did not get one")
 				} else {
diff --git a/controllers/argocd/notifications/notifications_test.go b/controllers/argocd/notifications/notifications_test.go
index 4f8822e6..005ad557 100644
--- a/controllers/argocd/notifications/notifications_test.go
+++ b/controllers/argocd/notifications/notifications_test.go
@@ -7,43 +7,20 @@ import (
 	"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)
-)
+var testExpectedLabels = common.DefaultLabels(argocdcommon.TestArgoCDName, argocdcommon.TestNamespace, NotificationsControllerComponent)
 
 func makeTestNotificationsReconciler(t *testing.T, objs ...runtime.Object) *NotificationsReconciler {
 	s := scheme.Scheme
 	assert.NoError(t, argoproj.AddToScheme(s))
 
 	cl := fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...).Build()
-	logger := ctrl.Log.WithName(ArgoCDNotificationsControllerComponent)
+	logger := ctrl.Log.WithName(NotificationsControllerComponent)
 
 	return &NotificationsReconciler{
 		Client:   cl,
diff --git a/controllers/argocd/notifications/role_test.go b/controllers/argocd/notifications/role_test.go
index 5c052c93..93ae8b45 100644
--- a/controllers/argocd/notifications/role_test.go
+++ b/controllers/argocd/notifications/role_test.go
@@ -90,7 +90,7 @@ func TestNotificationsReconciler_DeleteRole(t *testing.T) {
 	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 err := nr.deleteRole(resourceName, ns.Name); (err != nil) != tt.wantErr {
 				if tt.wantErr {
 					t.Errorf("Expected error but did not get one")
 				} else {
diff --git a/controllers/argocd/notifications/rolebinding_test.go b/controllers/argocd/notifications/rolebinding_test.go
index 02f43c0d..f0017994 100644
--- a/controllers/argocd/notifications/rolebinding_test.go
+++ b/controllers/argocd/notifications/rolebinding_test.go
@@ -65,8 +65,8 @@ func TestNotificationsReconciler_reconcileRoleBinding(t *testing.T) {
 			if err != nil {
 				t.Fatalf("Could not get updated RoleBinding: %v", err)
 			}
-			assert.Equal(t, testRoleRef, updatedRoleBinding.RoleRef)
-			assert.Equal(t, testSubjects, updatedRoleBinding.Subjects)
+			assert.Equal(t, argocdcommon.TestRoleRef, updatedRoleBinding.RoleRef)
+			assert.Equal(t, argocdcommon.TestSubjects, updatedRoleBinding.Subjects)
 		})
 	}
 }
@@ -91,7 +91,7 @@ func TestNotificationsReconciler_DeleteRoleBinding(t *testing.T) {
 	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 err := nr.deleteRoleBinding(resourceName, ns.Name); (err != nil) != tt.wantErr {
 				if tt.wantErr {
 					t.Errorf("Expected error but did not get one")
 				} else {
diff --git a/controllers/argocd/notifications/secret_test.go b/controllers/argocd/notifications/secret_test.go
index 743f4782..e51911cf 100644
--- a/controllers/argocd/notifications/secret_test.go
+++ b/controllers/argocd/notifications/secret_test.go
@@ -67,7 +67,7 @@ func TestNotificationsReconciler_DeleteSecret(t *testing.T) {
 	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 err := nr.deleteSecret(ns.Name); (err != nil) != tt.wantErr {
 				if tt.wantErr {
 					t.Errorf("Expected error but did not get one")
 				} else {
diff --git a/controllers/argocd/notifications/serviceaccount_test.go b/controllers/argocd/notifications/serviceaccount_test.go
index 426c1d71..30a18d42 100644
--- a/controllers/argocd/notifications/serviceaccount_test.go
+++ b/controllers/argocd/notifications/serviceaccount_test.go
@@ -4,11 +4,9 @@ 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"
 )
 
@@ -16,18 +14,6 @@ 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
@@ -41,13 +27,6 @@ func TestNotificationsReconciler_reconcileServiceAccount(t *testing.T) {
 			},
 			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) {
@@ -90,7 +69,7 @@ func TestNotificationsReconciler_DeleteServiceAccount(t *testing.T) {
 	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 err := nr.deleteServiceAccount(resourceName, ns.Name); (err != nil) != tt.wantErr {
 				if tt.wantErr {
 					t.Errorf("Expected error but did not get one")
 				} else {
diff --git a/controllers/argocd/status_test.go b/controllers/argocd/status_test.go
index ce75bcb3..45035616 100644
--- a/controllers/argocd/status_test.go
+++ b/controllers/argocd/status_test.go
@@ -283,7 +283,7 @@ func TestArgoCDReconciler_reconcileStatusApplicationSetController(t *testing.T)
 	assert.Equal(t, "Unknown", a.Status.ApplicationSetController)
 
 	a.Spec.ApplicationSet = &argoproj.ArgoCDApplicationSet{}
-	assert.NoError(t, r.reconcileApplicationSetController(a))
+	assert.NoError(t, r.AppsetController.Reconcile())
 	assert.NoError(t, r.reconcileStatusApplicationSetController(a))
 	assert.Equal(t, "Pending", a.Status.ApplicationSetController)
 }
diff --git a/tests/controller/applicationset/1-001_enable_applicationset/00-assert.yaml b/tests/controller/applicationset/1-001_enable_applicationset/00-assert.yaml
new file mode 100644
index 00000000..1d52decd
--- /dev/null
+++ b/tests/controller/applicationset/1-001_enable_applicationset/00-assert.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+  applicationSetController: Unknown
diff --git a/tests/controller/applicationset/1-001_enable_applicationset/00-errors.yaml b/tests/controller/applicationset/1-001_enable_applicationset/00-errors.yaml
new file mode 100644
index 00000000..bc12445e
--- /dev/null
+++ b/tests/controller/applicationset/1-001_enable_applicationset/00-errors.yaml
@@ -0,0 +1,6 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  applicationSetController:
diff --git a/tests/controller/applicationset/1-001_enable_applicationset/00-install.yaml b/tests/controller/applicationset/1-001_enable_applicationset/00-install.yaml
new file mode 100644
index 00000000..5e0159bd
--- /dev/null
+++ b/tests/controller/applicationset/1-001_enable_applicationset/00-install.yaml
@@ -0,0 +1,5 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec: {}
diff --git a/tests/controller/applicationset/1-001_enable_applicationset/01-assert.yaml b/tests/controller/applicationset/1-001_enable_applicationset/01-assert.yaml
new file mode 100644
index 00000000..91a105e7
--- /dev/null
+++ b/tests/controller/applicationset/1-001_enable_applicationset/01-assert.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+  applicationSetController: Running
\ No newline at end of file
diff --git a/tests/controller/applicationset/1-001_enable_applicationset/01-enable_applicationset.yaml b/tests/controller/applicationset/1-001_enable_applicationset/01-enable_applicationset.yaml
new file mode 100644
index 00000000..8bc5da47
--- /dev/null
+++ b/tests/controller/applicationset/1-001_enable_applicationset/01-enable_applicationset.yaml
@@ -0,0 +1,6 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  applicationSet: {}
\ No newline at end of file
diff --git a/tests/controller/applicationset/1-002_applicationset_w_invalid_image/00-assert.yaml b/tests/controller/applicationset/1-002_applicationset_w_invalid_image/00-assert.yaml
new file mode 100644
index 00000000..2f018b96
--- /dev/null
+++ b/tests/controller/applicationset/1-002_applicationset_w_invalid_image/00-assert.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+  applicationSetController: Running
diff --git a/tests/controller/applicationset/1-002_applicationset_w_invalid_image/00-install.yaml b/tests/controller/applicationset/1-002_applicationset_w_invalid_image/00-install.yaml
new file mode 100644
index 00000000..d746a6eb
--- /dev/null
+++ b/tests/controller/applicationset/1-002_applicationset_w_invalid_image/00-install.yaml
@@ -0,0 +1,6 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  applicationSet: {}
diff --git a/tests/controller/applicationset/1-002_applicationset_w_invalid_image/01-assert.yaml b/tests/controller/applicationset/1-002_applicationset_w_invalid_image/01-assert.yaml
new file mode 100644
index 00000000..239621b0
--- /dev/null
+++ b/tests/controller/applicationset/1-002_applicationset_w_invalid_image/01-assert.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
diff --git a/tests/controller/applicationset/1-002_applicationset_w_invalid_image/01-invalid-appset-image.yaml b/tests/controller/applicationset/1-002_applicationset_w_invalid_image/01-invalid-appset-image.yaml
new file mode 100644
index 00000000..a427db2a
--- /dev/null
+++ b/tests/controller/applicationset/1-002_applicationset_w_invalid_image/01-invalid-appset-image.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  applicationSet:
+    image: quay.io/argoproj/argocd@sha256:8283a9f06033c2377dc61b03daf49
\ No newline at end of file
diff --git a/tests/controller/applicationset/1-003_delete_appset_deployment/00-assert.yaml b/tests/controller/applicationset/1-003_delete_appset_deployment/00-assert.yaml
new file mode 100644
index 00000000..2f018b96
--- /dev/null
+++ b/tests/controller/applicationset/1-003_delete_appset_deployment/00-assert.yaml
@@ -0,0 +1,7 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  phase: Available
+  applicationSetController: Running
diff --git a/tests/controller/applicationset/1-003_delete_appset_deployment/00-install.yaml b/tests/controller/applicationset/1-003_delete_appset_deployment/00-install.yaml
new file mode 100644
index 00000000..d746a6eb
--- /dev/null
+++ b/tests/controller/applicationset/1-003_delete_appset_deployment/00-install.yaml
@@ -0,0 +1,6 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+spec:
+  applicationSet: {}
diff --git a/tests/controller/applicationset/1-003_delete_appset_deployment/01-assert.yaml b/tests/controller/applicationset/1-003_delete_appset_deployment/01-assert.yaml
new file mode 100644
index 00000000..08811b4b
--- /dev/null
+++ b/tests/controller/applicationset/1-003_delete_appset_deployment/01-assert.yaml
@@ -0,0 +1,6 @@
+apiVersion: argoproj.io/v1alpha1
+kind: ArgoCD
+metadata:
+  name: example-argocd
+status:
+  applicationSetController: Pending
\ No newline at end of file
diff --git a/tests/controller/applicationset/1-003_delete_appset_deployment/01-delete-existing-deployment.yaml b/tests/controller/applicationset/1-003_delete_appset_deployment/01-delete-existing-deployment.yaml
new file mode 100644
index 00000000..ea61013e
--- /dev/null
+++ b/tests/controller/applicationset/1-003_delete_appset_deployment/01-delete-existing-deployment.yaml
@@ -0,0 +1,4 @@
+apiVersion: kuttl.dev/v1beta1
+kind: TestStep
+commands:
+  - command: kubectl delete deployment example-argocd-applicationset-controller -n $NAMESPACE
\ No newline at end of file
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
index 239621b0..12dbc9e8 100644
--- a/tests/controller/notifications/1-003_validate_notifications/05-verify-email.yaml
+++ b/tests/controller/notifications/1-003_validate_notifications/05-verify-email.yaml
@@ -2,16 +2,16 @@ 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')
+    pods=$(kubectl get pods -n $NAMESPACE -o NAME | grep "applicationset-controller")
 
-    if [ $exit_code=0 ]; then
-        exit 0
-    else
-        exit 1
-    fi
\ No newline at end of file
+    for pod in $pods; do
+        ready_status=$(kubectl get pod $pod -n $NAMESPACE -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}')
+
+        if [[ $ready_status == "False" ]]; then
+            echo "Pod $pod is not in Ready state as image is invalid."
+            exit 0
+        fi
+    done
+
+    echo "Unexpected: All pods are in Ready state."
+    exit 1