Repository: kubesphere/ks-devops
Pull request #768 (2022-08-05 10:28:25)

Support to render step templates in JSON format
<!-- 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:

### 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
-->
See also https://github.com/kubesphere/ks-devops/issues/693

### 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"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/768?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#768](https://codecov.io/gh/kubesphere/ks-devops/pull/768?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (e0d209a) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/9a6e4176fb3e5aa2c5310b0fdb5810aab8adedc8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (9a6e417) will **decrease** coverage by `0.01%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master     #768      +/-   ##
==========================================
- Coverage   79.06%   79.04%   -0.02%     
==========================================
  Files         165      165              
  Lines       24027    24033       +6     
==========================================
+ Hits        18996    18997       +1     
- Misses       4768     4772       +4     
- Partials      263      264       +1     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `79.04% <100.00%> (-0.02%)` | :arrow_down: |

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

| [Impacted Files](https://codecov.io/gh/kubesphere/ks-devops/pull/768?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/api/devops/v1alpha3/steptemplate\_types.go](https://codecov.io/gh/kubesphere/ks-devops/pull/768/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2FwaS9kZXZvcHMvdjFhbHBoYTMvc3RlcHRlbXBsYXRlX3R5cGVzLmdv) | `100.00% <ø> (ø)` | |
| [pkg/api/devops/v1alpha3/steptemplate\_render.go](https://codecov.io/gh/kubesphere/ks-devops/pull/768/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2FwaS9kZXZvcHMvdjFhbHBoYTMvc3RlcHRlbXBsYXRlX3JlbmRlci5nbw==) | `93.22% <100.00%> (+0.76%)` | :arrow_up: |
| [...ontrollers/jenkins/pipeline/pipeline\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/768/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9waXBlbGluZS9waXBlbGluZV9jb250cm9sbGVyLmdv) | `39.39% <0.00%> (-3.79%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)


Patch:
diff --git a/Makefile b/Makefile
index dd76cf6..38c03c6 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,7 @@ endif
 all: test lint
 
 # Run tests
-test: fmt vet # generate manifests
+test: fmt vet generate manifests
 	go test ./... -coverprofile coverage.out
 
 # Build manager binary
diff --git a/cmd/controller/app/controllers.go b/cmd/controller/app/controllers.go
index 4e409e3..f2be746 100644
--- a/cmd/controller/app/controllers.go
+++ b/cmd/controller/app/controllers.go
@@ -113,7 +113,7 @@ func getAllControllers(mgr manager.Manager, client k8s.Client, informerFactory i
 		Client:        mgr.GetClient(),
 		ArgoNamespace: s.ArgoCDOption.Namespace,
 	}
-	argocdImageUpdaterReconciler := &argocd.ImageUpdaterReconciler{
+	argcdImageUpdaterReconciler := &argocd.ImageUpdaterReconciler{
 		Client: mgr.GetClient(),
 	}
 	gitRepoReconcilers := gitrepository.GetReconcilers(mgr.GetClient())
@@ -211,8 +211,8 @@ func getAllControllers(mgr manager.Manager, client k8s.Client, informerFactory i
 			}
 			return argocdAppReconciler.SetupWithManager(mgr)
 		},
-		argocdImageUpdaterReconciler.GetGroupName() + "-image-updater": func(mgr manager.Manager) error {
-			return argocdImageUpdaterReconciler.SetupWithManager(mgr)
+		argcdImageUpdaterReconciler.GetGroupName() + "-image-updater": func(mgr manager.Manager) error {
+			return argcdImageUpdaterReconciler.SetupWithManager(mgr)
 		},
 		"fluxcd": func(mgr manager.Manager) error {
 			return fluxcdGitRepoReconciler.SetupWithManager(mgr)
diff --git a/config/crd/bases/devops.kubesphere.io_clustersteptemplates.yaml b/config/crd/bases/devops.kubesphere.io_clustersteptemplates.yaml
index 5351cd9..4bac942 100644
--- a/config/crd/bases/devops.kubesphere.io_clustersteptemplates.yaml
+++ b/config/crd/bases/devops.kubesphere.io_clustersteptemplates.yaml
@@ -48,8 +48,13 @@ spec:
                       type: string
                     name:
                       type: string
+                    options:
+                      type: string
                     required:
                       type: boolean
+                    type:
+                      description: ParameterType represents the type of parameter
+                      type: string
                   required:
                   - name
                   type: object
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index f376f95..8e4dcb0 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -130,6 +130,17 @@ rules:
   - list
   - update
   - watch
+- apiGroups:
+  - devops.kubesphere.io
+  resources:
+  - clustersteptemplates
+  verbs:
+  - create
+  - delete
+  - get
+  - list
+  - update
+  - watch
 - apiGroups:
   - devops.kubesphere.io
   resources:
diff --git a/go.mod b/go.mod
index 92062ca..002ab13 100644
--- a/go.mod
+++ b/go.mod
@@ -24,8 +24,8 @@ require (
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/h2non/gock v1.0.9
 	github.com/jenkins-x/go-scm v1.11.4
-	github.com/jenkins-zh/jenkins-client v0.0.12
-	github.com/jenkins-zh/jenkins-client/pkg/k8s v0.0.0-20220803063034-4f2584fd4a90
+	github.com/jenkins-zh/jenkins-client v0.0.13
+	github.com/jenkins-zh/jenkins-client/pkg/k8s v0.0.0-20220811083317-bb6f87a3f866
 	github.com/kubesphere/sonargo v0.0.2
 	github.com/onsi/ginkgo v1.16.4
 	github.com/onsi/gomega v1.15.0
diff --git a/go.sum b/go.sum
index 27b6a14..6ec7d17 100644
--- a/go.sum
+++ b/go.sum
@@ -339,10 +339,10 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jenkins-zh/jenkins-cli v0.0.32/go.mod h1:uE1mH9PNITrg0sugv6HXuM/CSddg0zxXoYu3w57I3JY=
-github.com/jenkins-zh/jenkins-client v0.0.12 h1:rtFhug88U2TSedgX6gUFkODid/E6qnfJxj7OwjLjBU4=
-github.com/jenkins-zh/jenkins-client v0.0.12/go.mod h1:ICBk7OOoTafVP//f/VfKZ34c0ff8vJwVnOsF9btiMYU=
-github.com/jenkins-zh/jenkins-client/pkg/k8s v0.0.0-20220803063034-4f2584fd4a90 h1:QMUbSp4/FSA0rGcWu85wOJ4FHQHFbxNpQArgdE90N+k=
-github.com/jenkins-zh/jenkins-client/pkg/k8s v0.0.0-20220803063034-4f2584fd4a90/go.mod h1:zetE8JfNmxkot7OCNXJyS/QwxFnfcUFzrs/jg4U2gAw=
+github.com/jenkins-zh/jenkins-client v0.0.13 h1:MIBq36M9RYaJTVOE9sHmD/NOXzf4el9qZ4RRr/1/Lxw=
+github.com/jenkins-zh/jenkins-client v0.0.13/go.mod h1:ICBk7OOoTafVP//f/VfKZ34c0ff8vJwVnOsF9btiMYU=
+github.com/jenkins-zh/jenkins-client/pkg/k8s v0.0.0-20220811083317-bb6f87a3f866 h1:MQwSe0z1hju5X9c5BEfQAvkb4Fub1ZoHLo+ZLJ7mAyo=
+github.com/jenkins-zh/jenkins-client/pkg/k8s v0.0.0-20220811083317-bb6f87a3f866/go.mod h1:zetE8JfNmxkot7OCNXJyS/QwxFnfcUFzrs/jg4U2gAw=
 github.com/jenkins-zh/jenkins-formulas v0.0.5/go.mod h1:zS8fm8u5L6FcjZM0QznXsLV9T2UtSVK+hT6Sm76iUZ4=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
 github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
diff --git a/pkg/api/devops/v1alpha3/steptemplate_render.go b/pkg/api/devops/v1alpha3/steptemplate_render.go
index 084396a..e0bb0c3 100644
--- a/pkg/api/devops/v1alpha3/steptemplate_render.go
+++ b/pkg/api/devops/v1alpha3/steptemplate_render.go
@@ -18,6 +18,7 @@ package v1alpha3
 
 import (
 	"bytes"
+	"encoding/json"
 	"fmt"
 	v1 "k8s.io/api/core/v1"
 	"strings"
@@ -43,43 +44,74 @@ func (t *StepTemplateSpec) Render(param map[string]string, secret *v1.Secret) (o
 		output, err = shellRender(t.Template, param, secret)
 	}
 
-	if t.Secret.Wrap {
+	if t.Secret.Wrap && secret != nil {
 		output = wrapWithCredential(t.Secret.Type, secret.Name, output)
 	}
 
 	if err == nil && t.Container != "" {
-		output = fmt.Sprintf(`container("%s") {
-%s
-}`, t.Container, addIndent(output))
+		output = fmt.Sprintf(`
+{
+  "arguments": {
+	"isLiteral": true,
+	"value": "%s"
+  },
+  "children": [%s],
+  "name": "container"
+}`, t.Container, output)
 	}
+
+	output = jsonFormat(output)
 	return
 }
 
-func addIndent(txt string) string {
-	items := strings.Split(txt, "\n")
-	return "\t" + strings.Join(items, "\n\t")
+func jsonFormat(content string) string {
+	var buf bytes.Buffer
+	if err := json.Indent(&buf, []byte(content), "", "  "); err == nil {
+		return strings.TrimSpace(buf.String())
+	}
+	return strings.TrimSpace(content)
 }
 
 func wrapWithCredential(secretType, secretName, target string) string {
 	switch secretType {
 	case string(v1.SecretTypeBasicAuth), string(SecretTypeBasicAuth):
-		return fmt.Sprintf(`withCredential[usernamePassword(credentialsId : "%s" ,passwordVariable : 'PASSWORDVARIABLE' ,usernameVariable : 'USERNAMEVARIABLE')]) {
-%s
-}`, secretName, addIndent(target))
+		target = fmt.Sprintf(`{
+      "arguments": {
+        "isLiteral": false,
+        "value": "${[usernamePassword(credentialsId: '%s', passwordVariable: 'PASSWORDVARIABLE' ,usernameVariable : 'USERNAMEVARIABLE')]}"
+      },
+      "children": [%s],
+      "name": "withCredentials"
+    }`, secretName, target)
 	case string(v1.SecretTypeBootstrapToken), string(SecretTypeSecretText):
-		return fmt.Sprintf(`withCredential[string(credentialsId : "%s" ,variable : 'VARIABLE')]) {
-%s
-}`, secretName, addIndent(target))
+		target = fmt.Sprintf(`{
+  "arguments": {
+    "isLiteral": false,
+    "value": "${[string(credentialsId: '%s', variable: 'VARIABLE')]}"
+  },
+  "children": [%s],
+  "name": "withCredentials"
+}`, secretName, target)
 	case string(SecretTypeKubeConfig):
-		return fmt.Sprintf(`withCredential[kubeconfigContent(credentialsId : "%s" ,variable : 'VARIABLE')]) {
-%s
-}`, secretName, addIndent(target))
+		target = fmt.Sprintf(`{
+  "arguments": {
+    "isLiteral": false,
+    "value": "${[kubeconfigContent(credentialsId: '%s', variable: 'VARIABLE')]}"
+  },
+  "children": [%s],
+  "name": "withCredentials"
+}`, secretName, target)
 	case string(SecretTypeSSHAuth):
-		return fmt.Sprintf(`withCredential[sshUserPrivateKey(credentialsId : "%s" ,keyFileVariable : 'KEYFILEVARIABLE' ,passphraseVariable : 'PASSPHRASEVARIABLE' ,usernameVariable : 'SSHUSERPRIVATEKEY')]) {
-%s
-}`, secretName, addIndent(target))
+		target = fmt.Sprintf(`{
+  "arguments": {
+    "isLiteral": false,
+    "value": "${[sshUserPrivateKey(credentialsId: '%s', keyFileVariable : 'KEYFILEVARIABLE' ,passphraseVariable : 'PASSPHRASEVARIABLE' ,usernameVariable : 'SSHUSERPRIVATEKEY')]}"
+  },
+  "children": [%s],
+  "name": "withCredentials"
+}`, secretName, target)
 	}
-	return target
+	return jsonFormat(target)
 }
 
 func dslRender(dslTpl string, param map[string]string, secret *v1.Secret) (output string, err error) {
@@ -104,9 +136,18 @@ func dslRender(dslTpl string, param map[string]string, secret *v1.Secret) (outpu
 
 func shellRender(shellTpl string, param map[string]string, secret *v1.Secret) (output string, err error) {
 	if output, err = dslRender(shellTpl, param, secret); err == nil {
-		output = fmt.Sprintf(`sh '''
-%s
-'''`, addIndent(output))
+		output = fmt.Sprintf(`{
+"arguments": [
+  {
+	"key": "script",
+	"value": {
+	  "isLiteral": true,
+	  "value": "%s"
+	}
+  }
+],
+"name": "sh"
+}`, strings.ReplaceAll(output, "\n", "\\n"))
 	}
 	return
 }
diff --git a/pkg/api/devops/v1alpha3/steptemplate_types.go b/pkg/api/devops/v1alpha3/steptemplate_types.go
index 6189645..7be7bf9 100644
--- a/pkg/api/devops/v1alpha3/steptemplate_types.go
+++ b/pkg/api/devops/v1alpha3/steptemplate_types.go
@@ -38,12 +38,34 @@ type SecretInStep struct {
 
 // ParameterInStep is the parameter which used in a step
 type ParameterInStep struct {
-	Name         string `json:"name"`
-	Required     bool   `json:"required,omitempty"`
-	Display      string `json:"display,omitempty"`
-	DefaultValue string `json:"defaultValue,omitempty" yaml:"defaultValue"`
+	Name         string        `json:"name"`
+	Type         ParameterType `json:"type,omitempty"`
+	Required     bool          `json:"required,omitempty"`
+	Display      string        `json:"display,omitempty"`
+	DefaultValue string        `json:"defaultValue,omitempty" yaml:"defaultValue"`
+	Options      string        `json:"options,omitempty"`
 }
 
+// ParameterType represents the type of parameter
+type ParameterType string
+
+const (
+	// ParameterTypeString represents a parameter in string format, expect this is a single line
+	ParameterTypeString ParameterType = "string"
+	// ParameterTypeText represents a parameter in string format, expect this is multi-line
+	ParameterTypeText ParameterType = "text"
+	// ParameterTypeNumber represents a parameter in number format
+	ParameterTypeNumber ParameterType = "number"
+	// ParameterTypeCode represents a parameter in string format that contains some code lines
+	ParameterTypeCode ParameterType = "code"
+	// ParameterTypeBool represents a parameter in boolean format
+	ParameterTypeBool ParameterType = "bool"
+	// ParameterTypeEnum represents a parameter in enum format
+	ParameterTypeEnum ParameterType = "enum"
+	// ParameterTypeSecret represents a parameter in secret format
+	ParameterTypeSecret ParameterType = "secret"
+)
+
 // StepTemplateStatus defines the observed state of ClusterStepTemplate
 type StepTemplateStatus struct {
 	Phase StepTemplatePhase `json:"phase"`
diff --git a/pkg/kapis/devops/v1alpha3/steptemplate/handler.go b/pkg/kapis/devops/v1alpha3/steptemplate/handler.go
index 5170e2c..e851c22 100644
--- a/pkg/kapis/devops/v1alpha3/steptemplate/handler.go
+++ b/pkg/kapis/devops/v1alpha3/steptemplate/handler.go
@@ -21,8 +21,11 @@ import (
 	"fmt"
 	"github.com/emicklei/go-restful"
 	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"kubesphere.io/devops/pkg/apiserver/query"
+	resourcesV1alpha3 "kubesphere.io/devops/pkg/models/resources/v1alpha3"
 	"net/http"
 )
 
@@ -31,7 +34,19 @@ func (h *handler) clusterStepTemplates(req *restful.Request, resp *restful.Respo
 
 	clusterStepTemplateList := &v1alpha3.ClusterStepTemplateList{}
 	err := h.List(ctx, clusterStepTemplateList)
-	writeResponse(clusterStepTemplateList, err, resp)
+
+	queryParam := query.ParseQueryParameter(req)
+	apiResult := resourcesV1alpha3.ToListResult(convertToObject(clusterStepTemplateList.Items), queryParam, resourcesV1alpha3.NamedHandler{})
+
+	writeResponse(apiResult, err, resp)
+}
+
+func convertToObject(prs []v1alpha3.ClusterStepTemplate) []runtime.Object {
+	var result []runtime.Object
+	for i := range prs {
+		result = append(result, &prs[i])
+	}
+	return result
 }
 
 func (h *handler) getClusterStepTemplate(req *restful.Request, resp *restful.Response) {
@@ -60,7 +75,7 @@ func (h *handler) renderClusterStepTemplate(req *restful.Request, resp *restful.
 		fmt.Printf("something goes wrong when getting secret, error: %v\n", err)
 	}
 
-	param := make(map[string]string)
+	param := map[string]string{}
 	// get the parameters from request
 	if err = req.ReadEntity(&param); err != nil {
 		// TODO considering have logger output instead of the std output
diff --git a/pkg/kapis/devops/v1alpha3/steptemplate/route.go b/pkg/kapis/devops/v1alpha3/steptemplate/route.go
index 8951a08..acf345b 100644
--- a/pkg/kapis/devops/v1alpha3/steptemplate/route.go
+++ b/pkg/kapis/devops/v1alpha3/steptemplate/route.go
@@ -35,6 +35,9 @@ var (
 	SecretNamespaceQueryParameter = restful.QueryParameter("secretNamespace", "The namespace of a secret")
 )
 
+// TODO perhaps we can find a better way to declaim the permission needs of the apiserver
+//+kubebuilder:rbac:groups=devops.kubesphere.io,resources=clustersteptemplates,verbs=get;list;update;delete;create;watch
+
 // RegisterRoutes registry the handlers of the stepTemplates
 func RegisterRoutes(service *restful.WebService, options *common.Options) {
 	h := &handler{options.GenericClient}
@@ -45,7 +48,7 @@ func RegisterRoutes(service *restful.WebService, options *common.Options) {
 		To(h.getClusterStepTemplate).
 		Param(ClusterStepTemplate).
 		Doc("Return a specific ClusterStepTemplate"))
-	service.Route(service.GET("/clustersteptemplates/{clustersteptemplate}/render").
+	service.Route(service.POST("/clustersteptemplates/{clustersteptemplate}/render").
 		To(h.renderClusterStepTemplate).
 		Param(ClusterStepTemplate).
 		Param(SecretNameQueryParameter).
diff --git a/pkg/models/resources/v1alpha3/devops/devops.go b/pkg/models/resources/v1alpha3/devops/devops.go
deleted file mode 100644
index 0986ea6..0000000
--- a/pkg/models/resources/v1alpha3/devops/devops.go
+++ /dev/null
@@ -1,52 +0,0 @@
-/*
-Copyright 2019 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 devops
-
-import (
-	"k8s.io/apimachinery/pkg/runtime"
-
-	"kubesphere.io/devops/pkg/api"
-	"kubesphere.io/devops/pkg/apiserver/query"
-	ksinformers "kubesphere.io/devops/pkg/client/informers/externalversions"
-	"kubesphere.io/devops/pkg/models/resources/v1alpha3"
-)
-
-type devopsGetter struct {
-	informers ksinformers.SharedInformerFactory
-}
-
-func New(ksinformer ksinformers.SharedInformerFactory) v1alpha3.Interface {
-	return &devopsGetter{informers: ksinformer}
-}
-
-func (n devopsGetter) Get(_, name string) (runtime.Object, error) {
-	return n.informers.Devops().V1alpha3().DevOpsProjects().Lister().Get(name)
-}
-
-func (n devopsGetter) List(_ string, query *query.Query) (*api.ListResult, error) {
-	projects, err := n.informers.Devops().V1alpha3().DevOpsProjects().Lister().List(query.Selector())
-	if err != nil {
-		return nil, err
-	}
-
-	var result []runtime.Object
-	for _, project := range projects {
-		result = append(result, project)
-	}
-
-	return v1alpha3.DefaultList(result, query, v1alpha3.DefaultCompare(), v1alpha3.DefaultFilter()), nil
-}
diff --git a/pkg/models/resources/v1alpha3/interface.go b/pkg/models/resources/v1alpha3/interface.go
index 4421afc..238c40f 100644
--- a/pkg/models/resources/v1alpha3/interface.go
+++ b/pkg/models/resources/v1alpha3/interface.go
@@ -166,6 +166,22 @@ func DefaultCompare() CompareFunc {
 	}
 }
 
+// NameCompare returns a compare function that compare by name
+func NameCompare() CompareFunc {
+	return func(left runtime.Object, right runtime.Object, field query.Field) bool {
+		leftOma, ok := left.(metav1.ObjectMetaAccessor)
+		if !ok {
+			return false
+		}
+		rightOma, ok := right.(metav1.ObjectMetaAccessor)
+		if !ok {
+			return false
+		}
+		field = "!" + query.FieldName
+		return DefaultObjectMetaCompare(leftOma.GetObjectMeta(), rightOma.GetObjectMeta(), field)
+	}
+}
+
 // DefaultFilter creates a default ObjectMeta filter function.
 func DefaultFilter() FilterFunc {
 	return func(obj runtime.Object, filter query.Filter) bool {
@@ -182,8 +198,11 @@ func DefaultObjectMetaCompare(left, right metav1.Object, sortBy query.Field) boo
 	switch sortBy {
 	// ?sortBy=name
 	case query.FieldName:
-		// sort the name in ascending order
+		// sort the name in descending order
 		return strings.Compare(left.GetName(), right.GetName()) > 0
+	case "!" + query.FieldName:
+		// sort the name in ascending order
+		return strings.Compare(left.GetName(), right.GetName()) < 0
 	//	?sortBy=creationTimestamp
 	default:
 		fallthrough
diff --git a/pkg/models/resources/v1alpha3/namedHandler.go b/pkg/models/resources/v1alpha3/namedHandler.go
new file mode 100644
index 0000000..b4998ff
--- /dev/null
+++ b/pkg/models/resources/v1alpha3/namedHandler.go
@@ -0,0 +1,36 @@
+/*
+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 v1alpha3
+
+// NamedHandler represents a handler that sort the list by name
+type NamedHandler struct {
+}
+
+// Comparator compare the list by name with ascending
+func (h NamedHandler) Comparator() CompareFunc {
+	return NameCompare()
+}
+
+// Filter is the default filter
+func (h NamedHandler) Filter() FilterFunc {
+	return DefaultFilter()
+}
+
+// Transformer is the default transformer
+func (h NamedHandler) Transformer() TransformFunc {
+	return NoTransformFunc()
+}


Test patch:
diff --git a/controllers/fluxcd/git-repository-controller_test.go b/controllers/fluxcd/git-repository-controller_test.go
index 6b141a4..79d1288 100644
--- a/controllers/fluxcd/git-repository-controller_test.go
+++ b/controllers/fluxcd/git-repository-controller_test.go
@@ -18,8 +18,6 @@ package fluxcd
 
 import (
 	"context"
-	"fmt"
-	"github.com/go-logr/logr"
 	"github.com/stretchr/testify/assert"
 	v1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
@@ -27,7 +25,6 @@ import (
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/tools/record"
-	"kubesphere.io/devops/controllers/core"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 	ctrl "sigs.k8s.io/controller-runtime"
@@ -324,42 +321,3 @@ func TestGitRepositoryReconciler_GetName(t *testing.T) {
 		assert.Equal(t, "FluxGitRepositoryReconciler", r.GetName())
 	})
 }
-
-func TestGitRepositoryReconciler_SetupWithManager(t *testing.T) {
-	schema, err := v1alpha3.SchemeBuilder.Register().Build()
-	assert.Nil(t, err)
-
-	type fields struct {
-		Client   client.Client
-		log      logr.Logger
-		recorder record.EventRecorder
-	}
-	type args struct {
-		mgr ctrl.Manager
-	}
-	tests := []struct {
-		name    string
-		fields  fields
-		args    args
-		wantErr assert.ErrorAssertionFunc
-	}{{
-		name: "normal",
-		args: args{
-			mgr: &core.FakeManager{
-				Client: fake.NewFakeClientWithScheme(schema),
-				Scheme: schema,
-			},
-		},
-		wantErr: core.NoErrors,
-	}}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			r := &GitRepositoryReconciler{
-				Client:   tt.fields.Client,
-				log:      tt.fields.log,
-				recorder: tt.fields.recorder,
-			}
-			tt.wantErr(t, r.SetupWithManager(tt.args.mgr), fmt.Sprintf("SetupWithManager(%v)", tt.args.mgr))
-		})
-	}
-}
diff --git a/pkg/api/devops/v1alpha3/steptemplate_render_test.go b/pkg/api/devops/v1alpha3/steptemplate_render_test.go
index 1ee8c06..1a8c30b 100644
--- a/pkg/api/devops/v1alpha3/steptemplate_render_test.go
+++ b/pkg/api/devops/v1alpha3/steptemplate_render_test.go
@@ -17,7 +17,8 @@ limitations under the License.
 package v1alpha3
 
 import (
-	"github.com/stretchr/testify/assert"
+	"github.com/google/go-cmp/cmp"
+	"io/ioutil"
 	v1 "k8s.io/api/core/v1"
 	v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"testing"
@@ -44,7 +45,7 @@ docker build {{.param.context}} -t {{.param.tag}}`,
 		}},
 	}
 	stepTemplateWithDSL := &StepTemplateSpec{
-		Template: `echo "1"`,
+		Template: readFile("testdata/dsl-echo.json"),
 		Runtime:  "dsl",
 	}
 
