Repository: kubesphere/ks-devops
Pull request #834 (2022-10-08 00:29:13)

doc: pipelinerun GC description
<!-- 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
-->
/kind documentation

### 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 #

### 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 **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 **linuxsuren** 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":["linuxsuren"]} -->

Patch:
diff --git a/OWNERS b/OWNERS
index 5b46da3..af649ed 100644
--- a/OWNERS
+++ b/OWNERS
@@ -1,5 +1,6 @@
 approvers:
-    - linuxsuren
+    - chilianyi
+    - zheng1
 
 reviewers:
-    - JohnNiang
+    - linuxsuren
diff --git a/cmd/apiserver/app/options/options.go b/cmd/apiserver/app/options/options.go
index a85d738..b0fbd8c 100644
--- a/cmd/apiserver/app/options/options.go
+++ b/cmd/apiserver/app/options/options.go
@@ -69,6 +69,7 @@ func (s *ServerRunOptions) Flags() (fss cliflag.NamedFlagSets) {
 	s.SonarQubeOptions.AddFlags(fss.FlagSet("sonarqube"), s.SonarQubeOptions)
 	s.S3Options.AddFlags(fss.FlagSet("s3"), s.S3Options)
 	s.ArgoCDOption.AddFlags(fss.FlagSet("argocd"))
+	s.FluxCDOption.AddFlags(fss.FlagSet("fluxcd"))
 
 	fs = fss.FlagSet("klog")
 	local := flag.NewFlagSet("klog", flag.ExitOnError)
diff --git a/controllers/jenkins/pipeline/pipeline_metadata_controller.go b/controllers/jenkins/pipeline/pipeline_metadata_controller.go
index 2ff8e02..d60adf6 100644
--- a/controllers/jenkins/pipeline/pipeline_metadata_controller.go
+++ b/controllers/jenkins/pipeline/pipeline_metadata_controller.go
@@ -171,6 +171,13 @@ var pipelineMetadataPredicate = predicate.Funcs{
 		return false
 	},
 	UpdateFunc: func(ue event.UpdateEvent) bool {
+		oldPipeline, okOld := ue.ObjectOld.(*v1alpha3.Pipeline)
+		newPipeline, okNew := ue.ObjectNew.(*v1alpha3.Pipeline)
+		if okOld && okNew {
+			if !reflect.DeepEqual(oldPipeline.Spec, newPipeline.Spec) {
+				return true
+			}
+		}
 		return false
 	},
 	GenericFunc: func(ge event.GenericEvent) bool {
diff --git a/docs/gc.md b/docs/gc.md
new file mode 100644
index 0000000..827c41f
--- /dev/null
+++ b/docs/gc.md
@@ -0,0 +1,8 @@
+We prefer to use the built-in PipelineRun GC instead of Jenkins itself. Please feel free to configure [the `CronJob`](https://github.com/kubesphere-sigs/ks-devops-helm-chart/blob/464a1a9854561ef5666433b8d975b89cced07494/charts/ks-devops/templates/cronjob-gc.yaml) according to your requirement.
+
+Normally, you could find it from namespace `kubesphere-devops-system`. You could specifiy the following options:
+
+* `maxAge` is the maximum time to live for PipelineRuns
+* `maxCount` is the max number of the PipelineRuns 
+
+See also the Helm chart [values setting](https://github.com/kubesphere-sigs/ks-devops-helm-chart/blob/464a1a9854561ef5666433b8d975b89cced07494/charts/ks-devops/values.yaml#L17).
diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index 82b6472..a725528 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -165,7 +165,7 @@ func (s *APIServer) installKubeSphereAPIs() {
 	))
 	wss = append(wss, gitops.AddToContainer(s.container, &common.Options{
 		GenericClient: s.Client,
-	}, s.Config.ArgoCDOption)...)
+	}, s.Config.ArgoCDOption, s.Config.FluxCDOption)...)
 	doc.AddSwaggerService(wss, s.container)
 }
 
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 27f5151..17e5490 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -85,6 +85,7 @@ type Config struct {
 	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"`
+	FluxCDOption          *FluxCDOption                      `json:"fluxcd,omitempty" yaml:"fluxcd,omitempty" mapstructure:"fluxcd"`
 	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"`
@@ -99,6 +100,7 @@ func New() *Config {
 		S3Options:         s3.NewS3Options(),
 		AuthMode:          AuthModeToken,
 		ArgoCDOption:      &ArgoCDOption{},
+		FluxCDOption:      &FluxCDOption{},
 	}
 }
 
diff --git a/pkg/config/argocd.go b/pkg/config/gitops.go
similarity index 53%
rename from pkg/config/argocd.go
rename to pkg/config/gitops.go
index 57acb19..ee9cdff 100644
--- a/pkg/config/argocd.go
+++ b/pkg/config/gitops.go
@@ -16,15 +16,41 @@ limitations under the License.
 
 package config
 
-import "github.com/spf13/pflag"
+import (
+	"github.com/spf13/pflag"
+	v1alpha1 "kubesphere.io/devops/pkg/api/gitops/v1alpha1"
+)
 
 // ArgoCDOption as the ArgoCD integration configuration
 type ArgoCDOption struct {
+	Enabled   bool   `json:"enabled,omitempty" yaml:"enabled,omitempty" description:"enabled FluxCD"`
 	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) {
+	fs.BoolVar(&o.Enabled, "argocd-enabled", false, "Enable ArgoCD APIs")
 	// 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")
 }
