Repository: kubesphere/ks-devops
Pull request #483 (2022-03-03 07:05:09)

Support rendering Template with parameters
### What type of PR is this?

/kind feature
/kind api-change

### What this PR does / why we need it:

Support rendering Template with parameters. So that we could pass parameters to the template render API:

```json
{
  "parameters": [
    {
      "name": "cloneURL",
      "value": "https://github.com/halo-dev/halo.git"
    },
    {
      "name": "buildOnly",
      "value": "false"
    }
  ]
}
```

### Which issue(s) this PR fixes:

Fixes #

### Special notes for reviewers:

About parameters validations and default parametrs, I will handle them in the future.

#### How to test?

> Docker image for test: `johnniang/devops-apiserver:dev-v3.2.1-6c40b47`

1. Create a cluster for test
2. Install all CRDs and ClusterTemplate sample
    ```bash
     kubectl apply -f config/crd/bases/
    kubectl apply -f config/samples/devops_v1alpha1_clustertemplate.yaml 
    ```
4. Run DevOps APIServer
5. Try to render the ClusterTemplate sample:
    ```bash
    curl -X POST --header 'Content-Type: application/json' --header 'Accept: application/json' -d '{ \ 
       "parameters": [ \ 
         { \ 
           "name": "cloneURL", \ 
           "value": "https://github.com/halo-dev/halo.git" \ 
         }, \ 
         { \ 
           "name": "buildOnly", \ 
           "value": "false" \ 
         } \ 
       ] \ 
     }' 'http://localhost:9090/kapis/devops.kubesphere.io/v1alpha3/clustertemplates/clustertemplate-sample/render'
    ```

Please check the following list before waiting reviewers:

- [x] 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
- [x] 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
```

/cc @kubesphere/sig-devops 


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

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/483#" title="Author self-approved">JohnNiang</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **linuxsuren** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @linuxsuren` in a comment when ready.

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/483?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#483](https://codecov.io/gh/kubesphere/ks-devops/pull/483?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (6c40b47) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/6f5a8d610bca7cc4795053e870d0b6cfd3ba0833?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (6f5a8d6) will **decrease** coverage by `0.03%`.
> The diff coverage is `74.19%`.

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

