Repository: kubesphere/ks-devops
Pull request #651 (2022-06-07 07:42:21)

Add an API for accessing the clusterStepTemplates

<!-- 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
-->
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: @LinuxSuRen: PR needs rebase.

<details>

Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/651?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#651](https://codecov.io/gh/kubesphere/ks-devops/pull/651?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (ba2e33d) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/058ff4db54298fc62f99807eb04b66cf55c037c9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (058ff4d) will **increase** coverage by `0.08%`.
> The diff coverage is `91.81%`.

```diff
@@            Coverage Diff             @@
##           master     #651      +/-   ##
==========================================
+ Coverage   77.39%   77.48%   +0.08%     
==========================================
  Files         150      153       +3     
  Lines       23417    23527     +110     
==========================================
+ Hits        18123    18229     +106     
- Misses       5074     5075       +1     
- Partials      220      223       +3     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.48% <91.81%> (+0.08%)` | :arrow_up: |

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

| [Impacted Files](https://codecov.io/gh/kubesphere/ks-devops/pull/651?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/kapis/devops/v1alpha3/steptemplate/handler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9zdGVwdGVtcGxhdGUvaGFuZGxlci5nbw==) | `90.10% <90.10%> (ø)` | |
| [pkg/api/devops/v1alpha3/steptemplate\_types.go](https://codecov.io/gh/kubesphere/ks-devops/pull/651/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% <100.00%> (ø)` | |
| [pkg/kapis/devops/v1alpha3/register.go](https://codecov.io/gh/kubesphere/ks-devops/pull/651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9yZWdpc3Rlci5nbw==) | `100.00% <100.00%> (ø)` | |
| [pkg/kapis/devops/v1alpha3/steptemplate/route.go](https://codecov.io/gh/kubesphere/ks-devops/pull/651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9zdGVwdGVtcGxhdGUvcm91dGUuZ28=) | `100.00% <100.00%> (ø)` | |
| [.../jenkins/devopsproject/devopsproject\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9kZXZvcHNwcm9qZWN0L2Rldm9wc3Byb2plY3RfY29udHJvbGxlci5nbw==) | `43.22% <0.00%> (+2.60%)` | :arrow_up: |

------

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


Patch:
diff --git a/config/crd/bases/devops.kubesphere.io_clustersteptemplates.yaml b/config/crd/bases/devops.kubesphere.io_clustersteptemplates.yaml
new file mode 100644
index 0000000..0b4ae33
--- /dev/null
+++ b/config/crd/bases/devops.kubesphere.io_clustersteptemplates.yaml
@@ -0,0 +1,91 @@
+
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.6.2
+  creationTimestamp: null
+  name: clustersteptemplates.devops.kubesphere.io
+spec:
+  group: devops.kubesphere.io
+  names:
+    kind: ClusterStepTemplate
+    listKind: ClusterStepTemplateList
+    plural: clustersteptemplates
+    singular: clustersteptemplate
+  scope: Cluster
+  versions:
+  - name: v1alpha3
+    schema:
+      openAPIV3Schema:
+        description: ClusterStepTemplate is the Schema for the steptemplates API
+        properties:
+          apiVersion:
+            description: 'APIVersion defines the versioned schema of this representation
+              of an object. Servers should convert recognized schemas to the latest
+              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
+            type: string
+          kind:
+            description: 'Kind is a string value representing the REST resource this
+              object represents. Servers may infer this from the endpoint the client
+              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: StepTemplateSpec defines the desired state of ClusterStepTemplate
+            properties:
+              container:
+                type: string
+              parameters:
+                items:
+                  properties:
+                    defaultValue:
+                      type: string
+                    display:
+                      type: string
+                    name:
+                      type: string
+                    required:
+                      type: boolean
+                  required:
+                  - name
+                  type: object
+                type: array
+              runtime:
+                type: string
+              secret:
+                properties:
+                  mapping:
+                    additionalProperties:
+                      type: string
+                    type: object
+                  type:
+                    type: string
+                  wrap:
+                    type: boolean
+                type: object
+              template:
+                type: string
+            type: object
+          status:
+            description: StepTemplateStatus defines the observed state of ClusterStepTemplate
+            properties:
+              phase:
+                description: StepTemplatePhase represents the phase of the Step template
+                type: string
+            required:
+            - phase
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
+status:
+  acceptedNames:
+    kind: ""
+    plural: ""
+  conditions: []
+  storedVersions: []
diff --git a/config/crd/bases/devops.kubesphere.io_pipelines.yaml b/config/crd/bases/devops.kubesphere.io_pipelines.yaml
index 5a33f15..62243d3 100644
--- a/config/crd/bases/devops.kubesphere.io_pipelines.yaml
+++ b/config/crd/bases/devops.kubesphere.io_pipelines.yaml
@@ -307,8 +307,6 @@ spec:
                           type: string
                         description:
                           type: string
-                        isQuoted:
-                          type: boolean
                         name:
                           type: string
                         type:
@@ -318,30 +316,6 @@ spec:
                       - type
                       type: object
                     type: array
-                  parametersFrom:
-                    items:
-                      properties:
-                        apiGroup:
-                          description: APIGroup is the group for the resource being
-                            referenced. If APIGroup is not specified, the specified
-                            Kind must be in the core API group. For any other third-party
-                            types, APIGroup is required.
-                          type: string
-                        kind:
-                          description: Kind is the type of resource being referenced
-                          type: string
-                        mode:
-                          type: string
-                        name:
-                          description: Name is the name of resource being referenced
-                          type: string
-                        valuesKey:
-                          type: string
-                      required:
-                      - kind
-                      - name
-                      type: object
-                    type: array
                   remote_trigger:
                     properties:
                       token:
diff --git a/config/samples/devops_v1alpha3_steptemplate.yaml b/config/samples/devops_v1alpha3_steptemplate.yaml
new file mode 100644
index 0000000..9ea553d
--- /dev/null
+++ b/config/samples/devops_v1alpha3_steptemplate.yaml
@@ -0,0 +1,8 @@
+apiVersion: devops.kubesphere.io/v1alpha3
+kind: ClusterStepTemplate
+metadata:
+  name: steptemplate-sample
+spec:
+  runtime: shell
+  template: |
+    echo 1
diff --git a/docs/README.md b/docs/README.md
index 1a429bc..ed3d00e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -8,3 +8,9 @@ This is the right place if you want to know more details about `ks-devops`.
 * [Swagger Support](swagger.md)
 * [Addon management](addon.md)
 * [Pipeline Template Design](pipeline-template.md)
+
+## Create a new CRD
+
+```shell
+kubebuilder create api --group devops.kubesphere.io --version v1alpha3 --kind Todo
+```
\ No newline at end of file
diff --git a/pkg/api/devops/v1alpha3/steptemplate_types.go b/pkg/api/devops/v1alpha3/steptemplate_types.go
new file mode 100644
index 0000000..1f03b12
--- /dev/null
+++ b/pkg/api/devops/v1alpha3/steptemplate_types.go
@@ -0,0 +1,96 @@
+/*
+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 v1alpha3
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// StepTemplateSpec defines the desired state of ClusterStepTemplate
+type StepTemplateSpec struct {
+	Secret     SecretInStep      `json:"secret,omitempty"`
+	Container  string            `json:"container,omitempty"`
+	Runtime    string            `json:"runtime,omitempty"`
+	Template   string            `json:"template,omitempty"`
+	Parameters []ParameterInStep `json:"parameters,omitempty"`
+}
+
+// SecretInStep is the secret which used in a step
+type SecretInStep struct {
+	Type    string            `json:"type,omitempty"`
+	Wrap    bool              `json:"wrap,omitempty"`
+	Mapping map[string]string `json:"mapping,omitempty"`
+}
+
+// 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"`
+}
+
+// StepTemplateStatus defines the observed state of ClusterStepTemplate
+type StepTemplateStatus struct {
+	Phase StepTemplatePhase `json:"phase"`
+}
+
+// StepTemplatePhase represents the phase of the Step template
+type StepTemplatePhase string
+
+var (
+	// StepTemplatePhaseReady indicates the step template is ready to use
+	StepTemplatePhaseReady StepTemplatePhase = "ready"
+	// StepTemplatePhaseInit indicates the step template is not ready to use
+	StepTemplatePhaseInit StepTemplatePhase = "init"
+)
+
+//+kubebuilder:object:root=true
+//+kubebuilder:subresource:status
+//+kubebuilder:resource:scope=Cluster
+
+// ClusterStepTemplate is the Schema for the steptemplates API
+type ClusterStepTemplate struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   StepTemplateSpec   `json:"spec,omitempty"`
+	Status StepTemplateStatus `json:"status,omitempty"`
+}
+
+//+kubebuilder:object:root=true
+
+// ClusterStepTemplateList contains a list of ClusterStepTemplate
+type ClusterStepTemplateList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []ClusterStepTemplate `json:"items"`
+}
+
+// DefaultSecretKeyMapping mainly used as the Jenkinsfile environment variables
+var DefaultSecretKeyMapping = map[string]string{
+	"passwordVariable":   "PASSWORDVARIABLE",
+	"usernameVariable":   "USERNAMEVARIABLE",
+	"variable":           "VARIABLE",
+	"sshUserPrivateKey":  "SSHUSERPRIVATEKEY",
+	"keyFileVariable":    "KEYFILEVARIABLE",
+	"passphraseVariable": "PASSPHRASEVARIABLE",
+}
+
+func init() {
+	SchemeBuilder.Register(&ClusterStepTemplate{}, &ClusterStepTemplateList{})
+}
diff --git a/pkg/api/devops/v1alpha3/zz_generated.deepcopy.go b/pkg/api/devops/v1alpha3/zz_generated.deepcopy.go
index 0608c5b..de24385 100644
--- a/pkg/api/devops/v1alpha3/zz_generated.deepcopy.go
+++ b/pkg/api/devops/v1alpha3/zz_generated.deepcopy.go
@@ -319,6 +319,65 @@ func (in *BitbucketServerSource) DeepCopy() *BitbucketServerSource {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterStepTemplate) DeepCopyInto(out *ClusterStepTemplate) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	out.Status = in.Status
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStepTemplate.
+func (in *ClusterStepTemplate) DeepCopy() *ClusterStepTemplate {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterStepTemplate)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ClusterStepTemplate) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ClusterStepTemplateList) DeepCopyInto(out *ClusterStepTemplateList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]ClusterStepTemplate, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterStepTemplateList.
+func (in *ClusterStepTemplateList) DeepCopy() *ClusterStepTemplateList {
+	if in == nil {
+		return nil
+	}
+	out := new(ClusterStepTemplateList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *ClusterStepTemplateList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ClusterTemplate) DeepCopyInto(out *ClusterTemplate) {
 	*out = *in
@@ -927,6 +986,21 @@ func (in *ParameterDefinition) DeepCopy() *ParameterDefinition {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ParameterInStep) DeepCopyInto(out *ParameterInStep) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ParameterInStep.
+func (in *ParameterInStep) DeepCopy() *ParameterInStep {
+	if in == nil {
+		return nil
+	}
+	out := new(ParameterInStep)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ParameterValidation) DeepCopyInto(out *ParameterValidation) {
 	*out = *in
@@ -1234,6 +1308,28 @@ func (in *SCM) DeepCopy() *SCM {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *SecretInStep) DeepCopyInto(out *SecretInStep) {
+	*out = *in
+	if in.Mapping != nil {
+		in, out := &in.Mapping, &out.Mapping
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretInStep.
+func (in *SecretInStep) DeepCopy() *SecretInStep {
+	if in == nil {
+		return nil
+	}
+	out := new(SecretInStep)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SignatureKey) DeepCopyInto(out *SignatureKey) {
 	*out = *in
@@ -1264,6 +1360,42 @@ func (in *SingleSvnSource) DeepCopy() *SingleSvnSource {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StepTemplateSpec) DeepCopyInto(out *StepTemplateSpec) {
+	*out = *in
+	in.Secret.DeepCopyInto(&out.Secret)
+	if in.Parameters != nil {
+		in, out := &in.Parameters, &out.Parameters
+		*out = make([]ParameterInStep, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepTemplateSpec.
+func (in *StepTemplateSpec) DeepCopy() *StepTemplateSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(StepTemplateSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *StepTemplateStatus) DeepCopyInto(out *StepTemplateStatus) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StepTemplateStatus.
+func (in *StepTemplateStatus) DeepCopy() *StepTemplateStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(StepTemplateStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *SvnSource) DeepCopyInto(out *SvnSource) {
 	*out = *in
diff --git a/pkg/kapis/devops/v1alpha3/register.go b/pkg/kapis/devops/v1alpha3/register.go
index 23041ac..3c564af 100644
--- a/pkg/kapis/devops/v1alpha3/register.go
+++ b/pkg/kapis/devops/v1alpha3/register.go
@@ -21,6 +21,7 @@ package v1alpha3
 import (
 	"github.com/jenkins-zh/jenkins-client/pkg/core"
 	"kubesphere.io/devops/pkg/jwt/token"
+	"kubesphere.io/devops/pkg/kapis/devops/v1alpha3/steptemplate"
 	"net/http"
 
 	"github.com/emicklei/go-restful"
@@ -69,6 +70,9 @@ func AddToContainer(container *restful.Container, devopsClient devopsClient.Inte
 		template.RegisterRoutes(service, &common.Options{
 			GenericClient: client,
 		})
+		steptemplate.RegisterRoutes(service, &common.Options{
+			GenericClient: client,
+		})
 		webhook.RegisterWebhooks(client, service, tokenIssue, jenkins)
 		container.Add(service)
 	}
diff --git a/pkg/kapis/devops/v1alpha3/steptemplate/handler.go b/pkg/kapis/devops/v1alpha3/steptemplate/handler.go
new file mode 100644
index 0000000..e809719
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha3/steptemplate/handler.go
@@ -0,0 +1,179 @@
+/*
+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 steptemplate
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"github.com/emicklei/go-restful"
+	"html/template"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"net/http"
+)
+
+func (h *handler) clusterStepTemplates(req *restful.Request, resp *restful.Response) {
+	ctx := context.TODO()
+
+	clusterStepTemplateList := &v1alpha3.ClusterStepTemplateList{}
+	err := h.List(ctx, clusterStepTemplateList)
+	writeResponse(clusterStepTemplateList, err, resp)
+}
+
+func (h *handler) getClusterStepTemplate(req *restful.Request, resp *restful.Response) {
+	ctx := context.TODO()
+	name := req.PathParameter(ClusterStepTemplate.Data().Name)
+
+	clusterStepTemplate := &v1alpha3.ClusterStepTemplate{}
+	err := h.Get(ctx, types.NamespacedName{Name: name}, clusterStepTemplate)
+	writeResponse(clusterStepTemplate, err, resp)
+}
+
+func (h *handler) renderClusterStepTemplate(req *restful.Request, resp *restful.Response) {
+	ctx := context.TODO()
+	name := req.PathParameter(ClusterStepTemplate.Data().Name)
+
+	var err error
+	clusterStepTemplate := &v1alpha3.ClusterStepTemplate{}
+	if err = h.Get(ctx, types.NamespacedName{Name: name}, clusterStepTemplate); err != nil {
+		_ = resp.WriteError(http.StatusInternalServerError, err)
+		return
+	}
+
+	var secret *v1.Secret
+	if secret, err = h.getSecret(req); err != nil {
+		// TODO considering have logger output instead of the std output
+		fmt.Printf("something goes wrong when getting secret, error: %v\n", err)
+	}
+
+	// get default param value from the template
+	param := make(map[string]string)
+	for i := range clusterStepTemplate.Spec.Parameters {
+		item := clusterStepTemplate.Spec.Parameters[i]
+		if item.DefaultValue != "" {
+			param[item.Name] = item.DefaultValue
+		}
+	}
+
+	// get the parameters from request
+	if err = req.ReadEntity(&param); err != nil {
+		// TODO considering have logger output instead of the std output
+		fmt.Printf("something goes wrong when getting parameter from request body, error: %v\n", err)
+	}
+
+	var output string
+	output, err = stepTemplateRender(&clusterStepTemplate.Spec, param, secret)
+	writeResponse(map[string]string{
+		"data": output,
+	}, err, resp)
+}
+
+func (h *handler) getSecret(req *restful.Request) (secret *v1.Secret, err error) {
+	secretName := req.QueryParameter(SecretNameQueryParameter.Data().Name)
+	secretNamespace := req.QueryParameter(SecretNamespaceQueryParameter.Data().Name)
+	if secretName != "" || secretNamespace != "" {
+		secret = &v1.Secret{}
+		err = h.Get(context.Background(), types.NamespacedName{
+			Namespace: secretNamespace,
+			Name:      secretName,
+		}, secret)
+	}
+	return
+}
+
+func writeResponse(object interface{}, err error, resp *restful.Response) {
+	if err == nil {
+		_ = resp.WriteAsJson(object)
+	} else {
+		_ = resp.WriteError(http.StatusInternalServerError, err)
+	}
+}
+
+func stepTemplateRender(stepTemplate *v1alpha3.StepTemplateSpec, param map[string]string, secret *v1.Secret) (output string, err error) {
+	switch stepTemplate.Runtime {
+	case "dsl":
+		output, err = dslRender(stepTemplate.Template, param, secret)
+	case "shell":
+		fallthrough
+	default:
+		output, err = shellRender(stepTemplate.Template, param, secret)
+	}
+
+	if stepTemplate.Secret.Wrap {
+		output = wrapWithCredential(stepTemplate.Secret.Type, secret.Name, output)
+	}
+
+	if err == nil && stepTemplate.Container != "" {
+		output = fmt.Sprintf(`container("%s") {
+	%s
+}`, stepTemplate.Container, output)
+	}
+	return
+}
+
+func wrapWithCredential(secretType, secretName, target string) string {
+	switch secretType {
+	case string(v1.SecretTypeBasicAuth), string(v1alpha3.SecretTypeBasicAuth):
+		return fmt.Sprintf(`withCredential[usernamePassword(credentialsId : "%s" ,passwordVariable : 'PASSWORDVARIABLE' ,usernameVariable : 'USERNAMEVARIABLE')]) {
+	%s
+}`, secretName, target)
+	case string(v1.SecretTypeBootstrapToken), string(v1alpha3.SecretTypeSecretText):
+		return fmt.Sprintf(`withCredential[string(credentialsId : "%s" ,variable : 'VARIABLE')]) {
+	%s
+}`, secretName, target)
+	case string(v1alpha3.SecretTypeKubeConfig):
+		return fmt.Sprintf(`withCredential[kubeconfigContent(credentialsId : "%s" ,variable : 'VARIABLE')]) {
+	%s
+}`, secretName, target)
+	case string(v1alpha3.SecretTypeSSHAuth):
+		return fmt.Sprintf(`withCredential[sshUserPrivateKey(credentialsId : "%s" ,keyFileVariable : 'KEYFILEVARIABLE' ,passphraseVariable : 'PASSPHRASEVARIABLE' ,usernameVariable : 'SSHUSERPRIVATEKEY')]) {
+	%s
+}`, secretName, target)
+	}
+	return target
+}
+
+func dslRender(dslTpl string, param map[string]string, secret *v1.Secret) (output string, err error) {
+	output = dslTpl
+
+	var tpl *template.Template
+	if tpl, err = template.New("shell").Parse(output); err != nil {
+		return
+	}
+
+	data := map[string]interface{}{
+		"param":  param,
+		"secret": secret,
+	}
+
+	buf := bytes.NewBuffer([]byte{})
+	if err = tpl.Execute(buf, data); err == nil {
+		output = buf.String()
+	}
+	return
+}
+
+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
+'''`, output)
+	}
+	return
+}
diff --git a/pkg/kapis/devops/v1alpha3/steptemplate/route.go b/pkg/kapis/devops/v1alpha3/steptemplate/route.go
new file mode 100644
index 0000000..8951a08
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha3/steptemplate/route.go
@@ -0,0 +1,55 @@
+/*
+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 steptemplate
+
+import (
+	"github.com/emicklei/go-restful"
+	"kubesphere.io/devops/pkg/kapis/devops/v1alpha3/common"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+)
+
+type handler struct {
+	client.Client
+}
+
+var (
+	// ClusterStepTemplate is path parameter definition of clustersteptemplate.
+	ClusterStepTemplate = restful.PathParameter("clustersteptemplate", "The name of clustersteptemplate")
+	// SecretNameQueryParameter is a query parameter of secret
+	SecretNameQueryParameter = restful.QueryParameter("secret", "The name of a secret")
+	// SecretNamespaceQueryParameter is a query parameter of the secret namespace
+	SecretNamespaceQueryParameter = restful.QueryParameter("secretNamespace", "The namespace of a secret")
+)
+
+// RegisterRoutes registry the handlers of the stepTemplates
+func RegisterRoutes(service *restful.WebService, options *common.Options) {
+	h := &handler{options.GenericClient}
+	service.Route(service.GET("/clustersteptemplates").
+		To(h.clusterStepTemplates).
+		Doc("Return the cluster level stepTemplate list"))
+	service.Route(service.GET("/clustersteptemplates/{clustersteptemplate}").
+		To(h.getClusterStepTemplate).
+		Param(ClusterStepTemplate).
+		Doc("Return a specific ClusterStepTemplate"))
+	service.Route(service.GET("/clustersteptemplates/{clustersteptemplate}/render").
+		To(h.renderClusterStepTemplate).
+		Param(ClusterStepTemplate).
+		Param(SecretNameQueryParameter).
+		Param(SecretNamespaceQueryParameter).
+		Reads(map[string]string{}, "The parameters of the ClusterStepTemplate").
+		Doc("Render a specific ClusterStepTemplate, then return it"))
+}


Test patch:
diff --git a/pkg/kapis/devops/v1alpha3/steptemplate/handler_test.go b/pkg/kapis/devops/v1alpha3/steptemplate/handler_test.go
new file mode 100644
index 0000000..9248448
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha3/steptemplate/handler_test.go
@@ -0,0 +1,175 @@
+/*
+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 steptemplate
+
+import (
+	v1 "k8s.io/api/core/v1"
+	v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"testing"
+)
+
+func Test_handler_stepTemplateRender(t *testing.T) {
+	stepTemplate := &v1alpha3.StepTemplateSpec{
+		Template: `echo 1`,
+		Runtime:  "shell",
+	}
+	stepTemplateWithParameters := &v1alpha3.StepTemplateSpec{
+		Template: `docker login -u $USERNAMEVARIABLE -p $PASSWORDVARIABLE
+docker build {{.param.context}} -t {{.param.tag}}`,
+		Runtime:   "shell",
+		Container: "base",
+		Secret: v1alpha3.SecretInStep{
+			Wrap: true,
+			Type: string(v1.SecretTypeBasicAuth),
+		},
+		Parameters: []v1alpha3.ParameterInStep{{
+			Name: "context",
+		}, {
+			Name: "tag",
+		}},
+	}
+	stepTemplateWithDSL := &v1alpha3.StepTemplateSpec{
+		Template: `echo "1"`,
+		Runtime:  "dsl",
+	}
+
+	type args struct {
+		stepTemplate *v1alpha3.StepTemplateSpec
+		param        map[string]string
+		secret       *v1.Secret
+	}
+	tests := []struct {
+		name       string
+		args       args
+		wantOutput string
+		wantErr    bool
+	}{{
+		name: "a simple shell without any parameters",
+		args: args{
+			stepTemplate: stepTemplate,
+		},
+		wantOutput: `sh '''
+echo 1
+'''`,
+		wantErr: false,
+	}, {
+		name: "docker build command with parameters",
+		args: args{
+			stepTemplate: stepTemplateWithParameters,
+			param: map[string]string{
+				"context": "dir",
+				"tag":     "image:tag",
+			},
+			secret: &v1.Secret{
+				ObjectMeta: v12.ObjectMeta{
+					Name: "docker",
+				},
+				Type: v1.SecretTypeBasicAuth,
+				Data: map[string][]byte{
+					v1.BasicAuthUsernameKey: []byte("username"),
+					v1.BasicAuthPasswordKey: []byte("password"),
+				},
+			},
+		},
+		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,
+	}, {
+		name: "a simple dsl without any parameters",
+		args: args{
+			stepTemplate: stepTemplateWithDSL,
+		},
+		wantOutput: `echo "1"`,
+		wantErr:    false,
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotOutput, err := stepTemplateRender(tt.args.stepTemplate, tt.args.param, tt.args.secret)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("stepTemplateRender() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if gotOutput != tt.wantOutput {
+				t.Errorf("stepTemplateRender() gotOutput = %v, want %v", gotOutput, tt.wantOutput)
+			}
+		})
+	}
+}
+
+func Test_wrapWithCredential(t *testing.T) {
+	type args struct {
+		secretType string
+		secretName string
+		target     string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{{
+		name: "unknown secret type",
+		args: args{
+			secretType: "unknown",
+			target:     "echo 1",
+		},
+		want: "echo 1",
+	}, {
+		name: "secret as kubeconfig type",
+		args: args{
+			secretType: string(v1alpha3.SecretTypeKubeConfig),
+			secretName: "config",
+			target:     "echo 1",
+		},
+		want: `withCredential[kubeconfigContent(credentialsId : "config" ,variable : 'VARIABLE')]) {
+	echo 1
+}`,
+	}, {
+		name: "secret as secret text type",
+		args: args{
+			secretType: string(v1alpha3.SecretTypeSecretText),
+			secretName: "config",
+			target:     "echo 1",
+		},
+		want: `withCredential[string(credentialsId : "config" ,variable : 'VARIABLE')]) {
+	echo 1
+}`,
+	}, {
+		name: "secret as ssh auth type",
+		args: args{
+			secretType: string(v1alpha3.SecretTypeSSHAuth),
+			secretName: "config",
+			target:     "echo 1",
+		},
+		want: `withCredential[sshUserPrivateKey(credentialsId : "config" ,keyFileVariable : 'KEYFILEVARIABLE' ,passphraseVariable : 'PASSPHRASEVARIABLE' ,usernameVariable : 'SSHUSERPRIVATEKEY')]) {
+	echo 1
+}`,
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := wrapWithCredential(tt.args.secretType, tt.args.secretName, tt.args.target); got != tt.want {
+				t.Errorf("wrapWithCredential() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/kapis/devops/v1alpha3/steptemplate/route_test.go b/pkg/kapis/devops/v1alpha3/steptemplate/route_test.go
new file mode 100644
index 0000000..bcf04d9
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha3/steptemplate/route_test.go
@@ -0,0 +1,149 @@
+/*
+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 steptemplate
+
+import (
+	"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/runtime"
+	runtimeSchema "k8s.io/apimachinery/pkg/runtime/schema"
+	"kubesphere.io/devops/pkg/api"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	ksruntime "kubesphere.io/devops/pkg/apiserver/runtime"
+	"kubesphere.io/devops/pkg/kapis/devops/v1alpha3/common"
+	"net/http"
+	"net/http/httptest"
+	"sigs.k8s.io/controller-runtime/pkg/client/fake"
+	"testing"
+)
+
+func TestAPIs(t *testing.T) {
+	schema, err := v1alpha3.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+
+	err = v1.SchemeBuilder.AddToScheme(schema)
+	assert.Nil(t, err)
+
+	type args struct {
+		api     string
+		getBody func() io.Reader
+	}
+	tests := []struct {
+		name         string
+		args         args
+		getInstances func() []runtime.Object
+		wantCode     int
+		verify       func([]byte, *testing.T)
+	}{{
+		name: "an empty list of the clusterStepTemplate",
+		args: args{
+			api: "/clustersteptemplates",
+		},
+		wantCode: http.StatusOK,
+	}, {
+		name: "the whole list of the clusterStepTemplate",
+		args: args{
+			api: "/clustersteptemplates",
+		},
+		getInstances: func() []runtime.Object {
+			return []runtime.Object{&v1alpha3.ClusterStepTemplate{}}
+		},
+		wantCode: http.StatusOK,
+	}, {
+		name: "get a clusterStepTemplate by name",
+		args: args{
+			api: "/clustersteptemplates/fake",
+		},
+		getInstances: func() []runtime.Object {
+			return []runtime.Object{&v1alpha3.ClusterStepTemplate{
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "fake",
+				},
+			}}
+		},
+		wantCode: http.StatusOK,
+	}, {
+		name: "render a clusterStepTemplate by name",
+		args: args{
+			api: "/clustersteptemplates/fake/render?secret=secret&secretNamespace=ns",
+		},
+		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": "sh '''\necho 2\n'''"
+}`, string(bytes))
+		},
+		wantCode: http.StatusOK,
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if tt.getInstances == nil {
+				tt.getInstances = func() []runtime.Object {
+					return []runtime.Object{}
+				}
+			}
+
+			ws := ksruntime.NewWebService(runtimeSchema.GroupVersion{Group: api.GroupName, Version: "v1alpha3"})
+			RegisterRoutes(ws, &common.Options{
+				GenericClient: fake.NewFakeClientWithScheme(schema, tt.getInstances()...),
+			})
+			container := restful.NewContainer()
+			container.Add(ws)
+
+			var requestBody io.Reader
+			if tt.args.getBody != nil {
+				requestBody = tt.args.getBody()
+			}
+
+			httpRequest, _ := http.NewRequest(http.MethodGet,
+				"http://fake.com/kapis/devops.kubesphere.io/v1alpha3"+tt.args.api, requestBody)
+			httpWriter := httptest.NewRecorder()
+			container.Dispatch(httpWriter, httpRequest)
+			assert.Equal(t, tt.wantCode, httpWriter.Code)
+
+			if tt.verify != nil {
+				tt.verify(httpWriter.Body.Bytes(), t)
+			}
+		})
+	}
+}