+
+// FluxCDOption as the FluxCD integration configuration
+type FluxCDOption struct {
+	Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty" description:"enabled FluxCD"`
+}
+
+// AddFlags adds the flags which related to fluxcd
+func (o *FluxCDOption) AddFlags(fs *pflag.FlagSet) {
+	fs.BoolVar(&o.Enabled, "fluxcd-enabled", false, "Enable FluxCD APIs")
+}
+
+// GetGitOpsEngine return gitops engine type
+func GetGitOpsEngine(argoOption *ArgoCDOption, fluxOption *FluxCDOption) v1alpha1.Engine {
+	if argoOption.Enabled && !fluxOption.Enabled {
+		return v1alpha1.ArgoCD
+	}
+	if fluxOption.Enabled && !argoOption.Enabled {
+		return v1alpha1.FluxCD
+	}
+	return ""
+}
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/handler.go b/pkg/kapis/gitops/v1alpha1/argocd/handler.go
index 1369317..26c0f12 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/handler.go
+++ b/pkg/kapis/gitops/v1alpha1/argocd/handler.go
@@ -23,12 +23,10 @@ import (
 	"k8s.io/apiserver/pkg/authentication/user"
 	utilretry "k8s.io/client-go/util/retry"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
-	"kubesphere.io/devops/pkg/apiserver/query"
 	apiserverrequest "kubesphere.io/devops/pkg/apiserver/request"
 	"kubesphere.io/devops/pkg/config"
 	"kubesphere.io/devops/pkg/kapis/common"
-	"kubesphere.io/devops/pkg/models/resources/v1alpha3"
-	"kubesphere.io/devops/pkg/utils/k8sutil"
+	"kubesphere.io/devops/pkg/kapis/gitops/v1alpha1/gitops"
 	"net/http"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
@@ -42,40 +40,54 @@ var invalidRequestBodyError = restful.NewError(http.StatusBadRequest,
 var unauthenticatedError = restful.NewError(http.StatusUnauthorized,
 	"unauthenticated request")
 
-func (h *handler) applicationList(req *restful.Request, res *restful.Response) {
+func (h *handler) createApplication(req *restful.Request, res *restful.Response) {
+	var err error
 	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
-	healthStatus := common.GetQueryParameter(req, healthStatusQueryParam)
-	syncStatus := common.GetQueryParameter(req, syncStatusQueryParam)
 
-	applicationList := &v1alpha1.ApplicationList{}
-	matchingLabels := client.MatchingLabels{}
-	if syncStatus != "" {
-		matchingLabels[v1alpha1.SyncStatusLabelKey] = syncStatus
-	}
-	if healthStatus != "" {
-		matchingLabels[v1alpha1.HealthStatusLabelKey] = healthStatus
-	}
-	if err := h.List(context.Background(), applicationList, client.InNamespace(namespace), matchingLabels); err != nil {
-		common.Response(req, res, applicationList, err)
-		return
+	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)
 	}
+	common.Response(req, res, application, err)
+}
 
-	queryParam := query.ParseQueryParameter(req)
-	list := v1alpha3.DefaultList(toObjects(applicationList.Items), queryParam, v1alpha3.DefaultCompare(), v1alpha3.DefaultFilter(), nil)
+func (h *handler) applicationSummary(request *restful.Request, response *restful.Response) {
+	namespace := common.GetPathParameter(request, common.NamespacePathParameter)
 
-	common.Response(req, res, list, nil)
+	summary, err := h.populateApplicationSummary(namespace)
+	common.Response(request, response, summary, err)
 }
 
-func (h *handler) getApplication(req *restful.Request, res *restful.Response) {
-	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
-	name := common.GetPathParameter(req, pathParameterApplication)
+func (h *handler) populateApplicationSummary(namespace string) (*ApplicationsSummary, error) {
+	applicationList := &v1alpha1.ApplicationList{}
+	if err := h.List(context.Background(), applicationList, client.InNamespace(namespace)); err != nil {
+		return nil, err
+	}
+	summary := &ApplicationsSummary{
+		HealthStatus: map[string]int{},
+		SyncStatus:   map[string]int{},
+	}
+	summary.Total = len(applicationList.Items)
 
-	application := &v1alpha1.Application{}
-	err := h.Get(context.Background(), types.NamespacedName{
-		Namespace: namespace,
-		Name:      name,
-	}, application)
-	common.Response(req, res, application, err)
+	for i := range applicationList.Items {
+		app := &applicationList.Items[i]
+		// accumulate health status
+		healthStatus := app.GetLabels()[v1alpha1.HealthStatusLabelKey]
+		if healthStatus != "" {
+			summary.HealthStatus[healthStatus]++
+		}
+		// accumulate sync status
+		syncStatus := app.GetLabels()[v1alpha1.SyncStatusLabelKey]
+		if syncStatus != "" {
+			summary.SyncStatus[syncStatus]++
+		}
+	}
+	return summary, nil
 }
 
 func (h *handler) handleSyncApplication(req *restful.Request, res *restful.Response) {
@@ -169,75 +181,6 @@ func (h *handler) updateOperation(namespace, name string, operation *v1alpha1.Op
 	})
 }
 
-func (h *handler) delApplication(req *restful.Request, res *restful.Response) {
-	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
-	name := common.GetPathParameter(req, pathParameterApplication)
-	cascade := common.GetQueryParameter(req, cascadeQueryParam)
-
-	ctx := context.Background()
-	application := &v1alpha1.Application{}
-	objectKey := types.NamespacedName{
-		Namespace: namespace,
-		Name:      name,
-	}
-	err := h.Get(ctx, objectKey, application)
-	if err == nil {
-		// add the Argo CD resources finalizer if cascade is true
-		if cascade == "true" {
-			if k8sutil.AddFinalizer(&application.ObjectMeta, v1alpha1.ArgoCDResourcesFinalizer) {
-				if err = h.Update(ctx, application); err != nil {
-					common.Response(req, res, application, err)
-					return
-				}
-
-				if err = h.Get(ctx, objectKey, application); err != nil {
-					common.Response(req, res, application, err)
-					return
-				}
-			}
-		}
-		err = h.Delete(ctx, application)
-	}
-	common.Response(req, res, application, err)
-}
-
-func (h *handler) createApplication(req *restful.Request, res *restful.Response) {
-	var err error
-	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
-
-	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)
-	}
-
-	common.Response(req, res, application, err)
-}
-
-func (h *handler) updateApplication(req *restful.Request, res *restful.Response) {
-	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
-	name := common.GetPathParameter(req, pathParameterApplication)
-
-	var err error
-	application := &v1alpha1.Application{}
-	if err = req.ReadEntity(application); err == nil {
-		latestApp := &v1alpha1.Application{}
-		err = h.Get(context.Background(), types.NamespacedName{
-			Namespace: namespace,
-			Name:      name,
-		}, latestApp)
-		if err == nil {
-			application.ResourceVersion = latestApp.ResourceVersion
-			err = h.Update(context.Background(), application)
-		}
-	}
-	common.Response(req, res, application, err)
-}
-
 func (h *handler) getClusters(req *restful.Request, res *restful.Response) {
 	ctx := context.Background()
 
@@ -269,48 +212,14 @@ func (h *handler) getClusters(req *restful.Request, res *restful.Response) {
 	common.Response(req, res, argoClusters, err)
 }
 
-func (h *handler) applicationSummary(request *restful.Request, response *restful.Response) {
-	namespace := common.GetPathParameter(request, common.NamespacePathParameter)
-
-	summary, err := h.populateApplicationSummary(namespace)
-	common.Response(request, response, summary, err)
-}
-
-func (h *handler) populateApplicationSummary(namespace string) (*ApplicationsSummary, error) {
-	applicationList := &v1alpha1.ApplicationList{}
-	if err := h.List(context.Background(), applicationList, client.InNamespace(namespace)); err != nil {
-		return nil, err
-	}
-	summary := &ApplicationsSummary{
-		HealthStatus: map[string]int{},
-		SyncStatus:   map[string]int{},
-	}
-	summary.Total = len(applicationList.Items)
-
-	for i := range applicationList.Items {
-		app := &applicationList.Items[i]
-		// accumulate health status
-		healthStatus := app.GetLabels()[v1alpha1.HealthStatusLabelKey]
-		if healthStatus != "" {
-			summary.HealthStatus[healthStatus]++
-		}
-		// accumulate sync status
-		syncStatus := app.GetLabels()[v1alpha1.SyncStatusLabelKey]
-		if syncStatus != "" {
-			summary.SyncStatus[syncStatus]++
-		}
-	}
-	return summary, nil
-}
-
 type handler struct {
-	client.Client
+	*gitops.Handler
 	ArgoCDNamespace string
 }
 
 func newHandler(options *common.Options, argoOption *config.ArgoCDOption) *handler {
 	return &handler{
-		Client:          options.GenericClient,
+		Handler:         gitops.NewHandler(options),
 		ArgoCDNamespace: argoOption.Namespace,
 	}
 }
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/route.go b/pkg/kapis/gitops/v1alpha1/argocd/route.go
index d31f697..1f9f17d 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/route.go
+++ b/pkg/kapis/gitops/v1alpha1/argocd/route.go
@@ -64,7 +64,7 @@ type ApplicationSyncRequest struct {
 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).
+		To(handler.ApplicationList).
 		Param(common.NamespacePathParameter).
 		Param(common.PageQueryParameter).
 		Param(common.LimitQueryParameter).
@@ -90,7 +90,7 @@ func RegisterRoutes(service *restful.WebService, options *common.Options, argoOp
 		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
 
 	service.Route(service.GET("/namespaces/{namespace}/applications/{application}").
-		To(handler.getApplication).
+		To(handler.GetApplication).
 		Param(common.NamespacePathParameter).
 		Param(pathParameterApplication).
 		Doc("Get a particular application").
@@ -105,7 +105,7 @@ func RegisterRoutes(service *restful.WebService, options *common.Options, argoOp
 		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
 
 	service.Route(service.DELETE("/namespaces/{namespace}/applications/{application}").
-		To(handler.delApplication).
+		To(handler.DelApplication).
 		Param(common.NamespacePathParameter).
 		Param(pathParameterApplication).
 		Param(cascadeQueryParam).
@@ -113,7 +113,7 @@ func RegisterRoutes(service *restful.WebService, options *common.Options, argoOp
 		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
 
 	service.Route(service.PUT("/namespaces/{namespace}/applications/{application}").
-		To(handler.updateApplication).
+		To(handler.UpdateApplication).
 		Param(common.NamespacePathParameter).
 		Param(pathParameterApplication).
 		Reads(v1alpha1.Application{}).
diff --git a/pkg/kapis/gitops/v1alpha1/fluxcd/handler.go b/pkg/kapis/gitops/v1alpha1/fluxcd/handler.go
new file mode 100644
index 0000000..f3168c3
--- /dev/null
+++ b/pkg/kapis/gitops/v1alpha1/fluxcd/handler.go
@@ -0,0 +1,69 @@
+// Copyright 2022 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 fluxcd
+
+import (
+	"context"
+	"github.com/emicklei/go-restful"
+	v1 "k8s.io/api/core/v1"
+	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
+	"kubesphere.io/devops/pkg/config"
+	"kubesphere.io/devops/pkg/kapis/common"
+	"kubesphere.io/devops/pkg/kapis/gitops/v1alpha1/gitops"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func (h *handler) createApplication(req *restful.Request, res *restful.Response) {
+	var err error
+	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
+	application := &v1alpha1.Application{}
+	if err = req.ReadEntity(application); err == nil {
+		application.Namespace = namespace
+		err = h.Create(context.Background(), application)
+	}
+	common.Response(req, res, application, err)
+}
+
+func (h *handler) getClusters(req *restful.Request, res *restful.Response) {
+	ctx := context.Background()
+
+	secrets := &v1.SecretList{}
+	err := h.List(ctx, secrets, client.MatchingLabels{
+		"app.kubernetes.io/managed-by": "fluxcd-controller",
+	})
+
+	// Compatibility with the front end for now
+	//TODO: change to FluxApplicationDestination
+	fluxClusters := []v1alpha1.ApplicationDestination{{
+		Name: "in-cluster",
+	}}
+	for _, s := range secrets.Items {
+		fluxClusters = append(fluxClusters, v1alpha1.ApplicationDestination{
+			Name: s.Name,
+		})
+	}
+	common.Response(req, res, fluxClusters, err)
+}
+
+type handler struct {
+	*gitops.Handler
+}
+
+func newHandler(options *common.Options, fluxOption *config.FluxCDOption) *handler {
+	return &handler{
+		gitops.NewHandler(options),
+	}
+}
diff --git a/pkg/kapis/gitops/v1alpha1/fluxcd/route.go b/pkg/kapis/gitops/v1alpha1/fluxcd/route.go
new file mode 100644
index 0000000..f7b3a1c
--- /dev/null
+++ b/pkg/kapis/gitops/v1alpha1/fluxcd/route.go
@@ -0,0 +1,96 @@
+// Copyright 2022 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 fluxcd
+
+import (
+	"github.com/emicklei/go-restful"
+	"kubesphere.io/devops/pkg/api"
+	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
+	"kubesphere.io/devops/pkg/config"
+	"kubesphere.io/devops/pkg/kapis/common"
+	"net/http"
+)
+
+var (
+	// pathParameterApplication is a path parameter definition for application.
+	pathParameterApplication = restful.PathParameter("application", "The application name")
+	syncStatusQueryParam     = restful.QueryParameter("syncStatus", `Filter by sync status. Available values: "Unknown", "Synced" and "OutOfSync"`)
+	healthStatusQueryParam   = restful.QueryParameter("healthStatus", `Filter by health status. Available values: "Unknown", "Progressing", "Healthy", "Suspended", "Degraded" and "Missing"`)
+	cascadeQueryParam        = restful.QueryParameter("cascade",
+		"Delete both the app and its resources, rather than only the application if cascade is true").
+		DefaultValue("false").DataType("bool")
+)
+
+// ApplicationPageResult is the model of page result of Applications.
+type ApplicationPageResult struct {
+	Items      []v1alpha1.Application `json:"items"`
+	TotalItems int                    `json:"totalItems"`
+}
+
+// RegisterRoutes is for registering Argo CD Application routes into WebService.
+func RegisterRoutes(service *restful.WebService, options *common.Options, fluxOption *config.FluxCDOption) {
+	handler := newHandler(options, fluxOption)
+
+	// public
+	service.Route(service.GET("/namespaces/{namespace}/applications").
+		To(handler.ApplicationList).
+		Param(common.NamespacePathParameter).
+		Param(common.PageQueryParameter).
+		Param(common.LimitQueryParameter).
+		Param(common.NameQueryParameter).
+		Param(common.SortByQueryParameter).
+		Param(common.AscendingQueryParameter).
+		Param(syncStatusQueryParam).
+		Param(healthStatusQueryParam).
+		Doc("Search applications").
+		Returns(http.StatusOK, api.StatusOK, ApplicationPageResult{}))
+
+	service.Route(service.GET("/namespaces/{namespace}/applications/{application}").
+		To(handler.GetApplication).
+		Param(common.NamespacePathParameter).
+		Param(pathParameterApplication).
+		Doc("Get a particular application").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
+
+	service.Route(service.DELETE("/namespaces/{namespace}/applications/{application}").
+		To(handler.DelApplication).
+		Param(common.NamespacePathParameter).
+		Param(pathParameterApplication).
+		Param(cascadeQueryParam).
+		Doc("Delete a particular application").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
+
+	service.Route(service.PUT("/namespaces/{namespace}/applications/{application}").
+		To(handler.UpdateApplication).
+		Param(common.NamespacePathParameter).
+		Param(pathParameterApplication).
+		Reads(v1alpha1.Application{}).
+		Doc("Update a particular application").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
+
+	// fluxcd
+	service.Route(service.POST("/namespaces/{namespace}/applications").
+		To(handler.createApplication).
+		Param(common.NamespacePathParameter).
+		Reads(v1alpha1.Application{}).
+		Doc("Create an application").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
+
+	service.Route(service.GET("/clusters").
+		To(handler.getClusters).
+		Doc("Get the clusters list").
+		Returns(http.StatusOK, api.StatusOK, []v1alpha1.ApplicationDestination{}))
+}
diff --git a/pkg/kapis/gitops/v1alpha1/gitops/handler.go b/pkg/kapis/gitops/v1alpha1/gitops/handler.go
new file mode 100644
index 0000000..ba1b2f8
--- /dev/null
+++ b/pkg/kapis/gitops/v1alpha1/gitops/handler.go
@@ -0,0 +1,140 @@
+// Copyright 2022 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 gitops
+
+import (
+	"context"
+	"github.com/emicklei/go-restful"
+	"k8s.io/apimachinery/pkg/types"
+	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
+	"kubesphere.io/devops/pkg/apiserver/query"
+	"kubesphere.io/devops/pkg/kapis/common"
+	"kubesphere.io/devops/pkg/models/resources/v1alpha3"
+	"kubesphere.io/devops/pkg/utils/k8sutil"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+var (
+	// pathParameterApplication is a path parameter definition for application.
+	pathParameterApplication = restful.PathParameter("application", "The application name")
+	syncStatusQueryParam     = restful.QueryParameter("syncStatus", `Filter by sync status. Available values: "Unknown", "Synced" and "OutOfSync"`)
+	healthStatusQueryParam   = restful.QueryParameter("healthStatus", `Filter by health status. Available values: "Unknown", "Progressing", "Healthy", "Suspended", "Degraded" and "Missing"`)
+	cascadeQueryParam        = restful.QueryParameter("cascade",
+		"Delete both the app and its resources, rather than only the application if cascade is true").
+		DefaultValue("false").DataType("bool")
+)
+
+func (h *Handler) ApplicationList(req *restful.Request, res *restful.Response) {
+	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
+	healthStatus := common.GetQueryParameter(req, healthStatusQueryParam)
+	syncStatus := common.GetQueryParameter(req, syncStatusQueryParam)
+
+	applicationList := &v1alpha1.ApplicationList{}
+	matchingLabels := client.MatchingLabels{}
+	if syncStatus != "" {
+		matchingLabels[v1alpha1.SyncStatusLabelKey] = syncStatus
+	}
+	if healthStatus != "" {
+		matchingLabels[v1alpha1.HealthStatusLabelKey] = healthStatus
+	}
+	if err := h.List(context.Background(), applicationList, client.InNamespace(namespace), matchingLabels); err != nil {
+		common.Response(req, res, applicationList, err)
+		return
+	}
+
+	queryParam := query.ParseQueryParameter(req)
+	list := v1alpha3.DefaultList(ToObjects(applicationList.Items), queryParam, v1alpha3.DefaultCompare(), v1alpha3.DefaultFilter(), nil)
+
+	common.Response(req, res, list, nil)
+}
+
+func (h *Handler) GetApplication(req *restful.Request, res *restful.Response) {
+	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
+	name := common.GetPathParameter(req, pathParameterApplication)
+
+	application := &v1alpha1.Application{}
+	err := h.Get(context.Background(), types.NamespacedName{
+		Namespace: namespace,
+		Name:      name,
+	}, application)
+	common.Response(req, res, application, err)
+}
+
+func (h *Handler) DelApplication(req *restful.Request, res *restful.Response) {
+	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
+	name := common.GetPathParameter(req, pathParameterApplication)
+	cascade := common.GetQueryParameter(req, cascadeQueryParam)
+
+	ctx := context.Background()
+	application := &v1alpha1.Application{}
+	objectKey := types.NamespacedName{
+		Namespace: namespace,
+		Name:      name,
+	}
+	err := h.Get(ctx, objectKey, application)
+	if err == nil {
+		switch application.Spec.Kind {
+		case v1alpha1.ArgoCD:
+			// add the Argo CD resources finalizer if cascade is true
+			if cascade == "true" {
+				if k8sutil.AddFinalizer(&application.ObjectMeta, v1alpha1.ArgoCDResourcesFinalizer) {
+					if err = h.Update(ctx, application); err != nil {
+						common.Response(req, res, application, err)
+						return
+					}
+
+					if err = h.Get(ctx, objectKey, application); err != nil {
+						common.Response(req, res, application, err)
+						return
+					}
+				}
+			}
+		case v1alpha1.FluxCD:
+		}
+		err = h.Delete(ctx, application)
+	}
+	common.Response(req, res, application, err)
+}
+
+func (h *Handler) UpdateApplication(req *restful.Request, res *restful.Response) {
+	namespace := common.GetPathParameter(req, common.NamespacePathParameter)
+	name := common.GetPathParameter(req, pathParameterApplication)
+
+	var err error
+	application := &v1alpha1.Application{}
+	if err = req.ReadEntity(application); err == nil {
+		latestApp := &v1alpha1.Application{}
+		err = h.Get(context.Background(), types.NamespacedName{
+			Namespace: namespace,
+			Name:      name,
+		}, latestApp)
+		if err == nil {
+			application.ResourceVersion = latestApp.ResourceVersion
+			err = h.Update(context.Background(), application)
+		}
+	}
+	common.Response(req, res, application, err)
+}
+
+type Handler struct {
+	client.Client
+}
+
+func NewHandler(options *common.Options) *Handler {
+	return &Handler{
+		Client: options.GenericClient,
+	}
+}
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/util.go b/pkg/kapis/gitops/v1alpha1/gitops/util.go
similarity index 91%
rename from pkg/kapis/gitops/v1alpha1/argocd/util.go
rename to pkg/kapis/gitops/v1alpha1/gitops/util.go
index 8043101..93d0f2e 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/util.go
+++ b/pkg/kapis/gitops/v1alpha1/gitops/util.go
@@ -13,14 +13,14 @@
 // limitations under the License.
 //
 
-package argocd
+package gitops
 
 import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 )
 
-func toObjects(apps []v1alpha1.Application) []runtime.Object {
+func ToObjects(apps []v1alpha1.Application) []runtime.Object {
 	objs := make([]runtime.Object, len(apps))
 	for i := range apps {
 		objs[i] = &apps[i]
diff --git a/pkg/kapis/gitops/v1alpha1/registery.go b/pkg/kapis/gitops/v1alpha1/registery.go
index 2e28e93..4baebb9 100644
--- a/pkg/kapis/gitops/v1alpha1/registery.go
+++ b/pkg/kapis/gitops/v1alpha1/registery.go
@@ -23,19 +23,28 @@ import (
 	"kubesphere.io/devops/pkg/config"
 	"kubesphere.io/devops/pkg/kapis/common"
 	"kubesphere.io/devops/pkg/kapis/gitops/v1alpha1/argocd"
+	"kubesphere.io/devops/pkg/kapis/gitops/v1alpha1/fluxcd"
 )
 
 // TODO perhaps we can find a better way to declaim the permission needs of the apiserver
 //+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, argoOption *config.ArgoCDOption) []*restful.WebService {
+func AddToContainer(container *restful.Container, options *common.Options, argoOption *config.ArgoCDOption, fluxOption *config.FluxCDOption) []*restful.WebService {
 	services := []*restful.WebService{
 		runtime.NewWebService(v1alpha1.GroupVersion),
 		runtime.NewWebServiceWithoutGroup(v1alpha1.GroupVersion),
 	}
 	for _, service := range services {
-		argocd.RegisterRoutes(service, options, argoOption)
+		// dynamic register routers
+		switch config.GetGitOpsEngine(argoOption, fluxOption) {
+		case v1alpha1.ArgoCD:
+			argocd.RegisterRoutes(service, options, argoOption)
+		case v1alpha1.FluxCD:
+			fluxcd.RegisterRoutes(service, options, fluxOption)
+		default:
+			return nil
+		}
 		container.Add(service)
 	}
 	return services


Test patch:
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/handler_test.go b/pkg/kapis/gitops/v1alpha1/argocd/handler_test.go
index 681338b..5d114a3 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/handler_test.go
+++ b/pkg/kapis/gitops/v1alpha1/argocd/handler_test.go
@@ -25,299 +25,16 @@ import (
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/apiserver/pkg/authentication/user"
 	"k8s.io/client-go/kubernetes/scheme"
-	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 	"kubesphere.io/devops/pkg/apiserver/request"
 	"kubesphere.io/devops/pkg/kapis/common"
+	"kubesphere.io/devops/pkg/kapis/gitops/v1alpha1/gitops"
 	"net/http"
 	"net/http/httptest"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 	"testing"
-	"time"
 )
 
-func Test_getPathParameter(t *testing.T) {
-	type args struct {
-		req   func() *restful.Request
-		param *restful.Parameter
-	}
-	tests := []struct {
-		name string
-		args args
-		want string
-	}{{
-		name: "normal case",
-		args: args{
-			req: func() *restful.Request {
-				request := restful.NewRequest(&http.Request{})
-				request.PathParameters()["name"] = "good"
-				return request
-			},
-			param: restful.PathParameter("name", "desc"),
-		},
-		want: "good",
-	}, {
-		name: "wrong param name",
-		args: args{
-			req: func() *restful.Request {
-				request := restful.NewRequest(&http.Request{})
-				request.PathParameters()["name"] = "good"
-				return request
-			},
-			param: restful.PathParameter("fake", "desc"),
-		},
-		want: "",
-	}}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			assert.Equalf(t, tt.want, common.GetPathParameter(tt.args.req(), tt.args.param), "getPathParameter(%v, %v)", tt.args.req, tt.args.param)
-		})
-	}
-}
-
-func Test_handler_applicationList(t *testing.T) {
-	createRequest := func(uri, namespace string) *restful.Request {
-		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
-		request := restful.NewRequest(fakeRequest)
-		request.PathParameters()[common.NamespacePathParameter.Data().Name] = namespace
-		return request
-	}
-	createApp := func(name string, namespace string, labels map[string]string) *v1alpha1.Application {
-		return &v1alpha1.Application{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:            name,
-				Namespace:       namespace,
-				Labels:          labels,
-				ResourceVersion: "999",
-			},
-		}
-	}
-	createAppWithCreationTime := func(name string, namespace string, creationTimestamp time.Time) *v1alpha1.Application {
-		return &v1alpha1.Application{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:              name,
-				Namespace:         namespace,
-				CreationTimestamp: metav1.NewTime(creationTimestamp),
-				ResourceVersion:   "999",
-			},
-		}
-	}
-	current := time.Now()
-	yesterday := current.Add(-24 * time.Hour)
-	tomorrow := current.Add(24 * time.Hour)
-	type args struct {
-		req  *restful.Request
-		apps []v1alpha1.Application
-	}
-	tests := []struct {
-		name         string
-		args         args
-		wantResponse api.ListResult
-	}{{
-		name: "Should return empty list when namespaces mismatch",
-		args: args{
-			req: createRequest("/applications", "default"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", nil),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items:      []interface{}{},
-			TotalItems: 0,
-		},
-	}, {
-		name: "Should return correct list when namespaces match",
-		args: args{
-			req: createRequest("/applications", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", nil),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items: []interface{}{
-				createApp("fake-app-1", "fake-namespace", nil),
-			},
-			TotalItems: 1,
-		},
-	}, {
-		name: "Should return empty list when healthStatus is set but no status matches",
-		args: args{
-			req: createRequest("/applications?healthStatus=Unknown", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", map[string]string{
-					v1alpha1.HealthStatusLabelKey: "Healthy",
-				}),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items:      []interface{}{},
-			TotalItems: 0,
-		},
-	}, {
-		name: "Should return partial list when healthStatus is set but no status matches",
-		args: args{
-			req: createRequest("/applications?healthStatus=Unknown", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", map[string]string{
-					v1alpha1.HealthStatusLabelKey: "Healthy",
-				}),
-				*createApp("fake-app-2", "fake-namespace", map[string]string{
-					v1alpha1.HealthStatusLabelKey: "Unknown",
-				}),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items: []interface{}{
-				*createApp("fake-app-2", "fake-namespace", map[string]string{
-					v1alpha1.HealthStatusLabelKey: "Unknown",
-				}),
-			},
-			TotalItems: 1,
-		},
-	}, {
-		name: "Should return empty list when syncStatus is set but no status matches",
-		args: args{
-			req: createRequest("/applications?syncStatus=Unknown", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", map[string]string{
-					v1alpha1.SyncStatusLabelKey: "Synced",
-				}),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items:      []interface{}{},
-			TotalItems: 0,
-		},
-	}, {
-		name: "Should return partial list when syncStatus is set but no status matches",
-		args: args{
-			req: createRequest("/applications?syncStatus=Unknown", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", map[string]string{
-					v1alpha1.SyncStatusLabelKey: "Synced",
-				}),
-				*createApp("fake-app-2", "fake-namespace", map[string]string{
-					v1alpha1.SyncStatusLabelKey: "Unknown",
-				}),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items: []interface{}{
-				*createApp("fake-app-2", "fake-namespace", map[string]string{
-					v1alpha1.SyncStatusLabelKey: "Unknown",
-				}),
-			},
-			TotalItems: 1,
-		},
-	}, {
-		name: "Should return empty list when healthStatus and syncStatus are set but no status matches",
-		args: args{
-			req: createRequest("/applications?healthStatus=Unknown&syncStatus=Unknown", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", map[string]string{
-					v1alpha1.HealthStatusLabelKey: "Healthy",
-					v1alpha1.SyncStatusLabelKey:   "Synced",
-				}),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items:      []interface{}{},
-			TotalItems: 0,
-		},
-	}, {
-		name: "Should return applications when both healthStatus and syncStatus match at the same time",
-		args: args{
-			req: createRequest("/applications?healthStatus=Unknown&syncStatus=Unknown", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", map[string]string{
-					v1alpha1.HealthStatusLabelKey: "Unknown",
-					v1alpha1.SyncStatusLabelKey:   "Synced",
-				}),
-				*createApp("fake-app-2", "fake-namespace", map[string]string{
-					v1alpha1.HealthStatusLabelKey: "Unknown",
-					v1alpha1.SyncStatusLabelKey:   "Unknown",
-				}),
-			},
-		},
-		wantResponse: api.ListResult{Items: []interface{}{
-			*createApp("fake-app-2", "fake-namespace", map[string]string{
-				v1alpha1.HealthStatusLabelKey: "Unknown",
-				v1alpha1.SyncStatusLabelKey:   "Unknown",
-			}),
-		}, TotalItems: 1},
-	}, {
-		name: "Should filter with name",
-		args: args{
-			req: createRequest("/applications?name=app", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-1", "fake-namespace", nil),
-				*createApp("fake-bpp-1", "fake-namespace", nil),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items: []interface{}{
-				*createApp("fake-app-1", "fake-namespace", nil),
-			},
-			TotalItems: 1,
-		},
-	}, {
-		name: "Should sort by name in ascending order",
-		args: args{
-			req: createRequest("/applications?orderBy=name&ascending=true", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createApp("fake-app-2", "fake-namespace", nil),
-				*createApp("fake-app-1", "fake-namespace", nil),
-				*createApp("fake-app-3", "fake-namespace", nil),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items: []interface{}{
-				*createApp("fake-app-1", "fake-namespace", nil),
-				*createApp("fake-app-2", "fake-namespace", nil),
-				*createApp("fake-app-3", "fake-namespace", nil),
-			},
-			TotalItems: 3,
-		},
-	}, {
-		name: "Should sort by creationTimestamp in ascending order",
-		args: args{
-			req: createRequest("/applications?orderBy=creationTimestamp&ascending=true", "fake-namespace"),
-			apps: []v1alpha1.Application{
-				*createAppWithCreationTime("fake-app-2", "fake-namespace", current),
-				*createAppWithCreationTime("fake-app-1", "fake-namespace", yesterday),
-				*createAppWithCreationTime("fake-app-3", "fake-namespace", tomorrow),
-			},
-		},
-		wantResponse: api.ListResult{
-			Items: []interface{}{
-				*createAppWithCreationTime("fake-app-1", "fake-namespace", yesterday),
-				*createAppWithCreationTime("fake-app-2", "fake-namespace", current),
-				*createAppWithCreationTime("fake-app-3", "fake-namespace", tomorrow),
-			},
-			TotalItems: 3,
-		},
-	},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
-			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, toObjects(tt.args.apps)...)
-			h := &handler{
-				Client: fakeClient,
-			}
-			req := tt.args.req
-			recorder := httptest.NewRecorder()
-			resp := restful.NewResponse(recorder)
-			resp.SetRequestAccepts(restful.MIME_JSON)
-			h.applicationList(req, resp)
-			assert.Equal(t, 200, recorder.Code)
-			wantResponseBytes, err := json.Marshal(tt.wantResponse)
-			assert.Nil(t, err)
-			assert.JSONEq(t, string(wantResponseBytes), recorder.Body.String())
-		})
-	}
-}
-
 func Test_handler_handleSyncApplication(t *testing.T) {
 	createApp := func(name string, op *v1alpha1.Operation) *v1alpha1.Application {
 		return &v1alpha1.Application{
@@ -432,7 +149,24 @@ func Test_handler_handleSyncApplication(t *testing.T) {
 		name: "Should update operation field if app has no operation field",
 		fields: fields{
 			apps: []v1alpha1.Application{
-				*createApp("fake-app", nil),
+				{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "fake-app",
+						Namespace: "fake-namespace",
+					},
+					Spec: v1alpha1.ApplicationSpec{
+						ArgoApp: &v1alpha1.ArgoApplication{
+							Operation: nil,
+							Spec: v1alpha1.ArgoApplicationSpec{
+								SyncPolicy: &v1alpha1.SyncPolicy{
+									Automated: &v1alpha1.SyncPolicyAutomated{
+										Prune: true,
+									},
+								},
+							},
+						},
+					},
+				},
 			},
 		},
 		args: args{
@@ -467,9 +201,9 @@ func Test_handler_handleSyncApplication(t *testing.T) {
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
-			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, toObjects(tt.fields.apps)...)
+			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, gitops.ToObjects(tt.fields.apps)...)
 			h := &handler{
-				Client: fakeClient,
+				Handler: &gitops.Handler{Client: fakeClient},
 			}
 
 			recorder := httptest.NewRecorder()
@@ -482,3 +216,90 @@ func Test_handler_handleSyncApplication(t *testing.T) {
 		})
 	}
 }
+
+func Test_handler_updateOperation(t *testing.T) {
+	app := &v1alpha1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "fake-app",
+			Namespace: "fake-namespace",
+		},
+		Spec: v1alpha1.ApplicationSpec{
+			ArgoApp: &v1alpha1.ArgoApplication{
+				Operation: &v1alpha1.Operation{
+					Retry: v1alpha1.RetryStrategy{
+						Limit: 3,
+					},
+				},
+			},
+		},
+	}
+
+	invalidApp := app.DeepCopy()
+	invalidApp.Spec.ArgoApp = nil
+	type fields struct {
+		app *v1alpha1.Application
+	}
+	type args struct {
+		namespace string
+		name      string
+		operation *v1alpha1.Operation
+	}
+
+	tests := []struct {
+		name string
+		fields
+		args
+		wantErrMessage string
+	}{
+		{
+			name: "not found a argo app",
+			fields: fields{
+				app: app.DeepCopy(),
+			},
+			args: args{
+				namespace: "fake-namespace",
+				name:      "another-app",
+			},
+			wantErrMessage: `applications.gitops.kubesphere.io "another-app" not found`,
+		},
+		{
+			name: "argoApp is not Configured",
+			fields: fields{
+				app: invalidApp.DeepCopy(),
+			},
+			args: args{
+				namespace: "fake-namespace",
+				name:      "fake-app",
+			},
+			wantErrMessage: "[ServiceError:400] application is not initialized, please confirm you have already configured it",
+		},
+		{
+			name: "argoApp's Operation is not nil",
+			fields: fields{
+				app: app.DeepCopy(),
+			},
+			args: args{
+				namespace: "fake-namespace",
+				name:      "fake-app",
+				operation: &v1alpha1.Operation{
+					Retry: v1alpha1.RetryStrategy{
+						Limit: 10,
+					},
+				},
+			},
+			wantErrMessage: "[ServiceError:400] another operation is already in progress",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
+			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, tt.fields.app.DeepCopy())
+			h := &handler{
+				Handler: &gitops.Handler{Client: fakeClient},
+			}
+			_, err := h.updateOperation(tt.args.namespace, tt.args.name, tt.operation)
+			assert.Equal(t, tt.wantErrMessage, err.Error())
+		})
+	}
+}
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/route_test.go b/pkg/kapis/gitops/v1alpha1/argocd/route_test.go
index 0f5c89c..e3620e9 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/route_test.go
+++ b/pkg/kapis/gitops/v1alpha1/argocd/route_test.go
@@ -67,7 +67,7 @@ func TestRegisterRoutes(t *testing.T) {
 	}
 }
 
