Repository: kubesphere/ks-devops
Pull request #467 (2022-02-16 08:40:47)

Add API for the application custom resource

<!-- 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 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 feature

### What this PR does / why we need it:
This is part of #63 

### 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.
-->
```
None
```

### 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
```



Patch:
diff --git a/pkg/kapis/devops/v1alpha1/argocd/handler.go b/pkg/kapis/devops/v1alpha1/argocd/handler.go
new file mode 100644
index 0000000..2c30c6a
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha1/argocd/handler.go
@@ -0,0 +1,113 @@
+// 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 argocd
+
+import (
+	"context"
+	"github.com/emicklei/go-restful"
+	"k8s.io/apimachinery/pkg/types"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
+	"kubesphere.io/devops/pkg/kapis"
+	kapisv1alpha1 "kubesphere.io/devops/pkg/kapis/devops/v1alpha1/common"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+func (h *handler) applicationList(req *restful.Request, res *restful.Response) {
+	namespace := getPathParameter(req, kapisv1alpha1.DevopsPathParameter)
+
+	applicationList := &v1alpha1.ApplicationList{}
+	err := h.genericClient.List(context.Background(), applicationList, client.InNamespace(namespace))
+	response(req, res, applicationList, err)
+}
+
+func (h *handler) getApplication(req *restful.Request, res *restful.Response) {
+	namespace := getPathParameter(req, kapisv1alpha1.DevopsPathParameter)
+	name := getPathParameter(req, pathParameterApplication)
+
+	application := &v1alpha1.Application{}
+	err := h.genericClient.Get(context.Background(), types.NamespacedName{
+		Namespace: namespace,
+		Name:      name,
+	}, application)
+	response(req, res, application, err)
+}
+
+func (h *handler) delApplication(req *restful.Request, res *restful.Response) {
+	namespace := getPathParameter(req, kapisv1alpha1.DevopsPathParameter)
+	name := getPathParameter(req, pathParameterApplication)
+
+	application := &v1alpha1.Application{}
+	err := h.genericClient.Get(context.Background(), types.NamespacedName{
+		Namespace: namespace,
+		Name:      name,
+	}, application)
+	if err == nil {
+		err = h.genericClient.Delete(context.Background(), application)
+	}
+	response(req, res, application, err)
+}
+
+func (h *handler) createApplication(req *restful.Request, res *restful.Response) {
+	var err error
+	namespace := getPathParameter(req, kapisv1alpha1.DevopsPathParameter)
+
+	application := &v1alpha1.Application{}
+	if err = req.ReadEntity(application); err == nil {
+		application.Namespace = namespace
+		err = h.genericClient.Create(context.Background(), application)
+	}
+
+	response(req, res, application, err)
+}
+
+func (h *handler) updateApplication(req *restful.Request, res *restful.Response) {
+	namespace := getPathParameter(req, kapisv1alpha1.DevopsPathParameter)
+	name := getPathParameter(req, pathParameterApplication)
+
+	application := &v1alpha1.Application{}
+	err := h.genericClient.Get(context.Background(), types.NamespacedName{
+		Namespace: namespace,
+		Name:      name,
+	}, application)
+	if err == nil {
+		if err = req.ReadEntity(application); err == nil {
+			err = h.genericClient.Update(context.Background(), application)
+		}
+	}
+	response(req, res, application, err)
+}
+
+func getPathParameter(req *restful.Request, param *restful.Parameter) string {
+	return req.PathParameter(param.Data().Name)
+}
+
+func response(req *restful.Request, res *restful.Response, object interface{}, err error) {
+	if err != nil {
+		kapis.HandleError(req, res, err)
+	} else {
+		_ = res.WriteEntity(object)
+	}
+}
+
+type handler struct {
+	genericClient client.Client
+}
+
+func newHandler(options *kapisv1alpha1.Options) *handler {
+	return &handler{
+		genericClient: options.GenericClient,
+	}
+}
diff --git a/pkg/kapis/devops/v1alpha1/argocd/route.go b/pkg/kapis/devops/v1alpha1/argocd/route.go
new file mode 100644
index 0000000..724f256
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha1/argocd/route.go
@@ -0,0 +1,68 @@
+// 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 argocd
+
+import (
+	"github.com/emicklei/go-restful"
+	"kubesphere.io/devops/pkg/api"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
+	kapisv1alpha1 "kubesphere.io/devops/pkg/kapis/devops/v1alpha1/common"
+	"net/http"
+)
+
+var (
+	// pathParameterApplication is a path parameter definition for application.
+	pathParameterApplication = restful.PathParameter("application", "The application name")
+)
+
+// RegisterRoutes is for registering Argo CD Application routes into WebService.
+func RegisterRoutes(service *restful.WebService, options *kapisv1alpha1.Options) {
+	handler := newHandler(options)
+	service.Route(service.GET("/devops/{devops}/applications").
+		To(handler.applicationList).
+		Param(kapisv1alpha1.DevopsPathParameter).
+		Doc("Get the application list").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.ApplicationList{}))
+
+	service.Route(service.POST("/devops/{devops}/applications").
+		To(handler.createApplication).
+		Param(kapisv1alpha1.DevopsPathParameter).
+		Reads(v1alpha1.Application{}).
+		Doc("Create an application").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
+
+	service.Route(service.GET("/devops/{devops}/applications/{application}").
+		To(handler.getApplication).
+		Param(kapisv1alpha1.DevopsPathParameter).
+		Param(pathParameterApplication).
+		Doc("Get a particular application").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
+
+	service.Route(service.DELETE("/devops/{devops}/applications/{application}").
+		To(handler.delApplication).
+		Param(kapisv1alpha1.DevopsPathParameter).
+		Param(pathParameterApplication).
+		Doc("Delete a particular application").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
+
+	service.Route(service.PUT("/devops/{devops}/applications/{application}").
+		To(handler.updateApplication).
+		Param(kapisv1alpha1.DevopsPathParameter).
+		Param(pathParameterApplication).
+		Reads(v1alpha1.Application{}).
+		Doc("Update a particular application").
+		Returns(http.StatusOK, api.StatusOK, v1alpha1.Application{}))
+}
diff --git a/pkg/kapis/devops/v1alpha1/common/options.go b/pkg/kapis/devops/v1alpha1/common/options.go
index 51ca192..126fffa 100644
--- a/pkg/kapis/devops/v1alpha1/common/options.go
+++ b/pkg/kapis/devops/v1alpha1/common/options.go
@@ -15,9 +15,17 @@
 
 package common
 
-import "sigs.k8s.io/controller-runtime/pkg/client"
+import (
+	"github.com/emicklei/go-restful"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
 
 // Options contain options needed by creating handlers.
 type Options struct {
 	GenericClient client.Client
 }
+
+var (
+	// DevopsPathParameter is a path parameter definition for devops.
+	DevopsPathParameter = restful.PathParameter("devops", "DevOps project name")
+)
diff --git a/pkg/kapis/devops/v1alpha1/register.go b/pkg/kapis/devops/v1alpha1/register.go
index d6bbf80..7a06453 100644
--- a/pkg/kapis/devops/v1alpha1/register.go
+++ b/pkg/kapis/devops/v1alpha1/register.go
@@ -19,6 +19,7 @@ import (
 	"github.com/emicklei/go-restful"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
 	"kubesphere.io/devops/pkg/apiserver/runtime"
+	"kubesphere.io/devops/pkg/kapis/devops/v1alpha1/argocd"
 	"kubesphere.io/devops/pkg/kapis/devops/v1alpha1/common"
 	"kubesphere.io/devops/pkg/kapis/devops/v1alpha1/template"
 )
@@ -30,6 +31,7 @@ func AddToContainer(container *restful.Container, options *common.Options) []*re
 	services = append(services, runtime.NewWebServiceWithoutGroup(v1alpha1.GroupVersion))
 	for _, service := range services {
 		template.RegisterRoutes(service, options)
+		argocd.RegisterRoutes(service, options)
 		container.Add(service)
 	}
 	return services
diff --git a/pkg/kapis/devops/v1alpha1/template/handler.go b/pkg/kapis/devops/v1alpha1/template/handler.go
index 0b89ca6..6de8805 100644
--- a/pkg/kapis/devops/v1alpha1/template/handler.go
+++ b/pkg/kapis/devops/v1alpha1/template/handler.go
@@ -48,7 +48,7 @@ func (h *handler) handleQuery(request *restful.Request, response *restful.Respon
 }
 
 func (h *handler) queryTemplate(request *restful.Request) (*api.ListResult, error) {
-	devopsName := request.PathParameter(DevopsPathParameter.Data().Name)
+	devopsName := request.PathParameter(kapisv1alpha1.DevopsPathParameter.Data().Name)
 	queryParam := query.ParseQueryParameter(request)
 	templateList := &v1alpha1.TemplateList{}
 	if err := h.genericClient.List(context.Background(), templateList, client.InNamespace(devopsName)); err != nil {
@@ -66,7 +66,7 @@ func (h *handler) handleGet(request *restful.Request, response *restful.Response
 }
 
 func (h *handler) getTemplate(request *restful.Request) (*v1alpha1.Template, error) {
-	devopsName := request.PathParameter(DevopsPathParameter.Data().Name)
+	devopsName := request.PathParameter(kapisv1alpha1.DevopsPathParameter.Data().Name)
 	templateName := request.PathParameter(TemplatePathParameter.Data().Name)
 	template := &v1alpha1.Template{}
 	if err := h.genericClient.Get(context.Background(), client.ObjectKey{Namespace: devopsName, Name: templateName}, template); err != nil {
diff --git a/pkg/kapis/devops/v1alpha1/template/route.go b/pkg/kapis/devops/v1alpha1/template/route.go
index 49771bb..71f7d10 100644
--- a/pkg/kapis/devops/v1alpha1/template/route.go
+++ b/pkg/kapis/devops/v1alpha1/template/route.go
@@ -28,8 +28,6 @@ import (
 )
 
 var (
-	// DevopsPathParameter is a path parameter definition for devops.
-	DevopsPathParameter = restful.PathParameter("devops", "DevOps project name")
 	// TemplatePathParameter is a path parameter definition for template.
 	TemplatePathParameter = restful.PathParameter("template", "Template name")
 )
@@ -39,14 +37,14 @@ func RegisterRoutes(service *restful.WebService, options *kapisv1alpha1.Options)
 	handler := newHandler(options)
 	service.Route(service.GET("/devops/{devops}/templates").
 		To(handler.handleQuery).
-		Param(DevopsPathParameter).
+		Param(kapisv1alpha1.DevopsPathParameter).
 		Doc("Query templates for a DevOps Project.").
 		Returns(http.StatusOK, api.StatusOK, api.ListResult{Items: []interface{}{}}).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsTemplateTag}))
 
 	service.Route(service.GET("/devops/{devops}/templates/{template}").
 		To(handler.handleGet).
-		Param(DevopsPathParameter).
+		Param(kapisv1alpha1.DevopsPathParameter).
 		Param(TemplatePathParameter).
 		Doc("Get template").
 		Returns(http.StatusOK, api.StatusOK, v1alpha1.Template{}).
@@ -54,7 +52,7 @@ func RegisterRoutes(service *restful.WebService, options *kapisv1alpha1.Options)
 
 	service.Route(service.POST("/devops/{devops}/templates/{template}/render").
 		To(handler.handleRender).
-		Param(DevopsPathParameter).
+		Param(kapisv1alpha1.DevopsPathParameter).
 		Param(TemplatePathParameter).
 		Doc(fmt.Sprintf("Render template and return render result into annotations (%s/%s) inside template", devops.GroupName, devops.RenderResultAnnoKey)).
 		Returns(http.StatusOK, api.StatusOK, v1alpha1.Template{}).


Test patch:
diff --git a/pkg/kapis/devops/v1alpha1/argocd/handler_test.go b/pkg/kapis/devops/v1alpha1/argocd/handler_test.go
new file mode 100644
index 0000000..644c067
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha1/argocd/handler_test.go
@@ -0,0 +1,62 @@
+// 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 argocd
+
+import (
+	"github.com/emicklei/go-restful"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"testing"
+)
+
+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, getPathParameter(tt.args.req(), tt.args.param), "getPathParameter(%v, %v)", tt.args.req, tt.args.param)
+		})
+	}
+}
diff --git a/pkg/kapis/devops/v1alpha1/argocd/route_test.go b/pkg/kapis/devops/v1alpha1/argocd/route_test.go
new file mode 100644
index 0000000..dfbb381
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha1/argocd/route_test.go
@@ -0,0 +1,259 @@
+// 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 argocd
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/emicklei/go-restful"
+	"github.com/stretchr/testify/assert"
+	"io"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"kubesphere.io/devops/pkg/apiserver/runtime"
+	kapisv1alpha1 "kubesphere.io/devops/pkg/kapis/devops/v1alpha1/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 := v1alpha3.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+
+	type args struct {
+		service *restful.WebService
+		options *kapisv1alpha1.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: &kapisv1alpha1.Options{GenericClient: fake.NewFakeClientWithScheme(schema)},
+		},
+		verify: func(t *testing.T, service *restful.WebService) {
+			assert.Equal(t, 5, len(service.Routes()))
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			RegisterRoutes(tt.args.service, tt.args.options)
+			tt.verify(t, tt.args.service)
+		})
+	}
+}
+
+func TestAPIs(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+
+	app := v1alpha1.Application{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: "ns",
+			Name:      "app",
+		},
+	}
+
+	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 an empty list of the applications",
+		request: request{
+			method: http.MethodGet,
+			uri:    "/devops/fake/applications",
+		},
+		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)
+
+			items, _, err := unstructured.NestedSlice(list.Object, "items")
+			assert.Equal(t, 0, len(items))
+			assert.NotNil(t, err)
+		},
+	}, {
+		name: "get a normal list of the applications",
+		request: request{
+			method: http.MethodGet,
+			uri:    "/devops/ns/applications",
+		},
+		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)
+
+			items, _, err := unstructured.NestedSlice(list.Object, "items")
+			assert.Equal(t, 1, len(items))
+			assert.Nil(t, err)
+		},
+	}, {
+		name: "get a normal application",
+		request: request{
+			method: http.MethodGet,
+			uri:    "/devops/ns/applications/app",
+		},
+		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)
+
+			name, _, err := unstructured.NestedString(list.Object, "metadata", "name")
+			assert.Equal(t, "app", name)
+			assert.Nil(t, err)
+		},
+	}, {
+		name: "delete an application",
+		request: request{
+			method: http.MethodDelete,
+			uri:    "/devops/ns/applications/app",
+		},
+		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)
+
+			name, _, err := unstructured.NestedString(list.Object, "metadata", "name")
+			assert.Equal(t, "app", name)
+			assert.Nil(t, err)
+		},
+	}, {
+		name: "create an application",
+		request: request{
+			method: http.MethodPost,
+			uri:    "/devops/ns/applications",
+			body: func() io.Reader {
+				return bytes.NewBuffer([]byte(`{
+  "apiVersion": "devops.kubesphere.io/v1alpha1",
+  "kind": "Application",
+  "metadata": {
+    "name": "fake"
+  },
+  "spec": {
+    "argoApp": {
+      "project": "default"
+    }
+  }
+}`))
+			},
+		},
+		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:    "/devops/ns/applications",
+			body: func() io.Reader {
+				return bytes.NewBuffer([]byte(`fake`))
+			},
+		},
+		k8sclient:    fake.NewFakeClientWithScheme(schema),
+		responseCode: http.StatusInternalServerError,
+	}, {
+		name: "update an application",
+		request: request{
+			method: http.MethodPut,
+			uri:    "/devops/ns/applications/app",
+			body: func() io.Reader {
+				return bytes.NewBuffer([]byte(`{
+  "apiVersion": "devops.kubesphere.io/v1alpha1",
+  "kind": "Application",
+  "metadata": {
+    "name": "app",
+    "namespace": "ns"
+  },
+  "spec": {
+    "argoApp": {
+      "project": "good"
+    }
+  }
+}`))
+			},
+		},
+		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)
+
+			project, _, err := unstructured.NestedString(list.Object, "spec", "argoApp", "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, &kapisv1alpha1.Options{GenericClient: tt.k8sclient})
+
+			container := restful.NewContainer()
+			container.Add(wsWithGroup)
+
+			api := fmt.Sprintf("http://fake.com/kapis/devops.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/devops/v1alpha1/template/handler_test.go b/pkg/kapis/devops/v1alpha1/template/handler_test.go
index 9c0a14e..56af2e2 100644
--- a/pkg/kapis/devops/v1alpha1/template/handler_test.go
+++ b/pkg/kapis/devops/v1alpha1/template/handler_test.go
@@ -127,7 +127,7 @@ func Test_handler_handleQuery(t *testing.T) {
 	createRequest := func(uri, devopsName string) *restful.Request {
 		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
 		request := restful.NewRequest(fakeRequest)
-		request.PathParameters()[DevopsPathParameter.Data().Name] = devopsName
+		request.PathParameters()[kapisv1alpha1.DevopsPathParameter.Data().Name] = devopsName
 		return request
 	}
 	type args struct {
@@ -213,7 +213,7 @@ func Test_handler_handleGet(t *testing.T) {
 	createRequest := func(uri, devopsName, templateName string) *restful.Request {
 		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
 		request := restful.NewRequest(fakeRequest)
-		request.PathParameters()[DevopsPathParameter.Data().Name] = devopsName
+		request.PathParameters()[kapisv1alpha1.DevopsPathParameter.Data().Name] = devopsName
 		request.PathParameters()[TemplatePathParameter.Data().Name] = templateName
 		return request
 	}
@@ -285,7 +285,7 @@ func Test_handler_handleRender(t *testing.T) {
 	createRequest := func(uri, devopsName, templateName string) *restful.Request {
 		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
 		request := restful.NewRequest(fakeRequest)
-		request.PathParameters()[DevopsPathParameter.Data().Name] = devopsName
+		request.PathParameters()[kapisv1alpha1.DevopsPathParameter.Data().Name] = devopsName
 		request.PathParameters()[TemplatePathParameter.Data().Name] = templateName
 		return request
 	}