Repository: kubesphere/ks-devops
Pull request #979 (2023-07-27 04:50:25)

Fixed the issue of some application resources not being deleted when …
…deleting multiple apps in the cascade

<!-- Thanks for sending a pull request! Here are some tips for you:

1. If you want **faster** PR reviews, read how: https://github.com/kubesphere/community/blob/master/developer-guide/development/the-pr-author-guide-to-getting-through-code-review.md
2. In case you want to know how your PR got reviewed, read: https://github.com/kubesphere/community/blob/master/developer-guide/development/code-review-guide.md
3. Here are some coding conventions followed by the KubeSphere community: https://github.com/kubesphere/community/blob/master/developer-guide/development/coding-conventions.md
4. Additional open-source best practice: https://github.com/LinuxSuRen/open-source-best-practice
-->

### What type of PR is this?
<!-- 
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design
/kind chore

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->

### What this PR does / why we need it:

### Which issue(s) this PR fixes:
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
Please leave it or change # to be None if there is no corresponding issue that exists
-->
Fixes #https://github.com/kubesphere/ks-devops/issues/570

### Special notes for reviewers:
<!--
You can use the following command to let the DevOps SIG members help you to review your PR.
/cc @kubesphere/sig-devops 
And please avoid cc any individual.
-->
Please check the following list before waiting reviewers:

- [ ] Already committed the CRD files to [the Helm Chart](https://github.com/kubesphere-sigs/ks-devops-helm-chart/) if you created some new CRDs
- [ ] Already [added the permission](https://github.com/kubesphere/ks-installer/blob/9e063b085a0e43fdb3d0d9e3e7f4149146f14b9c/roles/ks-core/prepare/files/ks-init/role-templates.yaml) for the new API
- [ ] Already added the RBAC markers for the new controllers

### Does this PR introduce a user-facing change??
<!--
If no, just write "None" in the release-note block below.
If yes, a release note is required:
Enter your extended-release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

Please keep the note be same as your PR title if you believe it should be in the release notes.
-->
```release-note
Fixed the issue of some application resources not being deleted when cascade delete multiple applications.
```


ks-ci-bot: @chilianyi: Adding the "do-not-merge/release-note-label-needed" label because no release-note block was detected, please follow our [release note process](https://git.k8s.io/community/contributors/guide/release-notes.md) to remove it.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
ks-ci-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please ask for approval from **chilianyi** after the PR has been reviewed.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["chilianyi"]} -->

Patch:
diff --git a/.gitignore b/.gitignore
index 93f6953..7d015cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
 *.so
 *.dylib
 bin
+scripts
 
 # Test binary, build with `go test -c`
 *.test
diff --git a/controllers/argocd/application_controller.go b/controllers/argocd/application_controller.go
index 990532f..5df88be 100644
--- a/controllers/argocd/application_controller.go
+++ b/controllers/argocd/application_controller.go
@@ -95,13 +95,11 @@ func (r *ApplicationReconciler) reconcileArgoApplication(app *v1alpha1.Applicati
 			}
 			err = nil
 		} else {
-			err = r.Delete(ctx, argoApp)
+			err = r.DeleteArgoApp(app, argoApp)
 		}
 
 		if err == nil {
-			k8sutil.RemoveFinalizer(&app.ObjectMeta, v1alpha1.ApplicationFinalizerName)
-			k8sutil.RemoveFinalizer(&app.ObjectMeta, v1alpha1.ArgoCDResourcesFinalizer)
-			err = r.Update(context.TODO(), app)
+			err = r.RemoveAppFinalizer(app)
 		}
 		return
 	}