```diff
@@            Coverage Diff             @@
##           master     #483      +/-   ##
==========================================
- Coverage   77.18%   77.14%   -0.04%     
==========================================
  Files         133      133              
  Lines       22731    22752      +21     
==========================================
+ Hits        17544    17552       +8     
- Misses       4994     5003       +9     
- Partials      193      197       +4     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `77.14% <74.19%> (-0.04%)` | :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/483?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [...vops/v1alpha3/template/cluster\_template\_handler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/483/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy90ZW1wbGF0ZS9jbHVzdGVyX3RlbXBsYXRlX2hhbmRsZXIuZ28=) | `80.76% <50.00%> (-10.14%)` | :arrow_down: |
| [...kapis/devops/v1alpha3/template/template\_handler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/483/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy90ZW1wbGF0ZS90ZW1wbGF0ZV9oYW5kbGVyLmdv) | `84.37% <50.00%> (-8.49%)` | :arrow_down: |
| [pkg/kapis/devops/v1alpha3/template/render.go](https://codecov.io/gh/kubesphere/ks-devops/pull/483/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy90ZW1wbGF0ZS9yZW5kZXIuZ28=) | `88.23% <88.23%> (-11.77%)` | :arrow_down: |
| [pkg/kapis/devops/v1alpha3/template/route.go](https://codecov.io/gh/kubesphere/ks-devops/pull/483/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy90ZW1wbGF0ZS9yb3V0ZS5nbw==) | `100.00% <100.00%> (ø)` | |
| [controllers/s2irun/s2irun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/483/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvczJpcnVuL3MyaXJ1bl9jb250cm9sbGVyLmdv) | `33.57% <0.00%> (-3.65%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/483?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/483?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [6f5a8d6...6c40b47](https://codecov.io/gh/kubesphere/ks-devops/pull/483?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/cmd/controller/app/server.go b/cmd/controller/app/server.go
index 7772ab5..e7011f9 100644
--- a/cmd/controller/app/server.go
+++ b/cmd/controller/app/server.go
@@ -121,8 +121,13 @@ func Run(s *options.DevOpsControllerManagerOptions, stopCh <-chan struct{}) erro
 	if s.JenkinsOptions != nil && len(s.JenkinsOptions.Host) != 0 {
 		// Make sure that Jenkins host is not empty
 		devopsClient, err = jclient.NewJenkinsClient(s.JenkinsOptions)
-		if err != nil {
-			return fmt.Errorf("failed to connect jenkins, please check jenkins status, error: %v", err)
+		if !s.JenkinsOptions.SkipVerify && err != nil {
+			errMsg := fmt.Sprintf("failed to connect jenkins, please check jenkins status, error: %v", err)
+			if s.JenkinsOptions.SkipVerify {
+				fmt.Println(errMsg)
+			} else {
+				return fmt.Errorf(errMsg)
+			}
 		}
 	}
 
diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml
index 1d163de..8fbdc53 100644
--- a/config/crd/kustomization.yaml
+++ b/config/crd/kustomization.yaml
@@ -3,7 +3,6 @@
 # It should be run by config/default
 resources:
 - bases/devops.kubesphere.io_pipelines.yaml
-- bases/devops.kubesphere.io_fakes.yaml
 - bases/devops.kubesphere.io_devopsprojects.yaml
 - bases/devops.kubesphere.io_s2ibinaries.yaml
 - bases/devops.kubesphere.io_s2ibuilders.yaml
diff --git a/config/samples/devops_v1alpha1_application.yaml b/config/samples/devops_v1alpha1_application.yaml
index c762f53..4ab2fca 100644
--- a/config/samples/devops_v1alpha1_application.yaml
+++ b/config/samples/devops_v1alpha1_application.yaml
@@ -1,4 +1,4 @@
-apiVersion: devops.kubesphere.io/v1alpha1
+apiVersion: gitops.kubesphere.io/v1alpha1
 kind: Application
 metadata:
   name: app
@@ -6,14 +6,14 @@ spec:
   argoApp:
     project: default
     source:
-      repoURL: https://github.com/kubesphere-sigs/ks-versions
+      repoURL: https://gitlab.com/LinuxSuRen1/argocd
       targetRevision: HEAD
-      path: .
+      path: test
       directory:
         recurse: true
     destination:
       server: https://kubernetes.default.svc
-      namespace: ks-version
+      namespace: default
     syncPolicy:
       automated:
         prune: true
diff --git a/config/samples/devops_v1alpha1_clustertemplate.yaml b/config/samples/devops_v1alpha1_clustertemplate.yaml
index 6603c5e..ce304ff 100644
--- a/config/samples/devops_v1alpha1_clustertemplate.yaml
+++ b/config/samples/devops_v1alpha1_clustertemplate.yaml
@@ -1,4 +1,4 @@
-apiVersion: devops.kubesphere.io/v1alpha1
+apiVersion: devops.kubesphere.io/v1alpha3
 kind: ClusterTemplate
 metadata:
   name: clustertemplate-sample
@@ -36,10 +36,9 @@ spec:
         stages {
             stage('Checkout') {
                 steps {
-                    checkout poll: false, scm: [$class: 'GitSCM', branches: [[name: '*/master']], extensions: [[$class: 'CloneOption', depth: 1, noTags: true, reference: '', shallow: true], [$class: 'SubmoduleOption', depth: 1, disableSubmodules: false, parentCredentials: false, recursiveSubmodules: true, reference: '', shallow: true, trackingSubmodules: false]], userRemoteConfigs: [[url: '{{ .params.gitCloneURL }}']]]
+                    git branch: '{{.params.revision}}', url: '{{.params.cloneURL}}'
                 }
             }
-            {{if not .buildOnly}}
             stage('Gradle Check') {
                 steps {
                     container('gradle') {
@@ -54,10 +53,12 @@ spec:
                     }
                 }
             }
+            {{if ne .params.buildOnly "true"}}
             stage('Archive Assets') {
                 steps {
                     archiveArtifacts '**/build/libs/*.jar'
                 }
             }
+            {{end}}
         }
     }
\ No newline at end of file
diff --git a/pkg/apis/apis.go b/pkg/apis/apis.go
index 4e10f6e..35c8e73 100644
--- a/pkg/apis/apis.go
+++ b/pkg/apis/apis.go
@@ -21,6 +21,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"kubesphere.io/devops/pkg/api/gitops/v1alpha1"
 )
 
 // addToSchemes may be used to add all resources defined in the project to a Scheme
@@ -33,5 +34,7 @@ func AddToScheme(s *runtime.Scheme) {
 
 func init() {
 	// Register the types with the Scheme so the components can map objects to GroupVersionKinds and back
-	addToSchemes = append(addToSchemes, v1alpha3.SchemeBuilder.AddToScheme)
+	addToSchemes = append(addToSchemes,
+		v1alpha3.SchemeBuilder.AddToScheme,
+		v1alpha1.SchemeBuilder.AddToScheme)
 }
diff --git a/pkg/client/devops/jclient/jenkins.go b/pkg/client/devops/jclient/jenkins.go
index 096144a..adacb32 100644
--- a/pkg/client/devops/jclient/jenkins.go
+++ b/pkg/client/devops/jclient/jenkins.go
@@ -31,8 +31,9 @@ type JenkinsClient struct {
 
 // ApplyNewSource apply a new source
 func (j *JenkinsClient) ApplyNewSource(s string) (err error) {
-	client := casc.Manager{
-		JenkinsCore: j.Core,
+	client := casc.Manager{}
+	if j != nil {
+		client.JenkinsCore = j.Core
 	}
 	if err = client.CheckNewSource(s); err == nil {
 		err = client.Replace(s)
diff --git a/pkg/client/devops/jenkins/options.go b/pkg/client/devops/jenkins/options.go
index edcfd00..07e2421 100644
--- a/pkg/client/devops/jenkins/options.go
+++ b/pkg/client/devops/jenkins/options.go
@@ -32,6 +32,7 @@ type Options struct {
 	Namespace       string        `json:"namespace,omitempty" yaml:"namespace"`
 	WorkerNamespace string        `json:"workerNamespace,omitempty" yaml:"workerNamespace"`
 	ReloadCasCDelay time.Duration `json:"reloadCasCDelay,omitempty" yaml:"reloadCasCDelay"`
+	SkipVerify      bool
 }
 
 // NewJenkinsOptions returns a `zero` instance
@@ -90,6 +91,8 @@ func (s *Options) AddFlags(fs *pflag.FlagSet, c *Options) {
 
 	fs.IntVar(&s.MaxConnections, "jenkins-max-connections", c.MaxConnections, ""+
 		"Maximum allowed connections to Jenkins. ")
+	fs.BoolVar(&s.SkipVerify, "jenkins-skip-verify", false,
+		"Indicate if you want to skip the Jenkins connection verify")
 
 	fs.StringVar(&s.Namespace, "namespace", c.Namespace, "Namespace where devops system is in.")
 	fs.StringVar(&s.WorkerNamespace, "worker-namespace", c.WorkerNamespace, "Namespace where Jenkins agent workers are in.")
diff --git a/pkg/client/git/client.go b/pkg/client/git/client.go
index 0749ac4..f405c53 100644
--- a/pkg/client/git/client.go
+++ b/pkg/client/git/client.go
@@ -82,10 +82,12 @@ func (c *ClientFactory) getTokenFromSecret(secretRef *v1.SecretReference) (token
 	}
 
 	switch gitSecret.Type {
-	case v1.SecretTypeBasicAuth:
+	case v1.SecretTypeBasicAuth, "credential.devops.kubesphere.io/basic-auth":
 		token = string(gitSecret.Data[v1.BasicAuthPasswordKey])
 	case v1.SecretTypeOpaque:
 		token = string(gitSecret.Data[v1.ServiceAccountTokenKey])
+	case "credential.devops.kubesphere.io/secret-text":
+		token = string(gitSecret.Data["secret"])
 	}
 	return
 }
diff --git a/pkg/kapis/devops/v1alpha3/template/cluster_template_handler.go b/pkg/kapis/devops/v1alpha3/template/cluster_template_handler.go
index 7cda246..02e9010 100644
--- a/pkg/kapis/devops/v1alpha3/template/cluster_template_handler.go
+++ b/pkg/kapis/devops/v1alpha3/template/cluster_template_handler.go
@@ -18,6 +18,7 @@ package template
 import (
 	"context"
 	"github.com/emicklei/go-restful"
+	"io"
 	"k8s.io/apimachinery/pkg/runtime"
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
@@ -34,7 +35,15 @@ func (h *handler) handleQueryClusterTemplates(request *restful.Request, response
 
 func (h *handler) handleRenderClusterTemplate(request *restful.Request, response *restful.Response) {
 	templateName := request.PathParameter(ClusterTemplatePathParameter.Data().Name)
-	kapis.ResponseWriter{Response: response}.WriteEntityOrError(h.renderClusterTemplate(templateName))
+
+	//var parameters []Parameter
+	var renderBody RenderBody
+	if err := request.ReadEntity(&renderBody); err != nil && err != io.EOF {
+		kapis.HandleError(request, response, err)
+		return
+	}
+
+	kapis.ResponseWriter{Response: response}.WriteEntityOrError(h.renderClusterTemplate(templateName, renderBody.Parameters))
 }
 
 func (h *handler) queryClusterTemplates(commonQuery *query.Query) (*api.ListResult, error) {
@@ -57,12 +66,13 @@ func (h *handler) getClusterTemplate(templateName string) (*v1alpha3.ClusterTemp
 	return template, nil
 }
 
-func (h *handler) renderClusterTemplate(templateName string) (v1alpha3.TemplateObject, error) {
+func (h *handler) renderClusterTemplate(templateName string, parameters []Parameter) (v1alpha3.TemplateObject, error) {
 	template, err := h.getClusterTemplate(templateName)
 	if err != nil {
 		return nil, err
 	}
-	return render(template), nil
+
+	return render(template, parameters)
 }
 
 func clusterTemplatesToObjects(templates []v1alpha3.ClusterTemplate) []runtime.Object {
diff --git a/pkg/kapis/devops/v1alpha3/template/render.go b/pkg/kapis/devops/v1alpha3/template/render.go
index 50b23ee..77de4e6 100644
--- a/pkg/kapis/devops/v1alpha3/template/render.go
+++ b/pkg/kapis/devops/v1alpha3/template/render.go
@@ -16,19 +16,47 @@
 package template
 
 import (
+	"bytes"
 	"kubesphere.io/devops/pkg/api/devops"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	tmpl "text/template"
 )
 
-func render(templateObjectCopy v1alpha3.TemplateObject) v1alpha3.TemplateObject {
-	templateObjectCopy = templateObjectCopy.DeepCopyObject().(v1alpha3.TemplateObject)
-	// TODO Render template using parameters
-	template := templateObjectCopy.TemplateSpec().Template
+const parametersKey = "params"
 
-	// set template into annotations
-	if templateObjectCopy.GetAnnotations() == nil {
-		templateObjectCopy.SetAnnotations(map[string]string{})
+// Parameter is a pair of name and value.
+type Parameter struct {
+	Name  string      `json:"name"`
+	Value interface{} `json:"value"`
+}
+
+func render(templateObject v1alpha3.TemplateObject, parameters []Parameter) (v1alpha3.TemplateObject, error) {
+	templateObject = templateObject.DeepCopyObject().(v1alpha3.TemplateObject)
+	rawTemplate := templateObject.TemplateSpec().Template
+	template := tmpl.New("pipeline-template")
+	if _, err := template.Parse(rawTemplate); err != nil {
+		return nil, err
+	}
+
+	// TODO Verify required parameters and default parameters
+	// check the required parameters
+	parameterMap := map[string]interface{}{}
+	for _, parameter := range parameters {
+		parameterMap[parameter.Name] = parameter.Value
+	}
+
+	parametersData := map[string]map[string]interface{}{}
+	parametersData[parametersKey] = parameterMap
+
+	buffer := &bytes.Buffer{}
+	if err := template.Execute(buffer, parametersData); err != nil {
+		return nil, err
+	}
+	renderResult := buffer.String()
+
+	if templateObject.GetAnnotations() == nil {
+		templateObject.SetAnnotations(map[string]string{})
 	}
-	templateObjectCopy.GetAnnotations()[devops.GroupName+devops.RenderResultAnnoKey] = template
-	return templateObjectCopy
+	templateObject.GetAnnotations()[devops.GroupName+devops.RenderResultAnnoKey] = renderResult
+	return templateObject, nil
 }
diff --git a/pkg/kapis/devops/v1alpha3/template/route.go b/pkg/kapis/devops/v1alpha3/template/route.go
index dc384e7..339286a 100644
--- a/pkg/kapis/devops/v1alpha3/template/route.go
+++ b/pkg/kapis/devops/v1alpha3/template/route.go
@@ -40,6 +40,11 @@ type PageResult struct {
 	Total int                 `json:"total"`
 }
 
+// RenderBody is the model of request body of render API.
+type RenderBody struct {
+	Parameters []Parameter `json:"parameters"`
+}
+
 // RegisterRoutes is for registering template routes into WebService.
 func RegisterRoutes(service *restful.WebService, options *common.Options) {
 	handler := newHandler(options)
@@ -63,6 +68,7 @@ func RegisterRoutes(service *restful.WebService, options *common.Options) {
 		To(handler.handleRenderTemplate).
 		Param(common.DevopsPathParameter).
 		Param(TemplatePathParameter).
+		Reads(RenderBody{}).
 		Doc(fmt.Sprintf("Render template and return render result into annotations (%s/%s) inside template", devops.GroupName, devops.RenderResultAnnoKey)).
 		Returns(http.StatusOK, api.StatusOK, v1alpha3.Template{}).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsTemplateTag}))
@@ -77,6 +83,7 @@ func RegisterRoutes(service *restful.WebService, options *common.Options) {
 	service.Route(service.POST("/clustertemplates/{clustertemplate}/render").
 		To(handler.handleRenderClusterTemplate).
 		Param(ClusterTemplatePathParameter).
+		Reads(RenderBody{}).
 		Doc("Render cluster template.").
 		Returns(http.StatusOK, api.StatusOK, v1alpha3.ClusterTemplate{}).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsClusterTemplateTag}))
diff --git a/pkg/kapis/devops/v1alpha3/template/template_handler.go b/pkg/kapis/devops/v1alpha3/template/template_handler.go
index d32bf14..af15abf 100644
--- a/pkg/kapis/devops/v1alpha3/template/template_handler.go
+++ b/pkg/kapis/devops/v1alpha3/template/template_handler.go
@@ -18,6 +18,7 @@ package template
 import (
 	"context"
 	"github.com/emicklei/go-restful"
+	"io"
 	"k8s.io/apimachinery/pkg/runtime"
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
@@ -66,16 +67,21 @@ func (h *handler) getTemplate(devopsName, templateName string) (*v1alpha3.Templa
 func (h *handler) handleRenderTemplate(request *restful.Request, response *restful.Response) {
 	devopsName := request.PathParameter(common.DevopsPathParameter.Data().Name)
 	templateName := request.PathParameter(TemplatePathParameter.Data().Name)
+	var renderBody RenderBody
+	if err := request.ReadEntity(&renderBody); err != nil && err != io.EOF {
+		kapis.HandleError(request, response, err)
+		return
+	}
 
-	kapis.ResponseWriter{Response: response}.WriteEntityOrError(h.renderTemplate(devopsName, templateName))
+	kapis.ResponseWriter{Response: response}.WriteEntityOrError(h.renderTemplate(devopsName, templateName, renderBody.Parameters))
 }
 
-func (h *handler) renderTemplate(devopsName, templateName string) (v1alpha3.TemplateObject, error) {
+func (h *handler) renderTemplate(devopsName, templateName string, parameters []Parameter) (v1alpha3.TemplateObject, error) {
 	tmpl, err := h.getTemplate(devopsName, templateName)
 	if err != nil {
 		return nil, err
 	}
-	return render(tmpl), nil
+	return render(tmpl, parameters)
 }
 
 func templatesToObjects(templates []v1alpha3.Template) []runtime.Object {


Test patch:
diff --git a/pkg/client/git/client_test.go b/pkg/client/git/client_test.go
index 22f939c..814bafa 100644
--- a/pkg/client/git/client_test.go
+++ b/pkg/client/git/client_test.go
@@ -56,6 +56,26 @@ func TestGetClient(t *testing.T) {
 			v1.ServiceAccountTokenKey: []byte("token"),
 		},
 	}
+	textSecret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "opaqueSecret",
+			Namespace: "ns",
+		},
+		Type: "credential.devops.kubesphere.io/secret-text",
+		Data: map[string][]byte{
+			v1.ServiceAccountTokenKey: []byte("token"),
+		},
+	}
+	ksBasicAuthSecret := &v1.Secret{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "opaqueSecret",
+			Namespace: "ns",
+		},
+		Type: "credential.devops.kubesphere.io/basic-auth",
+		Data: map[string][]byte{
+			v1.BasicAuthPasswordKey: []byte("token"),
+		},
+	}
 	type fields struct {
 		provider  string
 		secretRef *v1.SecretReference
@@ -118,6 +138,28 @@ func TestGetClient(t *testing.T) {
 			assert.Nil(t, err, i)
 			return false
 		},
+	}, {
+		name: "gitlab provider - text secret",
+		fields: fields{
+			k8sClient: fake.NewFakeClientWithScheme(schema, textSecret.DeepCopy()),
+			provider:  "gitlab",
+			secretRef: &v1.SecretReference{Namespace: "ns", Name: "opaqueSecret"},
+		},
+		wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
+			assert.Nil(t, err, i)
+			return false
+		},
+	}, {
+		name: "gitlab provider - ks basic auth secret",
+		fields: fields{
+			k8sClient: fake.NewFakeClientWithScheme(schema, ksBasicAuthSecret.DeepCopy()),
+			provider:  "gitlab",
+			secretRef: &v1.SecretReference{Namespace: "ns", Name: "opaqueSecret"},
+		},
+		wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
+			assert.Nil(t, err, i)
+			return false
+		},
 	}}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
diff --git a/pkg/kapis/devops/v1alpha3/template/cluster_template_handler_test.go b/pkg/kapis/devops/v1alpha3/template/cluster_template_handler_test.go
index 80049c6..b8714b8 100644
--- a/pkg/kapis/devops/v1alpha3/template/cluster_template_handler_test.go
+++ b/pkg/kapis/devops/v1alpha3/template/cluster_template_handler_test.go
@@ -179,6 +179,7 @@ func Test_handler_handleRenderClusterTemplate(t *testing.T) {
 	}
 	createRequest := func(uri, templateName string) *restful.Request {
 		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
+		fakeRequest.Header.Add(restful.HEADER_ContentType, restful.MIME_JSON)
 		request := restful.NewRequest(fakeRequest)
 		request.PathParameters()[ClusterTemplatePathParameter.Data().Name] = templateName
 		return request
diff --git a/pkg/kapis/devops/v1alpha3/template/render_test.go b/pkg/kapis/devops/v1alpha3/template/render_test.go
index cb68b8c..a2cdc5e 100644
--- a/pkg/kapis/devops/v1alpha3/template/render_test.go
+++ b/pkg/kapis/devops/v1alpha3/template/render_test.go
@@ -16,6 +16,7 @@
 package template
 
 import (
+	"fmt"
 	"github.com/stretchr/testify/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"kubesphere.io/devops/pkg/api/devops"
@@ -24,38 +25,85 @@ import (
 )
 
 func Test_render(t *testing.T) {
+	createTemplate := func(name, template string) v1alpha3.TemplateObject {
+		return &v1alpha3.Template{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: name,
+			},
+			Spec: v1alpha3.TemplateSpec{
+				Template: template,
+			},
+		}
+	}
 	type args struct {
-		templateObject v1alpha3.TemplateObject
+		template   v1alpha3.TemplateObject
+		parameters []Parameter
 	}
 	tests := []struct {
-		name   string
-		args   args
-		verify func(t *testing.T, object v1alpha3.TemplateObject)
+		name    string
+		args    args
+		wantErr assert.ErrorAssertionFunc
+		verify  func(*testing.T, v1alpha3.TemplateObject)
 	}{{
-		name: "Should render template into annotations",
+		name: "Should render template without parameters",
 		args: args{
-			templateObject: &v1alpha3.Template{
-				ObjectMeta: metav1.ObjectMeta{
-					Name: "fake-template",
-				},
-				Spec: v1alpha3.TemplateSpec{
-					Template: "fake-template-content",
-				},
-			},
+			template:   createTemplate("fake-name", "fake-template"),
+			parameters: nil,
 		},
-		verify: func(t *testing.T, object v1alpha3.TemplateObject) {
-			gotRenderResult := object.GetAnnotations()[devops.GroupName+devops.RenderResultAnnoKey]
-			wantRenderResult := object.TemplateSpec().Template
-			assert.Equal(t, wantRenderResult, gotRenderResult)
+		verify: func(t *testing.T, template v1alpha3.TemplateObject) {
+			got := template.GetAnnotations()[devops.GroupName+devops.RenderResultAnnoKey]
+			assert.Equal(t, "fake-template", got)
 		},
-	},
-	}
+		wantErr: assert.NoError,
+	}, {
+		name: "Should render template with parameters",
+		args: args{
+			template: createTemplate("fake-name", "The number should be {{ .params.number }}"),
+			parameters: []Parameter{{
+				Name:  "number",
+				Value: "233",
+			}},
+		},
+		verify: func(t *testing.T, template v1alpha3.TemplateObject) {
+			got := template.GetAnnotations()[devops.GroupName+devops.RenderResultAnnoKey]
+			assert.Equal(t, "The number should be 233", got)
+		},
+		wantErr: assert.NoError,
+	}, {
+		name: "Should render incorrectly without corresponding parameter",
+		args: args{
+			template: createTemplate("fake-name", "The number should be: {{ .params.number }}"),
+			parameters: []Parameter{{
+				Name:  "name",
+				Value: "fake-name",
+			}},
+		},
+		verify: func(t *testing.T, template v1alpha3.TemplateObject) {
+			got := template.GetAnnotations()[devops.GroupName+devops.RenderResultAnnoKey]
+			assert.Equal(t, "The number should be: <no value>", got)
+		},
+		wantErr: assert.NoError,
+	}, {
+		name: "Should return error if the template is invalid",
+		args: args{
+			template: createTemplate("fake-name", "{{}}"),
+			parameters: []Parameter{{
+				Name:  "name",
+				Value: "fake-name",
+			}},
+		},
+		verify: func(t *testing.T, template v1alpha3.TemplateObject) {
+			assert.Nil(t, template)
+		},
+		wantErr: assert.Error,
+	}}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			templateObject := render(tt.args.templateObject)
-			if tt.verify != nil {
-				tt.verify(t, templateObject)
+			template, err := render(tt.args.template, tt.args.parameters)
+			if !tt.wantErr(t, err, fmt.Sprintf("render(%v, %v)", tt.args.template, tt.args.parameters)) {
+				return
 			}
+			tt.verify(t, template)
 		})
 	}
 }
diff --git a/pkg/kapis/devops/v1alpha3/template/route_test.go b/pkg/kapis/devops/v1alpha3/template/route_test.go
index bc25990..c4a0e65 100644
--- a/pkg/kapis/devops/v1alpha3/template/route_test.go
+++ b/pkg/kapis/devops/v1alpha3/template/route_test.go
@@ -83,7 +83,9 @@ func TestRegisterRoutes(t *testing.T) {
 			uri := fmt.Sprintf("/kapis/%s/%s%s",
 				v1alpha3.GroupVersion.Group, v1alpha3.GroupVersion.Version, tt.args.uri)
 			recorder := httptest.NewRecorder()
-			request, _ := http.NewRequest(tt.args.method, uri, nil)
+			request := httptest.NewRequest(tt.args.method, uri, nil)
+			request.Header.Add(restful.HEADER_ContentType, restful.MIME_JSON)
+
 			container.ServeHTTP(recorder, request)
 			if recorder.Code == 404 {
 				assert.NotContains(t, recorder.Body.String(), "Page Not Found")
diff --git a/pkg/kapis/devops/v1alpha3/template/template_handler_test.go b/pkg/kapis/devops/v1alpha3/template/template_handler_test.go
index 01c6c30..040ff4f 100644
--- a/pkg/kapis/devops/v1alpha3/template/template_handler_test.go
+++ b/pkg/kapis/devops/v1alpha3/template/template_handler_test.go
@@ -84,6 +84,7 @@ func Test_templatesToObjects(t *testing.T) {
 		})
 	}
 }
+
 func Test_handler_handleQueryTemplates(t *testing.T) {
 	createTemplate := func(name string) *v1alpha3.Template {
 		return &v1alpha3.Template{
@@ -251,6 +252,7 @@ func Test_handler_handleRenderTemplate(t *testing.T) {
 	}
 	createRequest := func(uri, devopsName, templateName string) *restful.Request {
 		fakeRequest := httptest.NewRequest(http.MethodGet, uri, nil)
+		fakeRequest.Header.Add(restful.HEADER_ContentType, restful.MIME_JSON)
 		request := restful.NewRequest(fakeRequest)
 		request.PathParameters()[common.DevopsPathParameter.Data().Name] = devopsName
 		request.PathParameters()[TemplatePathParameter.Data().Name] = templateName