Repository: kubesphere/ks-devops
Pull request #437 (2022-01-10 10:20:18)

Using the new Jenkins client library
<!-- 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 feature
/kind design

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind chore

### What this PR does / why we need it:
We intend to use [jenkins-zh/jenkins-client](https://github.com/jenkins-zh/jenkins-client) to replace current XML based Jenkins client.

### 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.
-->
You can use the following image to test this PR:
```
surenpi/devops-controller:dev-v3.2.1-f7661f6
```

### 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
```

This PR will replace https://github.com/kubesphere/ks-devops/pull/313

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

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/437#" title="Author self-approved">LinuxSuRen</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)~~ [LinuxSuRen]

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: Before the upstream PR is https://github.com/jenkins-zh/jenkins-client/pull/43 got merged, we need to hold it.

/hold

codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/437?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#437](https://codecov.io/gh/kubesphere/ks-devops/pull/437?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (f7661f6) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/83760ce69f9595112b1fffc4625a39f9ebc3f9ea?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (83760ce) will **increase** coverage by `0.85%`.
> The diff coverage is `78.57%`.

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

```diff
@@            Coverage Diff             @@
##           master     #437      +/-   ##
==========================================
+ Coverage   75.46%   76.32%   +0.85%     
==========================================
  Files         108      107       -1     
  Lines       22314    22077     -237     
==========================================
+ Hits        16840    16850      +10     
+ Misses       5309     5062     -247     
  Partials      165      165              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `76.32% <78.57%> (+0.85%)` | :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/437?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/client/devops/jenkins/jenkins.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2NsaWVudC9kZXZvcHMvamVua2lucy9qZW5raW5zLmdv) | `0.00% <0.00%> (ø)` | |