@@ -153,26 +151,28 @@ func (r *ApplicationReconciler) reconcileArgoApplication(app *v1alpha1.Applicati
 		} else {
 			var newArgoApp *unstructured.Unstructured
 			if newArgoApp, err = createUnstructuredApplication(app); err == nil {
-				argoApp.Object["spec"] = newArgoApp.Object["spec"]
-				argoApp.Object["operation"] = newArgoApp.Object["operation"]
-
-				// append annotations and labels
-				copyArgoAnnotationsAndLabels(newArgoApp, argoApp)
-
-				argoApp.SetFinalizers(newArgoApp.GetFinalizers())
 				err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
-					latestArgoApp := createBareArgoCDApplicationObject()
-					if err = r.Get(context.Background(), types.NamespacedName{
+					updateArgoApp := createBareArgoCDApplicationObject()
+					if err = r.Get(ctx, types.NamespacedName{
 						Namespace: argoApp.GetNamespace(),
 						Name:      argoApp.GetName(),
-					}, latestArgoApp); err != nil {
+					}, updateArgoApp); err != nil {
 						return
 					}
 
-					argoApp.SetResourceVersion(latestArgoApp.GetResourceVersion())
-					err = r.Update(ctx, argoApp)
+					updateArgoApp.Object["spec"] = newArgoApp.Object["spec"]
+					updateArgoApp.Object["operation"] = newArgoApp.Object["operation"]
+
+					// append annotations and labels
+					copyArgoAnnotationsAndLabels(newArgoApp, updateArgoApp)
+					updateArgoApp.SetFinalizers(newArgoApp.GetFinalizers())
+
+					err = r.Update(ctx, updateArgoApp)
 					return
 				})
+				if err != nil {
+					return
+				}
 			}
 		}
 	}
@@ -261,18 +261,22 @@ func copyArgoAnnotationsAndLabels(app metav1.Object, argoApp metav1.Object) {
 }
 
 func (r *ApplicationReconciler) addArgoAppNameIntoLabels(namespace, name, argoAppName string) (err error) {
-	app := &v1alpha1.Application{}
 	ctx := context.Background()
-	if err = r.Client.Get(ctx, types.NamespacedName{
-		Namespace: namespace,
-		Name:      name,
-	}, app); err == nil {
-		if app.Labels == nil {
-			app.Labels = make(map[string]string)
+
+	err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
+		updateApp := &v1alpha1.Application{}
+		if err = r.Client.Get(ctx, types.NamespacedName{
+			Namespace: namespace,
+			Name:      name,
+		}, updateApp); err == nil {
+			if updateApp.Labels == nil {
+				updateApp.Labels = make(map[string]string)
+			}
+			updateApp.Labels[v1alpha1.ArgoCDAppNameLabelKey] = argoAppName
+			err = r.Update(ctx, updateApp)
 		}
-		app.Labels[v1alpha1.ArgoCDAppNameLabelKey] = argoAppName
-		err = r.Update(ctx, app)
-	}
+		return
+	})
 	return
 }
 
@@ -290,7 +294,6 @@ func (r *ApplicationReconciler) setArgoProject(app *v1alpha1.Application) (err e
 	}, latestApp); err != nil {
 		return client.IgnoreNotFound(err)
 	}
-	err = r.Update(ctx, latestApp)
 
 	// there is a appProject in the same namespace
 	needUpdate := false
@@ -299,14 +302,83 @@ func (r *ApplicationReconciler) setArgoProject(app *v1alpha1.Application) (err e
 		needUpdate = true
 	}
 
-	if k8sutil.AddFinalizer(&latestApp.ObjectMeta, v1alpha1.ApplicationFinalizerName) || needUpdate {
-		if err = r.Update(context.TODO(), latestApp); err != nil {
+	if k8sutil.AddFinalizer(&latestApp.ObjectMeta, v1alpha1.ApplicationFinalizerName) {
+		needUpdate = true
+	}
+
+	if needUpdate {
+		if err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
+			updateApp := &v1alpha1.Application{}
+			if err = r.Get(ctx, types.NamespacedName{
+				Namespace: app.Namespace,
+				Name:      app.Name,
+			}, updateApp); err != nil {
+				return
+			}
+			updateApp.Spec.ArgoApp.Spec.Project = latestApp.Spec.ArgoApp.Spec.Project
+			updateApp.ObjectMeta.Finalizers = latestApp.ObjectMeta.Finalizers
+			err = r.Update(ctx, updateApp)
+			return
+		}); err != nil {
 			return
 		}
 	}
 	return
 }
 
