Repository: kubesphere/ks-devops
Pull request #497 (2022-03-11 08:04:19)

Put the argocd application status to ks application
<!-- 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:
/kind feature

This feature can help us to get the application status. See the following test results:

```yaml
spec:
  argoApp:
    destination:
      namespace: default
      server: https://kubernetes.default.svc
    project: default
    source:
      directory:
        jsonnet: {}
        recurse: true
      path: test
      repoURL: https://gitlab.com/LinuxSuRen1/argocd
      targetRevision: HEAD
    syncPolicy:
      automated:
        prune: true
status:
  argoApp: '{"conditions":[{"lastTransitionTime":"2022-03-11T07:47:54Z","message":"failed
    to sync cluster https://10.43.0.1:443: failed to load initial state of resource
    Namespace: namespaces is forbidden: User \"system:serviceaccount:default:argocd-application-controller\"
    cannot list resource \"namespaces\" in API group \"\" at the cluster scope","type":"ComparisonError"},{"lastTransitionTime":"2022-03-11T07:47:54Z","message":"failed
    to sync cluster https://10.43.0.1:443: failed to load initial state of resource
    Namespace: namespaces is forbidden: User \"system:serviceaccount:default:argocd-application-controller\"
    cannot list resource \"namespaces\" in API group \"\" at the cluster scope","type":"ComparisonError"}],"health":{"status":"Healthy"},"reconciledAt":"2022-03-11T07:47:54Z","sync":{"comparedTo":{"destination":{"namespace":"default","server":"https://kubernetes.default.svc"},"source":{"directory":{"jsonnet":{},"recurse":true},"path":"test","repoURL":"https://gitlab.com/LinuxSuRen1/argocd","targetRevision":"HEAD"}},"status":"Unknown"}}'
```

### 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

### 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/497#" 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/497?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#497](https://codecov.io/gh/kubesphere/ks-devops/pull/497?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (cfe055d) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/8bb14916fe928db9e8a2f9714390a81f9d485f80?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (8bb1491) will **increase** coverage by `0.02%`.
> The diff coverage is `81.25%`.

> :exclamation: Current head cfe055d differs from pull request most recent head c57ebf2. Consider uploading reports for the commit c57ebf2 to get more accurate results

