Repository: kubesphere/ks-devops
Pull request #519 (2022-03-28 02:23:05)

Provide a flag to set the namespace of the ArgoCD instance
<!-- 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:
This PR provides a flag to set the namespace of the Argo CD instance: `--argocd-namespace=xxx`. Users need to use this flag if they install Argo CD to other namespaces instead of `argocd`.

### 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 #

### 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

```
kubespheredev/devops-apiserver:dev-v3.2.1-48fbb49
```

```
kubespheredev/devops-controller:dev-v3.2.1-48fbb49
```

### 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
None
```


ks-ci-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/519#" title="Author self-approved">LinuxSuRen</a>*

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

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

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

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

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":[]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/519?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#519](https://codecov.io/gh/kubesphere/ks-devops/pull/519?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (ee8fc99) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/2454e148d70f0b1050348d70df20efcb74b741ec?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (2454e14) will **decrease** coverage by `0.19%`.
> The diff coverage is `45.94%`.

```diff
@@            Coverage Diff             @@
##           master     #519      +/-   ##
==========================================
- Coverage   77.65%   77.45%   -0.20%     
==========================================
  Files         140      140              
  Lines       22847    22910      +63     
==========================================
+ Hits        17741    17745       +4     
- Misses       4911     4971      +60     
+ Partials      195      194       -1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.45% <45.94%> (-0.20%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/kubesphere/ks-devops/pull/519?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [...ers/argocd/argocd-application-status-controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvYXJnb2NkL2FyZ29jZC1hcHBsaWNhdGlvbi1zdGF0dXMtY29udHJvbGxlci5nbw==) | `36.06% <33.33%> (-29.90%)` | :arrow_down: |
| [controllers/argocd/application\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvYXJnb2NkL2FwcGxpY2F0aW9uX2NvbnRyb2xsZXIuZ28=) | `42.26% <43.39%> (-9.66%)` | :arrow_down: |
| [pkg/kapis/gitops/v1alpha1/argocd/handler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2dpdG9wcy92MWFscGhhMS9hcmdvY2QvaGFuZGxlci5nbw==) | `95.89% <100.00%> (+0.23%)` | :arrow_up: |
| [pkg/kapis/gitops/v1alpha1/argocd/route.go](https://codecov.io/gh/kubesphere/ks-devops/pull/519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2dpdG9wcy92MWFscGhhMS9hcmdvY2Qvcm91dGUuZ28=) | `100.00% <100.00%> (ø)` | |
| [.../jenkins/devopsproject/devopsproject\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/519/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9kZXZvcHNwcm9qZWN0L2Rldm9wc3Byb2plY3RfY29udHJvbGxlci5nbw==) | `40.62% <0.00%> (-2.61%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/519?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/519?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [2454e14...ee8fc99](https://codecov.io/gh/kubesphere/ks-devops/pull/519?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).

LinuxSuRen: /hold
LinuxSuRen: /unhold
JohnNiang: /lgtm

Patch:
diff --git a/cmd/apiserver/app/options/options.go b/cmd/apiserver/app/options/options.go
index e37271d..b5e19ed 100644
--- a/cmd/apiserver/app/options/options.go
+++ b/cmd/apiserver/app/options/options.go
@@ -68,6 +68,7 @@ func (s *ServerRunOptions) Flags() (fss cliflag.NamedFlagSets) {
 	s.JenkinsOptions.AddFlags(fss.FlagSet("devops"), s.JenkinsOptions)
 	s.SonarQubeOptions.AddFlags(fss.FlagSet("sonarqube"), s.SonarQubeOptions)
 	s.S3Options.AddFlags(fss.FlagSet("s3"), s.S3Options)
+	s.ArgoCDOption.AddFlags(fss.FlagSet("argocd"))
 
 	fs = fss.FlagSet("klog")
 	local := flag.NewFlagSet("klog", flag.ExitOnError)
diff --git a/cmd/apiserver/app/server.go b/cmd/apiserver/app/server.go
index f267bde..dc3f801 100644
--- a/cmd/apiserver/app/server.go
+++ b/cmd/apiserver/app/server.go
@@ -35,6 +35,9 @@ func NewAPIServerCommand() (cmd *cobra.Command) {
 	// Load configuration from file
 	conf, err := config.TryLoadFromDisk()
 	if err == nil {
+		if conf.ArgoCDOption == nil {
+			conf.ArgoCDOption = &config.ArgoCDOption{}
+		}
 		s = &options.ServerRunOptions{
 			GenericServerRunOptions: s.GenericServerRunOptions,
 			Config:                  conf,
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 484b9a3..ba903d1 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -20,6 +20,7 @@ rules:
   resources:
   - events
   verbs:
+  - create
   - patch
 - apiGroups:
   - ""
diff --git a/controllers/argocd/application_controller.go b/controllers/argocd/application_controller.go
index 2970a07..36af299 100644
--- a/controllers/argocd/application_controller.go
+++ b/controllers/argocd/application_controller.go
@@ -20,6 +20,7 @@ import (
 	"context"
 	"fmt"
 	"github.com/go-logr/logr"
+	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/types"
@@ -28,10 +29,12 @@ import (
 	"kubesphere.io/devops/pkg/utils/k8sutil"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 )
 
 //+kubebuilder:rbac:groups=gitops.kubesphere.io,resources=applications,verbs=get;list;update
-//+kubebuilder:rbac:groups=argoproj.io,resources=applications,verbs=get;list;create;update
+//+kubebuilder:rbac:groups=argoproj.io,resources=applications,verbs=get;list;create;update;delete
+//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch
 
 // ApplicationReconciler is the reconciler of the Application
 type ApplicationReconciler struct {
@@ -41,6 +44,10 @@ type ApplicationReconciler struct {
 }
 
 // Reconcile makes sure the Application and ArgoCD application works well
+// Consider all the ArgoCD application need to be in one particular namespace. But the Application from this project can be in different namespaces.
+// In order to avoid the naming conflict, we will check if the name existing the target namespace. Take the original name
+// as the generatedName of the ArgoCD application if there is a potential conflict. In the most cases, we can keep the original name
+// same to the ArgoCD Application name.
 func (r *ApplicationReconciler) Reconcile(req ctrl.Request) (result ctrl.Result, err error) {
 	ctx := context.Background()
 	r.log.Info(fmt.Sprintf("start to reconcile application: %s", req.String()))
@@ -61,10 +68,46 @@ func (r *ApplicationReconciler) reconcileArgoApplication(app *v1alpha1.Applicati
 	ctx := context.Background()
 
 	argoApp := createBareArgoCDApplicationObject()
+	argoCDNamespace, ok := app.Labels[v1alpha1.ArgoCDLocationLabelKey]
+	if !ok || "" == argoCDNamespace {
+		r.recorder.Eventf(app, corev1.EventTypeWarning, "Invalid",
+			"Cannot find the namespace of the Argo CD instance from key: %s", v1alpha1.ArgoCDLocationLabelKey)
+		return
+	}
+	argoCDAppName, hasArgoName := app.Labels[v1alpha1.ArgoCDAppNameLabelKey]
+	if argoCDAppName == "" {
+		argoCDAppName = app.GetName()
+	}
+
+	// the application was deleted
+	if !app.ObjectMeta.DeletionTimestamp.IsZero() {
+		if err = r.Get(ctx, types.NamespacedName{
+			Namespace: argoCDNamespace,
+			Name:      argoCDAppName,
+		}, argoApp); err != nil {
+			if !apierrors.IsNotFound(err) {
+				return
+			}
+			err = nil
+		} else {
+			err = r.Delete(ctx, argoApp)
+		}
+
+		if err == nil {
+			k8sutil.RemoveFinalizer(&app.ObjectMeta, v1alpha1.ApplicationFinalizerName)
+			err = r.Update(context.TODO(), app)
+		}
+		return
+	}
+	if k8sutil.AddFinalizer(&app.ObjectMeta, v1alpha1.ApplicationFinalizerName) {
+		if err = r.Update(context.TODO(), app); err != nil {
+			return
+		}
+	}
 
 	if err = r.Get(ctx, types.NamespacedName{
-		Namespace: app.GetNamespace(),
-		Name:      app.GetName(),
+		Namespace: argoCDNamespace,
+		Name:      argoCDAppName,
 	}, argoApp); err != nil {
 		if !apierrors.IsNotFound(err) {
 			return
@@ -74,13 +117,40 @@ func (r *ApplicationReconciler) reconcileArgoApplication(app *v1alpha1.Applicati
 			return
 		}
 
-		err = r.Create(ctx, argoApp)
+		argoApp.SetName(app.Name)
+		argoApp.SetNamespace(argoCDNamespace)
+		if err = r.Create(ctx, argoApp); err == nil {
+			err = r.addArgoAppNameIntoLabels(app.GetNamespace(), app.GetName(), argoApp.GetName())
+		} else {
+			data, _ := argoApp.MarshalJSON()
+			r.log.Error(err, "failed to create application to ArgoCD", "data", string(data))
+			r.recorder.Eventf(app, corev1.EventTypeWarning, "FailedWithArgoCD",
+				"failed to create application to ArgoCD, error is: %v", err)
+		}
 	} else {
-		var newArgoApp *unstructured.Unstructured
-		if newArgoApp, err = createUnstructuredApplication(app); err == nil {
-			argoApp.Object["spec"] = newArgoApp.Object["spec"]
-			k8sutil.AddOwnerReference(argoApp, app.TypeMeta, app.ObjectMeta)
-			err = r.Update(ctx, argoApp)
+		if !hasArgoName {
+			// naming conflict happened
+			if argoApp, err = createUnstructuredApplication(app); err != nil {
+				return
+			}
+			argoApp.SetGenerateName(app.GetName())
+			argoApp.SetName("")
+			argoApp.SetNamespace(argoCDNamespace)
+
+			if err = r.Create(ctx, argoApp); err == nil {
+				err = r.addArgoAppNameIntoLabels(app.GetNamespace(), app.GetName(), argoApp.GetName())
+			} else {
+				data, _ := argoApp.MarshalJSON()
+				r.log.Error(err, "failed to create application to ArgoCD", "data", string(data))
+				r.recorder.Eventf(app, corev1.EventTypeWarning, "FailedWithArgoCD",
+					"failed to create application to ArgoCD, error is: %v", err)
+			}
+		} else {
+			var newArgoApp *unstructured.Unstructured
+			if newArgoApp, err = createUnstructuredApplication(app); err == nil {
+				argoApp.Object["spec"] = newArgoApp.Object["spec"]
+				err = r.Update(ctx, argoApp)
+			}
 		}
 	}
 	return
@@ -103,6 +173,11 @@ func createUnstructuredApplication(app *v1alpha1.Application) (result *unstructu
 	}
 
 	newArgoApp := createBareArgoCDApplicationObject()
+	newArgoApp.SetLabels(map[string]string{
+		v1alpha1.ArgoCDAppControlByLabelKey: "ks-devops",
+		v1alpha1.AppNamespaceLabelKey:       app.Namespace,
+		v1alpha1.AppNameLabelKey:            app.Name,
+	})
 	newArgoApp.SetName(app.GetName())
 	newArgoApp.SetNamespace(app.GetNamespace())
 
@@ -115,13 +190,25 @@ func createUnstructuredApplication(app *v1alpha1.Application) (result *unstructu
 			return nil, err
 		}
 	}
-
-	// set owner reference
-	k8sutil.AddOwnerReference(newArgoApp, app.TypeMeta, app.ObjectMeta)
-
 	return newArgoApp, nil
 }
 
+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)
+		}
+		app.Labels[v1alpha1.ArgoCDAppNameLabelKey] = argoAppName
+		err = r.Update(ctx, app)
+	}
+	return
+}
+
 // GetName returns the name of this reconciler
 func (r *ApplicationReconciler) GetName() string {
 	return "ApplicationController"
@@ -138,6 +225,7 @@ func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	r.log = ctrl.Log.WithName(r.GetName())
 	r.recorder = mgr.GetEventRecorderFor(r.GetName())
 	return ctrl.NewControllerManagedBy(mgr).
+		WithEventFilter(predicate.GenerationChangedPredicate{}).
 		For(&v1alpha1.Application{}).
 		Complete(r)
 }
diff --git a/controllers/argocd/argocd-application-status-controller.go b/controllers/argocd/argocd-application-status-controller.go
index da39981..a5d9d8c 100644
--- a/controllers/argocd/argocd-application-status-controller.go
+++ b/controllers/argocd/argocd-application-status-controller.go
@@ -19,8 +19,11 @@ package argocd
 import (
 	"context"
 	"encoding/json"
+	"fmt"
 	"github.com/go-logr/logr"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/tools/record"
@@ -28,6 +31,7 @@ import (
 	"reflect"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 )
 
 //+kubebuilder:rbac:groups=gitops.kubesphere.io,resources=applications,verbs=get;update
@@ -44,14 +48,26 @@ type ApplicationStatusReconciler struct {
 // Reconcile is the entrypoint of the controller
 func (r *ApplicationStatusReconciler) Reconcile(req ctrl.Request) (result ctrl.Result, err error) {
 	var argoCDApp *unstructured.Unstructured
+	r.log.Info(fmt.Sprintf("start to reconcile ArgoCD application: %s", req.String()))
+
 	if argoCDApp, err = getArgoCDApplication(r.Client, req.NamespacedName); err != nil {
 		err = client.IgnoreNotFound(err)
 		return
 	}
 
+	appNs := argoCDApp.GetLabels()[v1alpha1.AppNamespaceLabelKey]
+	appName := argoCDApp.GetLabels()[v1alpha1.AppNameLabelKey]
+	if appName == "" || appNs == "" {
+		return
+	}
+
 	ctx := context.Background()
 	app := &v1alpha1.Application{}
-	if err = r.Get(ctx, req.NamespacedName, app); err != nil {
+	if err = r.Get(ctx, types.NamespacedName{
+		Namespace: appNs,
+		Name:      appName,
+	}, app); err != nil {
+		r.log.Error(err, "cannot find application with namespace: %s, name: %s", appNs, appName)
 		err = nil
 		return
 	}
@@ -86,7 +102,6 @@ func (r *ApplicationStatusReconciler) Reconcile(req ctrl.Request) (result ctrl.R
 				return
 			}
 
-			// update status subresource
 			app.Status.ArgoApp = string(statusData)
 			err = r.Status().Update(ctx, app)
 		}
@@ -128,7 +143,12 @@ func (r *ApplicationStatusReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	argoApp := createBareArgoCDApplicationObject()
 	r.log = ctrl.Log.WithName(r.GetName())
 	r.recorder = mgr.GetEventRecorderFor(r.GetName())
+	var withLabelPredicate = predicate.NewPredicateFuncs(func(meta metav1.Object, object runtime.Object) (ok bool) {
+		_, ok = meta.GetLabels()[v1alpha1.ArgoCDAppControlByLabelKey]
+		return
+	})
 	return ctrl.NewControllerManagedBy(mgr).
 		For(argoApp).
+		WithEventFilter(withLabelPredicate).
 		Complete(r)
 }
diff --git a/pkg/api/gitops/v1alpha1/constants.go b/pkg/api/gitops/v1alpha1/constants.go
index 555f44b..4ea92a4 100644
--- a/pkg/api/gitops/v1alpha1/constants.go
+++ b/pkg/api/gitops/v1alpha1/constants.go
@@ -16,7 +16,15 @@
 package v1alpha1
 
 const (
-	GroupName            = "gitops.kubesphere.io"
-	HealthStatusLabelKey = GroupName + "/health-status"
-	SyncStatusLabelKey   = GroupName + "/sync-status"
+	GroupName                  = "gitops.kubesphere.io"
+	HealthStatusLabelKey       = GroupName + "/health-status"
+	SyncStatusLabelKey         = GroupName + "/sync-status"
+	ArgoCDLocationLabelKey     = GroupName + "/argocd-location"
+	ArgoCDAppNameLabelKey      = GroupName + "/argocd-application"
+	ArgoCDAppControlByLabelKey = GroupName + "/argocd-application-control-by"
+	AppNamespaceLabelKey       = GroupName + "/application-namespace"
+	AppNameLabelKey            = GroupName + "/application-name"
 )
+
+// ApplicationFinalizerName is the name of PipelineRun finalizer
+const ApplicationFinalizerName = "application." + GroupName
diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index d3a0c43..dfb5bd5 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -161,7 +161,7 @@ func (s *APIServer) installKubeSphereAPIs() {
 	))
 	wss = append(wss, gitops.AddToContainer(s.container, &common.Options{
 		GenericClient: s.Client,
-	})...)
+	}, s.Config.ArgoCDOption)...)
 	doc.AddSwaggerService(wss, s.container)
 }
 
diff --git a/pkg/config/argocd.go b/pkg/config/argocd.go
new file mode 100644
index 0000000..57acb19
--- /dev/null
+++ b/pkg/config/argocd.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2022 The KubeSphere Authors.
+
+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 config
+
+import "github.com/spf13/pflag"
+
+// ArgoCDOption as the ArgoCD integration configuration
+type ArgoCDOption struct {
+	Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty" description:"Which namespace the ArgoCD located"`
+}
+
+// AddFlags adds the flags which related to argocd
+func (o *ArgoCDOption) AddFlags(fs *pflag.FlagSet) {
+	// see also https://argo-cd.readthedocs.io/en/stable/getting_started/
+	fs.StringVarP(&o.Namespace, "argocd-namespace", o.Namespace, "argocd", "Which namespace the ArgoCD located")
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 7bba944..941549f 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -84,12 +84,13 @@ type Config struct {
 	RedisOptions          *cache.Options                     `json:"redis,omitempty" yaml:"redis,omitempty" mapstructure:"redis"`
 	S3Options             *s3.Options                        `json:"s3,omitempty" yaml:"s3,omitempty" mapstructure:"s3"`
 	SonarQubeOptions      *sonarqube.Options                 `json:"sonarqube,omitempty" yaml:"sonarQube,omitempty" mapstructure:"sonarqube"`
+	ArgoCDOption          *ArgoCDOption                      `json:"argocd,omitempty" yaml:"argocd,omitempty" mapstructure:"argocd"`
 	AuthenticationOptions *authoptions.AuthenticationOptions `json:"authentication,omitempty" yaml:"authentication,omitempty" mapstructure:"authentication"`
 	AuthMode              AuthMode                           `json:"authMode,omitempty" yaml:"authMode,omitempty" mapstructure:"authMode"`
 	JWTSecret             string                             `json:"jwtSecret,omitempty" yaml:"jwtSecret,omitempty" mapstructure:"jwtSecret"`
 }
 
-// newConfig creates a default non-empty Config
+// New creates a default non-empty Config
 func New() *Config {
 	return &Config{
 		SonarQubeOptions:  sonarqube.NewSonarQubeOptions(),
@@ -126,7 +127,7 @@ func TryLoadFromDisk() (*Config, error) {
 	return conf, nil
 }
 
-// convertToMap simply converts config to map[string]bool
+// ToMap simply converts config to map[string]bool
 // to hide sensitive information
 func (conf *Config) ToMap() map[string]bool {
 	conf.stripEmptyOptions()
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/handler.go b/pkg/kapis/gitops/v1alpha1/argocd/handler.go
index a7755c0..4dace01 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/handler.go
+++ b/pkg/kapis/gitops/v1alpha1/argocd/handler.go
@@ -22,6 +22,7 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 	"kubesphere.io/devops/pkg/apiserver/query"
+	"kubesphere.io/devops/pkg/config"
 	"kubesphere.io/devops/pkg/kapis/common"
 	"kubesphere.io/devops/pkg/models/resources/v1alpha3"
 	"sigs.k8s.io/controller-runtime/pkg/client"
@@ -85,6 +86,10 @@ func (h *handler) createApplication(req *restful.Request, res *restful.Response)
 	application := &v1alpha1.Application{}
 	if err = req.ReadEntity(application); err == nil {
 		application.Namespace = namespace
+		if application.Labels == nil {
+			application.Labels = make(map[string]string)
+		}
+		application.Labels[v1alpha1.ArgoCDLocationLabelKey] = h.ArgoCDNamespace
 		err = h.Create(context.Background(), application)
 	}
 
@@ -171,10 +176,12 @@ func (h *handler) populateApplicationSummary(namespace string) (*ApplicationsSum
 
 type handler struct {
 	client.Client
+	ArgoCDNamespace string
 }
 
-func newHandler(options *common.Options) *handler {
+func newHandler(options *common.Options, argoOption *config.ArgoCDOption) *handler {
 	return &handler{
-		Client: options.GenericClient,
+		Client:          options.GenericClient,
+		ArgoCDNamespace: argoOption.Namespace,
 	}
 }
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/route.go b/pkg/kapis/gitops/v1alpha1/argocd/route.go
index 90d1fb9..f9704c3 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/route.go
+++ b/pkg/kapis/gitops/v1alpha1/argocd/route.go
@@ -20,6 +20,7 @@ import (
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 	"kubesphere.io/devops/pkg/apiserver/query"
+	"kubesphere.io/devops/pkg/config"
 	"kubesphere.io/devops/pkg/kapis/common"
 	"net/http"
 )
@@ -45,8 +46,8 @@ type ApplicationsSummary struct {
 }
 
 // RegisterRoutes is for registering Argo CD Application routes into WebService.
-func RegisterRoutes(service *restful.WebService, options *common.Options) {
-	handler := newHandler(options)
+func RegisterRoutes(service *restful.WebService, options *common.Options, argoOption *config.ArgoCDOption) {
+	handler := newHandler(options, argoOption)
 	service.Route(service.GET("/namespaces/{namespace}/applications").
 		To(handler.applicationList).
 		Param(common.NamespacePathParameter).
diff --git a/pkg/kapis/gitops/v1alpha1/registery.go b/pkg/kapis/gitops/v1alpha1/registery.go
index 8c8a836..2e28e93 100644
--- a/pkg/kapis/gitops/v1alpha1/registery.go
+++ b/pkg/kapis/gitops/v1alpha1/registery.go
@@ -20,6 +20,7 @@ import (
 	"github.com/emicklei/go-restful"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 	"kubesphere.io/devops/pkg/apiserver/runtime"
+	"kubesphere.io/devops/pkg/config"
 	"kubesphere.io/devops/pkg/kapis/common"
 	"kubesphere.io/devops/pkg/kapis/gitops/v1alpha1/argocd"
 )
@@ -28,13 +29,13 @@ import (
 //+kubebuilder:rbac:groups=gitops.kubesphere.io,resources=applications,verbs=get;list;update;delete;create;watch
 
 // AddToContainer adds web services into web service container.
-func AddToContainer(container *restful.Container, options *common.Options) []*restful.WebService {
+func AddToContainer(container *restful.Container, options *common.Options, argoOption *config.ArgoCDOption) []*restful.WebService {
 	services := []*restful.WebService{
 		runtime.NewWebService(v1alpha1.GroupVersion),
 		runtime.NewWebServiceWithoutGroup(v1alpha1.GroupVersion),
 	}
 	for _, service := range services {
-		argocd.RegisterRoutes(service, options)
+		argocd.RegisterRoutes(service, options, argoOption)
 		container.Add(service)
 	}
 	return services
diff --git a/pkg/utils/k8sutil/objectmeta.go b/pkg/utils/k8sutil/objectmeta.go
index e1dad2b..28131a2 100644
--- a/pkg/utils/k8sutil/objectmeta.go
+++ b/pkg/utils/k8sutil/objectmeta.go
@@ -22,8 +22,11 @@ import (
 )
 
 // AddFinalizer adds an finalizer
-func AddFinalizer(objectMeta *metav1.ObjectMeta, finalizer string) {
+func AddFinalizer(objectMeta *metav1.ObjectMeta, finalizer string) (added bool) {
+	count := len(objectMeta.Finalizers)
 	objectMeta.Finalizers = sliceutil.AddToSlice(finalizer, objectMeta.Finalizers)
+	added = len(objectMeta.Finalizers) > count
+	return
 }
 
 // RemoveFinalizer removes an finalizer


Test patch:
diff --git a/controllers/argocd/application_controller_test.go b/controllers/argocd/application_controller_test.go
index a4f11c4..77c3363 100644
--- a/controllers/argocd/application_controller_test.go
+++ b/controllers/argocd/application_controller_test.go
@@ -24,7 +24,6 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/tools/record"
-	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -58,11 +57,6 @@ func Test_createUnstructuredApplication(t *testing.T) {
 		},
 		verify: func(t *testing.T, gotResult *unstructured.Unstructured, gotErr error) {
 			assert.Nil(t, gotErr)
-
-			// make sure it has the ownerReference
-			refs, _, _ := unstructured.NestedSlice(gotResult.Object, "metadata", "ownerReferences")
-			assert.NotNil(t, refs)
-			assert.Equal(t, 1, len(refs))
 		},
 	}, {
 		name: "with some specific fields, with default values",
@@ -100,13 +94,16 @@ func Test_createUnstructuredApplication(t *testing.T) {
 }
 
 func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
-	schema, err := v1alpha3.SchemeBuilder.Register().Build()
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
 	assert.Nil(t, err)
 
 	app := &v1alpha1.Application{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "fake",
 			Namespace: "fake",
+			Labels: map[string]string{
+				v1alpha1.ArgoCDLocationLabelKey: "fake",
+			},
 		},
 		Spec: v1alpha1.ApplicationSpec{
 			ArgoApp: &v1alpha1.ArgoApplication{
@@ -129,9 +126,8 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 	argoApp.SetNamespace("fake")
 
 	type fields struct {
-		Client   client.Client
-		log      logr.Logger
-		recorder record.EventRecorder
+		Client client.Client
+		log    logr.Logger
 	}
 	type args struct {
 		app *v1alpha1.Application
@@ -144,7 +140,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 	}{{
 		name: "without Argo Application",
 		fields: fields{
-			Client: fake.NewFakeClientWithScheme(schema),
+			Client: fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
 		},
 		args: args{
 			app: app.DeepCopy(),
@@ -167,7 +163,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 	}, {
 		name: "with Argo Application",
 		fields: fields{
-			Client: fake.NewFakeClientWithScheme(schema, argoApp.DeepCopy()),
+			Client: fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
 		},
 		args: args{
 			app: app.DeepCopy(),
@@ -191,7 +187,7 @@ func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 			r := &ApplicationReconciler{
 				Client:   tt.fields.Client,
 				log:      tt.fields.log,
-				recorder: tt.fields.recorder,
+				recorder: &record.FakeRecorder{},
 			}
 			err := r.reconcileArgoApplication(tt.args.app)
 			tt.verify(t, tt.fields.Client, err)
diff --git a/controllers/argocd/argocd-application-status-controller_test.go b/controllers/argocd/argocd-application-status-controller_test.go
index 35df0eb..1b56ab6 100644
--- a/controllers/argocd/argocd-application-status-controller_test.go
+++ b/controllers/argocd/argocd-application-status-controller_test.go
@@ -18,7 +18,6 @@ package argocd
 
 import (
 	"fmt"
-	"github.com/go-logr/logr"
 	"github.com/stretchr/testify/assert"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -29,6 +28,7 @@ import (
 	controllerruntime "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/controller-runtime/pkg/log"
 	"testing"
 )
 
@@ -123,9 +123,7 @@ func TestArgoCDApplicationStatusReconciler_Reconcile(t *testing.T) {
 	defaultArgoCDApp.SetNamespace("ns")
 
 	type fields struct {
-		Client   client.Client
-		log      logr.Logger
-		recorder record.EventRecorder
+		Client client.Client
 	}
 	type args struct {
 		req controllerruntime.Request
@@ -192,8 +190,8 @@ func TestArgoCDApplicationStatusReconciler_Reconcile(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			r := &ApplicationStatusReconciler{
 				Client:   tt.fields.Client,
-				log:      tt.fields.log,
-				recorder: tt.fields.recorder,
+				log:      log.NullLogger{},
+				recorder: &record.FakeRecorder{},
 			}
 			gotResult, err := r.Reconcile(tt.args.req)
 			if !tt.wantErr(t, err, fmt.Sprintf("Reconcile(%v)", tt.args.req)) {
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/route_test.go b/pkg/kapis/gitops/v1alpha1/argocd/route_test.go
index 45dd23c..b6b2d28 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/route_test.go
+++ b/pkg/kapis/gitops/v1alpha1/argocd/route_test.go
@@ -27,6 +27,7 @@ import (
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 	"kubesphere.io/devops/pkg/apiserver/runtime"
+	"kubesphere.io/devops/pkg/config"
 	"kubesphere.io/devops/pkg/kapis/common"
 	"net/http"
 	"net/http/httptest"
@@ -60,7 +61,7 @@ func TestRegisterRoutes(t *testing.T) {
 	}}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			RegisterRoutes(tt.args.service, tt.args.options)
+			RegisterRoutes(tt.args.service, tt.args.options, &config.ArgoCDOption{})
 			tt.verify(t, tt.args.service)
 		})
 	}
@@ -322,7 +323,7 @@ func TestAPIs(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			wsWithGroup := runtime.NewWebService(v1alpha1.GroupVersion)
-			RegisterRoutes(wsWithGroup, &common.Options{GenericClient: tt.k8sclient})
+			RegisterRoutes(wsWithGroup, &common.Options{GenericClient: tt.k8sclient}, &config.ArgoCDOption{})
 
 			container := restful.NewContainer()
 			container.Add(wsWithGroup)
diff --git a/pkg/utils/k8sutil/objectmeta_test.go b/pkg/utils/k8sutil/objectmeta_test.go
index fdccada..235f69a 100644
--- a/pkg/utils/k8sutil/objectmeta_test.go
+++ b/pkg/utils/k8sutil/objectmeta_test.go
@@ -27,7 +27,11 @@ type demoCR struct {
 }
 
 func TestAddFinalizer(t *testing.T) {
-	demo := &demoCR{}
+	demo := &demoCR{
+		ObjectMeta: metav1.ObjectMeta{
+			Finalizers: []string{"fake"},
+		},
+	}
 
 	type args struct {
 		objectMeta *metav1.ObjectMeta
@@ -37,19 +41,32 @@ func TestAddFinalizer(t *testing.T) {
 		name   string
 		args   args
 		verify func(t *testing.T, meta *metav1.ObjectMeta)
+		expect bool
 	}{{
 		name: "normal case",
 		args: args{
-			objectMeta: &demo.ObjectMeta,
+			objectMeta: demo.DeepCopy(),
 			finalizer:  "abc",
 		},
+		expect: true,
 		verify: func(t *testing.T, meta *metav1.ObjectMeta) {
-			assert.ElementsMatch(t, []string{"abc"}, meta.Finalizers)
+			assert.ElementsMatch(t, []string{"fake", "abc"}, meta.Finalizers)
+		},
+	}, {
+		name: "add with a duplicated finalizer",
+		args: args{
+			objectMeta: demo.DeepCopy(),
+			finalizer:  "fake",
+		},
+		expect: false,
+		verify: func(t *testing.T, meta *metav1.ObjectMeta) {
+			assert.ElementsMatch(t, []string{"fake"}, meta.Finalizers)
 		},
 	}}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			AddFinalizer(tt.args.objectMeta, tt.args.finalizer)
+			result := AddFinalizer(tt.args.objectMeta, tt.args.finalizer)
+			assert.Equal(t, tt.expect, result)
 			tt.verify(t, tt.args.objectMeta)
 		})
 	}