-func TestAPIs(t *testing.T) {
+func TestArgoAPIs(t *testing.T) {
 	schema, err := v1alpha1.SchemeBuilder.Register().Build()
 	assert.Nil(t, err)
 	err = v1.SchemeBuilder.AddToScheme(schema)
@@ -116,6 +116,129 @@ func TestAPIs(t *testing.T) {
 		},
 	}
 
+	type request struct {
+		method string
+		uri    string
+		body   func() io.Reader
+	}
+	tests := []struct {
+		name         string
+		request      request
+		responseCode int
+		k8sclient    client.Client
+		verify       func(t *testing.T, body []byte)
+	}{{
+		name: "get clusters, no expected data",
+		request: request{
+			method: http.MethodGet,
+			uri:    "/clusters",
+		},
+		k8sclient:    fake.NewFakeClientWithScheme(schema, nonArgoClusterSecret.DeepCopy()),
+		responseCode: http.StatusOK,
+		verify: func(t *testing.T, body []byte) {
+			assert.Equal(t, `[
+ {
+  "server": "https://kubernetes.default.svc",
+  "name": "in-cluster"
+ }
+]`, string(body))
+		},
+	}, {
+		name: "get clusters, have invalid data",
+		request: request{
+			method: http.MethodGet,
+			uri:    "/clusters",
+		},
+		k8sclient:    fake.NewFakeClientWithScheme(schema, invalidArgoClusterSecret.DeepCopy()),
+		responseCode: http.StatusOK,
+		verify: func(t *testing.T, body []byte) {
+			assert.Equal(t, `[
+ {
+  "server": "https://kubernetes.default.svc",
+  "name": "in-cluster"
+ }
+]`, string(body))
+		},
+	}, {
+		name: "get clusters, have the expected data",
+		request: request{
+			method: http.MethodGet,
+			uri:    "/clusters",
+		},
+		k8sclient:    fake.NewFakeClientWithScheme(schema, validArgoClusterSecret.DeepCopy()),
+		responseCode: http.StatusOK,
+		verify: func(t *testing.T, body []byte) {
+			assert.Equal(t, `[
+ {
+  "server": "https://kubernetes.default.svc",
+  "name": "in-cluster"
+ },
+ {
+  "server": "server",
+  "name": "name"
+ }
+]`, string(body))
+		},
+	}, {
+		name: "get applications summary",
+		request: request{
+			method: http.MethodGet,
+			uri:    "/namespaces/ns/application-summary",
+		},
+		k8sclient:    fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
+		responseCode: http.StatusOK,
+		verify: func(t *testing.T, body []byte) {
+			assert.JSONEq(t, `{"total": 1, "healthStatus": { "Healthy": 1 }, "syncStatus": { "Synced": 1 }}`, string(body))
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			wsWithGroup := runtime.NewWebService(v1alpha1.GroupVersion)
+			RegisterRoutes(wsWithGroup, &common.Options{GenericClient: tt.k8sclient}, &config.ArgoCDOption{})
+
+			container := restful.NewContainer()
+			container.Add(wsWithGroup)
+
+			api := fmt.Sprintf("http://fake.com/kapis/gitops.kubesphere.io/%s%s", v1alpha1.GroupVersion.Version, tt.request.uri)
+			var body io.Reader
+			if tt.request.body != nil {
+				body = tt.request.body()
+			}
+			req, err := http.NewRequest(tt.request.method, api, body)
+			req.Header.Set("Content-Type", "application/json")
+			assert.Nil(t, err)
+
+			httpWriter := httptest.NewRecorder()
+			container.Dispatch(httpWriter, req)
+			assert.Equal(t, tt.responseCode, httpWriter.Code)
+
+			if tt.verify != nil {
+				tt.verify(t, httpWriter.Body.Bytes())
+			}
+		})
+	}
+}
+
+func TestPublicAPIs(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+	err = v1.SchemeBuilder.AddToScheme(schema)
+	assert.Nil(t, err)
+
+	argoApp := v1alpha1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "ns",
+			Name:      "app",
+			Labels: map[string]string{
+				v1alpha1.HealthStatusLabelKey: "Healthy",
+				v1alpha1.SyncStatusLabelKey:   "Synced",
+			},
+		},
+		Spec: v1alpha1.ApplicationSpec{
+			Kind: v1alpha1.ArgoCD,
+		},
+	}
+
 	type request struct {
 		method string
 		uri    string
@@ -143,12 +266,12 @@ func TestAPIs(t *testing.T) {
 			assert.Equal(t, 0, len(list.Items))
 		},
 	}, {
-		name: "get a normal list of the applications",
+		name: "get a normal list of the argo applications",
 		request: request{
 			method: http.MethodGet,
 			uri:    "/namespaces/ns/applications",
 		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
+		k8sclient:    fake.NewFakeClientWithScheme(schema, argoApp.DeepCopy()),
 		responseCode: http.StatusOK,
 		verify: func(t *testing.T, body []byte) {
 			list := &api.ListResult{}
@@ -159,12 +282,12 @@ func TestAPIs(t *testing.T) {
 			assert.Nil(t, err)
 		},
 	}, {
-		name: "get a normal application",
+		name: "get a normal argo application",
 		request: request{
 			method: http.MethodGet,
 			uri:    "/namespaces/ns/applications/app",
 		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
+		k8sclient:    fake.NewFakeClientWithScheme(schema, argoApp.DeepCopy()),
 		responseCode: http.StatusOK,
 		verify: func(t *testing.T, body []byte) {
 			list := &unstructured.Unstructured{}
@@ -176,12 +299,12 @@ func TestAPIs(t *testing.T) {
 			assert.Nil(t, err)
 		},
 	}, {
-		name: "delete an application",
+		name: "delete an argo application",
 		request: request{
 			method: http.MethodDelete,
 			uri:    "/namespaces/ns/applications/app",
 		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
+		k8sclient:    fake.NewFakeClientWithScheme(schema, argoApp.DeepCopy()),
 		responseCode: http.StatusOK,
 		verify: func(t *testing.T, body []byte) {
 			list := &unstructured.Unstructured{}
@@ -193,12 +316,12 @@ func TestAPIs(t *testing.T) {
 			assert.Nil(t, err)
 		},
 	}, {
-		name: "delete an application by cascade",
+		name: "delete an argo application by cascade",
 		request: request{
 			method: http.MethodDelete,
 			uri:    "/namespaces/ns/applications/app?cascade=true",
 		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
+		k8sclient:    fake.NewFakeClientWithScheme(schema, argoApp.DeepCopy()),
 		responseCode: http.StatusOK,
 		verify: func(t *testing.T, body []byte) {
 			list := &unstructured.Unstructured{}
@@ -211,19 +334,21 @@ func TestAPIs(t *testing.T) {
 			assert.Equal(t, []interface{}{"resources-finalizer.argocd.argoproj.io"}, finalizers)
 			assert.Nil(t, err)
 		},
-	}, {
-		name: "create an application",
-		request: request{
-			method: http.MethodPost,
-			uri:    "/namespaces/ns/applications",
-			body: func() io.Reader {
-				return bytes.NewBuffer([]byte(`{
+	},
+		{
+			name: "create an argo application",
+			request: request{
+				method: http.MethodPost,
+				uri:    "/namespaces/ns/applications",
+				body: func() io.Reader {
+					return bytes.NewBuffer([]byte(`{
   "apiVersion": "devops.kubesphere.io/v1alpha1",
   "kind": "Application",
   "metadata": {
     "name": "fake"
   },
   "spec": {
+	"kind": "argocd",
     "argoApp": {
       "spec": {
         "project": "default"
@@ -231,37 +356,38 @@ func TestAPIs(t *testing.T) {
     }
   }
 }`))
+				},
 			},
-		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema),
-		responseCode: http.StatusOK,
-		verify: func(t *testing.T, body []byte) {
-			list := &unstructured.Unstructured{}
-			err := yaml.Unmarshal(body, list)
-			assert.Nil(t, err)
+			k8sclient:    fake.NewFakeClientWithScheme(schema),
+			responseCode: http.StatusOK,
+			verify: func(t *testing.T, body []byte) {
+				list := &unstructured.Unstructured{}
+				err := yaml.Unmarshal(body, list)
+				assert.Nil(t, err)
 
-			name, _, err := unstructured.NestedString(list.Object, "metadata", "name")
-			assert.Equal(t, "fake", name)
-			assert.Nil(t, err)
-		},
-	}, {
-		name: "create an application, invalid payload",
-		request: request{
-			method: http.MethodPost,
-			uri:    "/namespaces/ns/applications",
-			body: func() io.Reader {
-				return bytes.NewBuffer([]byte(`fake`))
+				name, _, err := unstructured.NestedString(list.Object, "metadata", "name")
+				assert.Equal(t, "fake", name)
+				assert.Nil(t, err)
 			},
 		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema),
-		responseCode: http.StatusInternalServerError,
-	}, {
-		name: "update an application",
-		request: request{
-			method: http.MethodPut,
-			uri:    "/namespaces/ns/applications/app",
-			body: func() io.Reader {
-				return bytes.NewBuffer([]byte(`{
+		{
+			name: "create an argo application, invalid payload",
+			request: request{
+				method: http.MethodPost,
+				uri:    "/namespaces/ns/applications",
+				body: func() io.Reader {
+					return bytes.NewBuffer([]byte(`fake`))
+				},
+			},
+			k8sclient:    fake.NewFakeClientWithScheme(schema),
+			responseCode: http.StatusInternalServerError,
+		}, {
+			name: "update an argo application",
+			request: request{
+				method: http.MethodPut,
+				uri:    "/namespaces/ns/applications/app",
+				body: func() io.Reader {
+					return bytes.NewBuffer([]byte(`{
   "apiVersion": "devops.kubesphere.io/v1alpha1",
   "kind": "Application",
   "metadata": {
@@ -269,6 +395,7 @@ func TestAPIs(t *testing.T) {
     "namespace": "ns"
   },
   "spec": {
+	"kind": "argocd",
     "argoApp": {
       "spec": {
         "project": "good"
@@ -276,87 +403,27 @@ func TestAPIs(t *testing.T) {
     }
   }
 }`))
+				},
 			},
-		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
-		responseCode: http.StatusOK,
-		verify: func(t *testing.T, body []byte) {
-			list := &unstructured.Unstructured{}
-			err := yaml.Unmarshal(body, list)
-			assert.Nil(t, err)
+			k8sclient:    fake.NewFakeClientWithScheme(schema, argoApp.DeepCopy()),
+			responseCode: http.StatusOK,
+			verify: func(t *testing.T, body []byte) {
+				list := &unstructured.Unstructured{}
+				err := yaml.Unmarshal(body, list)
+				assert.Nil(t, err)
 
-			project, _, err := unstructured.NestedString(list.Object, "spec", "argoApp", "spec", "project")
-			assert.Equal(t, "good", project)
-			assert.Nil(t, err)
-		},
-	}, {
-		name: "get clusters, no expected data",
-		request: request{
-			method: http.MethodGet,
-			uri:    "/clusters",
-		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, nonArgoClusterSecret.DeepCopy()),
-		responseCode: http.StatusOK,
-		verify: func(t *testing.T, body []byte) {
-			assert.Equal(t, `[
- {
-  "server": "https://kubernetes.default.svc",
-  "name": "in-cluster"
- }
-]`, string(body))
-		},
-	}, {
-		name: "get clusters, have invalid data",
-		request: request{
-			method: http.MethodGet,
-			uri:    "/clusters",
-		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, invalidArgoClusterSecret.DeepCopy()),
-		responseCode: http.StatusOK,
-		verify: func(t *testing.T, body []byte) {
-			assert.Equal(t, `[
- {
-  "server": "https://kubernetes.default.svc",
-  "name": "in-cluster"
- }
-]`, string(body))
-		},
-	}, {
-		name: "get clusters, have the expected data",
-		request: request{
-			method: http.MethodGet,
-			uri:    "/clusters",
-		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, validArgoClusterSecret.DeepCopy()),
-		responseCode: http.StatusOK,
-		verify: func(t *testing.T, body []byte) {
-			assert.Equal(t, `[
- {
-  "server": "https://kubernetes.default.svc",
-  "name": "in-cluster"
- },
- {
-  "server": "server",
-  "name": "name"
- }
-]`, string(body))
-		},
-	}, {
-		name: "get applications summary",
-		request: request{
-			method: http.MethodGet,
-			uri:    "/namespaces/ns/application-summary",
-		},
-		k8sclient:    fake.NewFakeClientWithScheme(schema, app.DeepCopy()),
-		responseCode: http.StatusOK,
-		verify: func(t *testing.T, body []byte) {
-			assert.JSONEq(t, `{"total": 1, "healthStatus": { "Healthy": 1 }, "syncStatus": { "Synced": 1 }}`, string(body))
-		},
-	}}
+				project, _, err := unstructured.NestedString(list.Object, "spec", "argoApp", "spec", "project")
+				assert.Equal(t, "good", project)
+				assert.Nil(t, err)
+			},
+		}}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			wsWithGroup := runtime.NewWebService(v1alpha1.GroupVersion)
-			RegisterRoutes(wsWithGroup, &common.Options{GenericClient: tt.k8sclient}, &config.ArgoCDOption{})
+			RegisterRoutes(wsWithGroup, &common.Options{GenericClient: tt.k8sclient}, &config.ArgoCDOption{
+				Enabled:   true,
+				Namespace: "argocd",
+			})
 
 			container := restful.NewContainer()
 			container.Add(wsWithGroup)
diff --git a/pkg/kapis/gitops/v1alpha1/fluxcd/handler_test.go b/pkg/kapis/gitops/v1alpha1/fluxcd/handler_test.go
new file mode 100644
index 0000000..9330691
--- /dev/null
+++ b/pkg/kapis/gitops/v1alpha1/fluxcd/handler_test.go
@@ -0,0 +1,116 @@
+// Copyright 2022 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 fluxcd
+
+import (
+	"encoding/json"
+	"github.com/emicklei/go-restful"
+	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/client-go/kubernetes/scheme"
+	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
+	"kubesphere.io/devops/pkg/kapis/gitops/v1alpha1/gitops"
+	"net/http"
+	"net/http/httptest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"testing"
+)
+
+func Test_handler_applicationGet(t *testing.T) {
+	createRequest := func(uri string) *restful.Request {
+		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
+		request := restful.NewRequest(fakeRequest)
+		return request
+	}
+
+	type fields struct {
+		Client client.Client
+	}
+	type args struct {
+		req     *restful.Request
+		secrets v1.SecretList
+	}
+
+	tests := []struct {
+		name         string
+		fields       fields
+		args         args
+		wantResponse []v1alpha1.ApplicationDestination
+	}{
+		{
+			name: "get a member clusters",
+			args: args{
+				req: createRequest("/clusters"),
+				secrets: v1.SecretList{
+					Items: []v1.Secret{
+						{
+							ObjectMeta: metav1.ObjectMeta{
+								Name:      "member-cluster",
+								Namespace: "ns",
+								Labels: map[string]string{
+									"app.kubernetes.io/managed-by": "fluxcd-controller",
+								},
+							},
+						},
+					},
+				},
+			},
+			wantResponse: []v1alpha1.ApplicationDestination{
+				{
+					Name: "in-cluster",
+				},
+				{
+					Name: "member-cluster",
+				},
+			},
+		},
+		{
+			name: "get host cluster",
+			args: args{
+				req: createRequest("/clusters"),
+				secrets: v1.SecretList{
+					Items: nil,
+				},
+			},
+			wantResponse: []v1alpha1.ApplicationDestination{
+				{
+					Name: "in-cluster",
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
+			utilruntime.Must(v1.AddToScheme(scheme.Scheme))
+			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, tt.args.secrets.DeepCopy())
+			h := handler{Handler: &gitops.Handler{Client: fakeClient}}
+			req := tt.args.req
+			recorder := httptest.NewRecorder()
+			resp := restful.NewResponse(recorder)
+			resp.SetRequestAccepts(restful.MIME_JSON)
+			h.getClusters(req, resp)
+			assert.Equal(t, 200, recorder.Code)
+			wantResponseBytes, err := json.Marshal(tt.wantResponse)
+			assert.Nil(t, err)
+			assert.JSONEq(t, string(wantResponseBytes), recorder.Body.String())
+		})
+	}
+}
diff --git a/pkg/kapis/gitops/v1alpha1/fluxcd/route_test.go b/pkg/kapis/gitops/v1alpha1/fluxcd/route_test.go
new file mode 100644
index 0000000..be01a2c
--- /dev/null
+++ b/pkg/kapis/gitops/v1alpha1/fluxcd/route_test.go
@@ -0,0 +1,364 @@
+// Copyright 2022 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 fluxcd
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/emicklei/go-restful"
+	"github.com/stretchr/testify/assert"
+	"io"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"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"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"sigs.k8s.io/yaml"
+	"testing"
+)
+
+func TestRegisterRoutes(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+
+	type args struct {
+		service *restful.WebService
+		options *common.Options
+	}
+	tests := []struct {
+		name   string
+		args   args
+		verify func(t *testing.T, service *restful.WebService)
+	}{{
+		name: "normal case",
+		args: args{
+			service: runtime.NewWebService(v1alpha1.GroupVersion),
+			options: &common.Options{GenericClient: fake.NewFakeClientWithScheme(schema)},
+		},
+		verify: func(t *testing.T, service *restful.WebService) {
+			assert.Greater(t, len(service.Routes()), 0)
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			RegisterRoutes(tt.args.service, tt.args.options, &config.FluxCDOption{})
+			tt.verify(t, tt.args.service)
+		})
+	}
+}
+
+func TestPublicAPIs(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+	err = v1.SchemeBuilder.AddToScheme(schema)
+	assert.Nil(t, err)
+
+	fluxApp := v1alpha1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "ns",
+			Name:      "app",
+		},
+		Spec: v1alpha1.ApplicationSpec{
+			Kind: v1alpha1.FluxCD,
+		},
+	}
+
+	type request struct {
+		method string
+		uri    string
+		body   func() io.Reader
+	}
+	tests := []struct {
+		name         string
+		request      request
+		responseCode int
+		k8sclient    client.Client
+		verify       func(t *testing.T, body []byte)
+	}{
+		{
+			name: "get a normal list of the flux applications",
+			request: request{
+				method: http.MethodGet,
+				uri:    "/namespaces/ns/applications",
+			},
+			k8sclient:    fake.NewFakeClientWithScheme(schema, fluxApp.DeepCopy()),
+			responseCode: http.StatusOK,
+			verify: func(t *testing.T, body []byte) {
+				list := &api.ListResult{}
+				err := yaml.Unmarshal(body, list)
+				assert.Nil(t, err)
+
+				assert.Equal(t, 1, len(list.Items))
+				assert.Nil(t, err)
+			},
+		},
+		{
+			name: "get a normal flux application",
+			request: request{
+				method: http.MethodGet,
+				uri:    "/namespaces/ns/applications/app",
+			},
+			k8sclient:    fake.NewFakeClientWithScheme(schema, fluxApp.DeepCopy()),
+			responseCode: http.StatusOK,
+			verify: func(t *testing.T, body []byte) {
+				app := &unstructured.Unstructured{}
+				err := yaml.Unmarshal(body, app)
+				assert.Nil(t, err)
+
+				name, _, err := unstructured.NestedString(app.Object, "metadata", "name")
+				assert.Equal(t, "app", name)
+				assert.Nil(t, err)
+			},
+		},
+		{
+			name: "delete a flux application",
+			request: request{
+				method: http.MethodDelete,
+				uri:    "/namespaces/ns/applications/app",
+			},
+			k8sclient:    fake.NewFakeClientWithScheme(schema, fluxApp.DeepCopy()),
+			responseCode: http.StatusOK,
+			verify: func(t *testing.T, body []byte) {
+				app := &unstructured.Unstructured{}
+				err := yaml.Unmarshal(body, app)
+				assert.Nil(t, err)
+
+				name, _, err := unstructured.NestedString(app.Object, "metadata", "name")
+				assert.Equal(t, "app", name)
+				assert.Nil(t, err)
+			},
+		},
+		{
+			name: "create a flux application",
+			request: request{
+				method: http.MethodPost,
+				uri:    "/namespaces/ns/applications",
+				body: func() io.Reader {
+					return bytes.NewBuffer([]byte(`{
+  "apiVersion": "gitops.kubesphere.io/v1alpha1",
+  "kind": "Application",
+  "metadata": {
+    "name": "app",
+    "labels": {
+        "gitops.kubesphere.io/save-helm-template": "true"
+    }
+  },
+  "spec": {
+    "kind": "fluxcd",
+    "fluxApp": {
+      "spec": {
+        "source": {
+          "sourceRef": {
+            "kind": "GitRepository",
+            "name": "fluxcd-github-repo",
+            "namespace": "fake-ns"
+          }
+        },
+        "config": {
+          "helmRelease": {
+            "chart": {
+              "chart": "./helm-chart",
+              "version": "0.1.0",
+              "interval": "5m0s",
+              "reconcileStrategy": "Revision",
+              "valuesFiles": [
+                "./helm-chart/values.yaml",
+                "./helm-chart/aliyun-values.yaml"
+              ]
+            },
+            "deploy": [
+              {
+                "destination": {
+                  "targetNamespace": "helm-app"
+                },
+                "interval": "1m0s",
+                "install": {
+                  "createNamespace": true
+                },
+                "upgrade": {
+                  "remediation": {
+                    "remediateLastFailure": true
+                  },
+                  "force": true
+                }
+              },
+              {
+                "destination": {
+                  "targetNamespace": "another-helm-app"
+                },
+                "interval": "1m0s",
+                "install": {
+                  "createNamespace": true
+                },
+                "upgrade": {
+                  "remediation": {
+                    "remediateLastFailure": true
+                  },
+                  "force": true
+                }
+              }
+            ]
+          }
+        }
+      }
+    }
+  }
+}`))
+				},
+			},
+			k8sclient:    fake.NewFakeClientWithScheme(schema),
+			responseCode: http.StatusOK,
+			verify: func(t *testing.T, body []byte) {
+				app := &unstructured.Unstructured{}
+				err := yaml.Unmarshal(body, app)
+				assert.Nil(t, err)
+
+				name, _, err := unstructured.NestedString(app.Object, "metadata", "name")
+				assert.Equal(t, "app", name)
+				assert.Nil(t, err)
+			},
+		},
+		{
+			name: "update a flux application",
+			request: request{
+				method: http.MethodPut,
+				uri:    "/namespaces/ns/applications/app",
+				body: func() io.Reader {
+					return bytes.NewBuffer([]byte(`{
+  "apiVersion": "gitops.kubesphere.io/v1alpha1",
+  "kind": "Application",
+  "metadata": {
+    "name": "app",
+	"namespace": "ns",
+    "labels": {
+        "gitops.kubesphere.io/save-helm-template": "true"
+    }
+  },
+  "spec": {
+    "kind": "fluxcd",
+    "fluxApp": {
+      "spec": {
+        "source": {
+          "sourceRef": {
+            "kind": "GitRepository",
+            "name": "fluxcd-github-repo",
+            "namespace": "fake-ns"
+          }
+        },
+        "config": {
+          "helmRelease": {
+            "chart": {
+              "chart": "./helm-chart",
+              "version": "0.1.0",
+              "interval": "5m0s",
+              "reconcileStrategy": "Revision",
+              "valuesFiles": [
+                "./helm-chart/values.yaml",
+                "./helm-chart/aliyun-values.yaml"
+              ]
+            },
+            "deploy": [
+              {
+                "destination": {
+                  "targetNamespace": "helm-app"
+                },
+                "interval": "1m0s",
+                "install": {
+                  "createNamespace": true
+                },
+                "upgrade": {
+                  "remediation": {
+                    "remediateLastFailure": true
+                  },
+                  "force": true
+                },
+                "values": {
+                    "replicaCount": 3
+                }
+              },
+              {
+                "destination": {
+                  "targetNamespace": "another-helm-app"
+                },
+                "interval": "1m0s",
+                "install": {
+                  "createNamespace": true
+                },
+                "upgrade": {
+                  "remediation": {
+                    "remediateLastFailure": true
+                  },
+                  "force": true
+                }
+              }
+            ]
+          }
+        }
+      }
+    }
+  }
+}`))
+				},
+			},
+			k8sclient:    fake.NewFakeClientWithScheme(schema, fluxApp.DeepCopy()),
+			responseCode: http.StatusOK,
+			verify: func(t *testing.T, body []byte) {
+				app := &unstructured.Unstructured{}
+				err := yaml.Unmarshal(body, app)
+				assert.Nil(t, err)
+
+				deploy, _, err := unstructured.NestedSlice(app.Object, "spec", "fluxApp", "spec", "config", "helmRelease", "deploy")
+				replicas, _, err := unstructured.NestedInt64(deploy[0].(map[string]interface{}), "values", "replicaCount")
+				assert.Equal(t, int64(3), replicas)
+				assert.Nil(t, err)
+			},
+		}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			wsWithGroup := runtime.NewWebService(v1alpha1.GroupVersion)
+			RegisterRoutes(wsWithGroup, &common.Options{GenericClient: tt.k8sclient}, &config.FluxCDOption{
+				Enabled: true,
+			})
+
+			container := restful.NewContainer()
+			container.Add(wsWithGroup)
+
+			api := fmt.Sprintf("http://fake.com/kapis/gitops.kubesphere.io/%s%s", v1alpha1.GroupVersion.Version, tt.request.uri)
+			var body io.Reader
+			if tt.request.body != nil {
+				body = tt.request.body()
+			}
+			req, err := http.NewRequest(tt.request.method, api, body)
+			req.Header.Set("Content-Type", "application/json")
+			assert.Nil(t, err)
+
+			httpWriter := httptest.NewRecorder()
+			container.Dispatch(httpWriter, req)
+			assert.Equal(t, tt.responseCode, httpWriter.Code)
+
+			if tt.verify != nil {
+				tt.verify(t, httpWriter.Body.Bytes())
+			}
+		})
+	}
+}
diff --git a/pkg/kapis/gitops/v1alpha1/gitops/handler_test.go b/pkg/kapis/gitops/v1alpha1/gitops/handler_test.go
new file mode 100644
index 0000000..6e62e38
--- /dev/null
+++ b/pkg/kapis/gitops/v1alpha1/gitops/handler_test.go
@@ -0,0 +1,559 @@
+// Copyright 2022 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 gitops
+
+import (
+	"bytes"
+	"encoding/json"
+	"github.com/emicklei/go-restful"
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	"k8s.io/client-go/kubernetes/scheme"
+	"kubesphere.io/devops/pkg/api"
+	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
+	"kubesphere.io/devops/pkg/kapis/common"
+	"net/http"
+	"net/http/httptest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"testing"
+	"time"
+)
+
+func Test_getPathParameter(t *testing.T) {
+	type args struct {
+		req   func() *restful.Request
+		param *restful.Parameter
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{{
+		name: "normal case",
+		args: args{
+			req: func() *restful.Request {
+				request := restful.NewRequest(&http.Request{})
+				request.PathParameters()["name"] = "good"
+				return request
+			},
+			param: restful.PathParameter("name", "desc"),
+		},
+		want: "good",
+	}, {
+		name: "wrong param name",
+		args: args{
+			req: func() *restful.Request {
+				request := restful.NewRequest(&http.Request{})
+				request.PathParameters()["name"] = "good"
+				return request
+			},
+			param: restful.PathParameter("fake", "desc"),
+		},
+		want: "",
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.want, common.GetPathParameter(tt.args.req(), tt.args.param), "getPathParameter(%v, %v)", tt.args.req, tt.args.param)
+		})
+	}
+}
+
+func Test_handler_applicationList(t *testing.T) {
+	createRequest := func(uri, namespace string) *restful.Request {
+		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
+		request := restful.NewRequest(fakeRequest)
+		request.PathParameters()[common.NamespacePathParameter.Data().Name] = namespace
+		return request
+	}
+	createApp := func(name string, namespace string, labels map[string]string) *v1alpha1.Application {
+		return &v1alpha1.Application{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:            name,
+				Namespace:       namespace,
+				Labels:          labels,
+				ResourceVersion: "999",
+			},
+		}
+	}
+	createAppWithCreationTime := func(name string, namespace string, creationTimestamp time.Time) *v1alpha1.Application {
+		return &v1alpha1.Application{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:              name,
+				Namespace:         namespace,
+				CreationTimestamp: metav1.NewTime(creationTimestamp),
+				ResourceVersion:   "999",
+			},
+		}
+	}
+	current := time.Now()
+	yesterday := current.Add(-24 * time.Hour)
+	tomorrow := current.Add(24 * time.Hour)
+	type args struct {
+		req  *restful.Request
+		apps []v1alpha1.Application
+	}
+	tests := []struct {
+		name         string
+		args         args
+		wantResponse api.ListResult
+	}{{
+		name: "Should return empty list when namespaces mismatch",
+		args: args{
+			req: createRequest("/applications", "default"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", nil),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items:      []interface{}{},
+			TotalItems: 0,
+		},
+	}, {
+		name: "Should return correct list when namespaces match",
+		args: args{
+			req: createRequest("/applications", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", nil),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items: []interface{}{
+				createApp("fake-app-1", "fake-namespace", nil),
+			},
+			TotalItems: 1,
+		},
+	}, {
+		name: "Should return empty list when healthStatus is set but no status matches",
+		args: args{
+			req: createRequest("/applications?healthStatus=Unknown", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", map[string]string{
+					v1alpha1.HealthStatusLabelKey: "Healthy",
+				}),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items:      []interface{}{},
+			TotalItems: 0,
+		},
+	}, {
+		name: "Should return partial list when healthStatus is set but no status matches",
+		args: args{
+			req: createRequest("/applications?healthStatus=Unknown", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", map[string]string{
+					v1alpha1.HealthStatusLabelKey: "Healthy",
+				}),
+				*createApp("fake-app-2", "fake-namespace", map[string]string{
+					v1alpha1.HealthStatusLabelKey: "Unknown",
+				}),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items: []interface{}{
+				*createApp("fake-app-2", "fake-namespace", map[string]string{
+					v1alpha1.HealthStatusLabelKey: "Unknown",
+				}),
+			},
+			TotalItems: 1,
+		},
+	}, {
+		name: "Should return empty list when syncStatus is set but no status matches",
+		args: args{
+			req: createRequest("/applications?syncStatus=Unknown", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", map[string]string{
+					v1alpha1.SyncStatusLabelKey: "Synced",
+				}),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items:      []interface{}{},
+			TotalItems: 0,
+		},
+	}, {
+		name: "Should return partial list when syncStatus is set but no status matches",
+		args: args{
+			req: createRequest("/applications?syncStatus=Unknown", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", map[string]string{
+					v1alpha1.SyncStatusLabelKey: "Synced",
+				}),
+				*createApp("fake-app-2", "fake-namespace", map[string]string{
+					v1alpha1.SyncStatusLabelKey: "Unknown",
+				}),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items: []interface{}{
+				*createApp("fake-app-2", "fake-namespace", map[string]string{
+					v1alpha1.SyncStatusLabelKey: "Unknown",
+				}),
+			},
+			TotalItems: 1,
+		},
+	}, {
+		name: "Should return empty list when healthStatus and syncStatus are set but no status matches",
+		args: args{
+			req: createRequest("/applications?healthStatus=Unknown&syncStatus=Unknown", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", map[string]string{
+					v1alpha1.HealthStatusLabelKey: "Healthy",
+					v1alpha1.SyncStatusLabelKey:   "Synced",
+				}),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items:      []interface{}{},
+			TotalItems: 0,
+		},
+	}, {
+		name: "Should return applications when both healthStatus and syncStatus match at the same time",
+		args: args{
+			req: createRequest("/applications?healthStatus=Unknown&syncStatus=Unknown", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", map[string]string{
+					v1alpha1.HealthStatusLabelKey: "Unknown",
+					v1alpha1.SyncStatusLabelKey:   "Synced",
+				}),
+				*createApp("fake-app-2", "fake-namespace", map[string]string{
+					v1alpha1.HealthStatusLabelKey: "Unknown",
+					v1alpha1.SyncStatusLabelKey:   "Unknown",
+				}),
+			},
+		},
+		wantResponse: api.ListResult{Items: []interface{}{
+			*createApp("fake-app-2", "fake-namespace", map[string]string{
+				v1alpha1.HealthStatusLabelKey: "Unknown",
+				v1alpha1.SyncStatusLabelKey:   "Unknown",
+			}),
+		}, TotalItems: 1},
+	}, {
+		name: "Should filter with name",
+		args: args{
+			req: createRequest("/applications?name=app", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-1", "fake-namespace", nil),
+				*createApp("fake-bpp-1", "fake-namespace", nil),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items: []interface{}{
+				*createApp("fake-app-1", "fake-namespace", nil),
+			},
+			TotalItems: 1,
+		},
+	}, {
+		name: "Should sort by name in ascending order",
+		args: args{
+			req: createRequest("/applications?orderBy=name&ascending=true", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createApp("fake-app-2", "fake-namespace", nil),
+				*createApp("fake-app-1", "fake-namespace", nil),
+				*createApp("fake-app-3", "fake-namespace", nil),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items: []interface{}{
+				*createApp("fake-app-1", "fake-namespace", nil),
+				*createApp("fake-app-2", "fake-namespace", nil),
+				*createApp("fake-app-3", "fake-namespace", nil),
+			},
+			TotalItems: 3,
+		},
+	}, {
+		name: "Should sort by creationTimestamp in ascending order",
+		args: args{
+			req: createRequest("/applications?orderBy=creationTimestamp&ascending=true", "fake-namespace"),
+			apps: []v1alpha1.Application{
+				*createAppWithCreationTime("fake-app-2", "fake-namespace", current),
+				*createAppWithCreationTime("fake-app-1", "fake-namespace", yesterday),
+				*createAppWithCreationTime("fake-app-3", "fake-namespace", tomorrow),
+			},
+		},
+		wantResponse: api.ListResult{
+			Items: []interface{}{
+				*createAppWithCreationTime("fake-app-1", "fake-namespace", yesterday),
+				*createAppWithCreationTime("fake-app-2", "fake-namespace", current),
+				*createAppWithCreationTime("fake-app-3", "fake-namespace", tomorrow),
+			},
+			TotalItems: 3,
+		},
+	},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
+			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, ToObjects(tt.args.apps)...)
+			h := &Handler{
+				Client: fakeClient,
+			}
+			req := tt.args.req
+			recorder := httptest.NewRecorder()
+			resp := restful.NewResponse(recorder)
+			resp.SetRequestAccepts(restful.MIME_JSON)
+			h.ApplicationList(req, resp)
+			assert.Equal(t, 200, recorder.Code)
+			wantResponseBytes, err := json.Marshal(tt.wantResponse)
+			assert.Nil(t, err)
+			assert.JSONEq(t, string(wantResponseBytes), recorder.Body.String())
+		})
+	}
+}
+
+func Test_handler_applicationGet(t *testing.T) {
+	createRequest := func(uri, name, namespace string) *restful.Request {
+		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
+		request := restful.NewRequest(fakeRequest)
+		request.PathParameters()[common.NamespacePathParameter.Data().Name] = namespace
+		request.PathParameters()[pathParameterApplication.Data().Name] = name
+		return request
+	}
+
+	createApp := func(name string, namespace string, labels map[string]string) *v1alpha1.Application {
+		return &v1alpha1.Application{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:            name,
+				Namespace:       namespace,
+				Labels:          labels,
+				ResourceVersion: "999",
+			},
+		}
+	}
+
+	type fields struct {
+		Client client.Client
+	}
+	type args struct {
+		req  *restful.Request
+		apps []v1alpha1.Application
+	}
+
+	tests := []struct {
+		name         string
+		fields       fields
+		args         args
+		wantResponse v1alpha1.Application
+	}{
+		{
+			name: "get a normal argo application",
+			args: args{
+				req: createRequest("/applications", "fake-app", "fake-ns"),
+				apps: []v1alpha1.Application{
+					*createApp("fake-app", "fake-ns", nil),
+				},
+			},
+			wantResponse: *createApp("fake-app", "fake-ns", nil),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
+			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, ToObjects(tt.args.apps)...)
+			h := NewHandler(&common.Options{GenericClient: Handler{
+				Client: fakeClient,
+			}})
+			req := tt.args.req
+			recorder := httptest.NewRecorder()
+			resp := restful.NewResponse(recorder)
+			resp.SetRequestAccepts(restful.MIME_JSON)
+			h.GetApplication(req, resp)
+			assert.Equal(t, 200, recorder.Code)
+		})
+	}
+}
+
+func Test_handler_applicationDel(t *testing.T) {
+	createRequest := func(uri, name, namespace string) *restful.Request {
+		fakeRequest := httptest.NewRequest(http.MethodDelete, uri, nil)
+		request := restful.NewRequest(fakeRequest)
+		request.PathParameters()[common.NamespacePathParameter.Data().Name] = namespace
+		request.PathParameters()[pathParameterApplication.Data().Name] = name
+		return request
+	}
+
+	createArgoApp := func(name string, namespace string, labels map[string]string) *v1alpha1.Application {
+		return &v1alpha1.Application{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:            name,
+				Namespace:       namespace,
+				Labels:          labels,
+				ResourceVersion: "999",
+			},
+			Spec: v1alpha1.ApplicationSpec{
+				Kind: v1alpha1.ArgoCD,
+			},
+		}
+	}
+
+	createFluxApp := func(name string, namespace string, labels map[string]string) *v1alpha1.Application {
+		return &v1alpha1.Application{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:            name,
+				Namespace:       namespace,
+				Labels:          labels,
+				ResourceVersion: "999",
+			},
+			Spec: v1alpha1.ApplicationSpec{
+				Kind: v1alpha1.FluxCD,
+			},
+		}
+	}
+
+	type fields struct {
+		Client client.Client
+	}
+	type args struct {
+		req  *restful.Request
+		apps []v1alpha1.Application
+	}
+
+	tests := []struct {
+		name         string
+		fields       fields
+		args         args
+		wantResponse v1alpha1.Application
+	}{
+		{
+			name: "delete a normal argo application",
+			args: args{
+				req: createRequest("/applications", "fake-argo-app", "fake-ns"),
+				apps: []v1alpha1.Application{
+					*createArgoApp("fake-argo-app", "fake-ns", nil),
+				},
+			},
+			wantResponse: *createArgoApp("fake-argo-app", "fake-ns", nil),
+		},
+		{
+			name: "delete a normal argo application with cascade delete",
+			args: args{
+				req: createRequest("/applications", "fake-argo-app", "fake-ns"),
+				apps: []v1alpha1.Application{
+					*createArgoApp("fake-argo-app", "fake-ns", nil),
+				},
+			},
+			wantResponse: *createArgoApp("fake-argo-app", "fake-ns", nil),
+		},
+		{
+			name: "delete a normal flux application",
+			args: args{
+				req: createRequest("/applications", "fake-flux-app", "fake-ns"),
+				apps: []v1alpha1.Application{
+					*createFluxApp("fake-flux-app", "fake-ns", nil),
+				},
+			},
+			wantResponse: *createFluxApp("fake-flux-app", "fake-ns", nil),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
+			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, ToObjects(tt.args.apps)...)
+			h := NewHandler(&common.Options{GenericClient: Handler{
+				Client: fakeClient,
+			}})
+			req := tt.args.req
+			recorder := httptest.NewRecorder()
+			resp := restful.NewResponse(recorder)
+			resp.SetRequestAccepts(restful.MIME_JSON)
+			h.DelApplication(req, resp)
+			assert.Equal(t, 200, recorder.Code)
+		})
+	}
+}
+
+func Test_handler_applicationUpdate(t *testing.T) {
+
+	createRequest := func(uri, name, namespace string) *restful.Request {
+		fakeRequest := httptest.NewRequest(http.MethodPut, uri, bytes.NewBuffer([]byte(`{
+  "apiVersion": "devops.kubesphere.io/v1alpha1",
+  "kind": "Application",
+  "metadata": {
+ 	"namespace": "fake-ns",
+    "name": "fake-app"
+  },
+  "spec": {
+	"kind": "argocd",
+    "argoApp": {
+      "spec": {
+        "project": "good"
+      }
+    }
+  }
+}`)))
+		request := restful.NewRequest(fakeRequest)
+		request.PathParameters()[common.NamespacePathParameter.Data().Name] = namespace
+		request.PathParameters()[pathParameterApplication.Data().Name] = name
+		request.Request.Header.Set("Content-Type", "application/json")
+		return request
+	}
+
+	createApp := func(name string, namespace string, labels map[string]string) *v1alpha1.Application {
+		return &v1alpha1.Application{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      name,
+				Namespace: namespace,
+				Labels:    labels,
+			},
+		}
+	}
+
+	type fields struct {
+		Client client.Client
+	}
+	type args struct {
+		req  *restful.Request
+		apps []v1alpha1.Application
+	}
+
+	tests := []struct {
+		name         string
+		fields       fields
+		args         args
+		wantResponse v1alpha1.Application
+	}{
+		{
+			name: "update a normal argo application",
+			args: args{
+				req: createRequest("/applications", "fake-app", "fake-ns"),
+				apps: []v1alpha1.Application{
+					*createApp("fake-app", "fake-ns", nil),
+				},
+			},
+			wantResponse: *createApp("fake-app", "fake-ns", nil),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			utilruntime.Must(v1alpha1.AddToScheme(scheme.Scheme))
+			fakeClient := fake.NewFakeClientWithScheme(scheme.Scheme, ToObjects(tt.args.apps)...)
+			h := NewHandler(&common.Options{GenericClient: Handler{
+				Client: fakeClient,
+			}})
+			req := tt.args.req
+			recorder := httptest.NewRecorder()
+			resp := restful.NewResponse(recorder)
+			resp.SetRequestAccepts(restful.MIME_JSON)
+			h.UpdateApplication(req, resp)
+			assert.Equal(t, 200, recorder.Code)
+		})
+	}
+}
diff --git a/pkg/kapis/gitops/v1alpha1/argocd/util_test.go b/pkg/kapis/gitops/v1alpha1/gitops/util_test.go
similarity index 94%
rename from pkg/kapis/gitops/v1alpha1/argocd/util_test.go
rename to pkg/kapis/gitops/v1alpha1/gitops/util_test.go
index a110994..ad88b3a 100644
--- a/pkg/kapis/gitops/v1alpha1/argocd/util_test.go
+++ b/pkg/kapis/gitops/v1alpha1/gitops/util_test.go
@@ -13,7 +13,7 @@
 // limitations under the License.
 //
 