+// RemoveAppFinalizer the app will be deleted after remove finalizer
+func (r *ApplicationReconciler) RemoveAppFinalizer(app *v1alpha1.Application) (err error) {
+	ctx := context.Background()
+
+	err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
+		if err = r.Get(ctx, types.NamespacedName{
+			Namespace: app.Namespace,
+			Name:      app.Name,
+		}, app); err != nil {
+			return
+		}
+		k8sutil.RemoveFinalizer(&app.ObjectMeta, v1alpha1.ApplicationFinalizerName)
+		k8sutil.RemoveFinalizer(&app.ObjectMeta, v1alpha1.ArgoCDResourcesFinalizer)
+		err = r.Update(ctx, app)
+		return
+	})
+	return
+}
+
+// DeleteArgoApp will delete argo app, support cascade delete
+func (r *ApplicationReconciler) DeleteArgoApp(app *v1alpha1.Application, argoApp *unstructured.Unstructured) (err error) {
+	ctx := context.Background()
+
+	// support cascade delete
+	finalizers := app.GetFinalizers()
+	targetFinalizers := make([]string, 0)
+	for i := range finalizers {
+		finalizer := finalizers[i]
+		if strings.HasSuffix(finalizer, "argocd.argoproj.io") {
+			targetFinalizers = append(targetFinalizers, finalizer)
+		}
+	}
+	if len(targetFinalizers) > 0 {
+		err = retry.RetryOnConflict(retry.DefaultRetry, func() (err error) {
+			updateArgoApp := createBareArgoCDApplicationObject()
+			if err = r.Get(ctx, types.NamespacedName{
+				Namespace: argoApp.GetNamespace(),
+				Name:      argoApp.GetName(),
+			}, updateArgoApp); err != nil {
+				return
+			}
+			updateArgoApp.SetFinalizers(targetFinalizers)
+			err = r.Update(ctx, updateArgoApp)
+			return
+		})
+		if err != nil {
+			return
+		}
+	}
+
+	return r.Delete(ctx, argoApp)
+}
+
 // GetName returns the name of this reconciler
 func (r *ApplicationReconciler) GetName() string {
 	return "ApplicationController"
diff --git a/controllers/argocd/argocd-application-status-controller.go b/controllers/argocd/argocd-application-status-controller.go
index 44ac35d..98461cc 100644
--- a/controllers/argocd/argocd-application-status-controller.go
+++ b/controllers/argocd/argocd-application-status-controller.go
@@ -66,7 +66,7 @@ func (r *ApplicationStatusReconciler) Reconcile(ctx context.Context, req ctrl.Re
 		Namespace: appNs,
 		Name:      appName,
 	}, app); err != nil {
-		r.log.Error(err, "cannot find application with namespace: %s, name: %s", appNs, appName)
+		r.log.Error(err, "cannot find application, maybe deleted", "namespace", appNs, "name", appName)
 		err = nil
 		return
 	}
diff --git a/pkg/client/devops/jenkins/pipeline_internal.go b/pkg/client/devops/jenkins/pipeline_internal.go
index dd26c16..484d060 100644
--- a/pkg/client/devops/jenkins/pipeline_internal.go
+++ b/pkg/client/devops/jenkins/pipeline_internal.go
@@ -138,6 +138,8 @@ func updatePipelineConfigXml(config string, pipeline *devopsv1alpha3.NoScmPipeli
 		addOrUpdateElement(strategy, NumToKeepTag, pipeline.Discarder.NumToKeep)
 		addOrUpdateElement(strategy, ArtiDaysToKeepTag, "-1")
 		addOrUpdateElement(strategy, ArtiNumToKeepTag, "-1")
+	} else {
+		removeChildElement(properties, BuildDiscarderTag)
 	}
 
 	if pipeline.Parameters != nil { // overwrite parameters
diff --git a/pkg/kapis/gitops/v1alpha1/gitops/handler.go b/pkg/kapis/gitops/v1alpha1/gitops/handler.go
index 86f7f64..8c72382 100644
--- a/pkg/kapis/gitops/v1alpha1/gitops/handler.go
+++ b/pkg/kapis/gitops/v1alpha1/gitops/handler.go
@@ -19,6 +19,7 @@ import (
 	"context"
 	"github.com/emicklei/go-restful"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/klog/v2"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 	"kubesphere.io/devops/pkg/apiserver/query"
 	"kubesphere.io/devops/pkg/kapis/common"
@@ -100,6 +101,8 @@ func (h *Handler) DelApplication(req *restful.Request, res *restful.Response) {
 					}
 				}
 			}
