Repository: kubesphere/ks-devops
Pull request #210 (2021-08-30 09:15:04)

Support trigger a parameterized Pipeline in PipelineRun reconciler
### What this PR dose

Make parameters in PipelineRunSpec available.

### Why we need it

This PR is part of https://github.com/kubesphere/ks-devops/issues/41

### Special notes

Upstream PR: https://github.com/jenkins-zh/jenkins-client/pull/14

#### TODO

- [x] Support trigger a parameterized Pipeline in CLI https://github.com/jenkins-zh/jenkins-client/pull/14
- [x] Change https://github.com/jenkins-zh/jenkins-client dependent to latest after https://github.com/jenkins-zh/jenkins-client/pull/14 got merged.

/kind feature

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/210#" 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"]} -->
LinuxSuRen: One more checklist item might be: `support trigger a parameterized Pipeline in CLI`
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/210?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#210](https://codecov.io/gh/kubesphere/ks-devops/pull/210?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (b7d7836) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/0fe858fc6cf30523a18c456e7136a31459c70d9d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (0fe858f) will **increase** coverage by `0.02%`.
> The diff coverage is `46.15%`.

[![Impacted file tree graph](https://codecov.io/gh/kubesphere/ks-devops/pull/210/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/210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere)

```diff
@@            Coverage Diff            @@
##           master    #210      +/-   ##
=========================================
+ Coverage    5.56%   5.59%   +0.02%     
=========================================
  Files          79      79              
  Lines       21383   21392       +9     
=========================================
+ Hits         1190    1196       +6     
- Misses      20120   20123       +3     
  Partials       73      73              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `5.59% <46.15%> (+0.02%)` | :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/210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [...lers/jenkins/pipelinerun/pipelinerun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/210/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9waXBlbGluZXJ1bi9waXBlbGluZXJ1bl9jb250cm9sbGVyLmdv) | `0.00% <0.00%> (ø)` | |
| [controllers/jenkins/pipelinerun/utils.go](https://codecov.io/gh/kubesphere/ks-devops/pull/210/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvamVua2lucy9waXBlbGluZXJ1bi91dGlscy5nbw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/210?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/210?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [0fe858f...b7d7836](https://codecov.io/gh/kubesphere/ks-devops/pull/210?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/README.md b/README.md
index 672edd5..da1c9f7 100644
--- a/README.md
+++ b/README.md
@@ -5,35 +5,39 @@
 
 ## Get started
 
-1. Install KubeSphere via [kk](https://github.com/kubesphere/kubekey/) (or other ways). 
-    This is an optional step, basically we need a Kubernetes Cluster and the front-end of DevOps.
-1. Install `ks-devops` via [chart](charts/ks-devops)
-1. Replace the images of `ks-apiserver` and `ks-controller-manager`. In current phase, we need to use a temporary images of [KubeSphere](https://github.com/kubesphere/kubesphere/) 
-which comes from [the branch remove-devops-ctrl](https://github.com/LinuxSuRen/kubesphere/tree/remove-devops-ctrl):
+### Quick Start
 
-* `kubespheredev/ks-apiserver:remove-devops-ctrl`
-* `kubespheredev/ks-controller-manager:remove-devops-ctrl`
+- Install KubeSphere via [kk](https://github.com/kubesphere/kubekey/) (or other ways).
+
+  ```bash
+  kk create cluster --with-kubesphere nightly-$(date -d yesterday '+%Y%m%d')
+  ```
+
+- Enable DevOps application
+
+  ```bash
+  kubectl patch -nkubesphere-system cc ks-installer --type=json -p='[{"op": "replace", "path": "/spec/devops/enabled", "value": true}]'
+  ```
 
 Want to go into deep? Please checkout the [documentation](docs).
 
 ### Install it as a Helm Chart
 
 First, please clone this git repository. Then run command: `make install-chart`
- - Because the default registry uses `ghcr.io/kubesphere`, if you want to use `docker.io` as the registry for image pull, you can exec `helm install ks-devops chart/ks-devops --set image.registry=kubespheredev`
+
+- Because the default registry uses `ghcr.io/kubesphere`, if you want to use `docker.io` as the registry for image pull, you can exec `helm install ks-devops chart/ks-devops --set image.registry=kubespheredev`
 
 ### Run it locally
 
-Technically, [apiserver](cmd/apiserver) and [controller](cmd/controller) are all binary files. So, 
-it's possible to run them in your local environment. You just need to make sure that the connection 
-between your environment and a Kubernetes cluster works well. This is a default config file of these 
+Technically, [apiserver](cmd/apiserver) and [controller](cmd/controller) are all binary files. So,
+it's possible to run them in your local environment. You just need to make sure that the connection
+between your environment and a Kubernetes cluster works well. This is a default config file of these
 components, please see also [the sample file](config/samples/kubesphere.yaml).
 
 ### Development locally
 
 - Run [kind](https://github.com/kubernetes-sigs/kind) in local or remote machine
-  
 - Make sure that you can access cluster via kubectl command in local machine
-  
 - Execute the following command to install our CRDs:
 
 ```shell
@@ -87,9 +91,9 @@ golangci-lint run controllers/jenkinsconfig
 
 ## TODO
 
-* A separate front-end project of ks-devops
-* Auth support
-    * OIDC support as a default provider
+- A separate front-end project of ks-devops
+- Auth support
+  - OIDC support as a default provider
 
 ## Experimental support
 
@@ -104,6 +108,6 @@ It provides a dashboard for Kubernetes and ks-devops.
 - [Forum for Chinese speakers](https://kubesphere.com.cn/forum/t/DevOps)
 - [KubeSphere DevOps Special Interest Group](https://github.com/kubesphere/community/tree/master/sig-devops)
 
-## Additional links 
+## Additional links
 
 - [Collection of KubeSphere Devops related projects](docs/projects.md)
diff --git a/config/samples/devops_v1alpha4_pipelinerun_with_parameters.yaml b/config/samples/devops_v1alpha4_pipelinerun_with_parameters.yaml
new file mode 100644
index 0000000..730cbc7
--- /dev/null
+++ b/config/samples/devops_v1alpha4_pipelinerun_with_parameters.yaml
@@ -0,0 +1,75 @@
+---
+apiVersion: devops.kubesphere.io/v1alpha3
+kind: DevOpsProject
+metadata:
+  annotations:
+    kubesphere.io/creator: admin
+  name: demo-devopsproject
+status:
+  adminNamespace: default
+---
+apiVersion: devops.kubesphere.io/v1alpha3
+kind: Pipeline
+metadata:
+  annotations:
+    kubesphere.io/creator: admin
+  name: demo-pipeline
+  namespace: default
+spec:
+  pipeline:
+    disable_concurrent: true
+    discarder:
+      days_to_keep: "7"
+      num_to_keep: "10"
+    jenkinsfile: |
+      pipeline {
+        agent any
+
+        parameters {
+          string defaultValue: 'rick', description: 'just for testing', name: 'name', trim: true
+          booleanParam defaultValue: false, description: 'You can use this flag to debug your Pipeline', name: 'debug'
+          choice choices: ['v1.18.8', 'v1.18.9'], description: 'Please choose the target Kubernetes version', name: 'kubernetesVersion'
+        }
+
+        stages{
+          stage('simple'){
+            steps{
+              echo "My name is ${params.name}."
+              echo "Target Kubernetes version is " + params.kubernetesVersion
+
+              script {
+                if ("${params.debug}" == "true") {
+                  echo "You can put some debug info at here."
+                  echo "Another way to use param: " + params.name
+                }
+              }
+            }
+          }
+
+          stage('debug stage') {
+            when {
+              equals expected: true, actual: params.debug
+            }
+            steps {
+              echo "It's joke, there're debug info here."
+            }
+          }
+        }
+      }
+    name: good
+  type: pipeline
+
+---
+apiVersion: devops.kubesphere.io/v1alpha4
+kind: PipelineRun
+metadata:
+  name: demo-pipelinerun
+  namespace: default
+spec:
+  pipelineRef:
+    name: demo-pipeline
+  parameters:
+    - name: "debug"
+      value: "true"
+    - name: "name"
+      value: "devops"
diff --git a/controllers/jenkins/pipelinerun/pipelinerun_controller.go b/controllers/jenkins/pipelinerun/pipelinerun_controller.go
index 8a7ce49..492dc42 100644
--- a/controllers/jenkins/pipelinerun/pipelinerun_controller.go
+++ b/controllers/jenkins/pipelinerun/pipelinerun_controller.go
@@ -127,7 +127,7 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	}
 
 	// first run
-	pipelineBuild, err := r.triggerJenkinsJob(devopsProjectName, pipelineName)
+	pipelineBuild, err := r.triggerJenkinsJob(devopsProjectName, pipelineName, &pr.Spec)
 	if err != nil {
 		log.Error(err, "unable to run pipeline", "devopsProjectName", devopsProjectName, "pipeline", pipeline.Name)
 		return ctrl.Result{}, err
@@ -165,9 +165,12 @@ func (r *Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
 	return ctrl.Result{RequeueAfter: 1 * time.Second}, nil
 }
 
-func (r *Reconciler) triggerJenkinsJob(projectName, pipelineName string) (*job.PipelineBuild, error) {
-	boClient := job.BlueOceanClient{JenkinsCore: r.JenkinsCore, Organization: ""}
-	return boClient.Build("jenkins", projectName, pipelineName)
+func (r *Reconciler) triggerJenkinsJob(devopsProjectName, pipelineName string, prSpec *devopsv1alpha4.PipelineRunSpec) (*job.PipelineBuild, error) {
+	boClient := job.BlueOceanClient{JenkinsCore: r.JenkinsCore, Organization: "jenkins"}
+	return boClient.Build(job.BuildOption{
+		Pipelines:  []string{devopsProjectName, pipelineName},
+		Parameters: parameterConverter{parameters: prSpec.Parameters}.convert(),
+	})
 }
 
 func (r *Reconciler) getPipelineRunResult(projectName, pipelineName string, pr *devopsv1alpha4.PipelineRun) (*job.PipelineBuild, error) {
@@ -176,7 +179,7 @@ func (r *Reconciler) getPipelineRunResult(projectName, pipelineName string, pr *
 		return nil, fmt.Errorf("unable to get PipelineRun result due to not found run ID")
 	}
 	boClient := job.BlueOceanClient{JenkinsCore: r.JenkinsCore, Organization: "jenkins"}
-	return boClient.GetBuild("jenkins", runIDStr, projectName, pipelineName)
+	return boClient.GetBuild(runIDStr, projectName, pipelineName)
 }
 
 func (r *Reconciler) updateLabelsAndAnnotations(ctx context.Context, pr *devopsv1alpha4.PipelineRun) error {
diff --git a/controllers/jenkins/pipelinerun/utils.go b/controllers/jenkins/pipelinerun/utils.go
index e3f1748..d459050 100644
--- a/controllers/jenkins/pipelinerun/utils.go
+++ b/controllers/jenkins/pipelinerun/utils.go
@@ -42,7 +42,7 @@ type pipelineBuildApplier struct {
 	*job.PipelineBuild
 }
 
-func (pbApplier *pipelineBuildApplier) apply(prStatus *devopsv1alpha4.PipelineRunStatus) {
+func (pbApplier pipelineBuildApplier) apply(prStatus *devopsv1alpha4.PipelineRunStatus) {
 	condition := devopsv1alpha4.Condition{
 		Type:               devopsv1alpha4.ConditionReady,
 		Status:             devopsv1alpha4.ConditionUnknown,
@@ -76,7 +76,7 @@ func (pbApplier *pipelineBuildApplier) apply(prStatus *devopsv1alpha4.PipelineRu
 	prStatus.UpdateTime = &v1.Time{Time: time.Now()}
 }
 
-func (pbApplier *pipelineBuildApplier) whenPipelineRunFinished(condition *devopsv1alpha4.Condition, prStatus *devopsv1alpha4.PipelineRunStatus) {
+func (pbApplier pipelineBuildApplier) whenPipelineRunFinished(condition *devopsv1alpha4.Condition, prStatus *devopsv1alpha4.PipelineRunStatus) {
 	// mark as completed
 	if !pbApplier.EndTime.IsZero() {
 		prStatus.CompletionTime = &v1.Time{Time: pbApplier.EndTime.Time}
@@ -107,3 +107,19 @@ func (pbApplier *pipelineBuildApplier) whenPipelineRunFinished(condition *devops
 		prStatus.Phase = devopsv1alpha4.Failed
 	}
 }
+
+// parameterConverter is responsible to convert Parameter slice of PipelineRun into job.Parameter slice.
+type parameterConverter struct {
+	parameters []devopsv1alpha4.Parameter
+}
+
+func (converter parameterConverter) convert() []job.Parameter {
+	var params []job.Parameter
+	for _, param := range converter.parameters {
+		params = append(params, job.Parameter{
+			Name:  param.Name,
+			Value: param.Value,
+		})
+	}
+	return params
+}
diff --git a/go.mod b/go.mod
index 5a88763..89ea74d 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
 	github.com/golang/mock v1.6.0
 	github.com/google/go-cmp v0.5.5
 	github.com/google/go-querystring v1.1.0 // indirect
-	github.com/jenkins-zh/jenkins-client v0.0.2
+	github.com/jenkins-zh/jenkins-client v0.0.3-0.20210830140903-1ae131e1f28b
 	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 7ec3724..02415ae 100644
--- a/go.sum
+++ b/go.sum
@@ -385,8 +385,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
 github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
 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.2 h1:x4yHjGIkJ51n62Qybr6mz4wXWocb59UWDqRGpRJXO1A=
-github.com/jenkins-zh/jenkins-client v0.0.2/go.mod h1:ICBk7OOoTafVP//f/VfKZ34c0ff8vJwVnOsF9btiMYU=
+github.com/jenkins-zh/jenkins-client v0.0.3-0.20210830140903-1ae131e1f28b h1:+5d4wUkSGy/Ql5/ZBDAPjXA+Aab39V3nvw5Ec6TCHx8=
+github.com/jenkins-zh/jenkins-client v0.0.3-0.20210830140903-1ae131e1f28b/go.mod h1:ICBk7OOoTafVP//f/VfKZ34c0ff8vJwVnOsF9btiMYU=
 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/v1alpha4/zz_generated.deepcopy.go b/pkg/api/devops/v1alpha4/zz_generated.deepcopy.go
index 401460e..da536de 100644
--- a/pkg/api/devops/v1alpha4/zz_generated.deepcopy.go
+++ b/pkg/api/devops/v1alpha4/zz_generated.deepcopy.go
@@ -1,4 +1,3 @@
-//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index 74a9d8f..e02eb0d 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"github.com/jenkins-zh/jenkins-client/pkg/core"
 	"k8s.io/apiserver/pkg/authentication/authenticator"
 	"k8s.io/apiserver/pkg/authentication/request/bearertoken"
 	unionauth "k8s.io/apiserver/pkg/authentication/request/union"
@@ -121,6 +122,12 @@ func (s *APIServer) PrepareRun(stopCh <-chan struct{}) error {
 // Installation happens before all informers start to cache objects, so
 //   any attempt to list objects using listers will get empty results.
 func (s *APIServer) installKubeSphereAPIs() {
+	jenkinsCore := core.JenkinsCore{
+		URL:      s.Config.JenkinsOptions.Host,
+		UserName: s.Config.JenkinsOptions.Username,
+		Token:    s.Config.JenkinsOptions.Password,
+	}
+
 	urlruntime.Must(devopsv1alpha2.AddToContainer(s.container,
 		s.InformerFactory.KubeSphereSharedInformerFactory(),
 		s.DevopsClient,
@@ -128,7 +135,8 @@ func (s *APIServer) installKubeSphereAPIs() {
 		s.KubernetesClient.KubeSphere(),
 		s.S3Client,
 		s.Config.JenkinsOptions.Host,
-		s.KubernetesClient))
+		s.KubernetesClient,
+		jenkinsCore))
 	urlruntime.Must(devopsv1alpha3.AddToContainer(s.container, s.DevopsClient, s.KubernetesClient))
 	urlruntime.Must(oauth.AddToContainer(s.container,
 		auth.NewTokenOperator(
diff --git a/pkg/client/devops/jenkins/pure_request.go b/pkg/client/devops/jenkins/pure_request.go
index 43fa985..1f93dab 100644
--- a/pkg/client/devops/jenkins/pure_request.go
+++ b/pkg/client/devops/jenkins/pure_request.go
@@ -69,8 +69,6 @@ func (j *Jenkins) SendPureRequestWithHeaderResp(path string, httpParameters *dev
 			}
 		}
 	}
-	// TODO consider to remove below
-	SetBasicBearTokenHeader(&header)
 
 	newRequest := &http.Request{
 		Method:   httpParameters.Method,
diff --git a/pkg/client/devops/jenkins/request.go b/pkg/client/devops/jenkins/request.go
index f499788..10beb27 100644
--- a/pkg/client/devops/jenkins/request.go
+++ b/pkg/client/devops/jenkins/request.go
@@ -45,29 +45,6 @@ type APIRequest struct {
 	Suffix   string
 }
 
-// set basic token for jenkins auth
-func SetBasicBearTokenHeader(header *http.Header) error {
-	bearTokenArray := strings.Split(header.Get("Authorization"), " ")
-	bearFlag := bearTokenArray[0]
-	//var err error
-	// TODO need to add auth here
-	if strings.ToLower(bearFlag) == "bearer" {
-		//bearToken := bearTokenArray[1]
-		//if err != nil {
-		//	return err
-		//}
-		//claim := authtoken.Claims{}
-		//parser := jwt.Parser{}
-		//_, _, err = parser.ParseUnverified(bearToken, &claim)
-		//if err != nil {
-		//	return err
-		//}
-		//creds := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", claim.Username, bearToken)))
-		//header.Set("Authorization", fmt.Sprintf("Basic %s", creds))
-	}
-	return nil
-}
-
 func (ar *APIRequest) SetHeader(key string, value string) *APIRequest {
 	ar.Headers.Set(key, value)
 	return ar
diff --git a/pkg/kapis/devops/v1alpha2/proxy.go b/pkg/kapis/devops/v1alpha2/proxy.go
new file mode 100644
index 0000000..341bc03
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha2/proxy.go
@@ -0,0 +1,50 @@
+package v1alpha2
+
+import (
+	"fmt"
+	"github.com/emicklei/go-restful"
+	"github.com/jenkins-zh/jenkins-client/pkg/core"
+	"k8s.io/apimachinery/pkg/util/proxy"
+	"k8s.io/klog"
+	"net/http"
+	"strings"
+)
+
+type jenkinsProxy struct {
+	client       core.JenkinsCore
+	host         string
+	scheme       string
+	roundTripper http.RoundTripper
+}
+
+func newJenkinsProxy(client core.JenkinsCore, host, scheme string, roundTripper http.RoundTripper) *jenkinsProxy {
+	if roundTripper == nil {
+		roundTripper = http.DefaultTransport
+	}
+	return &jenkinsProxy{
+		client:       client,
+		host:         host,
+		scheme:       scheme,
+		roundTripper: roundTripper,
+	}
+}
+
+func (p *jenkinsProxy) proxyWithDevOps(request *restful.Request, response *restful.Response) {
+	u := request.Request.URL
+	devopsPath := request.PathParameter("devops")
+	u.Host = p.host
+	u.Scheme = p.scheme
+	u.Path = strings.Replace(request.Request.URL.Path, fmt.Sprintf("/kapis/%s/%s/devops/%s/jenkins",
+		GroupVersion.Group, GroupVersion.Version, devopsPath), "", 1)
+	u.Path = strings.Replace(u.Path, fmt.Sprintf("/%s/devops/%s/jenkins",
+		GroupVersion.Version, devopsPath), "", 1)
+	httpProxy := proxy.NewUpgradeAwareHandler(u, p.roundTripper, false, false, &errorResponder{})
+
+	if err := p.client.AuthHandle(request.Request); err != nil {
+		msg := "failed to set auth header for Jenkins API request"
+		klog.V(4).Infof("%s, error: %v", msg, err)
+		_, _ = response.Write([]byte(msg))
+		return
+	}
+	httpProxy.ServeHTTP(response, request.Request)
+}
diff --git a/pkg/kapis/devops/v1alpha2/register.go b/pkg/kapis/devops/v1alpha2/register.go
index 31aa6f6..aa05236 100644
--- a/pkg/kapis/devops/v1alpha2/register.go
+++ b/pkg/kapis/devops/v1alpha2/register.go
@@ -19,6 +19,7 @@ package v1alpha2
 import (
 	"context"
 	"fmt"
+	"github.com/jenkins-zh/jenkins-client/pkg/core"
 	"kubesphere.io/devops/pkg/apiserver/runtime"
 	"kubesphere.io/devops/pkg/client/k8s"
 	"net/url"
@@ -34,7 +35,6 @@ import (
 
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/client/clientset/versioned"
-	"kubesphere.io/devops/pkg/client/devops/jenkins"
 	"kubesphere.io/devops/pkg/client/informers/externalversions"
 	"kubesphere.io/devops/pkg/client/s3"
 	"kubesphere.io/devops/pkg/client/sonarqube"
@@ -49,17 +49,17 @@ var GroupVersion = schema.GroupVersion{Group: api.GroupName, Version: "v1alpha2"
 
 func AddToContainer(container *restful.Container, ksInformers externalversions.SharedInformerFactory,
 	devopsClient devops.Interface, sonarqubeClient sonarqube.SonarInterface, ksClient versioned.Interface,
-	s3Client s3.Interface, endpoint string, k8sClient k8s.Client) error {
+	s3Client s3.Interface, endpoint string, k8sClient k8s.Client, jenkinsClient core.JenkinsCore) error {
 	wsWithGroup := runtime.NewWebService(GroupVersion)
 	// the API endpoint with group version will be removed in the future release
 	if err := addToContainerWithWebService(container, ksInformers, devopsClient, sonarqubeClient, ksClient,
-		s3Client, endpoint, k8sClient, wsWithGroup); err != nil {
+		s3Client, endpoint, k8sClient, jenkinsClient, wsWithGroup); err != nil {
 		return err
 	}
 
 	ws := runtime.NewWebServiceWithoutGroup(GroupVersion)
 	if err := addToContainerWithWebService(container, ksInformers, devopsClient, sonarqubeClient, ksClient,
-		s3Client, endpoint, k8sClient, ws); err != nil {
+		s3Client, endpoint, k8sClient, jenkinsClient, ws); err != nil {
 		return err
 	}
 	return nil
@@ -67,7 +67,7 @@ func AddToContainer(container *restful.Container, ksInformers externalversions.S
 
 func addToContainerWithWebService(container *restful.Container, ksInformers externalversions.SharedInformerFactory,
 	devopsClient devops.Interface, sonarqubeClient sonarqube.SonarInterface, ksClient versioned.Interface,
-	s3Client s3.Interface, endpoint string, k8sClient k8s.Client, ws *restful.WebService) error {
+	s3Client s3.Interface, endpoint string, k8sClient k8s.Client, jenkinsClient core.JenkinsCore, ws *restful.WebService) error {
 	err := AddPipelineToWebService(ws, devopsClient, k8sClient)
 	if err != nil {
 		return err
@@ -83,7 +83,7 @@ func addToContainerWithWebService(container *restful.Container, ksInformers exte
 		return err
 	}
 
-	err = addJenkinsToContainer(ws, devopsClient, endpoint, k8sClient)
+	err = addJenkinsToContainer(ws, devopsClient, endpoint, jenkinsClient)
 	if err != nil {
 		return err
 	}
@@ -682,7 +682,7 @@ func AddS2IToWebService(webservice *restful.WebService, ksClient versioned.Inter
 	return nil
 }
 
-func addJenkinsToContainer(webservice *restful.WebService, devopsClient devops.Interface, endpoint string, client k8s.Client) error {
+func addJenkinsToContainer(webservice *restful.WebService, devopsClient devops.Interface, endpoint string, jenkinsClient core.JenkinsCore) error {
 	if devopsClient == nil {
 		return nil
 	}
@@ -700,7 +700,6 @@ func addJenkinsToContainer(webservice *restful.WebService, devopsClient devops.I
 			u := request.Request.URL
 			u.Host = parse.Host
 			u.Scheme = parse.Scheme
-			jenkins.SetBasicBearTokenHeader(&request.Request.Header)
 			u.Path = strings.Replace(request.Request.URL.Path, fmt.Sprintf("/kapis/%s/%s/jenkins", GroupVersion.Group, GroupVersion.Version), "", 1)
 			httpProxy := proxy.NewUpgradeAwareHandler(u, http.DefaultTransport, false, false, &errorResponder{})
 			httpProxy.ServeHTTP(response, request.Request)
@@ -708,28 +707,18 @@ func addJenkinsToContainer(webservice *restful.WebService, devopsClient devops.I
 		Returns(http.StatusOK, api.StatusOK, nil).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsJenkinsTag}))
 
-	handlerWithDevOps := func(request *restful.Request, response *restful.Response) {
-		u := request.Request.URL
-		devops := request.PathParameter("devops")
-		u.Host = parse.Host
-		u.Scheme = parse.Scheme
-		jenkins.SetBasicBearTokenHeader(&request.Request.Header)
-		u.Path = strings.Replace(request.Request.URL.Path, fmt.Sprintf("/kapis/%s/%s/devops/%s/jenkins",
-			GroupVersion.Group, GroupVersion.Version, devops), "", 1)
-		httpProxy := proxy.NewUpgradeAwareHandler(u, http.DefaultTransport, false, false, &errorResponder{})
-		httpProxy.ServeHTTP(response, request.Request)
-	}
+	jenkinsProxy := newJenkinsProxy(jenkinsClient, parse.Host, parse.Scheme, nil)
 	// some Jenkins API against with POST method
 	webservice.Route(webservice.GET("/devops/{devops}/jenkins/{path:*}").
 		Param(webservice.PathParameter("path", "Path stands for any suffix path.")).
 		Param(webservice.PathParameter("devops", "DevOps project's ID, e.g. project-RRRRAzLBlLEm")).
-		To(handlerWithDevOps).
+		To(jenkinsProxy.proxyWithDevOps).
 		Returns(http.StatusOK, api.StatusOK, nil).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsJenkinsTag}))
 	webservice.Route(webservice.POST("/devops/{devops}/jenkins/{path:*}").
 		Param(webservice.PathParameter("path", "Path stands for any suffix path.")).
 		Param(webservice.PathParameter("devops", "DevOps project's ID, e.g. project-RRRRAzLBlLEm")).
-		To(handlerWithDevOps).
+		To(jenkinsProxy.proxyWithDevOps).
 		Returns(http.StatusOK, api.StatusOK, nil).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsJenkinsTag}))
 	return nil


Test patch:
diff --git a/controllers/jenkins/pipelinerun/utils_test.go b/controllers/jenkins/pipelinerun/utils_test.go
index 7b653e2..c28b4c4 100644
--- a/controllers/jenkins/pipelinerun/utils_test.go
+++ b/controllers/jenkins/pipelinerun/utils_test.go
@@ -5,6 +5,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	devopsv1alpha4 "kubesphere.io/devops/pkg/api/devops/v1alpha4"
+	"reflect"
 	"testing"
 	"time"
 )
@@ -280,3 +281,53 @@ func Test_pipelineBuildApplier_apply(t *testing.T) {
 		})
 	}
 }
+
+func Test_parameterConverter_convert(t *testing.T) {
+	type fields struct {
+		parameters []devopsv1alpha4.Parameter
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   []job.Parameter
+	}{{
+		name: "Nil parameters",
+		fields: fields{
+			parameters: nil,
+		},
+		want: nil,
+	}, {
+		name: "Single parameter",
+		fields: fields{
+			parameters: []devopsv1alpha4.Parameter{
+				{Name: "fake_name", Value: "fake_value"},
+			},
+		},
+		want: []job.Parameter{
+			{Name: "fake_name", Value: "fake_value"},
+		},
+	}, {
+		name: "Two parameters",
+		fields: fields{
+			parameters: []devopsv1alpha4.Parameter{
+				{Name: "fake_name_1", Value: "fake_value_1"},
+				{Name: "fake_name_2", Value: "fake_value_2"},
+			},
+		},
+		want: []job.Parameter{
+			{Name: "fake_name_1", Value: "fake_value_1"},
+			{Name: "fake_name_2", Value: "fake_value_2"},
+		},
+	}}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			converter := parameterConverter{
+				parameters: tt.fields.parameters,
+			}
+			if got := converter.convert(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("convert() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/apiserver/authentication/token/jwt_test.go b/pkg/apiserver/authentication/token/jwt_test.go
index 46ea609..37832eb 100644
--- a/pkg/apiserver/authentication/token/jwt_test.go
+++ b/pkg/apiserver/authentication/token/jwt_test.go
@@ -82,7 +82,7 @@ func Test_getUserFromClaims(t *testing.T) {
 		name: "with key: username and sub, take sub first",
 		args: args{
 			claims: jwt.MapClaims{
-				"sub": "rick",
+				"sub":      "rick",
 				"username": "mark",
 			},
 		},
diff --git a/pkg/apiserver/runtime/runtime_test.go b/pkg/apiserver/runtime/runtime_test.go
index 2ad9960..d667278 100644
--- a/pkg/apiserver/runtime/runtime_test.go
+++ b/pkg/apiserver/runtime/runtime_test.go
@@ -22,5 +22,5 @@ func TestNewWebService(t *testing.T) {
 	})
 
 	assert.NotNil(t, ws)
-	assert.Equal(t, ApiRootPath + "/devops/v2", ws.RootPath())
+	assert.Equal(t, ApiRootPath+"/devops/v2", ws.RootPath())
 }
diff --git a/pkg/kapis/devops/v1alpha2/proxy_test.go b/pkg/kapis/devops/v1alpha2/proxy_test.go
new file mode 100644
index 0000000..5f4f7cb
--- /dev/null
+++ b/pkg/kapis/devops/v1alpha2/proxy_test.go
@@ -0,0 +1,94 @@
+package v1alpha2
+
+import (
+	"bytes"
+	"fmt"
+	"github.com/emicklei/go-restful"
+	"github.com/golang/mock/gomock"
+	"github.com/jenkins-zh/jenkins-client/pkg/core"
+	"github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp"
+	"github.com/stretchr/testify/assert"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"testing"
+)
+
+func TestJenkinsProxy(t *testing.T) {
+	testJenkinsProxy("fake jenkins response", true, t)
+	testJenkinsProxy("failed to set auth header for Jenkins API request", false, t)
+}
+
+func testJenkinsProxy(responseStr string, correctCrumb bool, t *testing.T) {
+	ctrl := gomock.NewController(t)
+	defer ctrl.Finish()
+
+	roundTripper := mhttp.NewMockRoundTripper(ctrl)
+	// common crumb request
+	if correctCrumb {
+		request, _ := http.NewRequest(http.MethodPost, "http://fake.com/gitlab/serverList", nil)
+		request.Header = map[string][]string{
+			"User-Agent": {""},
+		}
+		request.ProtoMajor = 1
+		request.ProtoMinor = 0
+		request.Proto = "HTTP/1.1"
+		core.PrepareForGetIssuer(roundTripper, "http://fake.com", "", "")
+		request.Header.Add("CrumbRequestField", "Crumb")
+		response := &http.Response{
+			StatusCode: 200,
+			Request:    request,
+			Body:       ioutil.NopCloser(bytes.NewBufferString(responseStr)),
+		}
+		roundTripper.EXPECT().
+			RoundTrip(core.NewVerboseRequestMatcher(request).WithBody().WithQuery()).Return(response, nil)
+	} else {
+		core.PrepareForGetIssuerWith500(roundTripper, "http://fake.com", "", "")
+	}
+
+	client := core.JenkinsCore{
+		URL:          "http://fake.com",
+		RoundTripper: roundTripper,
+	}
+	proxy := newJenkinsProxy(client, "fake.com", "http", roundTripper)
+
+	restfulRequest := restful.NewRequest(&http.Request{
+		Method: http.MethodPost,
+		URL: &url.URL{
+			Host: "target.com",
+			Path: fmt.Sprintf("/kapis/%s/%s/devops/%s/jenkins/gitlab/serverList",
+				GroupVersion.Group, GroupVersion.Version, "fake.devops"),
+		},
+		Header: map[string][]string{},
+	})
+	restfulRequest.PathParameters()["devops"] = "fake.devops"
+	httpResponse := &fakeHTTPResponse{}
+	restfulResponse := restful.NewResponse(httpResponse)
+	proxy.proxyWithDevOps(restfulRequest, restfulResponse)
+	assert.Equal(t, responseStr, httpResponse.data.String())
+}
+
+func TestNewJenkinsProxy(t *testing.T) {
+	assert.NotNil(t, newJenkinsProxy(core.JenkinsCore{}, "", "", nil))
+}
+
+type fakeHTTPResponse struct {
+	data       bytes.Buffer
+	statusCode int
+}
+
+func (r *fakeHTTPResponse) Header() http.Header {
+	return map[string][]string{}
+}
+
+func (r *fakeHTTPResponse) Write(b []byte) (int, error) {
+	return r.data.Write(b)
+}
+
+func (r *fakeHTTPResponse) WriteHeader(statusCode int) {
+	r.statusCode = statusCode
+}
+
+func (r *fakeHTTPResponse) CloseNotify() <-chan bool {
+	return nil
+}