@@ -63,9 +64,18 @@ docker build {{.param.context}} -t {{.param.tag}}`,
 		args: args{
 			stepTemplate: stepTemplate,
 		},
-		wantOutput: `sh '''
-	echo 1
-'''`,
+		wantOutput: `{
+  "arguments": [
+    {
+      "key": "script",
+      "value": {
+        "isLiteral": true,
+        "value": "echo 1"
+      }
+    }
+  ],
+  "name": "sh"
+}`,
 		wantErr: false,
 	}, {
 		name: "docker build command with parameters",
@@ -86,21 +96,14 @@ docker build {{.param.context}} -t {{.param.tag}}`,
 				},
 			},
 		},
-		wantOutput: `container("base") {
-	withCredential[usernamePassword(credentialsId : "docker" ,passwordVariable : 'PASSWORDVARIABLE' ,usernameVariable : 'USERNAMEVARIABLE')]) {
-		sh '''
-			docker login -u $USERNAMEVARIABLE -p $PASSWORDVARIABLE
-			docker build dir -t image:tag
-		'''
-	}
-}`,
-		wantErr: false,
+		wantOutput: readFile("testdata/docker-login.json"),
+		wantErr:    false,
 	}, {
 		name: "a simple dsl without any parameters",
 		args: args{
 			stepTemplate: stepTemplateWithDSL,
 		},
-		wantOutput: `echo "1"`,
+		wantOutput: readFile("testdata/dsl-echo.json"),
 		wantErr:    false,
 	}}
 	for _, tt := range tests {
@@ -113,6 +116,10 @@ docker build {{.param.context}} -t {{.param.tag}}`,
 			if gotOutput != tt.wantOutput {
 				t.Errorf("stepTemplateRender() gotOutput = %v, want %v", gotOutput, tt.wantOutput)
 			}
+
+			if diff := cmp.Diff(gotOutput, tt.wantOutput); len(diff) != 0 {
+				t.Errorf("%T differ (-got, +expected) %v", tt.wantOutput, diff)
+			}
 		})
 	}
 }
@@ -141,9 +148,7 @@ func Test_wrapWithCredential(t *testing.T) {
 			secretName: "config",
 			target:     "echo 1",
 		},
-		want: `withCredential[kubeconfigContent(credentialsId : "config" ,variable : 'VARIABLE')]) {
-	echo 1
-}`,
+		want: readFile("testdata/credential-kubeconfig.json"),
 	}, {
 		name: "secret as secret text type",
 		args: args{
@@ -151,9 +156,7 @@ func Test_wrapWithCredential(t *testing.T) {
 			secretName: "config",
 			target:     "echo 1",
 		},
-		want: `withCredential[string(credentialsId : "config" ,variable : 'VARIABLE')]) {
-	echo 1
-}`,
+		want: readFile("testdata/credential-string.json"),
 	}, {
 		name: "secret as ssh auth type",
 		args: args{
@@ -161,9 +164,7 @@ func Test_wrapWithCredential(t *testing.T) {
 			secretName: "config",
 			target:     "echo 1",
 		},
-		want: `withCredential[sshUserPrivateKey(credentialsId : "config" ,keyFileVariable : 'KEYFILEVARIABLE' ,passphraseVariable : 'PASSPHRASEVARIABLE' ,usernameVariable : 'SSHUSERPRIVATEKEY')]) {
-	echo 1
-}`,
+		want: readFile("testdata/credential-ssh.json"),
 	}}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
@@ -174,34 +175,9 @@ func Test_wrapWithCredential(t *testing.T) {
 	}
 }
 
-func Test_addIndent(t *testing.T) {
-	type args struct {
-		txt string
-	}
-	tests := []struct {
-		name string
-		args args
-		want string
-	}{{
-		name: "one line",
-		args: args{
-			txt: "one line",
-		},
-		want: "\tone line",
-	}, {
-		name: "three lines",
-		args: args{
-			txt: `one line
-two line
-three line`,
-		},
-		want: `	one line
-	two line
-	three line`,
-	}}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			assert.Equalf(t, tt.want, addIndent(tt.args.txt), "addIndent(%v)", tt.args.txt)
-		})
+func readFile(file string) string {
+	if data, err := ioutil.ReadFile(file); err == nil {
+		return string(data)
 	}
+	return ""
 }
diff --git a/pkg/api/devops/v1alpha3/testdata/credential-kubeconfig.json b/pkg/api/devops/v1alpha3/testdata/credential-kubeconfig.json
new file mode 100644
index 0000000..ec9db66
--- /dev/null
+++ b/pkg/api/devops/v1alpha3/testdata/credential-kubeconfig.json
@@ -0,0 +1,8 @@
+{
+  "arguments": {
+    "isLiteral": false,
+    "value": "${[kubeconfigContent(credentialsId: 'config', variable: 'VARIABLE')]}"
+  },
+  "children": [echo 1],
+  "name": "withCredentials"
+}
\ No newline at end of file
diff --git a/pkg/api/devops/v1alpha3/testdata/credential-ssh.json b/pkg/api/devops/v1alpha3/testdata/credential-ssh.json
new file mode 100644
index 0000000..f50b46c
--- /dev/null
+++ b/pkg/api/devops/v1alpha3/testdata/credential-ssh.json
@@ -0,0 +1,8 @@
+{
+  "arguments": {
+    "isLiteral": false,
+    "value": "${[sshUserPrivateKey(credentialsId: 'config', keyFileVariable : 'KEYFILEVARIABLE' ,passphraseVariable : 'PASSPHRASEVARIABLE' ,usernameVariable : 'SSHUSERPRIVATEKEY')]}"
+  },
+  "children": [echo 1],
+  "name": "withCredentials"
+}
\ No newline at end of file
diff --git a/pkg/api/devops/v1alpha3/testdata/credential-string.json b/pkg/api/devops/v1alpha3/testdata/credential-string.json
new file mode 100644
index 0000000..f93ca69
--- /dev/null
+++ b/pkg/api/devops/v1alpha3/testdata/credential-string.json
@@ -0,0 +1,8 @@
+{
+  "arguments": {
+    "isLiteral": false,
+    "value": "${[string(credentialsId: 'config', variable: 'VARIABLE')]}"
+  },
+  "children": [echo 1],
+  "name": "withCredentials"
+}
\ No newline at end of file
diff --git a/pkg/api/devops/v1alpha3/testdata/docker-login.json b/pkg/api/devops/v1alpha3/testdata/docker-login.json
new file mode 100644
index 0000000..4007744
--- /dev/null
+++ b/pkg/api/devops/v1alpha3/testdata/docker-login.json
@@ -0,0 +1,30 @@
+{
+  "arguments": {
+    "isLiteral": true,
+    "value": "base"
+  },
+  "children": [
+    {
+      "arguments": {
+        "isLiteral": false,
+        "value": "${[usernamePassword(credentialsId: 'docker', passwordVariable: 'PASSWORDVARIABLE' ,usernameVariable : 'USERNAMEVARIABLE')]}"
+      },
+      "children": [
+        {
+          "arguments": [
+            {
+              "key": "script",
+              "value": {
+                "isLiteral": true,
+                "value": "docker login -u $USERNAMEVARIABLE -p $PASSWORDVARIABLE\ndocker build dir -t image:tag"
+              }
+            }
+          ],
+          "name": "sh"
+        }
+      ],
+      "name": "withCredentials"
+    }
+  ],
+  "name": "container"
+}
\ No newline at end of file
diff --git a/pkg/api/devops/v1alpha3/testdata/dsl-echo.json b/pkg/api/devops/v1alpha3/testdata/dsl-echo.json
new file mode 100644
index 0000000..c51aecb
--- /dev/null
+++ b/pkg/api/devops/v1alpha3/testdata/dsl-echo.json
@@ -0,0 +1,12 @@
+{
+  "arguments": [
+    {
+      "key": "message",
+      "value": {
+        "isLiteral": true,
+        "value": "1"
+      }
+    }
+  ],
+  "name": "echo"
+}
\ No newline at end of file
diff --git a/pkg/api/devops/v1alpha3/testdata/sh-echo.json b/pkg/api/devops/v1alpha3/testdata/sh-echo.json
new file mode 100644
index 0000000..748e829
--- /dev/null
+++ b/pkg/api/devops/v1alpha3/testdata/sh-echo.json
@@ -0,0 +1,12 @@
+{
+  "arguments": [
+    {
+      "key": "script",
+      "value": {
+        "isLiteral": true,
+        "value": "echo 1"
+      }
+    }
+  ],
+  "name": "sh"
+}
\ No newline at end of file
diff --git a/pkg/kapis/devops/v1alpha3/steptemplate/route_test.go b/pkg/kapis/devops/v1alpha3/steptemplate/route_test.go
index c68691e..e30db99 100644
--- a/pkg/kapis/devops/v1alpha3/steptemplate/route_test.go
+++ b/pkg/kapis/devops/v1alpha3/steptemplate/route_test.go
@@ -17,6 +17,7 @@ limitations under the License.
 package steptemplate
 
 import (
+	"bytes"
 	"github.com/emicklei/go-restful"
 	"github.com/stretchr/testify/assert"
 	"io"
@@ -43,6 +44,7 @@ func TestAPIs(t *testing.T) {
 
 	type args struct {
 		api     string
+		method  string
 		getBody func() io.Reader
 	}
 	tests := []struct {
@@ -54,13 +56,15 @@ func TestAPIs(t *testing.T) {
 	}{{
 		name: "an empty list of the clusterStepTemplate",
 		args: args{
-			api: "/clustersteptemplates",
+			api:    "/clustersteptemplates",
+			method: http.MethodGet,
 		},
 		wantCode: http.StatusOK,
 	}, {
 		name: "the whole list of the clusterStepTemplate",
 		args: args{
-			api: "/clustersteptemplates",
+			api:    "/clustersteptemplates",
+			method: http.MethodGet,
 		},
 		getInstances: func() []runtime.Object {
 			return []runtime.Object{&v1alpha3.ClusterStepTemplate{}}
@@ -69,7 +73,8 @@ func TestAPIs(t *testing.T) {
 	}, {
 		name: "get a clusterStepTemplate by name",
 		args: args{
-			api: "/clustersteptemplates/fake",
+			api:    "/clustersteptemplates/fake",
+			method: http.MethodGet,
 		},
 		getInstances: func() []runtime.Object {
 			return []runtime.Object{&v1alpha3.ClusterStepTemplate{
@@ -82,7 +87,11 @@ func TestAPIs(t *testing.T) {
 	}, {
 		name: "render a clusterStepTemplate by name",
 		args: args{
-			api: "/clustersteptemplates/fake/render?secret=secret&secretNamespace=ns",
+			api:    "/clustersteptemplates/fake/render?secret=secret&secretNamespace=ns",
+			method: http.MethodPost,
+			getBody: func() io.Reader {
+				return bytes.NewBufferString(`{}`)
+			},
 		},
 		getInstances: func() []runtime.Object {
 			return []runtime.Object{&v1alpha3.ClusterStepTemplate{
@@ -110,7 +119,46 @@ func TestAPIs(t *testing.T) {
 		},
 		verify: func(bytes []byte, t *testing.T) {
 			assert.Equal(t, `{
- "data": "sh '''\n\techo 2\n'''"
+ "data": "{\n  \"arguments\": [\n    {\n      \"key\": \"script\",\n      \"value\": {\n        \"isLiteral\": true,\n        \"value\": \"echo 2\"\n      }\n    }\n  ],\n  \"name\": \"sh\"\n}"
+}`, string(bytes))
+		},
+		wantCode: http.StatusOK,
+	}, {
+		name: "render a clusterStepTemplate by namewith parameter body ",
+		args: args{
+			api:    "/clustersteptemplates/fake/render?secret=secret&secretNamespace=ns",
+			method: http.MethodPost,
+			getBody: func() io.Reader {
+				return bytes.NewBufferString(`{"number":"3"}`)
+			},
+		},
+		getInstances: func() []runtime.Object {
+			return []runtime.Object{&v1alpha3.ClusterStepTemplate{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "fake",
+				},
+				Spec: v1alpha3.StepTemplateSpec{
+					Parameters: []v1alpha3.ParameterInStep{{
+						Name:         "number",
+						DefaultValue: "2",
+					}},
+					Template: `echo {{.param.number}}`,
+				},
+			}, &v1.Secret{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "ns",
+					Name:      "secret",
+				},
+				Type: v1.SecretTypeBasicAuth,
+				Data: map[string][]byte{
+					v1.BasicAuthUsernameKey: []byte("username"),
+					v1.BasicAuthPasswordKey: []byte("password"),
+				},
+			}}
+		},
+		verify: func(bytes []byte, t *testing.T) {
+			assert.Equal(t, `{
+ "data": "{\n  \"arguments\": [\n    {\n      \"key\": \"script\",\n      \"value\": {\n        \"isLiteral\": true,\n        \"value\": \"echo 3\"\n      }\n    }\n  ],\n  \"name\": \"sh\"\n}"
 }`, string(bytes))
 		},
 		wantCode: http.StatusOK,
@@ -135,8 +183,9 @@ func TestAPIs(t *testing.T) {
 				requestBody = tt.args.getBody()
 			}
 
-			httpRequest, _ := http.NewRequest(http.MethodGet,
+			httpRequest, _ := http.NewRequest(tt.args.method,
 				"http://fake.com/kapis/devops.kubesphere.io/v1alpha3"+tt.args.api, requestBody)
+			httpRequest.Header.Set("Content-Type", "application/json")
 			httpWriter := httptest.NewRecorder()
 			container.Dispatch(httpWriter, httpRequest)
 			assert.Equal(t, tt.wantCode, httpWriter.Code)
diff --git a/pkg/models/resources/v1alpha3/interface_test.go b/pkg/models/resources/v1alpha3/interface_test.go
index 7b33c05..b8c1dcd 100644
--- a/pkg/models/resources/v1alpha3/interface_test.go
+++ b/pkg/models/resources/v1alpha3/interface_test.go
@@ -21,9 +21,10 @@ package v1alpha3
 import (
 	assert2 "github.com/stretchr/testify/assert"
 	"gotest.tools/assert"
-	v12 "k8s.io/api/core/v1"
-	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	"kubesphere.io/devops/pkg/apiserver/query"
@@ -157,50 +158,50 @@ func TestLabelsMatch(t *testing.T) {
 }
 
 func TestDefaultObjectMetaCompare(t *testing.T) {
-	now := v1.Now()
+	now := metav1.Now()
 	tables := []struct {
 		name              string
-		left              v1.ObjectMeta
-		right             v1.ObjectMeta
+		left              metav1.ObjectMeta
+		right             metav1.ObjectMeta
 		field             query.Field
 		expectedCmpResult bool
 	}{{
 		name: "compare name with descending order",
-		left: v1.ObjectMeta{
+		left: metav1.ObjectMeta{
 			Name: "b",
 		},
-		right: v1.ObjectMeta{
+		right: metav1.ObjectMeta{
 			Name: "a",
 		},
 		field:             query.FieldName,
 		expectedCmpResult: true,
 	}, {
 		name: "compare same name",
-		left: v1.ObjectMeta{
+		left: metav1.ObjectMeta{
 			Name: "a",
 		},
-		right: v1.ObjectMeta{
+		right: metav1.ObjectMeta{
 			Name: "a",
 		},
 		field:             query.FieldName,
 		expectedCmpResult: false,
 	}, {
 		name: "compare name with ascending order",
-		left: v1.ObjectMeta{
+		left: metav1.ObjectMeta{
 			Name: "a",
 		},
-		right: v1.ObjectMeta{
+		right: metav1.ObjectMeta{
 			Name: "b",
 		},
 		field:             query.FieldName,
 		expectedCmpResult: false,
 	}, {
 		name: "compare same creation timestamp",
-		left: v1.ObjectMeta{
+		left: metav1.ObjectMeta{
 			Name:              "a",
 			CreationTimestamp: now,
 		},
-		right: v1.ObjectMeta{
+		right: metav1.ObjectMeta{
 			Name:              "b",
 			CreationTimestamp: now,
 		},
@@ -208,31 +209,31 @@ func TestDefaultObjectMetaCompare(t *testing.T) {
 		expectedCmpResult: false,
 	}, {
 		name: "compare creation timestamp with descending order",
-		left: v1.ObjectMeta{
+		left: metav1.ObjectMeta{
 			CreationTimestamp: now,
 		},
-		right: v1.ObjectMeta{
-			CreationTimestamp: v1.NewTime(now.Truncate(time.Second)),
+		right: metav1.ObjectMeta{
+			CreationTimestamp: metav1.NewTime(now.Truncate(time.Second)),
 		},
 		field:             query.FieldCreationTimeStamp,
 		expectedCmpResult: true,
 	}, {
 		name: "compare creation timestamp with ascending order",
-		left: v1.ObjectMeta{
+		left: metav1.ObjectMeta{
 			CreationTimestamp: now,
 		},
-		right: v1.ObjectMeta{
-			CreationTimestamp: v1.NewTime(now.Add(time.Second)),
+		right: metav1.ObjectMeta{
+			CreationTimestamp: metav1.NewTime(now.Add(time.Second)),
 		},
 		field:             query.FieldCreationTimeStamp,
 		expectedCmpResult: false,
 	}, {
 		name: "compare others",
-		left: v1.ObjectMeta{
+		left: metav1.ObjectMeta{
 			CreationTimestamp: now,
 		},
-		right: v1.ObjectMeta{
-			CreationTimestamp: v1.NewTime(now.Add(time.Second)),
+		right: metav1.ObjectMeta{
+			CreationTimestamp: metav1.NewTime(now.Add(time.Second)),
 		},
 		field:             query.FieldUID,
 		expectedCmpResult: false,
@@ -269,12 +270,12 @@ func TestDefaultList(t *testing.T) {
 		name: "nil compareFunc, filterFunc and transform",
 		items: []runtime.Object{
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-a",
 				},
 			},
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-b",
 				},
 			},
@@ -285,12 +286,12 @@ func TestDefaultList(t *testing.T) {
 			TotalItems: 2,
 			Items: []interface{}{
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-a",
 					},
 				},
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-b",
 					},
 				},
@@ -300,12 +301,12 @@ func TestDefaultList(t *testing.T) {
 		name: "filter name",
 		items: []runtime.Object{
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-abcd",
 				},
 			},
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-efgh",
 				},
 			},
@@ -321,7 +322,7 @@ func TestDefaultList(t *testing.T) {
 			TotalItems: 1,
 			Items: []interface{}{
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-abcd",
 					},
 				},
@@ -331,12 +332,12 @@ func TestDefaultList(t *testing.T) {
 		name: "filter name",
 		items: []runtime.Object{
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-abcd",
 				},
 			},
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-efgh",
 				},
 			},
@@ -351,12 +352,12 @@ func TestDefaultList(t *testing.T) {
 			TotalItems: 2,
 			Items: []interface{}{
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-efgh",
 					},
 				},
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-abcd",
 					},
 				},
@@ -366,17 +367,17 @@ func TestDefaultList(t *testing.T) {
 		name: "filter and compare name",
 		items: []runtime.Object{
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-cdef",
 				},
 			},
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-abcd",
 				},
 			},
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-efgh",
 				},
 			},
@@ -394,12 +395,12 @@ func TestDefaultList(t *testing.T) {
 			TotalItems: 2,
 			Items: []interface{}{
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-abcd",
 					},
 				},
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-cdef",
 					},
 				},
@@ -409,13 +410,13 @@ func TestDefaultList(t *testing.T) {
 		name: "Filter nil items",
 		items: []runtime.Object{
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-abcd",
 				},
 			},
 			nil,
 			&v1alpha3.Pipeline{
-				ObjectMeta: v1.ObjectMeta{
+				ObjectMeta: metav1.ObjectMeta{
 					Name: "pipeline-cdef",
 				},
 			},
@@ -427,12 +428,12 @@ func TestDefaultList(t *testing.T) {
 			TotalItems: 2,
 			Items: []interface{}{
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-abcd",
 					},
 				},
 				&v1alpha3.Pipeline{
-					ObjectMeta: v1.ObjectMeta{
+					ObjectMeta: metav1.ObjectMeta{
 						Name: "pipeline-cdef",
 					},
 				},
@@ -457,7 +458,7 @@ func Test_noTransformFunc(t *testing.T) {
 		arg:  nil,
 	}, {
 		name: "Non-nil object",
-		arg:  &v12.Pod{},
+		arg:  &v1.Pod{},
 	},
 	}
 	for _, tt := range tests {
@@ -682,3 +683,250 @@ func TestFilterFunc_Or(t *testing.T) {
 		})
 	}
 }
+
+func TestDefaultObjectMetaFilter(t *testing.T) {
+	obj := &v1.ConfigMap{}
+	obj.SetUID(types.UID("uid"))
+	obj.SetName("test")
+	obj.SetNamespace("ns")
+	obj.SetOwnerReferences([]metav1.OwnerReference{{
+		UID:  "uid",
+		Kind: "kind",
+	}})
+	obj.SetAnnotations(map[string]string{
+		"anno-key": "anno-value",
+	})
+	obj.SetLabels(map[string]string{
+		"label-key": "label-value",
+	})
+
+	type args struct {
+		item   metav1.Object
+		filter query.Filter
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{{
+		name: "filter by names",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "names",
+				Value: "test,fake",
+			},
+		},
+		want: true,
+	}, {
+		name: "filter by names, not have name",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "names",
+				Value: "good,fake",
+			},
+		},
+		want: false,
+	}, {
+		name: "filter by name",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "name",
+				Value: "test",
+			},
+		},
+		want: true,
+	}, {
+		name: "filter by name, not match name",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "name",
+				Value: "fake",
+			},
+		},
+		want: false,
+	}, {
+		name: "filter by uid",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "uid",
+				Value: "uid",
+			},
+		},
+		want: true,
+	}, {
+		name: "filter by uid, not match with uid",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "uid",
+				Value: "fake",
+			},
+		},
+		want: false,
+	}, {
+		name: "filter by namespace",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "namespace",
+				Value: "ns",
+			},
+		},
+		want: true,
+	}, {
+		name: "filter by namespace, not match with namespace",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "namespace",
+				Value: "fake",
+			},
+		},
+		want: false,
+	}, {
+		name: "filter by ownerReference",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "ownerReference",
+				Value: "uid",
+			},
+		},
+		want: true,
+	}, {
+		name: "filter by ownerReference, not match",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "ownerReference",
+				Value: "fake",
+			},
+		},
+		want: false,
+	}, {
+		name: "filter by ownerReference kind",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "ownerKind",
+				Value: "kind",
+			},
+		},
+		want: true,
+	}, {
+		name: "filter by ownerReference kind, not match",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "ownerKind",
+				Value: "fake",
+			},
+		},
+		want: false,
+	}, {
+		name: "filter by annotation",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "annotation",
+				Value: "anno-key=anno-value",
+			},
+		},
+		want: true,
+	}, {
+		name: "filter by annotation, not match",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "annotation",
+				Value: "fake",
+			},
+		},
+		want: false,
+	}, {
+		name: "filter by label",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "label",
+				Value: "label-key=label-value",
+			},
+		},
+		want: true,
+	}, {
+		name: "filter by label, not match",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "label",
+				Value: "fake",
+			},
+		},
+		want: false,
+	}, {
+		name: "fake filter",
+		args: args{
+			item: obj.DeepCopy(),
+			filter: query.Filter{
+				Field: "fake",
+				Value: "fake",
+			},
+		},
+		want: true,
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert2.Equalf(t, tt.want, DefaultObjectMetaFilter(tt.args.item, tt.args.filter), "DefaultObjectMetaFilter(%v, %v)", tt.args.item, tt.args.filter)
+		})
+	}
+}
+
+func TestToListResult(t *testing.T) {
+	cm1 := &v1.ConfigMap{}
+	cm1.SetName("cm1")
+	cm2 := &v1.ConfigMap{}
+	cm2.SetName("cm2")
+
+	type args struct {
+		objects []runtime.Object
+		q       *query.Query
+		handler ListHandler
+	}
+	tests := []struct {
+		name string
+		args args
+		want *api.ListResult
+	}{{
+		name: "normal",
+		args: args{
+			objects: []runtime.Object{cm1, cm2},
+			q:       &query.Query{},
+			handler: nil,
+		},
+		want: &api.ListResult{
+			Items:      []interface{}{cm2, cm1},
+			TotalItems: 2,
+		},
+	}, {
+		name: "sort by name",
+		args: args{
+			objects: []runtime.Object{cm1, cm2},
+			q:       &query.Query{},
+			handler: NamedHandler{},
+		},
+		want: &api.ListResult{
+			Items:      []interface{}{cm1, cm2},
+			TotalItems: 2,
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert2.Equalf(t, tt.want, ToListResult(tt.args.objects, tt.args.q, tt.args.handler), "ToListResult(%v, %v, %v)", tt.args.objects, tt.args.q, tt.args.handler)
+		})
+	}
+}
diff --git a/pkg/models/resources/v1alpha3/devops/devops_test.go b/pkg/models/resources/v1alpha3/listhandler_test.go
similarity index 58%
rename from pkg/models/resources/v1alpha3/devops/devops_test.go
rename to pkg/models/resources/v1alpha3/listhandler_test.go
index 4ccbfc3..ede6ea3 100644
--- a/pkg/models/resources/v1alpha3/devops/devops_test.go
+++ b/pkg/models/resources/v1alpha3/listhandler_test.go
@@ -1,11 +1,11 @@
 /*
-Copyright 2019 The KubeSphere Authors.
+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
+     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,
@@ -14,4 +14,16 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package devops
+package v1alpha3
+
+import (
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func Test_defaultListHandler_Comparator(t *testing.T) {
+	d := defaultListHandler{}
+	assert.NotNil(t, d.Comparator())
+	assert.NotNil(t, d.Filter())
+	assert.NotNil(t, d.Transformer())
+}
diff --git a/pkg/models/resources/v1alpha3/namedHandler_test.go b/pkg/models/resources/v1alpha3/namedHandler_test.go
new file mode 100644
index 0000000..646bf5b
--- /dev/null
+++ b/pkg/models/resources/v1alpha3/namedHandler_test.go
@@ -0,0 +1,42 @@
+/*
+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 v1alpha3
+
+import (
+	"github.com/stretchr/testify/assert"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"testing"
+)
+
+func TestDefaultFunc(t *testing.T) {
+	h := NamedHandler{}
+	assert.NotNil(t, h.Filter())
+	assert.NotNil(t, h.Comparator())
+	assert.NotNil(t, h.Transformer())
+
+	objA := v1alpha3.Pipeline{}
+	objA.SetName("aaa")
+
+	objB := v1alpha3.Pipeline{}
+	objB.SetName("bbb")
+
+	result := h.Comparator()(objA.DeepCopy(), objB.DeepCopy(), "name")
+	assert.True(t, result)
+
+	result = h.Comparator()(objB.DeepCopy(), objA.DeepCopy(), "name")
+	assert.False(t, result)
+}