| [pkg/client/devops/jenkins/pipeline.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2NsaWVudC9kZXZvcHMvamVua2lucy9waXBlbGluZS5nbw==) | `5.11% <ø> (ø)` | |
| [pkg/client/devops/jenkins/project.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2NsaWVudC9kZXZvcHMvamVua2lucy9wcm9qZWN0Lmdv) | `0.00% <ø> (ø)` | |
| [pkg/client/devops/pipeline.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2NsaWVudC9kZXZvcHMvcGlwZWxpbmUuZ28=) | `100.00% <ø> (ø)` | |
| [pkg/kapis/devops/v1alpha2/devops.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMi9kZXZvcHMuZ28=) | `25.04% <0.00%> (ø)` | |
| [pkg/models/devops/devops.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL21vZGVscy9kZXZvcHMvZGV2b3BzLmdv) | `12.38% <0.00%> (ø)` | |
| [controllers/pipeline/pipeline\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvcGlwZWxpbmUvcGlwZWxpbmVfY29udHJvbGxlci5nbw==) | `38.23% <100.00%> (ø)` | |
| [pkg/client/devops/interface.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2NsaWVudC9kZXZvcHMvaW50ZXJmYWNlLmdv) | `100.00% <100.00%> (+100.00%)` | :arrow_up: |
| [pkg/kapis/devops/v1alpha2/register.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMi9yZWdpc3Rlci5nbw==) | `91.74% <100.00%> (ø)` | |
| [controllers/s2irun/s2irun\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/437/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: |
| ... and [3 more](https://codecov.io/gh/kubesphere/ks-devops/pull/437/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/437?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/437?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [83760ce...f7661f6](https://codecov.io/gh/kubesphere/ks-devops/pull/437?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).

LinuxSuRen: /unhold

/cc @kubesphere/sig-devops 
ks-ci-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/437#pullrequestreview-847853119" title="LGTM">JohnNiang</a>*, *<a href="https://github.com/kubesphere/ks-devops/pull/437#" title="Author self-approved">LinuxSuRen</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)~~ [LinuxSuRen]

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":[]} -->
ks-ci-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/437#pullrequestreview-847853629" title="Approved">JohnNiang</a>*, *<a href="https://github.com/kubesphere/ks-devops/pull/437#" title="Author self-approved">LinuxSuRen</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubesphere%2Fks-devops).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/kubesphere/ks-devops/blob/master/OWNERS)~~ [LinuxSuRen]

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":[]} -->

Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 0c9b2b6..e4ad77e 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -6,7 +6,6 @@ on:
     branches:
       - master
       - tekton-support
-      - jenkins-client-refactor
       - feat/*
       - fix/*
       - test-* # make it be easier for contributors to test
diff --git a/cmd/controller/app/server.go b/cmd/controller/app/server.go
index 36e6d8d..5341089 100644
--- a/cmd/controller/app/server.go
+++ b/cmd/controller/app/server.go
@@ -24,7 +24,7 @@ import (
 	"kubesphere.io/devops/cmd/controller/app/options"
 	"kubesphere.io/devops/pkg/apis"
 	"kubesphere.io/devops/pkg/client/devops"
-	"kubesphere.io/devops/pkg/client/devops/jenkins"
+	"kubesphere.io/devops/pkg/client/devops/jclient"
 	"kubesphere.io/devops/pkg/client/k8s"
 	"kubesphere.io/devops/pkg/client/s3"
 	"kubesphere.io/devops/pkg/config"
@@ -120,7 +120,7 @@ func Run(s *options.DevOpsControllerManagerOptions, stopCh <-chan struct{}) erro
 	var devopsClient devops.Interface
 	if s.JenkinsOptions != nil && len(s.JenkinsOptions.Host) != 0 {
 		// Make sure that Jenkins host is not empty
-		devopsClient, err = jenkins.NewDevopsClient(s.JenkinsOptions)
+		devopsClient, err = jclient.NewJenkinsClient(s.JenkinsOptions)
 		if err != nil {
 			return fmt.Errorf("failed to connect jenkins, please check jenkins status, error: %v", err)
 		}
diff --git a/controllers/pipeline/pipeline_controller.go b/controllers/pipeline/pipeline_controller.go
index fc953bd..195f711 100644
--- a/controllers/pipeline/pipeline_controller.go
+++ b/controllers/pipeline/pipeline_controller.go
@@ -223,7 +223,6 @@ func (c *Controller) syncHandler(key string) error {
 	//	klog.Warning(err)
 	//	return err
 	//}
-
 	pipeline, err := c.devOpsProjectLister.Pipelines(nsName).Get(name)
 	if err != nil {
 		if errors.IsNotFound(err) {
@@ -274,7 +273,7 @@ func (c *Controller) syncHandler(key string) error {
 				klog.V(8).Info(fmt.Sprintf("nothing was changed, pipeline '%v'", copyPipeline.Spec))
 			}
 		} else {
-			_, err := c.devopsClient.CreateProjectPipeline(nsName, copyPipeline)
+			_, err = c.devopsClient.CreateProjectPipeline(nsName, copyPipeline)
 			if err != nil {
 				klog.V(8).Info(err, fmt.Sprintf("failed to create copyPipeline %s ", key))
 				return err
@@ -288,7 +287,7 @@ func (c *Controller) syncHandler(key string) error {
 		if sliceutil.HasString(copyPipeline.ObjectMeta.Finalizers, devopsv1alpha3.PipelineFinalizerName) {
 			delSuccess := false
 			if _, err := c.devopsClient.DeleteProjectPipeline(nsName, pipeline.Name); err != nil {
-				// the status code should be 404 if the job does not exists
+				// the status code should be 404 if the job does not exist
 				if srvErr, ok := err.(restful.ServiceError); ok {
 					delSuccess = srvErr.Code == http.StatusNotFound
 				} else if srvErr, ok := err.(*devopsClient.ErrorResponse); ok {
diff --git a/go.mod b/go.mod
index 3b8cddb..aeb88c0 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,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.7
+	github.com/jenkins-zh/jenkins-client v0.0.8
 	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 46702c1..d237a38 100644
--- a/go.sum
+++ b/go.sum
@@ -322,8 +322,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/jenkins-zh/jenkins-cli v0.0.32/go.mod h1:uE1mH9PNITrg0sugv6HXuM/CSddg0zxXoYu3w57I3JY=
-github.com/jenkins-zh/jenkins-client v0.0.7 h1:E0EUOx1G3RVEUrvj8WGvI5WjDTRD4tcLm8FAg0iqJJ8=
-github.com/jenkins-zh/jenkins-client v0.0.7/go.mod h1:ICBk7OOoTafVP//f/VfKZ34c0ff8vJwVnOsF9btiMYU=
+github.com/jenkins-zh/jenkins-client v0.0.8 h1:leaVU9pYvWcAhpG4VkX7KUFIcvNp1qxwnnGVjkhSfMU=
+github.com/jenkins-zh/jenkins-client v0.0.8/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/v1alpha1/zz_generated.deepcopy.go b/pkg/api/devops/v1alpha1/zz_generated.deepcopy.go
index 98ee401..b364f8e 100644
--- a/pkg/api/devops/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/api/devops/v1alpha1/zz_generated.deepcopy.go
@@ -1,4 +1,3 @@
-//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/pkg/api/devops/v1alpha3/zz_generated.deepcopy.go b/pkg/api/devops/v1alpha3/zz_generated.deepcopy.go
index d8ff289..27798d0 100644
--- a/pkg/api/devops/v1alpha3/zz_generated.deepcopy.go
+++ b/pkg/api/devops/v1alpha3/zz_generated.deepcopy.go
@@ -1,4 +1,3 @@
-//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/pkg/client/devops/fake/fakeJenkinsClient.go b/pkg/client/devops/fake/fakeJenkinsClient.go
new file mode 100644
index 0000000..0b49dd5
--- /dev/null
+++ b/pkg/client/devops/fake/fakeJenkinsClient.go
@@ -0,0 +1,72 @@
+package fake
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/jenkins-zh/jenkins-client/pkg/core"
+	"github.com/jenkins-zh/jenkins-client/pkg/job"
+	"github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp"
+	"kubesphere.io/devops/pkg/client/devops/jclient"
+)
+
+// NewFakeJenkinsClient creates a fake Jenkins client
+func NewFakeJenkinsClient(roundTripper *mhttp.MockRoundTripper) (j *jclient.JenkinsClient) {
+	j = &jclient.JenkinsClient{
+		Core: core.JenkinsCore{
+			URL:          "http://localhost",
+			UserName:     "",
+			Token:        "",
+			RoundTripper: roundTripper,
+		},
+	}
+	return
+}
+
+// PrepareForGetProjectPipeline404 only for test
+func PrepareForGetProjectPipeline404(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string, path, name string) {
+	request, _ := http.NewRequest("GET", fmt.Sprintf("%s/job/%s/job/%s/api/json", rootURL, path, name), nil)
+	response := &http.Response{
+		StatusCode: 404,
+		Proto:      "HTTP/1.1",
+		Request:    request,
+		Body: ioutil.NopCloser(bytes.NewBufferString(`
+		{"name":""}
+		`)),
+	}
+	roundTripper.EXPECT().
+		RoundTrip(core.NewRequestMatcher(request)).Return(response, nil)
+}
+
+// PrepareForCreateProjectPipeline only for test
+func PrepareForCreateProjectPipeline(roundTripper *mhttp.MockRoundTripper, rootURL, user, password string, jobPayload job.CreateJobPayload, path string) {
+	PrepareForGetProjectPipeline404(roundTripper, rootURL, user, password, jobPayload.Name, path)
+
+	playLoadData, _ := json.Marshal(jobPayload)
+	formData := url.Values{
+		"json": {string(playLoadData)},
+		"name": {jobPayload.Name},
+		"mode": {jobPayload.Mode},
+		"from": {jobPayload.From},
+	}
+	payload := strings.NewReader(formData.Encode())
+	path = job.ParseJobPath(path)
+	requestCreate, _ := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/view/all%s/createItem", rootURL, path), payload)
+	requestCreate.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+	requestCreate.Header.Add("CrumbRequestField", "Crumb")
+	responseCreate := &http.Response{
+		StatusCode: 200,
+		Proto:      "HTTP/1.1",
+		Request:    requestCreate,
+		Body:       ioutil.NopCloser(bytes.NewBufferString("")),
+	}
+	roundTripper.EXPECT().
+		RoundTrip(core.NewRequestMatcher(requestCreate)).Return(responseCreate, nil)
+
+	core.PrepareForGetIssuer(roundTripper, rootURL, user, password)
+}
diff --git a/pkg/client/devops/fake/fakedevops.go b/pkg/client/devops/fake/fakedevops.go
index 9d981ca..ace75d1 100644
--- a/pkg/client/devops/fake/fakedevops.go
+++ b/pkg/client/devops/fake/fakedevops.go
@@ -303,7 +303,7 @@ func (d *Devops) CheckCron(projectName string, httpParameters *devops.HttpParame
 func (d *Devops) ToJenkinsfile(httpParameters *devops.HttpParameters) (*devops.ResJenkinsfile, error) {
 	return nil, nil
 }
-func (d *Devops) ToJson(httpParameters *devops.HttpParameters) (map[string]interface{}, error) {
+func (d *Devops) ToJSON(httpParameters *devops.HttpParameters) (map[string]interface{}, error) {
 	return nil, nil
 }
 
@@ -563,42 +563,6 @@ func (d *Devops) GetProjectPipelineConfig(projectId, pipelineId string) (*devops
 	return d.Pipelines[projectId][pipelineId], nil
 }
 
-func (d *Devops) AddGlobalRole(roleName string, ids devops.GlobalPermissionIds, overwrite bool) error {
-	return nil
-}
-
-func (d *Devops) AddProjectRole(roleName string, pattern string, ids devops.ProjectPermissionIds, overwrite bool) error {
-	return nil
-}
-
-func (d *Devops) DeleteProjectRoles(roleName ...string) error {
-	return nil
-}
-
-func (d *Devops) AssignProjectRole(roleName string, sid string) error {
-	return nil
-}
-
-func (d *Devops) UnAssignProjectRole(roleName string, sid string) error {
-	return nil
-}
-
-func (d *Devops) AssignGlobalRole(roleName string, sid string) error {
-	return nil
-}
-
-func (d *Devops) UnAssignGlobalRole(roleName string, sid string) error {
-	return nil
-}
-
-func (d *Devops) DeleteUserInProject(sid string) error {
-	return nil
-}
-
-func (d *Devops) GetGlobalRole(roleName string) (string, error) {
-	return "", nil
-}
-
 func (d *Devops) ReloadConfiguration() error {
 	return nil
 }
diff --git a/pkg/client/devops/interface.go b/pkg/client/devops/interface.go
index 21c62a6..4ce98a5 100644
--- a/pkg/client/devops/interface.go
+++ b/pkg/client/devops/interface.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"net/http"
 	"strconv"
+	"strings"
 
 	"github.com/asaskevich/govalidator"
 )
@@ -35,13 +36,12 @@ type Interface interface {
 
 	ProjectOperator
 
-	RoleOperator
-
 	ConfigurationOperator
 }
 
 func GetDevOpsStatusCode(devopsErr error) int {
-	if code, err := strconv.Atoi(devopsErr.Error()); err == nil {
+	errStr := strings.TrimPrefix(devopsErr.Error(), "unexpected status code: ")
+	if code, err := strconv.Atoi(errStr); err == nil {
 		message := http.StatusText(code)
 		if !govalidator.IsNull(message) {
 			return code
@@ -60,6 +60,12 @@ type ErrorResponse struct {
 }
 
 func (e *ErrorResponse) Error() string {
-	u := fmt.Sprintf("%s://%s%s", e.Response.Request.URL.Scheme, e.Response.Request.URL.Host, e.Response.Request.URL.RequestURI())
-	return fmt.Sprintf("%s %s: %d %s", e.Response.Request.Method, u, e.Response.StatusCode, e.Message)
+	var u string
+	var method string
+	if e.Response != nil && e.Response.Request != nil {
+		req := e.Response.Request
+		u = fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.URL.Host, req.URL.RequestURI())
+		method = req.Method
+	}
+	return fmt.Sprintf("%s %s: %d %s", method, u, e.Response.StatusCode, e.Message)
 }
diff --git a/pkg/client/devops/jclient/build.go b/pkg/client/devops/jclient/build.go
new file mode 100644
index 0000000..2a37d86
--- /dev/null
+++ b/pkg/client/devops/jclient/build.go
@@ -0,0 +1,13 @@
+package jclient
+
+import "kubesphere.io/devops/pkg/client/devops"
+
+// GetProjectPipelineBuildByType returns a build
+func (j *JenkinsClient) GetProjectPipelineBuildByType(projectID, pipelineID string, status string) (*devops.Build, error) {
+	return j.jenkins.GetProjectPipelineBuildByType(projectID, pipelineID, status)
+}
+
+// GetMultiBranchPipelineBuildByType returns a build
+func (j *JenkinsClient) GetMultiBranchPipelineBuildByType(projectID, pipelineID, branch string, status string) (*devops.Build, error) {
+	return j.jenkins.GetMultiBranchPipelineBuildByType(projectID, pipelineID, branch, status)
+}
diff --git a/pkg/client/devops/jclient/configuration.go b/pkg/client/devops/jclient/configuration.go
new file mode 100644
index 0000000..e31cc62
--- /dev/null
+++ b/pkg/client/devops/jclient/configuration.go
@@ -0,0 +1,6 @@
+package jclient
+
+// ReloadConfiguration reloads the Jenkins configuration
+func (j *JenkinsClient) ReloadConfiguration() error {
+	return j.jenkins.ReloadConfiguration()
+}
diff --git a/pkg/client/devops/jclient/credential.go b/pkg/client/devops/jclient/credential.go
new file mode 100644
index 0000000..cb7ea5c
--- /dev/null
+++ b/pkg/client/devops/jclient/credential.go
@@ -0,0 +1,26 @@
+package jclient
+
+import (
+	v1 "k8s.io/api/core/v1"
+	"kubesphere.io/devops/pkg/client/devops"
+)
+
+// CreateCredentialInProject creates a credential, then returns the ID
+func (j *JenkinsClient) CreateCredentialInProject(projectID string, credential *v1.Secret) (string, error) {
+	return j.jenkins.CreateCredentialInProject(projectID, credential)
+}
+
+// UpdateCredentialInProject updates a credential
+func (j *JenkinsClient) UpdateCredentialInProject(projectID string, credential *v1.Secret) (string, error) {
+	return j.jenkins.UpdateCredentialInProject(projectID, credential)
+}
+
+// GetCredentialInProject returns a credential
+func (j *JenkinsClient) GetCredentialInProject(projectID, id string) (*devops.Credential, error) {
+	return j.jenkins.GetCredentialInProject(projectID, id)
+}
+
+// DeleteCredentialInProject deletes a credential
+func (j *JenkinsClient) DeleteCredentialInProject(projectID, id string) (string, error) {
+	return j.jenkins.DeleteCredentialInProject(projectID, id)
+}
diff --git a/pkg/client/devops/jclient/jenkins.go b/pkg/client/devops/jclient/jenkins.go
new file mode 100644
index 0000000..3b9ee29
--- /dev/null
+++ b/pkg/client/devops/jclient/jenkins.go
@@ -0,0 +1,49 @@
+package jclient
+
+import (
+	"github.com/jenkins-zh/jenkins-client/pkg/casc"
+	"github.com/jenkins-zh/jenkins-client/pkg/core"
+	"kubesphere.io/devops/pkg/client/devops"
+	"kubesphere.io/devops/pkg/client/devops/jenkins"
+)
+
+// JenkinsClient represents a client of Jenkins
+type JenkinsClient struct {
+	Core    core.JenkinsCore
+	jenkins devops.Interface // For refactor purpose only
+}
+
+// ApplyNewSource apply a new source
+func (j *JenkinsClient) ApplyNewSource(s string) (err error) {
+	client := casc.Manager{
+		JenkinsCore: j.Core,
+	}
+	if err = client.CheckNewSource(s); err == nil {
+		err = client.Replace(s)
+	}
+	return
+}
+
+var _ devops.Interface = &JenkinsClient{}
+
+// NewJenkinsClient creates a Jenkins client
+func NewJenkinsClient(options *jenkins.Options) (jenkinsClient *JenkinsClient, err error) {
+	jenkinsCore := core.JenkinsCore{
+		URL:      options.Host,
+		UserName: options.Username,
+		Token:    options.Password,
+	}
+	crumbIssuer, e := jenkinsCore.GetCrumb()
+	if e != nil {
+		return
+	} else if crumbIssuer != nil {
+		jenkinsCore.JenkinsCrumb = *crumbIssuer
+	}
+
+	devopsClient, _ := jenkins.NewDevopsClient(options) // For refactor purpose only
+	jenkinsClient = &JenkinsClient{
+		Core:    jenkinsCore,
+		jenkins: devopsClient, // For refactor purpose only
+	}
+	return
+}
diff --git a/pkg/client/devops/jclient/pipeline.go b/pkg/client/devops/jclient/pipeline.go
new file mode 100644
index 0000000..23833e0
--- /dev/null
+++ b/pkg/client/devops/jclient/pipeline.go
@@ -0,0 +1,207 @@
+package jclient
+
+import (
+	"net/http"
+
+	"kubesphere.io/devops/pkg/client/devops"
+)
+
+// GetPipeline returns a pipeline
+func (j *JenkinsClient) GetPipeline(projectName, pipelineName string, httpParameters *devops.HttpParameters) (*devops.Pipeline, error) {
+	return j.jenkins.GetPipeline(projectName, pipelineName, httpParameters)
+}
+
+// ListPipelines lists the pipelines
+func (j *JenkinsClient) ListPipelines(httpParameters *devops.HttpParameters) (*devops.PipelineList, error) {
+	return j.jenkins.ListPipelines(httpParameters)
+}
+
+// GetPipelineRun returns a pipeline run
+func (j *JenkinsClient) GetPipelineRun(projectName, pipelineName, runID string, httpParameters *devops.HttpParameters) (*devops.PipelineRun, error) {
+	return j.jenkins.GetPipelineRun(projectName, pipelineName, runID, httpParameters)
+}
+
+// ListPipelineRuns returns the pipeline runs
+func (j *JenkinsClient) ListPipelineRuns(projectName, pipelineName string, httpParameters *devops.HttpParameters) (*devops.PipelineRunList, error) {
+	return j.jenkins.ListPipelineRuns(projectName, pipelineName, httpParameters)
+}
+
+// StopPipeline stops a pipeline run
+func (j *JenkinsClient) StopPipeline(projectName, pipelineName, runID string, httpParameters *devops.HttpParameters) (*devops.StopPipeline, error) {
+	return j.jenkins.StopPipeline(projectName, pipelineName, runID, httpParameters)
+}
+
+// ReplayPipeline replays a pipeline
+func (j *JenkinsClient) ReplayPipeline(projectName, pipelineName, runID string, httpParameters *devops.HttpParameters) (*devops.ReplayPipeline, error) {
+	return j.jenkins.ReplayPipeline(projectName, pipelineName, runID, httpParameters)
+}
+
+// RunPipeline triggers a pipeline
+func (j *JenkinsClient) RunPipeline(projectName, pipelineName string, httpParameters *devops.HttpParameters) (*devops.RunPipeline, error) {
+	return j.jenkins.RunPipeline(projectName, pipelineName, httpParameters)
+}
+
+// GetArtifacts returns the artifacts of a pipeline run
+func (j *JenkinsClient) GetArtifacts(projectName, pipelineName, runID string, httpParameters *devops.HttpParameters) ([]devops.Artifacts, error) {
+	return j.jenkins.GetArtifacts(projectName, pipelineName, runID, httpParameters)
+}
+
+// GetRunLog returns the log output of a pipeline run
+func (j *JenkinsClient) GetRunLog(projectName, pipelineName, runID string, httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.GetRunLog(projectName, pipelineName, runID, httpParameters)
+}
+
+// GetStepLog returns the log output of a step
+func (j *JenkinsClient) GetStepLog(projectName, pipelineName, runID, nodeID, stepID string, httpParameters *devops.HttpParameters) ([]byte, http.Header, error) {
+	return j.jenkins.GetStepLog(projectName, pipelineName, runID, nodeID, stepID, httpParameters)
+}
+
+// GetNodeSteps returns the node steps
+func (j *JenkinsClient) GetNodeSteps(projectName, pipelineName, runID, nodeID string, httpParameters *devops.HttpParameters) ([]devops.NodeSteps, error) {
+	return j.jenkins.GetNodeSteps(projectName, pipelineName, runID, nodeID, httpParameters)
+}
+
+// GetPipelineRunNodes returns the nodes of a pipeline run
+func (j *JenkinsClient) GetPipelineRunNodes(projectName, pipelineName, runID string, httpParameters *devops.HttpParameters) ([]devops.PipelineRunNodes, error) {
+	return j.jenkins.GetPipelineRunNodes(projectName, pipelineName, runID, httpParameters)
+}
+
+// SubmitInputStep submits a pipeline input step
+func (j *JenkinsClient) SubmitInputStep(projectName, pipelineName, runID, nodeID, stepID string, httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.SubmitInputStep(projectName, pipelineName, runID, nodeID, stepID, httpParameters)
+}
+
+// GetBranchPipeline returns the branch pipeline
+func (j *JenkinsClient) GetBranchPipeline(projectName, pipelineName, branchName string, httpParameters *devops.HttpParameters) (*devops.BranchPipeline, error) {
+	return j.jenkins.GetBranchPipeline(projectName, pipelineName, branchName, httpParameters)
+}
+
+// GetBranchPipelineRun returns the pipeline run
+func (j *JenkinsClient) GetBranchPipelineRun(projectName, pipelineName, branchName, runID string, httpParameters *devops.HttpParameters) (*devops.PipelineRun, error) {
+	return j.jenkins.GetBranchPipelineRun(projectName, pipelineName, branchName, runID, httpParameters)
+}
+
+// StopBranchPipeline stops a pipeline run
+func (j *JenkinsClient) StopBranchPipeline(projectName, pipelineName, branchName, runID string, httpParameters *devops.HttpParameters) (*devops.StopPipeline, error) {
+	return j.jenkins.StopBranchPipeline(projectName, pipelineName, branchName, runID, httpParameters)
+}
+
+// ReplayBranchPipeline replays a pipeline
+func (j *JenkinsClient) ReplayBranchPipeline(projectName, pipelineName, branchName, runID string, httpParameters *devops.HttpParameters) (*devops.ReplayPipeline, error) {
+	return j.jenkins.ReplayBranchPipeline(projectName, pipelineName, branchName, runID, httpParameters)
+}
+
+// RunBranchPipeline triggers a pipeline
+func (j *JenkinsClient) RunBranchPipeline(projectName, pipelineName, branchName string, httpParameters *devops.HttpParameters) (*devops.RunPipeline, error) {
+	return j.jenkins.RunBranchPipeline(projectName, pipelineName, branchName, httpParameters)
+}
+
+// GetBranchArtifacts returns the artifacts of a pipeline run
+func (j *JenkinsClient) GetBranchArtifacts(projectName, pipelineName, branchName, runID string, httpParameters *devops.HttpParameters) ([]devops.Artifacts, error) {
+	return j.jenkins.GetBranchArtifacts(projectName, pipelineName, branchName, runID, httpParameters)
+}
+
+// GetBranchRunLog returns the pipeline run log
+func (j *JenkinsClient) GetBranchRunLog(projectName, pipelineName, branchName, runID string, httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.GetBranchRunLog(projectName, pipelineName, branchName, runID, httpParameters)
+}
+
+// GetBranchStepLog returns the log output of a pipeline step
+func (j *JenkinsClient) GetBranchStepLog(projectName, pipelineName, branchName, runID, nodeID, stepID string, httpParameters *devops.HttpParameters) ([]byte, http.Header, error) {
+	return j.jenkins.GetBranchStepLog(projectName, pipelineName, branchName, runID, nodeID, stepID, httpParameters)
+}
+
+// GetBranchNodeSteps returns the node steps
+func (j *JenkinsClient) GetBranchNodeSteps(projectName, pipelineName, branchName, runID, nodeID string, httpParameters *devops.HttpParameters) ([]devops.NodeSteps, error) {
+	return j.jenkins.GetBranchNodeSteps(projectName, pipelineName, branchName, runID, nodeID, httpParameters)
+}
+
+// GetBranchPipelineRunNodes returns the pipeline run nodes
+func (j *JenkinsClient) GetBranchPipelineRunNodes(projectName, pipelineName, branchName, runID string, httpParameters *devops.HttpParameters) ([]devops.BranchPipelineRunNodes, error) {
+	return j.jenkins.GetBranchPipelineRunNodes(projectName, pipelineName, branchName, runID, httpParameters)
+}
+
+// SubmitBranchInputStep submits a pipeline input step
+func (j *JenkinsClient) SubmitBranchInputStep(projectName, pipelineName, branchName, runID, nodeID, stepID string, httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.SubmitBranchInputStep(projectName, pipelineName, branchName, runID, nodeID, stepID, httpParameters)
+}
+
+// GetPipelineBranch returns PipelineBranch
+func (j *JenkinsClient) GetPipelineBranch(projectName, pipelineName string, httpParameters *devops.HttpParameters) (*devops.PipelineBranch, error) {
+	return j.jenkins.GetPipelineBranch(projectName, pipelineName, httpParameters)
+}
+
+// ScanBranch scans a pipeline
+func (j *JenkinsClient) ScanBranch(projectName, pipelineName string, httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.ScanBranch(projectName, pipelineName, httpParameters)
+}
+
+// GetConsoleLog returns the log output
+func (j *JenkinsClient) GetConsoleLog(projectName, pipelineName string, httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.GetConsoleLog(projectName, pipelineName, httpParameters)
+}
+
+// GetCrumb returns the crumb
+func (j *JenkinsClient) GetCrumb(httpParameters *devops.HttpParameters) (*devops.Crumb, error) {
+	return j.jenkins.GetCrumb(httpParameters)
+}
+
+// GetSCMServers returns the scm servers
+func (j *JenkinsClient) GetSCMServers(scmID string, httpParameters *devops.HttpParameters) ([]devops.SCMServer, error) {
+	return j.jenkins.GetSCMServers(scmID, httpParameters)
+}
+
+// GetSCMOrg returns the scm org
+func (j *JenkinsClient) GetSCMOrg(scmID string, httpParameters *devops.HttpParameters) ([]devops.SCMOrg, error) {
+	return j.jenkins.GetSCMOrg(scmID, httpParameters)
+}
+
+// GetOrgRepo returns the org and repo
+func (j *JenkinsClient) GetOrgRepo(scmID, organizationID string, httpParameters *devops.HttpParameters) (devops.OrgRepo, error) {
+	return j.jenkins.GetOrgRepo(scmID, organizationID, httpParameters)
+}
+
+// CreateSCMServers creates a scm server
+func (j *JenkinsClient) CreateSCMServers(scmID string, httpParameters *devops.HttpParameters) (*devops.SCMServer, error) {
+	return j.jenkins.CreateSCMServers(scmID, httpParameters)
+}
+
+// GetNotifyCommit returns notify commit
+func (j *JenkinsClient) GetNotifyCommit(httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.GetNotifyCommit(httpParameters)
+}
+
+// GithubWebhook trigger a github webhook
+func (j *JenkinsClient) GithubWebhook(httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.GithubWebhook(httpParameters)
+}
+
+// GenericWebhook triggers a generic webhook
+func (j *JenkinsClient) GenericWebhook(httpParameters *devops.HttpParameters) ([]byte, error) {
+	return j.jenkins.GenericWebhook(httpParameters)
+}
+
+// Validate does the validation check
+func (j *JenkinsClient) Validate(scmID string, httpParameters *devops.HttpParameters) (*devops.Validates, error) {
+	return j.jenkins.Validate(scmID, httpParameters)
+}
+
+// CheckScriptCompile checks the script compile
+func (j *JenkinsClient) CheckScriptCompile(projectName, pipelineName string, httpParameters *devops.HttpParameters) (*devops.CheckScript, error) {
+	return j.jenkins.CheckScriptCompile(projectName, pipelineName, httpParameters)
+}
+
+// CheckCron does the cron check
+func (j *JenkinsClient) CheckCron(projectName string, httpParameters *devops.HttpParameters) (*devops.CheckCronRes, error) {
+	return j.jenkins.CheckCron(projectName, httpParameters)
+}
+
+// ToJenkinsfile sends a request to turn to jenkinsfile
+func (j *JenkinsClient) ToJenkinsfile(httpParameters *devops.HttpParameters) (*devops.ResJenkinsfile, error) {
+	return j.jenkins.ToJenkinsfile(httpParameters)
+}
+
+// ToJSON sends a request to turn JSOn
+func (j *JenkinsClient) ToJSON(httpParameters *devops.HttpParameters) (map[string]interface{}, error) {
+	return j.jenkins.ToJSON(httpParameters)
+}
diff --git a/pkg/client/devops/jclient/project.go b/pkg/client/devops/jclient/project.go
new file mode 100644
index 0000000..d675b4c
--- /dev/null
+++ b/pkg/client/devops/jclient/project.go
@@ -0,0 +1,16 @@
+package jclient
+
+// CreateDevOpsProject creates a devops project
+func (j *JenkinsClient) CreateDevOpsProject(projectID string) (string, error) {
+	return j.jenkins.CreateDevOpsProject(projectID)
+}
+
+// DeleteDevOpsProject deletes a devops project
+func (j *JenkinsClient) DeleteDevOpsProject(projectID string) error {
+	return j.jenkins.DeleteDevOpsProject(projectID)
+}
+
+// GetDevOpsProject returns the devops project
+func (j *JenkinsClient) GetDevOpsProject(projectID string) (string, error) {
+	return j.jenkins.GetDevOpsProject(projectID)
+}
diff --git a/pkg/client/devops/jclient/projectPipeline.go b/pkg/client/devops/jclient/projectPipeline.go
new file mode 100644
index 0000000..baebe1f
--- /dev/null
+++ b/pkg/client/devops/jclient/projectPipeline.go
@@ -0,0 +1,95 @@
+package jclient
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/emicklei/go-restful"
+	"github.com/jenkins-zh/jenkins-client/pkg/job"
+	"k8s.io/klog"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	devopsv1alpha3 "kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"kubesphere.io/devops/pkg/client/devops"
+)
+
+// CreateProjectPipeline creates the pipeline
+func (j *JenkinsClient) CreateProjectPipeline(projectID string, pipeline *v1alpha3.Pipeline) (string, error) {
+	jclient := job.Client{
+		JenkinsCore: j.Core,
+	}
+	projectPipelineName := fmt.Sprintf("%s %s", projectID, pipeline.Name)
+	job, _ := jclient.GetJob(projectPipelineName)
+	if job != nil {
+		err := fmt.Errorf("job name [%s] has been used", job.Name)
+		return "", restful.NewError(http.StatusConflict, err.Error())
+	}
+	switch pipeline.Spec.Type {
+	case devopsv1alpha3.NoScmPipelineType:
+		createPayload, err := getCreatePayload(pipeline.Spec.Pipeline)
+		if err != nil {
+			return "", restful.NewError(http.StatusInternalServerError, err.Error())
+		}
+		err = jclient.CreateJobInFolder(*createPayload, projectID)
+		if err != nil {
+			return "", restful.NewError(devops.GetDevOpsStatusCode(err), err.Error())
+		}
+		return pipeline.Name, nil
+	case devopsv1alpha3.MultiBranchPipelineType:
+		createPayload, err := getCreateMultiBranchPipelinePayload(pipeline.Spec.Pipeline)
+		if err != nil {
+			return "", restful.NewError(http.StatusInternalServerError, err.Error())
+		}
+		err = jclient.CreateJobInFolder(*createPayload, projectID)
+		if err != nil {
+			return "", restful.NewError(devops.GetDevOpsStatusCode(err), err.Error())
+		}
+		return pipeline.Name, nil
+	default:
+		err := fmt.Errorf("error unsupport job type")
+		klog.Errorf("%+v", err)
+		return "", restful.NewError(http.StatusBadRequest, err.Error())
+	}
+}
+
+// DeleteProjectPipeline deletes pipeline
+func (j *JenkinsClient) DeleteProjectPipeline(projectID string, pipelineID string) (string, error) {
+	jclient := job.Client{
+		JenkinsCore: j.Core,
+	}
+	projectPipelineName := fmt.Sprintf("%s %s", projectID, pipelineID)
+	err := jclient.Delete(projectPipelineName)
+	if err != nil {
+		return "", restful.NewError(devops.GetDevOpsStatusCode(err), err.Error())
+	}
+	return pipelineID, nil
+}
+
+// UpdateProjectPipeline updates pipeline
+func (j *JenkinsClient) UpdateProjectPipeline(projectID string, pipeline *devopsv1alpha3.Pipeline) (string, error) {
+	// TODO: Update a pipeline
+	return j.jenkins.UpdateProjectPipeline(projectID, pipeline)
+}
+
+// GetProjectPipelineConfig returns the pipeline config
+func (j *JenkinsClient) GetProjectPipelineConfig(projectID, pipelineID string) (*devopsv1alpha3.Pipeline, error) {
+	// TODO: get a pipeline config
+	return j.jenkins.GetProjectPipelineConfig(projectID, pipelineID)
+}
+
+func getCreatePayload(pipeline *devopsv1alpha3.NoScmPipeline) (jobPayload *job.CreateJobPayload, err error) {
+	// NoScmPipeline do not have copy mode to create a pipeline
+	jobPayload = &job.CreateJobPayload{
+		Mode: "org.jenkinsci.plugins.workflow.job.WorkflowJob",
+		Name: pipeline.Name,
+	}
+	return
+}
+
+func getCreateMultiBranchPipelinePayload(pipeline *devopsv1alpha3.NoScmPipeline) (jobPayload *job.CreateJobPayload, err error) {
+	// NoScmPipeline do not have copy mode to create a pipeline
+	jobPayload = &job.CreateJobPayload{
+		Mode: "org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject",
+		Name: pipeline.Name,
+	}
+	return
+}
diff --git a/pkg/client/devops/jenkins/jenkins.go b/pkg/client/devops/jenkins/jenkins.go
index c291bd5..e60ccb6 100644
--- a/pkg/client/devops/jenkins/jenkins.go
+++ b/pkg/client/devops/jenkins/jenkins.go
@@ -25,7 +25,6 @@ import (
 	"net/http"
 	"net/url"
 	"os"
-	"reflect"
 	"strconv"
 	"strings"
 
@@ -217,259 +216,6 @@ func (j *Jenkins) Poll() (int, error) {
 	return resp.StatusCode, nil
 }
 
-// query roleName exist or not
-// if return roleName means exist
-func (j *Jenkins) GetGlobalRole(roleName string) (string, error) {
-	roleResponse := &GlobalRoleResponse{
-		RoleName: roleName,
-	}
-	stringResponse := ""
-	response, err := j.Requester.Get("/role-strategy/strategy/getRole",
-		&stringResponse,
-		map[string]string{
-			"roleName": roleName,
-			"type":     GLOBAL_ROLE,
-		})
-	if err != nil {
-		return "", err
-	}
-	if response.StatusCode != http.StatusOK {
-		return "", errors.New(strconv.Itoa(response.StatusCode))
-	}
-	if stringResponse == "{}" {
-		return "", nil
-	}
-	err = json.Unmarshal([]byte(stringResponse), roleResponse)
-	if err != nil {
-		return "", err
-	}
-	return roleResponse.RoleName, nil
-}
-
-func (j *Jenkins) GetGlobalRoleHandler(roleName string) (*GlobalRole, error) {
-	name, err := j.GetGlobalRole(roleName)
-	if err != nil {
-		return nil, err
-	}
-	roleResponse := &GlobalRoleResponse{
-		RoleName: name,
-	}
-	if err != nil {
-		return nil, err
-	}
-	return &GlobalRole{
-		Jenkins: j,
-		Raw:     *roleResponse,
-	}, nil
-}
-
-// assign a global roleName to username(sid)
-func (j *Jenkins) AssignGlobalRole(roleName string, sid string) error {
-	globalRole, err := j.GetGlobalRoleHandler(roleName)
-	if err != nil {
-		return err
-	}
-	param := map[string]string{
-		"type":     GLOBAL_ROLE,
-		"roleName": globalRole.Raw.RoleName,
-		"sid":      sid,
-	}
-	responseString := ""
-	response, err := j.Requester.Post("/role-strategy/strategy/assignRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-// unassign a global roleName to username(sid)
-func (j *Jenkins) UnAssignGlobalRole(roleName string, sid string) error {
-	globalRole, err := j.GetGlobalRoleHandler(roleName)
-	if err != nil {
-		return err
-	}
-	param := map[string]string{
-		"type":     GLOBAL_ROLE,
-		"roleName": globalRole.Raw.RoleName,
-		"sid":      sid,
-	}
-	responseString := ""
-	response, err := j.Requester.Post("/role-strategy/strategy/unassignRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-func (j *Jenkins) GetProjectRole(roleName string) (*ProjectRole, error) {
-	roleResponse := &ProjectRoleResponse{
-		RoleName: roleName,
-	}
-	stringResponse := ""
-	response, err := j.Requester.Get("/role-strategy/strategy/getRole",
-		&stringResponse,
-		map[string]string{
-			"roleName": roleName,
-			"type":     PROJECT_ROLE,
-		})
-	if err != nil {
-		return nil, err
-	}
-	if response.StatusCode != http.StatusOK {
-		return nil, errors.New(strconv.Itoa(response.StatusCode))
-	}
-	if stringResponse == "{}" {
-		return nil, nil
-	}
-	err = json.Unmarshal([]byte(stringResponse), roleResponse)
-	if err != nil {
-		return nil, err
-	}
-	return &ProjectRole{
-		Jenkins: j,
-		Raw:     *roleResponse,
-	}, nil
-}
-
-// assign a project roleName to username(sid)
-func (j *Jenkins) AssignProjectRole(roleName string, sid string) error {
-	projectRole, err := j.GetProjectRole(roleName)
-	if err != nil {
-		return err
-	}
-	param := map[string]string{
-		"type":     PROJECT_ROLE,
-		"roleName": projectRole.Raw.RoleName,
-		"sid":      sid,
-	}
-	responseString := ""
-	response, err := j.Requester.Post("/role-strategy/strategy/assignRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-// unassign a project roleName to username(sid)
-func (j *Jenkins) UnAssignProjectRole(roleName string, sid string) error {
-	projectRole, err := j.GetProjectRole(roleName)
-	if err != nil {
-		return err
-	}
-	param := map[string]string{
-		"type":     PROJECT_ROLE,
-		"roleName": projectRole.Raw.RoleName,
-		"sid":      sid,
-	}
-	responseString := ""
-	response, err := j.Requester.Post("/role-strategy/strategy/unassignRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-// add a global roleName
-func (j *Jenkins) AddGlobalRole(roleName string, ids devops.GlobalPermissionIds, overwrite bool) error {
-	var idArray []string
-	values := reflect.ValueOf(ids)
-	for i := 0; i < values.NumField(); i++ {
-		field := values.Field(i)
-		if field.Bool() {
-			idArray = append(idArray, values.Type().Field(i).Tag.Get("json"))
-		}
-	}
-	param := map[string]string{
-		"roleName":      roleName,
-		"type":          GLOBAL_ROLE,
-		"permissionIds": strings.Join(idArray, ","),
-		"overwrite":     strconv.FormatBool(overwrite),
-	}
-	responseString := ""
-	response, err := j.Requester.Post("/role-strategy/strategy/addRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-// delete roleName from the project
-func (j *Jenkins) DeleteProjectRoles(roleName ...string) error {
-	responseString := ""
-
-	response, err := j.Requester.Post("/role-strategy/strategy/removeRoles", nil, &responseString, map[string]string{
-		"type":      PROJECT_ROLE,
-		"roleNames": strings.Join(roleName, ","),
-	})
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		fmt.Println(responseString)
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-// add roleName for project
-func (j *Jenkins) AddProjectRole(roleName string, pattern string, ids devops.ProjectPermissionIds, overwrite bool) error {
-	var idArray []string
-	values := reflect.ValueOf(ids)
-	for i := 0; i < values.NumField(); i++ {
-		field := values.Field(i)
-		if field.Bool() {
-			idArray = append(idArray, values.Type().Field(i).Tag.Get("json"))
-		}
-	}
-	param := map[string]string{
-		"roleName":      roleName,
-		"type":          PROJECT_ROLE,
-		"permissionIds": strings.Join(idArray, ","),
-		"overwrite":     strconv.FormatBool(overwrite),
-		"pattern":       pattern,
-	}
-	responseString := ""
-	response, err := j.Requester.Post("/role-strategy/strategy/addRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-func (j *Jenkins) DeleteUserInProject(username string) error {
-	param := map[string]string{
-		"type": PROJECT_ROLE,
-		"sid":  username,
-	}
-	responseString := ""
-	response, err := j.Requester.Post("/role-strategy/strategy/deleteSid", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
 func (j *Jenkins) GetPipeline(projectName, pipelineName string, httpParameters *devops.HttpParameters) (*devops.Pipeline, error) {
 	PipelineOjb := &Pipeline{
 		HttpParameters: httpParameters,
@@ -884,13 +630,13 @@ func (j *Jenkins) ToJenkinsfile(httpParameters *devops.HttpParameters) (*devops.
 	return res, err
 }
 
-func (j *Jenkins) ToJson(httpParameters *devops.HttpParameters) (map[string]interface{}, error) {
+func (j *Jenkins) ToJSON(httpParameters *devops.HttpParameters) (map[string]interface{}, error) {
 	PipelineOjb := &Pipeline{
 		HttpParameters: httpParameters,
 		Jenkins:        j,
 		Path:           ToJsonUrl,
 	}
-	res, err := PipelineOjb.ToJson()
+	res, err := PipelineOjb.ToJSON()
 	return res, err
 }
 
diff --git a/pkg/client/devops/jenkins/pipeline.go b/pkg/client/devops/jenkins/pipeline.go
index d261ca1..5c3d3e1 100644
--- a/pkg/client/devops/jenkins/pipeline.go
+++ b/pkg/client/devops/jenkins/pipeline.go
@@ -869,7 +869,7 @@ func (p *Pipeline) ToJenkinsfile() (*devops.ResJenkinsfile, error) {
 	return &jenkinsfile, err
 }
 
-func (p *Pipeline) ToJson() (result map[string]interface{}, err error) {
+func (p *Pipeline) ToJSON() (result map[string]interface{}, err error) {
 	var data []byte
 	if data, err = p.Jenkins.SendPureRequest(p.Path, p.HttpParameters); err == nil {
 		err = json.Unmarshal(data, &result)
diff --git a/pkg/client/devops/jenkins/project.go b/pkg/client/devops/jenkins/project.go
index 583b067..e1fe9b5 100644
--- a/pkg/client/devops/jenkins/project.go
+++ b/pkg/client/devops/jenkins/project.go
@@ -23,11 +23,6 @@ import (
 	"kubesphere.io/devops/pkg/client/devops"
 )
 
-type DevOpsProjectRoleResponse struct {
-	ProjectRole *ProjectRole
-	Err         error
-}
-
 func (j *Jenkins) CreateDevOpsProject(projectId string) (string, error) {
 	_, err := j.CreateFolder(projectId, "")
 	if err != nil {
diff --git a/pkg/client/devops/jenkins/role.go b/pkg/client/devops/jenkins/role.go
deleted file mode 100644
index 8baa2f2..0000000
--- a/pkg/client/devops/jenkins/role.go
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
-Copyright 2020 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 jenkins
-
-import (
-	"errors"
-	"net/http"
-	"reflect"
-	"strconv"
-	"strings"
-
-	"kubesphere.io/devops/pkg/client/devops"
-)
-
-type GlobalRoleResponse struct {
-	RoleName      string                     `json:"roleName"`
-	PermissionIds devops.GlobalPermissionIds `json:"permissionIds"`
-}
-
-type GlobalRole struct {
-	Jenkins *Jenkins
-	Raw     GlobalRoleResponse
-}
-
-type ProjectRole struct {
-	Jenkins *Jenkins
-	Raw     ProjectRoleResponse
-}
-
-type ProjectRoleResponse struct {
-	RoleName      string                      `json:"roleName"`
-	PermissionIds devops.ProjectPermissionIds `json:"permissionIds"`
-	Pattern       string                      `json:"pattern"`
-}
-
-func (j *GlobalRole) Update(ids devops.GlobalPermissionIds) error {
-	var idArray []string
-	values := reflect.ValueOf(ids)
-	for i := 0; i < values.NumField(); i++ {
-		field := values.Field(i)
-		if field.Bool() {
-			idArray = append(idArray, values.Type().Field(i).Tag.Get("json"))
-		}
-	}
-	param := map[string]string{
-		"roleName":      j.Raw.RoleName,
-		"type":          GLOBAL_ROLE,
-		"permissionIds": strings.Join(idArray, ","),
-		"overwrite":     strconv.FormatBool(true),
-	}
-	responseString := ""
-	response, err := j.Jenkins.Requester.Post("/role-strategy/strategy/addRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-// call jenkins api to update global role
-func (j *GlobalRole) AssignRole(sid string) error {
-	param := map[string]string{
-		"type":     GLOBAL_ROLE,
-		"roleName": j.Raw.RoleName,
-		"sid":      sid,
-	}
-	responseString := ""
-	response, err := j.Jenkins.Requester.Post("/role-strategy/strategy/assignRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-func (j *GlobalRole) UnAssignRole(sid string) error {
-	param := map[string]string{
-		"type":     GLOBAL_ROLE,
-		"roleName": j.Raw.RoleName,
-		"sid":      sid,
-	}
-	responseString := ""
-	response, err := j.Jenkins.Requester.Post("/role-strategy/strategy/unassignRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-// update ProjectPermissionIds to Project
-// pattern string means some project, like project-name/*
-func (j *ProjectRole) Update(pattern string, ids devops.ProjectPermissionIds) error {
-	var idArray []string
-	values := reflect.ValueOf(ids)
-	for i := 0; i < values.NumField(); i++ {
-		field := values.Field(i)
-		if field.Bool() {
-			idArray = append(idArray, values.Type().Field(i).Tag.Get("json"))
-		}
-	}
-	param := map[string]string{
-		"roleName":      j.Raw.RoleName,
-		"type":          PROJECT_ROLE,
-		"permissionIds": strings.Join(idArray, ","),
-		"overwrite":     strconv.FormatBool(true),
-		"pattern":       pattern,
-	}
-	responseString := ""
-	response, err := j.Jenkins.Requester.Post("/role-strategy/strategy/addRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-func (j *ProjectRole) AssignRole(sid string) error {
-	param := map[string]string{
-		"type":     PROJECT_ROLE,
-		"roleName": j.Raw.RoleName,
-		"sid":      sid,
-	}
-	responseString := ""
-	response, err := j.Jenkins.Requester.Post("/role-strategy/strategy/assignRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
-
-func (j *ProjectRole) UnAssignRole(sid string) error {
-	param := map[string]string{
-		"type":     PROJECT_ROLE,
-		"roleName": j.Raw.RoleName,
-		"sid":      sid,
-	}
-	responseString := ""
-	response, err := j.Jenkins.Requester.Post("/role-strategy/strategy/unassignRole", nil, &responseString, param)
-	if err != nil {
-		return err
-	}
-	if response.StatusCode != http.StatusOK {
-		return errors.New(strconv.Itoa(response.StatusCode))
-	}
-	return nil
-}
diff --git a/pkg/client/devops/pipeline.go b/pkg/client/devops/pipeline.go
index 2a945c9..016cf20 100644
--- a/pkg/client/devops/pipeline.go
+++ b/pkg/client/devops/pipeline.go
@@ -1175,5 +1175,5 @@ type PipelineOperator interface {
 	CheckScriptCompile(projectName, pipelineName string, httpParameters *HttpParameters) (*CheckScript, error)
 	CheckCron(projectName string, httpParameters *HttpParameters) (*CheckCronRes, error)
 	ToJenkinsfile(httpParameters *HttpParameters) (*ResJenkinsfile, error)
-	ToJson(httpParameters *HttpParameters) (map[string]interface{}, error)
+	ToJSON(httpParameters *HttpParameters) (map[string]interface{}, error)
 }
diff --git a/pkg/client/devops/role.go b/pkg/client/devops/role.go
index abef9e2..a4cf4fd 100644
--- a/pkg/client/devops/role.go
+++ b/pkg/client/devops/role.go
@@ -70,20 +70,3 @@ type ProjectPermissionIds struct {
 	RunReplay               bool `json:"hudson.model.Run.Replay"`
 	SCMTag                  bool `json:"hudson.scm.SCM.Tag"`
 }
-
-// describe the interface of DevOps to operator role
-type RoleOperator interface {
-	AddGlobalRole(roleName string, ids GlobalPermissionIds, overwrite bool) error
-	GetGlobalRole(roleName string) (string, error)
-
-	AddProjectRole(roleName string, pattern string, ids ProjectPermissionIds, overwrite bool) error
-	DeleteProjectRoles(roleName ...string) error
-
-	AssignProjectRole(roleName string, sid string) error
-	UnAssignProjectRole(roleName string, sid string) error
-
-	AssignGlobalRole(roleName string, sid string) error
-	UnAssignGlobalRole(roleName string, sid string) error
-
-	DeleteUserInProject(sid string) error
-}
diff --git a/pkg/kapis/devops/v1alpha2/devops.go b/pkg/kapis/devops/v1alpha2/devops.go
index 6d53c34..85d0ec8 100644
--- a/pkg/kapis/devops/v1alpha2/devops.go
+++ b/pkg/kapis/devops/v1alpha2/devops.go
@@ -873,8 +873,8 @@ func (h *ProjectPipelineHandler) ToJenkinsfile(req *restful.Request, resp *restf
 	resp.WriteAsJson(res)
 }
 
-func (h *ProjectPipelineHandler) ToJson(req *restful.Request, resp *restful.Response) {
-	res, err := h.devopsOperator.ToJson(req.Request)
+func (h *ProjectPipelineHandler) ToJSON(req *restful.Request, resp *restful.Response) {
+	res, err := h.devopsOperator.ToJSON(req.Request)
 	if err != nil {
 		parseErr(err, resp)
 		return
diff --git a/pkg/kapis/devops/v1alpha2/register.go b/pkg/kapis/devops/v1alpha2/register.go
index 53aee1a..24aa2cf 100644
--- a/pkg/kapis/devops/v1alpha2/register.go
+++ b/pkg/kapis/devops/v1alpha2/register.go
@@ -641,7 +641,7 @@ func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops
 		 * https://github.com/jenkinsci/pipeline-model-definition-plugin/blob/fc8d22192d7d3a17badc3b8af7191a84bb7fd4ca/EXTENDING.md#conversion-to-json-representation-from-jenkinsfile
 		 */
 		webservice.Route(webservice.POST("/tojson").
-			To(projectPipelineHandler.ToJson).
+			To(projectPipelineHandler.ToJSON).
 			Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsJenkinsfileTag}).
 			Consumes("application/x-www-form-urlencoded").
 			Produces("application/json", "charset=utf-8").
diff --git a/pkg/models/devops/devops.go b/pkg/models/devops/devops.go
index 24a8945..11c71b3 100644
--- a/pkg/models/devops/devops.go
+++ b/pkg/models/devops/devops.go
@@ -116,7 +116,7 @@ type DevopsOperator interface {
 	CheckCron(projectName string, req *http.Request) (*devops.CheckCronRes, error)
 
 	ToJenkinsfile(req *http.Request) (*devops.ResJenkinsfile, error)
-	ToJson(req *http.Request) (map[string]interface{}, error)
+	ToJSON(req *http.Request) (map[string]interface{}, error)
 }
 
 type devopsOperator struct {
@@ -912,9 +912,9 @@ func (d devopsOperator) ToJenkinsfile(req *http.Request) (*devops.ResJenkinsfile
 	return res, err
 }
 
-func (d devopsOperator) ToJson(req *http.Request) (map[string]interface{}, error) {
+func (d devopsOperator) ToJSON(req *http.Request) (map[string]interface{}, error) {
 
-	res, err := d.devopsClient.ToJson(convertToHttpParameters(req))
+	res, err := d.devopsClient.ToJSON(convertToHttpParameters(req))
 	if err != nil {
 		klog.Error(err)
 		return nil, err


Test patch:
diff --git a/controllers/pipeline/pipeline_controller_test.go b/controllers/pipeline/pipeline_controller_test.go
index d72b8a3..08bdbfd 100644
--- a/controllers/pipeline/pipeline_controller_test.go
+++ b/controllers/pipeline/pipeline_controller_test.go
@@ -23,7 +23,10 @@ import (
 
 	v1 "k8s.io/api/core/v1"
 
+	"github.com/golang/mock/gomock"
 	fakeDevOps "kubesphere.io/devops/pkg/client/devops/fake"
+	"kubesphere.io/devops/pkg/client/devops/jclient"
+
 	"kubesphere.io/devops/pkg/constants"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -39,6 +42,7 @@ import (
 
 	devops "kubesphere.io/devops/pkg/api/devops/v1alpha3"
 
+	"github.com/jenkins-zh/jenkins-client/pkg/mock/mhttp"
 	"kubesphere.io/devops/pkg/client/clientset/versioned/fake"
 	informers "kubesphere.io/devops/pkg/client/informers/externalversions"
 )
@@ -65,12 +69,19 @@ type fixture struct {
 	initDevOpsProject string
 	initPipeline      []*devops.Pipeline
 	expectPipeline    []*devops.Pipeline
+	jenkinsClient     *jclient.JenkinsClient
+	roundTripper      *mhttp.MockRoundTripper
 }
 
 func newFixture(t *testing.T) *fixture {
 	f := &fixture{}
 	f.t = t
 	f.objects = []runtime.Object{}
+
+	ctrl := gomock.NewController(t)
+	roundTripper := mhttp.NewMockRoundTripper(ctrl)
+	f.roundTripper = roundTripper
+	f.jenkinsClient = fakeDevOps.NewFakeJenkinsClient(roundTripper)
 	return f
 }
 
@@ -147,7 +158,6 @@ func (f *fixture) newController() (*Controller, informers.SharedInformerFactory,
 	i := informers.NewSharedInformerFactory(f.client, noResyncPeriodFunc())
 	k8sI := kubeinformers.NewSharedInformerFactory(f.kubeclient, noResyncPeriodFunc())
 	dI := fakeDevOps.NewWithPipelines(f.initDevOpsProject, f.initPipeline...)
-
 	c := NewController(f.kubeclient, f.client, dI, k8sI.Core().V1().Namespaces(),
 		i.Devops().V1alpha3().Pipelines())
 
@@ -212,7 +222,6 @@ func (f *fixture) runController(projectName string, startInformers bool, expectE
 		if !reflect.DeepEqual(actualPipeline, pipeline) {
 			f.t.Errorf(" pipeline %+v not match %+v", pipeline, actualPipeline)
 		}
-
 	}
 }
 
@@ -345,15 +354,27 @@ func TestCreatePipeline(t *testing.T) {
 	nsName := "test-123"
 	pipelineName := "test"
 	projectName := "test_project"
-
+	spec := devops.PipelineSpec{
+		Type: devops.NoScmPipelineType,
+		Pipeline: &devops.NoScmPipeline{
+			Name: pipelineName,
+		},
+	}
+	pipeline := newPipeline(nsName, pipelineName, spec, false, false)
 	ns := newNamespace(nsName, projectName)
-	pipeline := newPipeline(nsName, pipelineName, devops.PipelineSpec{}, false, false)
-	expectPipeline := newPipeline(nsName, pipelineName, devops.PipelineSpec{}, true, true)
+
 	f.pipelineLister = append(f.pipelineLister, pipeline)
 	f.namespaceLister = append(f.namespaceLister, ns)
 	f.objects = append(f.objects, pipeline)
 	f.initDevOpsProject = nsName
+
+	expectPipeline := pipeline.DeepCopy()
+	expectPipeline.Finalizers = []string{devops.PipelineFinalizerName}
+	expectPipeline.Annotations = map[string]string{
+		devops.PipelineSyncStatusAnnoKey: constants.StatusSuccessful,
+	}
 	f.expectPipeline = []*devops.Pipeline{expectPipeline}
+
 	f.run(getKey(pipeline, t))
 }
 
diff --git a/pkg/client/devops/interface_test.go b/pkg/client/devops/interface_test.go
new file mode 100644
index 0000000..5736330
--- /dev/null
+++ b/pkg/client/devops/interface_test.go
@@ -0,0 +1,55 @@
+package devops
+
+import (
+	"fmt"
+	"net/http"
+	"net/url"
+	"testing"
+)
+
+func TestGetDevOpsStatusCode(t *testing.T) {
+	type args struct {
+		devopsErr error
+	}
+	tests := []struct {
+		name string
+		args args
+		want int
+	}{{
+		name: "just be a number",
+		args: args{devopsErr: fmt.Errorf("%d", http.StatusAccepted)},
+		want: http.StatusAccepted,
+	}, {
+		name: "ErrorResponse",
+		args: args{
+			devopsErr: &ErrorResponse{
+				Response: &http.Response{
+					StatusCode: http.StatusNotFound,
+					Request: &http.Request{
+						Method: http.MethodGet,
+						URL: &url.URL{
+							Scheme: "http",
+							Host:   "host",
+						},
+					},
+				},
+			},
+		},
+		want: http.StatusNotFound,
+	}, {
+		name: "other error type",
+		args: args{devopsErr: fmt.Errorf("other error type")},
+		want: http.StatusInternalServerError,
+	}, {
+		name: "a formatted error message",
+		args: args{devopsErr: fmt.Errorf("unexpected status code: 404")},
+		want: http.StatusNotFound,
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := GetDevOpsStatusCode(tt.args.devopsErr); got != tt.want {
+				t.Errorf("GetDevOpsStatusCode() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}