[![Impacted file tree graph](https://codecov.io/gh/kubesphere/ks-devops/pull/497/graphs/tree.svg?width=650&height=150&src=pr&token=qSK4RBbyNg&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)](https://codecov.io/gh/kubesphere/ks-devops/pull/497?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)

```diff
@@            Coverage Diff             @@
##           master     #497      +/-   ##
==========================================
+ Coverage   77.24%   77.27%   +0.02%     
==========================================
  Files         133      134       +1     
  Lines       22842    22874      +32     
==========================================
+ Hits        17645    17676      +31     
- Misses       5000     5002       +2     
+ Partials      197      196       -1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.27% <81.25%> (+0.02%)` | :arrow_up: |

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/497?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/api/gitops/v1alpha1/application.go](https://codecov.io/gh/kubesphere/ks-devops/pull/497/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2FwaS9naXRvcHMvdjFhbHBoYTEvYXBwbGljYXRpb24uZ28=) | `100.00% <ø> (ø)` | |
| [...ers/argocd/argocd-application-status-controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/497/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvYXJnb2NkL2FyZ29jZC1hcHBsaWNhdGlvbi1zdGF0dXMtY29udHJvbGxlci5nbw==) | `81.25% <81.25%> (ø)` | |
| [controllers/s2irun/s2irun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/497/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvczJpcnVuL3MyaXJ1bl9jb250cm9sbGVyLmdv) | `37.22% <0.00%> (+3.64%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/497?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/497?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [8bb1491...c57ebf2](https://codecov.io/gh/kubesphere/ks-devops/pull/497?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).

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

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/497#pullrequestreview-908330597" title="LGTM">JohnNiang</a>*, *<a href="https://github.com/kubesphere/ks-devops/pull/497#" 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":[]} -->

Patch:
diff --git a/cmd/controller/app/controllers.go b/cmd/controller/app/controllers.go
index 56c2e3d..968d900 100644
--- a/cmd/controller/app/controllers.go
+++ b/cmd/controller/app/controllers.go
@@ -108,6 +108,9 @@ func getAllControllers(mgr manager.Manager, client k8s.Client, informerFactory i
 	argocdClusterReconciler := &argocd.MultiClusterReconciler{
 		Client: mgr.GetClient(),
 	}
+	argocdAppStatusReconciler := &argocd.ApplicationStatusReconciler{
+		Client: mgr.GetClient(),
+	}
 
 	return map[string]func(mgr manager.Manager) error{
 		"gitrepository": func(mgr manager.Manager) error {
@@ -185,6 +188,9 @@ func getAllControllers(mgr manager.Manager, client k8s.Client, informerFactory i
 			if err = argocdClusterReconciler.SetupWithManager(mgr); err != nil {
 				return
 			}
+			if err = argocdAppStatusReconciler.SetupWithManager(mgr); err != nil {
+				return
+			}
 			return argocdAppReconciler.SetupWithManager(mgr)
 		},
 	}
diff --git a/config/crd/bases/gitops.kubesphere.io_applications.yaml b/config/crd/bases/gitops.kubesphere.io_applications.yaml
index a602c18..1629b4f 100644
--- a/config/crd/bases/gitops.kubesphere.io_applications.yaml
+++ b/config/crd/bases/gitops.kubesphere.io_applications.yaml
@@ -442,9 +442,17 @@ spec:
                 - source
                 type: object
             type: object
+          status:
+            description: ApplicationStatus represents the status of the Application
+            properties:
+              argoApp:
+                type: string
+            type: object
         type: object
     served: true
     storage: true
+    subresources:
+      status: {}
 status:
   acceptedNames:
     kind: ""
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 08a35ff..5bcf7c9 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -206,3 +206,10 @@ rules:
   - get
   - list
   - update
+- apiGroups:
+  - gitops.kubesphere.io
+  resources:
+  - applications/status
+  verbs:
+  - get
+  - update
diff --git a/controllers/argocd/argocd-application-status-controller.go b/controllers/argocd/argocd-application-status-controller.go
new file mode 100644
index 0000000..13d92c6
--- /dev/null
+++ b/controllers/argocd/argocd-application-status-controller.go
@@ -0,0 +1,102 @@
+/*
+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 argocd
+
+import (
+	"context"
+	"encoding/json"
+	"github.com/go-logr/logr"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/tools/record"
+	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+//+kubebuilder:rbac:groups=gitops.kubesphere.io,resources=applications,verbs=get;update
+//+kubebuilder:rbac:groups=gitops.kubesphere.io,resources=applications/status,verbs=get;update
+//+kubebuilder:rbac:groups=argoproj.io,resources=applications,verbs=get;list
+
+// ApplicationStatusReconciler represents a controller to sync cluster to ArgoCD cluster
+type ApplicationStatusReconciler struct {
+	client.Client
+	log      logr.Logger
+	recorder record.EventRecorder
+}
+
+// Reconcile is the entrypoint of the controller
+func (r *ApplicationStatusReconciler) Reconcile(req ctrl.Request) (result ctrl.Result, err error) {
+	var argoCDApp *unstructured.Unstructured
+	if argoCDApp, err = getArgoCDApplication(r.Client, req.NamespacedName); err != nil {
+		err = client.IgnoreNotFound(err)
+		return
+	}
+
+	ctx := context.Background()
+	app := &v1alpha1.Application{}
+	if err = r.Get(ctx, req.NamespacedName, app); err != nil {
+		err = nil
+		return
+	}
+
+	var status map[string]interface{}
+	if status, _, err = unstructured.NestedMap(argoCDApp.Object, "status"); err == nil {
+		var statusData []byte
+		if statusData, err = json.Marshal(status); err == nil {
+			app.Status.ArgoApp = string(statusData)
+			err = r.Status().Update(ctx, app.DeepCopy())
+		}
+	}
+	return
+}
+
+func getArgoCDApplication(client client.Reader, namespacedName types.NamespacedName) (app *unstructured.Unstructured, err error) {
+	app = getArgoCDApplicationObject()
+
+	if err = client.Get(context.Background(), namespacedName, app); err != nil {
+		app = nil
+	}
+	return
+}
+
+// GetName returns the name of this controller
+func (r *ApplicationStatusReconciler) GetName() string {
+	return "ArgoCDApplicationStatusController"
+}
+
+// GetGroupName returns the group name of this controller
+func (r *ApplicationStatusReconciler) GetGroupName() string {
+	return controllerGroupName
+}
+
+func getArgoCDApplicationObject() *unstructured.Unstructured {
+	cluster := &unstructured.Unstructured{}
+	cluster.SetKind("Application")
+	cluster.SetAPIVersion("argoproj.io/v1alpha1")
+	return cluster.DeepCopy()
+}
+
+// SetupWithManager init the logger, recorder and filters
+func (r *ApplicationStatusReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	cluster := getArgoCDApplicationObject()
+	r.log = ctrl.Log.WithName(r.GetName())
+	r.recorder = mgr.GetEventRecorderFor(r.GetName())
+	return ctrl.NewControllerManagedBy(mgr).
+		For(cluster).
+		Complete(r)
+}
diff --git a/pkg/api/gitops/v1alpha1/application.go b/pkg/api/gitops/v1alpha1/application.go
index 84c1d38..2b80340 100644
--- a/pkg/api/gitops/v1alpha1/application.go
+++ b/pkg/api/gitops/v1alpha1/application.go
@@ -298,6 +298,7 @@ type SyncStrategyHook struct {
 // +genclient
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 // +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
 // +k8s:openapi-gen=true
 
 // Application represents an application the DevOps system
@@ -305,7 +306,13 @@ type Application struct {
 	metav1.TypeMeta   `json:",inline"`
 	metav1.ObjectMeta `json:"metadata,omitempty"`
 
-	Spec ApplicationSpec `json:"spec,omitempty"`
+	Spec   ApplicationSpec   `json:"spec,omitempty"`
+	Status ApplicationStatus `json:"status,omitempty"`
+}
+
+// ApplicationStatus represents the status of the Application
+type ApplicationStatus struct {
+	ArgoApp string `json:"argoApp,omitempty"`
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
diff --git a/pkg/api/gitops/v1alpha1/zz_generated.deepcopy.go b/pkg/api/gitops/v1alpha1/zz_generated.deepcopy.go
index cff3d31..3eb25af 100644
--- a/pkg/api/gitops/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/api/gitops/v1alpha1/zz_generated.deepcopy.go
@@ -31,6 +31,7 @@ func (in *Application) DeepCopyInto(out *Application) {
 	out.TypeMeta = in.TypeMeta
 	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
 	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Application.
@@ -314,6 +315,21 @@ func (in *ApplicationSpec) DeepCopy() *ApplicationSpec {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ApplicationStatus) DeepCopyInto(out *ApplicationStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationStatus.
+func (in *ApplicationStatus) DeepCopy() *ApplicationStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(ApplicationStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ArgoApplication) DeepCopyInto(out *ArgoApplication) {
 	*out = *in


Test patch:
diff --git a/controllers/argocd/application_controller_test.go b/controllers/argocd/application_controller_test.go
index 31a9f7f..1040318 100644
--- a/controllers/argocd/application_controller_test.go
+++ b/controllers/argocd/application_controller_test.go
@@ -97,14 +97,6 @@ func Test_createUnstructuredApplication(t *testing.T) {
 	}
 }
 
-func TestApplicationReconcilerBasicInterface(t *testing.T) {
-	nameGetter := &ApplicationReconciler{}
-	assert.NotEmpty(t, nameGetter.GetName())
-
-	groupNameGetter := &ApplicationReconciler{}
-	assert.NotEmpty(t, groupNameGetter.GetGroupName())
-}
-
 func TestApplicationReconciler_reconcileArgoApplication(t *testing.T) {
 	schema, err := v1alpha3.SchemeBuilder.Register().Build()
 	assert.Nil(t, err)
diff --git a/controllers/argocd/argocd-application-status-controller_test.go b/controllers/argocd/argocd-application-status-controller_test.go
new file mode 100644
index 0000000..85abde7
--- /dev/null
+++ b/controllers/argocd/argocd-application-status-controller_test.go
@@ -0,0 +1,205 @@
+/*
+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 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"
+	"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"
+	controllerruntime "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"testing"
+)
+
+func Test_getArgoCDApplication(t *testing.T) {
+	schema, err := v1alpha3.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+	err = v1.SchemeBuilder.AddToScheme(schema)
+	assert.Nil(t, err)
+
+	defaultApp := &unstructured.Unstructured{}
+	defaultApp.SetKind("Application")
+	defaultApp.SetAPIVersion("argoproj.io/v1alpha1")
+	defaultApp.SetName("name")
+	defaultApp.SetNamespace("ns")
+
+	type args struct {
+		client         client.Reader
+		namespacedName types.NamespacedName
+	}
+	tests := []struct {
+		name   string
+		args   args
+		verify func(t *testing.T, app *unstructured.Unstructured, err error)
+	}{{
+		name: "normal case",
+		args: args{
+			client: fake.NewFakeClientWithScheme(schema, defaultApp.DeepCopy()),
+			namespacedName: types.NamespacedName{
+				Namespace: "ns",
+				Name:      "name",
+			},
+		},
+		verify: func(t *testing.T, app *unstructured.Unstructured, err error) {
+			assert.Nil(t, err)
+			assert.Equal(t, "ns", app.GetNamespace())
+			assert.Equal(t, "name", app.GetName())
+		},
+	}, {
+		name: "not found",
+		args: args{
+			client: fake.NewFakeClientWithScheme(schema),
+		},
+		verify: func(t *testing.T, app *unstructured.Unstructured, err error) {
+			assert.NotNil(t, err)
+			assert.Nil(t, app)
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotApp, err := getArgoCDApplication(tt.args.client, tt.args.namespacedName)
+			tt.verify(t, gotApp, err)
+		})
+	}
+}
+
+func Test_getArgoCDApplicationObject(t *testing.T) {
+	tests := []struct {
+		name   string
+		verify func(t *testing.T, obj *unstructured.Unstructured)
+	}{{
+		name: "normal case",
+		verify: func(t *testing.T, obj *unstructured.Unstructured) {
+			assert.NotNil(t, obj)
+
+			assert.Equal(t, "Application", obj.GetKind())
+			assert.Equal(t, "argoproj.io/v1alpha1", obj.GetAPIVersion())
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.verify(t, getArgoCDApplicationObject())
+		})
+	}
+}
+
+func TestArgoCDApplicationStatusReconciler_Reconcile(t *testing.T) {
+	schema, err := v1alpha3.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+	err = v1.SchemeBuilder.AddToScheme(schema)
+	assert.Nil(t, err)
+	err = v1alpha1.SchemeBuilder.AddToScheme(schema)
+	assert.Nil(t, err)
+
+	defaultApp := &v1alpha1.Application{}
+	defaultApp.SetName("name")
+	defaultApp.SetNamespace("ns")
+
+	defaultArgoCDApp := &unstructured.Unstructured{}
+	defaultArgoCDApp.SetKind("Application")
+	defaultArgoCDApp.SetAPIVersion("argoproj.io/v1alpha1")
+	defaultArgoCDApp.SetName("name")
+	defaultArgoCDApp.SetNamespace("ns")
+
+	type fields struct {
+		Client   client.Client
+		log      logr.Logger
+		recorder record.EventRecorder
+	}
+	type args struct {
+		req controllerruntime.Request
+	}
+	tests := []struct {
+		name       string
+		fields     fields
+		args       args
+		wantResult controllerruntime.Result
+		wantErr    assert.ErrorAssertionFunc
+	}{{
+		name: "normal case",
+		fields: fields{
+			Client: fake.NewFakeClientWithScheme(schema, defaultArgoCDApp, defaultApp),
+		},
+		args: args{
+			req: controllerruntime.Request{
+				NamespacedName: types.NamespacedName{
+					Namespace: "ns",
+					Name:      "name",
+				},
+			},
+		},
+		wantResult: controllerruntime.Result{},
+		wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
+			return false
+		},
+	}, {
+		name: "not found ArgoCD application",
+		fields: fields{
+			Client: fake.NewFakeClientWithScheme(schema),
+		},
+		args: args{
+			req: controllerruntime.Request{
+				NamespacedName: types.NamespacedName{
+					Namespace: "ns",
+					Name:      "name",
+				},
+			},
+		},
+		wantResult: controllerruntime.Result{},
+		wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
+			return true
+		},
+	}, {
+		name: "not found ks application",
+		fields: fields{
+			Client: fake.NewFakeClientWithScheme(schema, defaultArgoCDApp),
+		},
+		args: args{
+			req: controllerruntime.Request{
+				NamespacedName: types.NamespacedName{
+					Namespace: "ns",
+					Name:      "name",
+				},
+			},
+		},
+		wantResult: controllerruntime.Result{},
+		wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
+			return true
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &ApplicationStatusReconciler{
+				Client:   tt.fields.Client,
+				log:      tt.fields.log,
+				recorder: tt.fields.recorder,
+			}
+			gotResult, err := r.Reconcile(tt.args.req)
+			if !tt.wantErr(t, err, fmt.Sprintf("Reconcile(%v)", tt.args.req)) {
+				return
+			}
+			assert.Equalf(t, tt.wantResult, gotResult, "Reconcile(%v)", tt.args.req)
+		})
+	}
+}
diff --git a/controllers/argocd/interface_test.go b/controllers/argocd/interface_test.go
new file mode 100644
index 0000000..28ffbc9
--- /dev/null
+++ b/controllers/argocd/interface_test.go
@@ -0,0 +1,68 @@
+/*
+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 argocd
+
+import (
+	"github.com/stretchr/testify/assert"
+	"kubesphere.io/devops/controllers/core"
+	"testing"
+)
+
+func TestInterfaceImplement(t *testing.T) {
+	type interInstance struct {
+		NamedReconciler core.NamedReconciler
+		GroupReconciler core.GroupReconciler
+	}
+
+	tests := []struct {
+		name     string
+		instance interInstance
+	}{{
+		name: "MultiClusterReconciler",
+		instance: interInstance{
+			NamedReconciler: &MultiClusterReconciler{},
+			GroupReconciler: &MultiClusterReconciler{},
+		},
+	}, {
+		name: "ApplicationReconciler",
+		instance: interInstance{
+			NamedReconciler: &ApplicationReconciler{},
+			GroupReconciler: &ApplicationReconciler{},
+		},
+	}, {
+		name: "ApplicationStatusReconciler",
+		instance: interInstance{
+			NamedReconciler: &ApplicationStatusReconciler{},
+			GroupReconciler: &ApplicationStatusReconciler{},
+		},
+	}, {
+		name: "Reconciler",
+		instance: interInstance{
+			NamedReconciler: &Reconciler{},
+			GroupReconciler: &Reconciler{},
+		},
+	}}
+	for i := range tests {
+		tt := tests[i]
+		t.Run(tt.name, func(t *testing.T) {
+			assert.NotNil(t, tt.instance.NamedReconciler)
+			assert.NotEmpty(t, tt.instance.NamedReconciler.GetName())
+			assert.NotNil(t, tt.instance.GroupReconciler)
+			assert.NotEmpty(t, tt.instance.GroupReconciler.GetGroupName())
+		})
+	}
+}
diff --git a/controllers/argocd/multi-cluster-controller_test.go b/controllers/argocd/multi-cluster-controller_test.go
index 4aa0cbf..fd819d0 100644
--- a/controllers/argocd/multi-cluster-controller_test.go
+++ b/controllers/argocd/multi-cluster-controller_test.go
@@ -130,31 +130,6 @@ users:
 	}
 }
 
-func TestInterfaceImplement(t *testing.T) {
-	tests := []struct {
-		name   string
-		verify func(t *testing.T)
-	}{{
-		name: "NamedReconciler",
-		verify: func(t *testing.T) {
-			reconciler := &MultiClusterReconciler{}
-			assert.NotEmpty(t, reconciler.GetName())
-		},
-	}, {
-		name: "GroupedReconciler",
-		verify: func(t *testing.T) {
-			reconciler := &MultiClusterReconciler{}
-			assert.NotEmpty(t, reconciler.GetGroupName())
-		},
-	}}
-	for i := range tests {
-		tt := tests[i]
-		t.Run(tt.name, func(t *testing.T) {
-			tt.verify(t)
-		})
-	}
-}
-
 func Test_getCluster(t *testing.T) {
 	schema, err := v1alpha3.SchemeBuilder.Register().Build()
 	assert.Nil(t, err)
diff --git a/controllers/argocd/project_controller_test.go b/controllers/argocd/project_controller_test.go
index 19750c6..e774f02 100644
--- a/controllers/argocd/project_controller_test.go
+++ b/controllers/argocd/project_controller_test.go
@@ -87,14 +87,6 @@ func TestCreateUnstructuredObject(t *testing.T) {
 		map[string]interface{}{"group": "group2", "kind": "kind2"}}, p2ClusterResourceWhitelist)
 }
 
-func TestBasicInterface(t *testing.T) {
-	nameGetter := &Reconciler{}
-	assert.NotEmpty(t, nameGetter.GetName())
-
-	groupNameGetter := &Reconciler{}
-	assert.NotEmpty(t, groupNameGetter.GetGroupName())
-}
-
 func TestReconcileArgoProject(t *testing.T) {
 	schema, err := v1alpha3.SchemeBuilder.Register().Build()
 	assert.Nil(t, err)