-package argocd
+package gitops
 
 import (
 	"github.com/stretchr/testify/assert"
@@ -23,7 +23,7 @@ import (
 	"testing"
 )
 
-func Test_toObjects(t *testing.T) {
+func Test_ToObjects(t *testing.T) {
 	createApplication := func(name, namespaces string) *v1alpha1.Application {
 		return &v1alpha1.Application{
 			ObjectMeta: metav1.ObjectMeta{
@@ -66,7 +66,7 @@ func Test_toObjects(t *testing.T) {
 	}}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			assert.Equalf(t, tt.want, toObjects(tt.args.apps), "toObjects(%v)", tt.args.apps)
+			assert.Equalf(t, tt.want, ToObjects(tt.args.apps), "toObjects(%v)", tt.args.apps)
 		})
 	}
 }
diff --git a/pkg/kapis/gitops/v1alpha1/registery_test.go b/pkg/kapis/gitops/v1alpha1/registery_test.go
new file mode 100644
index 0000000..db91442
--- /dev/null
+++ b/pkg/kapis/gitops/v1alpha1/registery_test.go
@@ -0,0 +1,136 @@
+/*
+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 v1alpha1
+
+import (
+	"github.com/emicklei/go-restful"
+	"github.com/stretchr/testify/assert"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
+	"kubesphere.io/devops/pkg/config"
+	"kubesphere.io/devops/pkg/kapis/common"
+	"kubesphere.io/devops/pkg/kapis/gitops/v1alpha1/gitops"
+	"net/http"
+	"net/http/httptest"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"strings"
+	"testing"
+)
+
+func TestArgoAPIsExist(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+	container := restful.NewContainer()
+	opt := &common.Options{
+		GenericClient: gitops.Handler{
+			Client: fake.NewFakeClientWithScheme(schema),
+		},
+	}
+	argoOption := &config.ArgoCDOption{Enabled: true, Namespace: "argocd"}
+	fluxOption := &config.FluxCDOption{Enabled: false}
+	AddToContainer(container, opt, argoOption, fluxOption)
+	type args struct {
+		method string
+		uri    string
+	}
+
+	tests := []struct {
+		name string
+		args
+		body       string
+		expectCode int
+	}{
+		{
+			name: "not found an application",
+			args: args{
+				method: http.MethodGet,
+				uri:    "/namespaces/fake-ns/applications",
+			},
+			expectCode: http.StatusNotFound,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			httpRequest, _ := http.NewRequest(tt.args.method, "http://fake.com/kapis/gitops.kubesphere.io/v1alpha3"+tt.args.uri, strings.NewReader(tt.body))
+			httpRequest.Header.Set("Content-Type", "application/json")
+
+			httpWriter := httptest.NewRecorder()
+			container.Dispatch(httpWriter, httpRequest)
+			assert.Equal(t, tt.expectCode, httpWriter.Code)
+		})
+	}
+}
+
+func TestFluxAPIsExist(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+	container := restful.NewContainer()
+	opt := &common.Options{
+		GenericClient: gitops.Handler{
+			Client: fake.NewFakeClientWithScheme(schema),
+		},
+	}
+	argoOption := &config.ArgoCDOption{Enabled: false, Namespace: "argocd"}
+	fluxOption := &config.FluxCDOption{Enabled: true}
+	AddToContainer(container, opt, argoOption, fluxOption)
+	type args struct {
+		method string
+		uri    string
+	}
+
+	tests := []struct {
+		name string
+		args
+		body       string
+		expectCode int
+	}{
+		{
+			name: "not found an application",
+			args: args{
+				method: http.MethodGet,
+				uri:    "/namespaces/fake-ns/applications",
+			},
+			expectCode: http.StatusNotFound,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			httpRequest, _ := http.NewRequest(tt.args.method, "http://fake.com/kapis/gitops.kubesphere.io/v1alpha3"+tt.args.uri, strings.NewReader(tt.body))
+			httpRequest.Header.Set("Content-Type", "application/json")
+
+			httpWriter := httptest.NewRecorder()
+			container.Dispatch(httpWriter, httpRequest)
+			assert.Equal(t, tt.expectCode, httpWriter.Code)
+		})
+	}
+}
+
+func Test_NonAPIsExist(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+	container := restful.NewContainer()
+	opt := &common.Options{
+		GenericClient: gitops.Handler{
+			Client: fake.NewFakeClientWithScheme(schema),
+		},
+	}
+	argoOption := &config.ArgoCDOption{Enabled: false, Namespace: "argocd"}
+	fluxOption := &config.FluxCDOption{Enabled: false}
+	wss := AddToContainer(container, opt, argoOption, fluxOption)
+	assert.Nil(t, wss)
+}