+		} else {
+			klog.Errorf("Application %s in namespace %s is not type argocd", name, namespace)
 		}
 		err = h.Delete(ctx, application)
 	}


Test patch:
diff --git a/controllers/argocd/application_controller_test.go b/controllers/argocd/application_controller_test.go
index 664d841..036e0e9 100644
--- a/controllers/argocd/application_controller_test.go
+++ b/controllers/argocd/application_controller_test.go
@@ -210,7 +210,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 	}{{
 		name: "without Argo Application",
 		fields: fields{
-			Client: fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
+			Client: fake.NewClientBuilder().WithScheme(schema).WithObjects(app.DeepCopy()).Build(),
 		},
 		args: args{
 			app: app.DeepCopy(),
@@ -233,7 +233,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 	}, {
 		name: "with Argo Application",
 		fields: fields{
-			Client: fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
+			Client: fake.NewClientBuilder().WithScheme(schema).WithObjects(app.DeepCopy()).Build(),
 		},
 		args: args{
 			app: app.DeepCopy(),
@@ -258,7 +258,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 	}, {
 		name: "update the existing argo application which not have labels",
 		fields: fields{
-			Client: fake.NewFakeClientWithScheme(schema, app.DeepCopy(), argoAppList.DeepCopy()),
+			Client: fake.NewClientBuilder().WithScheme(schema).WithObjects(app.DeepCopy()).WithLists(argoAppList.DeepCopy()).Build(),
 		},
 		args: args{
 			app: app.DeepCopy(),
@@ -269,7 +269,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 	}, {
 		name: "update the existing argo application which has labels",
 		fields: fields{
-			Client: fake.NewFakeClientWithScheme(schema, appWithLabel.DeepCopy(), argoAppWithLabel.DeepCopy()),
+			Client: fake.NewClientBuilder().WithScheme(schema).WithObjects(appWithLabel.DeepCopy()).WithObjects(argoAppWithLabel.DeepCopy()).Build(),
 		},
 		args: args{
 			app: appWithLabel.DeepCopy(),
@@ -665,7 +665,7 @@ func TestApplicationReconciler_SetupWithManager(t *testing.T) {
 		name: "normal",
 		args: args{
 			mgr: &core.FakeManager{
-				Client: fake.NewFakeClientWithScheme(schema),
+				Client: fake.NewClientBuilder().WithScheme(schema).Build(),
 				Scheme: schema,
 			},
 		},
@@ -682,3 +682,310 @@ func TestApplicationReconciler_SetupWithManager(t *testing.T) {
 		})
 	}
 }
+
+func TestApplicationReconciler_deleteArgoApp(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+
+	ctx := context.Background()
+	baseArgoApp := &unstructured.Unstructured{
+		Object: map[string]interface{}{
+			"apiVersion": "argoproj.io/v1alpha1",
+			"kind":       "Application",
+			"metadata": map[string]interface{}{
+				"name":      "test-app",
+				"namespace": "argocd",
+			},
+		},
+	}
+
+	tests := []struct {
+		name   string
+		verify func(*testing.T)
+	}{
+		{
+			name: "cascade",
+			verify: func(t *testing.T) {
+				argoApp := baseArgoApp.DeepCopy()
+				app := &v1alpha1.Application{
+					ObjectMeta: metav1.ObjectMeta{
+						Finalizers: []string{"finalizer-1", "resources-finalizer.argocd.argoproj.io", "finalizer-2"},
+					},
+				}
+				expectedFinalizers := []string{"resources-finalizer.argocd.argoproj.io"}
+				r := &ApplicationReconciler{
+					Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(baseArgoApp.DeepCopy()).Build(),
+					log:    logr.Logger{},
+				}
+				err = r.DeleteArgoApp(app, argoApp)
+				assert.NoError(t, err)
+
+				err = r.Get(ctx, types.NamespacedName{
+					Namespace: argoApp.GetNamespace(),
+					Name:      argoApp.GetName(),
+				}, argoApp)
+
+				assert.NoError(t, err)
+				assert.NotNil(t, argoApp.GetDeletionTimestamp())
+				assert.Equal(t, argoApp.GetFinalizers(), expectedFinalizers)
+			},
+		},
+		{
+			name: "cascade with other finalizer",
+			verify: func(t *testing.T) {
+				argoApp := baseArgoApp.DeepCopy()
+				argoApp.SetFinalizers([]string{"test-finalizer.argocd.argoproj.io"})
+				app := &v1alpha1.Application{
+					ObjectMeta: metav1.ObjectMeta{
+						Finalizers: []string{"finalizer-1", "resources-finalizer.argocd.argoproj.io", "finalizer-2"},
+					},
+				}
+				expectedFinalizers := []string{"resources-finalizer.argocd.argoproj.io"}
+				r := &ApplicationReconciler{
+					Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(baseArgoApp.DeepCopy()).Build(),
+					log:    logr.Logger{},
+				}
+				err = r.DeleteArgoApp(app, argoApp)
+				assert.NoError(t, err)
+
+				err = r.Get(ctx, types.NamespacedName{
+					Namespace: argoApp.GetNamespace(),
+					Name:      argoApp.GetName(),
+				}, argoApp)
+
+				assert.NoError(t, err)
+				assert.NotNil(t, argoApp.GetDeletionTimestamp())
+				assert.Equal(t, argoApp.GetFinalizers(), expectedFinalizers)
+			},
+		},
+		{
+			name: "normal",
+			verify: func(t *testing.T) {
+				argoApp := baseArgoApp.DeepCopy()
+				app := &v1alpha1.Application{
+					ObjectMeta: metav1.ObjectMeta{
+						Finalizers: []string{"finalizer-1", "finalizer-2"},
+					},
+				}
+				r := &ApplicationReconciler{
+					Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(baseArgoApp.DeepCopy()).Build(),
+					log:    logr.Logger{},
+				}
+				err = r.DeleteArgoApp(app, argoApp)
+				assert.NoError(t, err)
+
+				err = r.Get(ctx, types.NamespacedName{
+					Namespace: argoApp.GetNamespace(),
+					Name:      argoApp.GetName(),
+				}, argoApp)
+				assert.Error(t, err)
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.verify(t)
+		})
+	}
+}
+
+func TestApplicationReconciler_addArgoAppNameIntoLabels(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.NoError(t, err)
+	ctx := context.Background()
+
+	baseApp := &v1alpha1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "argocd",
+			Name:      "test-app",
+		},
+	}
+
+	labelApp := baseApp.DeepCopy()
+	labelApp.SetLabels(map[string]string{
+		"test-label": "test",
+	})
+
+	sameLabelApp := baseApp.DeepCopy()
+	sameLabelApp.SetLabels(map[string]string{
+		v1alpha1.ArgoCDAppNameLabelKey: "test",
+	})
+
+	type fields struct {
+		Client   client.Client
+		log      logr.Logger
+		recorder record.EventRecorder
+	}
+	type args struct {
+		namespace      string
+		name           string
+		argoAppName    string
+		expectedLabels map[string]string
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "empty label",
+			fields: fields{
+				Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(baseApp.DeepCopy()).Build(),
+				log:    logr.Logger{},
+			},
+			args: args{
+				namespace:   "argocd",
+				name:        "test-app",
+				argoAppName: "test-argo-app",
+				expectedLabels: map[string]string{
+					v1alpha1.ArgoCDAppNameLabelKey: "test-argo-app",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "with label",
+			fields: fields{
+				Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(labelApp.DeepCopy()).Build(),
+				log:    logr.Logger{},
+			},
+			args: args{
+				namespace:   "argocd",
+				name:        "test-app",
+				argoAppName: "test-argo-app",
+				expectedLabels: map[string]string{
+					v1alpha1.ArgoCDAppNameLabelKey: "test-argo-app",
+					"test-label":                   "test",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "same label",
+			fields: fields{
+				Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(sameLabelApp.DeepCopy()).Build(),
+				log:    logr.Logger{},
+			},
+			args: args{
+				namespace:   "argocd",
+				name:        "test-app",
+				argoAppName: "test-argo-app",
+				expectedLabels: map[string]string{
+					v1alpha1.ArgoCDAppNameLabelKey: "test-argo-app",
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "not exist app",
+			fields: fields{
+				Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(sameLabelApp.DeepCopy()).Build(),
+				log:    logr.Logger{},
+			},
+			args: args{
+				namespace: "not-exist",
+				name:      "not-exist",
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &ApplicationReconciler{
+				Client:   tt.fields.Client,
+				log:      tt.fields.log,
+				recorder: tt.fields.recorder,
+			}
+			if err := r.addArgoAppNameIntoLabels(tt.args.namespace, tt.args.name, tt.args.argoAppName); (err != nil) != tt.wantErr {
+				t.Errorf("addArgoAppNameIntoLabels() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if !tt.wantErr {
+				app := &v1alpha1.Application{}
+				err = r.Client.Get(ctx, types.NamespacedName{
+					Namespace: tt.args.namespace,
+					Name:      tt.args.name,
+				}, app)
+				assert.NoError(t, err)
+				assert.Equal(t, app.Labels, tt.args.expectedLabels)
+			}
+		})
+	}
+}
+
+func TestApplicationReconciler_RemoveAppFinalizer(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.NoError(t, err)
+	ctx := context.Background()
+
+	baseApp := &v1alpha1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "argocd",
+			Name:      "test-app",
+		},
+	}
+
+	noFinalizersApp := baseApp.DeepCopy()
+
+	finalizersApp := baseApp.DeepCopy()
+	finalizersApp.SetFinalizers([]string{v1alpha1.ApplicationFinalizerName, v1alpha1.ArgoCDResourcesFinalizer})
+
+	type fields struct {
+		Client   client.Client
+		log      logr.Logger
+		recorder record.EventRecorder
+	}
+	type args struct {
+		app *v1alpha1.Application
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "empty finalizer",
+			fields: fields{
+				Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(noFinalizersApp).Build(),
+				log:    logr.Logger{},
+			},
+			args: args{
+				app: noFinalizersApp,
+			},
+			wantErr: false,
+		},
+		{
+			name: "with finalizer",
+			fields: fields{
+				Client: fake.NewClientBuilder().WithScheme(schema).WithRuntimeObjects(finalizersApp).Build(),
+				log:    logr.Logger{},
+			},
+			args: args{
+				app: finalizersApp,
+			},
+			wantErr: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &ApplicationReconciler{
+				Client:   tt.fields.Client,
+				log:      tt.fields.log,
+				recorder: tt.fields.recorder,
+			}
+			if err := r.RemoveAppFinalizer(tt.args.app); (err != nil) != tt.wantErr {
+				t.Errorf("RemoveAppFinalizer() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if !tt.wantErr {
+				app := &v1alpha1.Application{}
+				err = r.Client.Get(ctx, types.NamespacedName{
+					Namespace: tt.args.app.Namespace,
+					Name:      tt.args.app.Name,
+				}, app)
+				assert.NoError(t, err)
+				assert.Empty(t, app.Finalizers)
+			}
+		})
+	}
+}