Repository: kubesphere/ks-devops
Pull request #435 (2022-01-07 15:54:42)

Fix the API for getting devops project
<!-- 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 cleanup
/kind documentation
/kind feature
/kind design
/kind chore

Optionally add one or more of the following kinds if applicable:
/kind api-change
/kind deprecation
/kind failing-test
/kind flake
/kind regression
-->
/kind bug

### What this PR does / why we need it:
Currently, the UI try to use this API to find if the desired name is duplicated. But for DevOpsProject, we should use the generateName instead of the resource real name.

### 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 #

This PR should be able to fix part of https://github.com/kubesphere/console/issues/2707

### 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.
-->
```
surenpi/devops-apiserver:dev-v3.2.1-0740803
```

### 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
Fix the API for getting devops project
```

## Test results

![image](https://user-images.githubusercontent.com/1450685/148721350-2fb197b0-4135-4503-88d4-eac335cf73e6.png)

![image](https://user-images.githubusercontent.com/1450685/148721343-dd9b3753-b0f4-40b8-aa06-31ca33517e41.png)

/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/435#" 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: I'm not sure if there are other purposes for this API. So I will do more tests on it.
/hold
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/435?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#435](https://codecov.io/gh/kubesphere/ks-devops/pull/435?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (620fb93) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/d2f50497bcfb8ee7faf1f2874894857545538d5d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (d2f5049) will **increase** coverage by `0.47%`.
> The diff coverage is `78.04%`.

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

```diff
@@            Coverage Diff             @@
##           master     #435      +/-   ##
==========================================
+ Coverage   12.22%   12.70%   +0.47%     
==========================================
  Files         108      109       +1     
  Lines       22261    22247      -14     
==========================================
+ Hits         2722     2826     +104     
+ Misses      19375    19249     -126     
- Partials      164      172       +8     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `12.70% <78.04%> (+0.47%)` | :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/435?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/kapis/devops/v1alpha3/handler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/435/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhMy9oYW5kbGVyLmdv) | `61.90% <65.38%> (+17.33%)` | :arrow_up: |
| [pkg/models/devops/devops.go](https://codecov.io/gh/kubesphere/ks-devops/pull/435/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL21vZGVscy9kZXZvcHMvZGV2b3BzLmdv) | `19.37% <100.00%> (+6.99%)` | :arrow_up: |
| [pkg/api/devops/v1alpha1/webhook.go](https://codecov.io/gh/kubesphere/ks-devops/pull/435/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2FwaS9kZXZvcHMvdjFhbHBoYTEvd2ViaG9vay5nbw==) | `100.00% <0.00%> (ø)` | |
| [pkg/api/devops/v1alpha1/gitrepository.go](https://codecov.io/gh/kubesphere/ks-devops/pull/435/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2FwaS9kZXZvcHMvdjFhbHBoYTEvZ2l0cmVwb3NpdG9yeS5nbw==) | `100.00% <0.00%> (ø)` | |
| [controllers/gitrepository/webhook\_notify.go](https://codecov.io/gh/kubesphere/ks-devops/pull/435/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvZ2l0cmVwb3NpdG9yeS93ZWJob29rX25vdGlmeS5nbw==) | `48.64% <0.00%> (ø)` | |
| [pkg/models/devops/common.go](https://codecov.io/gh/kubesphere/ks-devops/pull/435/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL21vZGVscy9kZXZvcHMvY29tbW9uLmdv) | `33.33% <0.00%> (+8.33%)` | :arrow_up: |
| [.../gitrepository/gitrepository\_webhook\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/435/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvZ2l0cmVwb3NpdG9yeS9naXRyZXBvc2l0b3J5X3dlYmhvb2tfY29udHJvbGxlci5nbw==) | `56.61% <0.00%> (+43.89%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/435?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/435?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [d2f5049...620fb93](https://codecov.io/gh/kubesphere/ks-devops/pull/435?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
LinuxSuRen: /hold
LinuxSuRen: /unhold

Some changes for API `/workspaces/{workspace}/devops/{devops}}`:

* query the DevOpsProject via `generateName` of the Kubernetes resource if `?generateName=true`
* query the real name of the Kubernetes resource for other cases


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/Makefile b/Makefile
index a62ded4..e958627 100644
--- a/Makefile
+++ b/Makefile
@@ -113,6 +113,9 @@ docker-build-push-tools: docker-build-tools docker-push-tools
 
 docker-build-push: docker-build-push-apiserver docker-build-push-controller
 
+swagger-ui:
+	git clone https://github.com/swagger-api/swagger-ui -b v2.2.10 --depth 1 bin/swagger-ui
+
 mock-gen:
 	mockgen -source=cmd/tools/jwt/app/configmap_updater.go -destination ./cmd/tools/jwt/app/mock_app/configmap_updater.go
 	mockgen -source=cmd/tools/jwt/app/kubernetes.go -destination ./cmd/tools/jwt/app/mock_app/kubernetes.go
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/config/dockerfiles/apiserver/Dockerfile b/config/dockerfiles/apiserver/Dockerfile
index b4fd4a7..cd027c4 100644
--- a/config/dockerfiles/apiserver/Dockerfile
+++ b/config/dockerfiles/apiserver/Dockerfile
@@ -17,11 +17,15 @@ COPY pkg pkg/
 # Build
 RUN CGO_ENABLED=0 GO111MODULE=on go build -a -o apiserver cmd/apiserver/apiserver.go
 
+# download Swagger UI files
+RUN git clone https://github.com/swagger-api/swagger-ui -b v2.2.10 --depth 1
+
 # Use distroless as minimal base image to package the manager binary
 # Refer to https://github.com/GoogleContainerTools/distroless for more details
 FROM gcr.io/distroless/static:nonroot
 WORKDIR /
 COPY --from=builder /workspace/apiserver .
+COPY --from=builder /workspace/swagger-ui/dist bin/swagger-ui
 USER nonroot:nonroot
 
 ENTRYPOINT ["/apiserver"]
diff --git a/controllers/gitrepository/gitrepository_webhook_controller.go b/controllers/gitrepository/gitrepository_webhook_controller.go
index 00c00dd..6719cd2 100644
--- a/controllers/gitrepository/gitrepository_webhook_controller.go
+++ b/controllers/gitrepository/gitrepository_webhook_controller.go
@@ -1,12 +1,9 @@
 /*
 Copyright 2021 The KubeSphere Authors.
-
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
-
     http://www.apache.org/licenses/LICENSE-2.0
-
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
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/docs/README.md b/docs/README.md
index 2ddb87f..47b4746 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,3 +5,4 @@ This is the right place if you want to know more details about `ks-devops`.
 * [installation](installation.md)
 * [projects](projects.md)
 * [e2e](e2e.md)
+* [Swagger Support](swagger.md)
diff --git a/docs/swagger.md b/docs/swagger.md
new file mode 100644
index 0000000..a186607
--- /dev/null
+++ b/docs/swagger.md
@@ -0,0 +1,3 @@
+You can explore all APIs via the Swagger UI:
+
+* The URL is `ip:port/apidocs/`
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 3b8cddb..950a787 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
 	github.com/drone/go-scm v1.16.2
 	github.com/emicklei/go-restful v2.9.6+incompatible
 	github.com/emicklei/go-restful-openapi v1.4.1
+	github.com/emicklei/go-restful-swagger12 v0.0.0-20201014110547-68ccff494617
 	github.com/fatih/structs v1.1.0
 	github.com/form3tech-oss/jwt-go v3.2.2+incompatible
 	github.com/go-logr/logr v0.3.0
@@ -23,7 +24,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..c581033 100644
--- a/go.sum
+++ b/go.sum
@@ -116,6 +116,8 @@ github.com/emicklei/go-restful v2.9.6+incompatible h1:tfrHha8zJ01ywiOEC1miGY8st1
 github.com/emicklei/go-restful v2.9.6+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful-openapi v1.4.1 h1:SocVTIQWnXyit4dotTrwmncBAjtRaBmfcHjo3XGcCm4=
 github.com/emicklei/go-restful-openapi v1.4.1/go.mod h1:kWQ8rQMVQ6G6lePwjDveJ00KjAUr/jq6z1X8DrDP3Gc=
+github.com/emicklei/go-restful-swagger12 v0.0.0-20201014110547-68ccff494617 h1:jri9taV4TK9oItoWJCofXJi21Dp/k25u32NnfphqLAY=
+github.com/emicklei/go-restful-swagger12 v0.0.0-20201014110547-68ccff494617/go.mod h1:qr0VowGBT4CS4Q8vFF8BSeKz34PuqKGxs/L0IAQA9DQ=
 github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -322,8 +324,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/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index 1596749..254391b 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -20,6 +20,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"kubesphere.io/devops/pkg/kapis/doc"
 	"net/http"
 	rt "runtime"
 	"time"
@@ -127,7 +128,7 @@ func (s *APIServer) PrepareRun(stopCh <-chan struct{}) error {
 	return nil
 }
 
-// Install all kubesphere api groups
+// Install all KubeSphere api groups
 // 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() {
@@ -137,7 +138,9 @@ func (s *APIServer) installKubeSphereAPIs() {
 		Token:    s.Config.JenkinsOptions.Password,
 	}
 
-	utilruntime.Must(devopsv1alpha2.AddToContainer(s.container,
+	var wss []*restful.WebService
+
+	v1alpha2WSS, err := devopsv1alpha2.AddToContainer(s.container,
 		s.InformerFactory.KubeSphereSharedInformerFactory(),
 		s.DevopsClient,
 		s.SonarClient,
@@ -145,13 +148,16 @@ func (s *APIServer) installKubeSphereAPIs() {
 		s.S3Client,
 		s.Config.JenkinsOptions.Host,
 		s.KubernetesClient,
-		jenkinsCore))
-	devopsv1alpha3.AddToContainer(s.container, s.DevopsClient, s.KubernetesClient, s.Client)
-	oauth.AddToContainer(s.container,
+		jenkinsCore)
+	utilruntime.Must(err)
+	wss = append(wss, v1alpha2WSS...)
+	wss = append(wss, devopsv1alpha3.AddToContainer(s.container, s.DevopsClient, s.KubernetesClient, s.Client)...)
+	wss = append(wss, oauth.AddToContainer(s.container,
 		auth.NewTokenOperator(
 			s.CacheClient,
 			s.Config.AuthenticationOptions),
-	)
+	))
+	doc.AddSwaggerService(wss, s.container)
 }
 
 func (s *APIServer) Run(stopCh <-chan struct{}) (err error) {
diff --git a/pkg/apiserver/filters/requestinfo.go b/pkg/apiserver/filters/requestinfo.go
index dca7ba2..c832204 100644
--- a/pkg/apiserver/filters/requestinfo.go
+++ b/pkg/apiserver/filters/requestinfo.go
@@ -78,6 +78,6 @@ func injectToken(req *http.Request) (requestWithTokenContext *http.Request) {
 	authorization := req.Header.Get("X-Authorization")
 	token := strings.ReplaceAll(authorization, "Bearer ", "")
 	token = strings.ReplaceAll(token, "bearer ", "")
-	requestWithTokenContext = req.WithContext(context.WithValue(req.Context(), constants.K8SToken, token))
+	requestWithTokenContext = req.WithContext(context.WithValue(req.Context(), constants.K8SToken, constants.ContextKeyK8SToken(token)))
 	return
 }
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..0d9da9a 100644
--- a/pkg/kapis/devops/v1alpha2/register.go
+++ b/pkg/kapis/devops/v1alpha2/register.go
@@ -49,20 +49,22 @@ 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, jenkinsClient core.JenkinsCore) error {
+	s3Client s3.Interface, endpoint string, k8sClient k8s.Client, jenkinsClient core.JenkinsCore) (wss []*restful.WebService, err error) {
 	wsWithGroup := runtime.NewWebService(GroupVersion)
+	wss = append(wss, wsWithGroup)
 	// the API endpoint with group version will be removed in the future release
-	if err := addToContainerWithWebService(container, ksInformers, devopsClient, sonarqubeClient, ksClient,
+	if err = addToContainerWithWebService(container, ksInformers, devopsClient, sonarqubeClient, ksClient,
 		s3Client, endpoint, k8sClient, jenkinsClient, wsWithGroup); err != nil {
-		return err
+		return
 	}
 
 	ws := runtime.NewWebServiceWithoutGroup(GroupVersion)
-	if err := addToContainerWithWebService(container, ksInformers, devopsClient, sonarqubeClient, ksClient,
+	wss = append(wss, ws)
+	if err = addToContainerWithWebService(container, ksInformers, devopsClient, sonarqubeClient, ksClient,
 		s3Client, endpoint, k8sClient, jenkinsClient, ws); err != nil {
-		return err
+		return
 	}
-	return nil
+	return
 }
 
 func addToContainerWithWebService(container *restful.Container, ksInformers externalversions.SharedInformerFactory,
@@ -641,7 +643,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/kapis/devops/v1alpha3/handler.go b/pkg/kapis/devops/v1alpha3/handler.go
index 8dc8a4f..3eb9839 100644
--- a/pkg/kapis/devops/v1alpha3/handler.go
+++ b/pkg/kapis/devops/v1alpha3/handler.go
@@ -46,23 +46,24 @@ func newDevOpsHandler(devopsClient devopsClient.Interface, k8sClient k8s.Client)
 	}
 }
 
-// devopsproject handler about get/list/post/put/delete
+// GetDevOpsProject handler about get/list/post/put/delete
 func (h *devopsHandler) GetDevOpsProject(request *restful.Request, response *restful.Response) {
 	workspace := request.PathParameter("workspace")
-	devops := request.PathParameter("devops")
+	devopsProject := request.PathParameter("devops")
+	generateNameFlag := request.QueryParameter("generateName")
 
 	if client, err := h.getDevOps(request); err == nil {
-		project, err := client.GetDevOpsProject(workspace, devops)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
+		var project *v1alpha3.DevOpsProject
+		var err error
+
+		switch generateNameFlag {
+		case "true":
+			project, err = client.GetDevOpsProjectByGenerateName(workspace, devopsProject)
+		default:
+			project, err = client.GetDevOpsProject(workspace, devopsProject)
 		}
-		_ = response.WriteEntity(project)
+
+		errorHandle(err, response, request, project)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -74,16 +75,7 @@ func (h *devopsHandler) ListDevOpsProject(request *restful.Request, response *re
 
 	if client, err := h.getDevOps(request); err == nil {
 		projectList, err := client.ListDevOpsProject(workspace, limit, offset)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(projectList)
+		errorHandle(err, response, request, projectList)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -133,16 +125,7 @@ func (h *devopsHandler) UpdateDevOpsProject(request *restful.Request, response *
 
 	if client, err := h.getDevOps(request); err == nil {
 		project, err := client.UpdateDevOpsProject(workspace, &devOpsProject)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(project)
+		errorHandle(err, response, request, project)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -154,16 +137,7 @@ func (h *devopsHandler) DeleteDevOpsProject(request *restful.Request, response *
 
 	if client, err := h.getDevOps(request); err == nil {
 		err := client.DeleteDevOpsProject(workspace, devops)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(servererr.None)
+		errorHandle(err, response, request, servererr.None)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -176,16 +150,7 @@ func (h *devopsHandler) GetPipeline(request *restful.Request, response *restful.
 
 	if client, err := h.getDevOps(request); err == nil {
 		obj, err := client.GetPipelineObj(devops, pipeline)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(obj)
+		errorHandle(err, response, request, obj)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -197,17 +162,7 @@ func (h *devopsHandler) ListPipeline(request *restful.Request, response *restful
 
 	if client, err := h.getDevOps(request); err == nil {
 		objs, err := client.ListPipelineObj(devops, query)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-
-		_ = response.WriteEntity(objs)
+		errorHandle(err, response, request, objs)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -226,16 +181,7 @@ func (h *devopsHandler) CreatePipeline(request *restful.Request, response *restf
 
 	if client, err := h.getDevOps(request); err == nil {
 		created, err := client.CreatePipelineObj(devops, &pipeline)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(created)
+		errorHandle(err, response, request, created)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -255,16 +201,7 @@ func (h *devopsHandler) UpdatePipeline(request *restful.Request, response *restf
 
 	if client, err := h.getDevOps(request); err == nil {
 		obj, err := client.UpdatePipelineObj(devops, &pipeline)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(obj)
+		errorHandle(err, response, request, obj)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -278,16 +215,7 @@ func (h *devopsHandler) DeletePipeline(request *restful.Request, response *restf
 
 	if client, err := h.getDevOps(request); err == nil {
 		err := client.DeletePipelineObj(devops, pipeline)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(servererr.None)
+		errorHandle(err, response, request, servererr.None)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -300,18 +228,7 @@ func (h *devopsHandler) GetCredential(request *restful.Request, response *restfu
 
 	if client, err := h.getDevOps(request); err == nil {
 		obj, err := client.GetCredentialObj(devops, credential)
-
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-
-		_ = response.WriteEntity(obj)
+		errorHandle(err, response, request, obj)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -323,16 +240,7 @@ func (h *devopsHandler) ListCredential(request *restful.Request, response *restf
 
 	if client, err := h.getDevOps(request); err == nil && client != nil {
 		objs, err := client.ListCredentialObj(devops, query)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(objs)
+		errorHandle(err, response, request, objs)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -351,16 +259,7 @@ func (h *devopsHandler) CreateCredential(request *restful.Request, response *res
 
 	if client, err := h.getDevOps(request); err == nil {
 		created, err := client.CreateCredentialObj(devops, &obj)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(created)
+		errorHandle(err, response, request, created)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
@@ -379,40 +278,35 @@ func (h *devopsHandler) UpdateCredential(request *restful.Request, response *res
 
 	if client, err := h.getDevOps(request); err == nil {
 		updated, err := client.UpdateCredentialObj(devops, &obj)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
-			return
-		}
-		_ = response.WriteEntity(updated)
+		errorHandle(err, response, request, updated)
 	} else {
 		api.HandleBadRequest(response, request, err)
 	}
 }
 
 func (h *devopsHandler) DeleteCredential(request *restful.Request, response *restful.Response) {
-	devops := request.PathParameter("devops")
+	devopsProject := request.PathParameter("devops")
 	credential := request.PathParameter("credential")
 
 	if client, err := h.getDevOps(request); err == nil {
-		err := client.DeleteCredentialObj(devops, credential)
-		if err != nil {
-			klog.Error(err)
-			if errors.IsNotFound(err) {
-				api.HandleNotFound(response, request, err)
-				return
-			}
-			api.HandleBadRequest(response, request, err)
+		err := client.DeleteCredentialObj(devopsProject, credential)
+		errorHandle(err, response, request, servererr.None)
+	} else {
+		api.HandleBadRequest(response, request, err)
+	}
+}
+
+func errorHandle(err error, response *restful.Response, request *restful.Request, obj interface{}) {
+	if err != nil {
+		klog.Error(err)
+		if errors.IsNotFound(err) {
+			api.HandleNotFound(response, request, err)
 			return
 		}
-		_ = response.WriteEntity(servererr.None)
-	} else {
 		api.HandleBadRequest(response, request, err)
+		return
 	}
+	_ = response.WriteEntity(obj)
 }
 
 func (h *devopsHandler) getDevOps(request *restful.Request) (operator devops.DevopsOperator, err error) {
diff --git a/pkg/kapis/devops/v1alpha3/register.go b/pkg/kapis/devops/v1alpha3/register.go
index 795b2bb..6fc05c8 100644
--- a/pkg/kapis/devops/v1alpha3/register.go
+++ b/pkg/kapis/devops/v1alpha3/register.go
@@ -43,23 +43,30 @@ import (
 var GroupVersion = schema.GroupVersion{Group: api.GroupName, Version: "v1alpha3"}
 
 // AddToContainer adds web service into container.
-func AddToContainer(container *restful.Container, devopsClient devopsClient.Interface, k8sClient k8s.Client, client client.Client) {
+func AddToContainer(container *restful.Container, devopsClient devopsClient.Interface,
+	k8sClient k8s.Client, client client.Client) (wss []*restful.WebService) {
 	ws := runtime.NewWebService(GroupVersion)
-	registerRoutes(devopsClient, k8sClient, ws)
+	wss = append(wss, ws)
+	registerRoutes(devopsClient, k8sClient, client, ws)
 	pipelinerun.RegisterRoutes(ws, client)
 	pipeline.RegisterRoutes(ws, client)
 	container.Add(ws)
 
 	ws = runtime.NewWebServiceWithoutGroup(GroupVersion)
-	registerRoutes(devopsClient, k8sClient, ws)
+	wss = append(wss, ws)
+	registerRoutes(devopsClient, k8sClient, client, ws)
 	pipelinerun.RegisterRoutes(ws, client)
 	pipeline.RegisterRoutes(ws, client)
 	container.Add(ws)
+	return
 }
 
-func registerRoutes(devopsClient devopsClient.Interface, k8sClient k8s.Client, ws *restful.WebService) {
+func registerRoutes(devopsClient devopsClient.Interface, k8sClient k8s.Client, client client.Client, ws *restful.WebService) {
 	handler := newDevOpsHandler(devopsClient, k8sClient)
-	// credential
+	registerRoutersForCredentials(handler, ws)
+}
+
+func registerRoutersForCredentials(handler *devopsHandler, ws *restful.WebService) {
 	ws.Route(ws.GET("/devops/{devops}/credentials").
 		To(handler.ListCredential).
 		Param(ws.PathParameter("devops", "devops name")).
@@ -169,8 +176,9 @@ func registerRoutes(devopsClient devopsClient.Interface, k8sClient k8s.Client, w
 		To(handler.GetDevOpsProject).
 		Param(ws.PathParameter("workspace", "workspace name")).
 		Param(ws.PathParameter("devops", "project name")).
-		Doc("Get the devopsproject of the specified workspace for the current user").
-		Returns(http.StatusOK, api.StatusOK, []v1alpha3.DevOpsProject{}).
+		 Param(ws.QueryParameter("generateName", "use '{devops}` as a generatName if 'generateName=true', or as a regular name")).
+		Doc("Get the devops project of the specified workspace for the current user").
+		Returns(http.StatusOK, api.StatusOK, v1alpha3.DevOpsProject{}).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectTag}))
 
 	ws.Route(ws.PUT("/workspaces/{workspace}/devops/{devops}").
diff --git a/pkg/kapis/doc/registry.go b/pkg/kapis/doc/registry.go
new file mode 100644
index 0000000..f8b10f2
--- /dev/null
+++ b/pkg/kapis/doc/registry.go
@@ -0,0 +1,16 @@
+package doc
+
+import (
+	"github.com/emicklei/go-restful"
+	swagger "github.com/emicklei/go-restful-swagger12"
+)
+
+// AddSwaggerService adds the Swagger service
+func AddSwaggerService(wss []*restful.WebService, c *restful.Container) {
+	config := swagger.Config{
+		WebServices:     wss,
+		ApiPath:         "/apidocs.json",
+		SwaggerPath:     "/apidocs/",
+		SwaggerFilePath: "bin/swagger-ui"}
+	swagger.RegisterSwaggerService(config, c)
+}
diff --git a/pkg/kapis/oauth/register.go b/pkg/kapis/oauth/register.go
index c94acff..e5b3845 100644
--- a/pkg/kapis/oauth/register.go
+++ b/pkg/kapis/oauth/register.go
@@ -27,8 +27,8 @@ import (
 	"kubesphere.io/devops/pkg/models/auth"
 )
 
-func AddToContainer(c *restful.Container, tokenOperator auth.TokenManagementInterface) {
-	ws := &restful.WebService{}
+func AddToContainer(c *restful.Container, tokenOperator auth.TokenManagementInterface) (ws *restful.WebService) {
+	ws = &restful.WebService{}
 	ws.Path("/oauth").
 		Consumes(restful.MIME_JSON).
 		Produces(restful.MIME_JSON)
@@ -45,4 +45,5 @@ func AddToContainer(c *restful.Container, tokenOperator auth.TokenManagementInte
 		Returns(http.StatusOK, api.StatusOK, TokenReview{}).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.AuthenticationTag}))
 	c.Add(ws)
+	return
 }
diff --git a/pkg/models/devops/devops.go b/pkg/models/devops/devops.go
index 24a8945..d67f1a5 100644
--- a/pkg/models/devops/devops.go
+++ b/pkg/models/devops/devops.go
@@ -23,6 +23,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"kubesphere.io/devops/pkg/constants"
 	"net/http"
 	"strings"
 	"sync"
@@ -53,6 +54,7 @@ const (
 type DevopsOperator interface {
 	CreateDevOpsProject(workspace string, project *v1alpha3.DevOpsProject) (*v1alpha3.DevOpsProject, error)
 	GetDevOpsProject(workspace string, projectName string) (*v1alpha3.DevOpsProject, error)
+	GetDevOpsProjectByGenerateName(workspace string, projectName string) (*v1alpha3.DevOpsProject, error)
 	DeleteDevOpsProject(workspace string, projectName string) error
 	UpdateDevOpsProject(workspace string, project *v1alpha3.DevOpsProject) (*v1alpha3.DevOpsProject, error)
 	ListDevOpsProject(workspace string, limit, offset int) (api.ListResult, error)
@@ -116,7 +118,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 {
@@ -169,17 +171,49 @@ func (d devopsOperator) CreateDevOpsProject(workspace string, project *v1alpha3.
 		klog.Error(err)
 		return nil, err
 	}
+
 	// metadata override
 	if project.Labels == nil {
 		project.Labels = make(map[string]string)
 	}
 	project.Name = ""
-	//project.Labels[tenantv1alpha1.WorkspaceLabel] = workspace
+	project.Labels[constants.WorkspaceLabelKey] = workspace
+
+	// set annotations
+	if project.Annotations == nil {
+		project.Annotations = make(map[string]string)
+	}
 	project.Annotations[devopsv1alpha3.DevOpeProjectSyncStatusAnnoKey] = StatusPending
 	project.Annotations[devopsv1alpha3.DevOpeProjectSyncTimeAnnoKey] = GetSyncNowTime()
+
+	// create it
 	return d.ksclient.DevopsV1alpha3().DevOpsProjects().Create(d.context, project, metav1.CreateOptions{})
 }
 
+// GetDevOpsProjectByGenerateName finds the DevOps project by workspace and project name
+// the projectName is the generateName instead of the real resource name
+func (d devopsOperator) GetDevOpsProjectByGenerateName(workspace string, projectName string) (project *v1alpha3.DevOpsProject, err error) {
+	var list *v1alpha3.DevOpsProjectList
+
+	if list, err = d.ksclient.DevopsV1alpha3().DevOpsProjects().List(d.context, metav1.ListOptions{
+		LabelSelector: fmt.Sprintf("%s=%s", constants.WorkspaceLabelKey, workspace),
+	}); err == nil {
+		for i := range list.Items {
+			item := list.Items[i]
+			if item.GenerateName == projectName {
+				project = &item
+				break
+			}
+		}
+
+		if project == nil {
+			err = fmt.Errorf("not found DevOpsProject by workspace: %s and projectName: %s", workspace, projectName)
+		}
+	}
+	return
+}
+
+// GetDevOpsProject finds the DevOps project by workspace and project name
 func (d devopsOperator) GetDevOpsProject(workspace string, projectName string) (*v1alpha3.DevOpsProject, error) {
 	return d.ksclient.DevopsV1alpha3().DevOpsProjects().Get(d.context, projectName, metav1.GetOptions{})
 }
@@ -912,9 +946,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
@@ -930,7 +964,7 @@ func (d devopsOperator) isGenerateNameUnique(workspace, generateName string) (bo
 		return false, err
 	}
 	for _, p := range projects.Items {
-		if p.GenerateName == generateName {
+		if p.Labels != nil && p.Labels[constants.WorkspaceLabelKey] == workspace && p.GenerateName == generateName {
 			return false, 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)
+			}
+		})
+	}
+}
diff --git a/pkg/kapis/devops/v1alpha2/register_test.go b/pkg/kapis/devops/v1alpha2/register_test.go
index fb8a4cc..c3f8e06 100644
--- a/pkg/kapis/devops/v1alpha2/register_test.go
+++ b/pkg/kapis/devops/v1alpha2/register_test.go
@@ -20,7 +20,7 @@ import (
 func TestAPIsExist(t *testing.T) {
 	httpWriter := httptest.NewRecorder()
 
-	err := AddToContainer(restful.DefaultContainer, nil, fakedevops.NewFakeDevops(nil),
+	_, err := AddToContainer(restful.DefaultContainer, nil, fakedevops.NewFakeDevops(nil),
 		nil,
 		fakeclientset.NewSimpleClientset(&v1alpha3.DevOpsProject{
 			ObjectMeta: metav1.ObjectMeta{Name: "fake"},
diff --git a/pkg/kapis/devops/v1alpha3/register_test.go b/pkg/kapis/devops/v1alpha3/register_test.go
index 32d819a..edff5e4 100644
--- a/pkg/kapis/devops/v1alpha3/register_test.go
+++ b/pkg/kapis/devops/v1alpha3/register_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"github.com/emicklei/go-restful"
 	"github.com/stretchr/testify/assert"
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	k8sfake "k8s.io/client-go/kubernetes/fake"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha1"
@@ -19,14 +20,18 @@ import (
 )
 
 func TestAPIsExist(t *testing.T) {
-	httpWriter := httptest.NewRecorder()
 	schema, err := v1alpha1.SchemeBuilder.Register().Build()
 	assert.Nil(t, err)
 
 	AddToContainer(restful.DefaultContainer, fakedevops.NewFakeDevops(nil),
-		k8s.NewFakeClientSets(k8sfake.NewSimpleClientset(), nil, nil, "", nil,
+		k8s.NewFakeClientSets(k8sfake.NewSimpleClientset(&v1.Secret{
+			ObjectMeta: metav1.ObjectMeta{Name: "fake", Namespace: "fake"},
+		}), nil, nil, "", nil,
 			fakeclientset.NewSimpleClientset(&v1alpha3.DevOpsProject{
 				ObjectMeta: metav1.ObjectMeta{Name: "fake"},
+				Status:     v1alpha3.DevOpsProjectStatus{AdminNamespace: "fake"},
+			}, &v1alpha3.Pipeline{
+				ObjectMeta: metav1.ObjectMeta{Name: "fake", Namespace: "fake"},
 			})),
 		fake.NewFakeClientWithScheme(schema))
 
@@ -133,8 +138,71 @@ func TestAPIsExist(t *testing.T) {
 			httpRequest, _ := http.NewRequest(tt.args.method,
 				"http://fake.com/kapis/devops.kubesphere.io/v1alpha3"+tt.args.uri, nil)
 			httpRequest = httpRequest.WithContext(context.WithValue(context.TODO(), constants.K8SToken, constants.ContextKeyK8SToken("")))
+
+			httpWriter := httptest.NewRecorder()
 			restful.DefaultContainer.Dispatch(httpWriter, httpRequest)
 			assert.NotEqual(t, httpWriter.Code, 404)
 		})
 	}
 }
+
+func TestGetDevOpsProject(t *testing.T) {
+	schema, err := v1alpha1.SchemeBuilder.Register().Build()
+	assert.Nil(t, err)
+	container := restful.NewContainer()
+
+	AddToContainer(container, fakedevops.NewFakeDevops(nil),
+		k8s.NewFakeClientSets(k8sfake.NewSimpleClientset(), nil, nil, "", nil,
+			fakeclientset.NewSimpleClientset(&v1alpha3.DevOpsProject{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "fake",
+					Name:         "generated-fake",
+					Labels: map[string]string{
+						constants.WorkspaceLabelKey: "ws",
+					},
+				},
+			})),
+		fake.NewFakeClientWithScheme(schema))
+
+	type args struct {
+		method string
+		uri    string
+	}
+	tests := []struct {
+		name       string
+		args       args
+		expectCode int
+	}{{
+		name: "normal case",
+		args: args{
+			method: http.MethodGet,
+			uri:    "/workspaces/ws/devops/generated-fake",
+		},
+		expectCode: http.StatusOK,
+	}, {
+		name: "find by a generateName",
+		args: args{
+			method: http.MethodGet,
+			uri:    "/workspaces/ws/devops/fake?generateName=true",
+		},
+		expectCode: http.StatusOK,
+	}, {
+		name: "wrong workspace name",
+		args: args{
+			method: http.MethodGet,
+			uri:    "/workspaces/fake/devops/fake?generateName=true",
+		},
+		expectCode: http.StatusBadRequest,
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			httpRequest, _ := http.NewRequest(tt.args.method,
+				"http://fake.com/kapis/devops.kubesphere.io/v1alpha3"+tt.args.uri, nil)
+			httpRequest = httpRequest.WithContext(context.WithValue(context.TODO(), constants.K8SToken, constants.ContextKeyK8SToken("")))
+
+			httpWriter := httptest.NewRecorder()
+			container.Dispatch(httpWriter, httpRequest)
+			assert.Equal(t, tt.expectCode, httpWriter.Code)
+		})
+	}
+}
diff --git a/pkg/kapis/doc/register_test.go b/pkg/kapis/doc/register_test.go
new file mode 100644
index 0000000..bea716c
--- /dev/null
+++ b/pkg/kapis/doc/register_test.go
@@ -0,0 +1,40 @@
+package doc
+
+import (
+	"bytes"
+	"github.com/emicklei/go-restful"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+func TestAPIsExist(t *testing.T) {
+	httpWriter := httptest.NewRecorder()
+
+	AddSwaggerService(nil, restful.DefaultContainer)
+
+	type args struct {
+		method string
+		uri    string
+	}
+	tests := []struct {
+		name string
+		args args
+	}{{
+		name: "check /apidocs.json",
+		args: args{
+			method: http.MethodGet,
+			uri:    "/apidocs.json",
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			httpRequest, _ := http.NewRequest(tt.args.method,
+				"http://fake.com"+tt.args.uri, bytes.NewBuffer([]byte("{}")))
+			httpRequest.Header.Set("Content-Type", "application/json")
+			restful.DefaultContainer.Dispatch(httpWriter, httpRequest)
+			assert.Equal(t, 200, httpWriter.Code)
+		})
+	}
+}
diff --git a/pkg/models/devops/devops_test.go b/pkg/models/devops/devops_test.go
index c1308a4..6bfbc17 100644
--- a/pkg/models/devops/devops_test.go
+++ b/pkg/models/devops/devops_test.go
@@ -17,6 +17,14 @@ limitations under the License.
 package devops
 
 import (
+	"context"
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
+	"kubesphere.io/devops/pkg/client/clientset/versioned"
+	fakeclientset "kubesphere.io/devops/pkg/client/clientset/versioned/fake"
+	"kubesphere.io/devops/pkg/constants"
 	"net/http"
 	"testing"
 
@@ -128,3 +136,191 @@ func TestGetBranchNodesDetail(t *testing.T) {
 		}
 	}
 }
+
+func Test_devopsOperator_CreateDevOpsProject(t *testing.T) {
+	type fields struct {
+		ksclient versioned.Interface
+	}
+	type args struct {
+		workspace string
+		project   *v1alpha3.DevOpsProject
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		args    args
+		verify  func(client versioned.Interface, args args, t *testing.T) bool
+		wantErr bool
+	}{{
+		name: "lack of the generate name",
+		fields: fields{
+			ksclient: fakeclientset.NewSimpleClientset(),
+		},
+		args: args{
+			workspace: "ws",
+			project:   &v1alpha3.DevOpsProject{},
+		},
+		wantErr: true,
+	}, {
+		name: "duplicated in the same workspace",
+		fields: fields{
+			ksclient: fakeclientset.NewSimpleClientset(&v1alpha3.DevOpsProject{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "fake",
+					Labels: map[string]string{
+						constants.WorkspaceLabelKey: "ws",
+					},
+				},
+			}),
+		},
+		args: args{
+			workspace: "ws",
+			project: &v1alpha3.DevOpsProject{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "fake",
+				},
+			},
+		},
+		wantErr: true,
+	}, {
+		name: "allow the same name in the different workspaces",
+		fields: fields{
+			ksclient: fakeclientset.NewSimpleClientset(&v1alpha3.DevOpsProject{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "fake",
+					Name:         "generated-name",
+					Labels: map[string]string{
+						constants.WorkspaceLabelKey: "ws1",
+					},
+				},
+			}),
+		},
+		args: args{
+			workspace: "ws",
+			project: &v1alpha3.DevOpsProject{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "fake",
+				},
+			},
+		},
+		wantErr: false,
+	}, {
+		name: "normal case",
+		fields: fields{
+			ksclient: fakeclientset.NewSimpleClientset(),
+		},
+		args: args{
+			workspace: "ws",
+			project: &v1alpha3.DevOpsProject{
+				ObjectMeta: v1.ObjectMeta{
+					GenerateName: "devops",
+				},
+			},
+		},
+		wantErr: false,
+		verify: func(client versioned.Interface, args args, t *testing.T) bool {
+			list, err := client.DevopsV1alpha3().DevOpsProjects().List(context.TODO(), metav1.ListOptions{})
+			assert.Nil(t, err)
+			assert.Equal(t, len(list.Items) > 0, true)
+
+			for i := range list.Items {
+				item := list.Items[i]
+
+				if item.GenerateName == args.project.GenerateName {
+					assert.NotNil(t, item.Annotations)
+					assert.NotEmpty(t, item.Annotations[v1alpha3.DevOpeProjectSyncStatusAnnoKey])
+					assert.NotEmpty(t, item.Annotations[v1alpha3.DevOpeProjectSyncTimeAnnoKey])
+
+					assert.NotNil(t, item.Labels)
+					assert.Equal(t, args.workspace, item.Labels[constants.WorkspaceLabelKey])
+					return true
+				}
+			}
+			return false
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := devopsOperator{
+				ksclient: tt.fields.ksclient,
+			}
+			got, err := d.CreateDevOpsProject(tt.args.workspace, tt.args.project)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("CreateDevOpsProject() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if tt.verify != nil && !tt.verify(d.ksclient, tt.args, t) {
+				t.Errorf("CreateDevOpsProject() got = %v", got)
+			}
+		})
+	}
+}
+
+func Test_devopsOperator_GetDevOpsProject(t *testing.T) {
+	type fields struct {
+		ksclient versioned.Interface
+	}
+	type args struct {
+		workspace   string
+		projectName string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		verify func(result *v1alpha3.DevOpsProject, resultErr error, t *testing.T)
+	}{{
+		name: "normal case",
+		fields: fields{
+			ksclient: fakeclientset.NewSimpleClientset(&v1alpha3.DevOpsProject{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "fake",
+					Name:         "generated-name",
+					Labels: map[string]string{
+						constants.WorkspaceLabelKey: "ws",
+					},
+				},
+			}),
+		},
+		args: args{
+			workspace:   "ws",
+			projectName: "fake",
+		},
+		verify: func(result *v1alpha3.DevOpsProject, resultErr error, t *testing.T) {
+			assert.Nil(t, resultErr)
+			assert.NotNil(t, result)
+			assert.Equal(t, "fake", result.GenerateName)
+			assert.Equal(t, "generated-name", result.Name)
+		},
+	}, {
+		name: "cannot find by the same generateName in the different workspaces",
+		fields: fields{
+			ksclient: fakeclientset.NewSimpleClientset(&v1alpha3.DevOpsProject{
+				ObjectMeta: metav1.ObjectMeta{
+					GenerateName: "fake",
+					Name:         "generated-name",
+					Labels: map[string]string{
+						constants.WorkspaceLabelKey: "ws1",
+					},
+				},
+			}),
+		},
+		args: args{
+			workspace:   "ws",
+			projectName: "fake",
+		},
+		verify: func(result *v1alpha3.DevOpsProject, resultErr error, t *testing.T) {
+			assert.NotNil(t, resultErr)
+			assert.Nil(t, result)
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := devopsOperator{
+				ksclient: tt.fields.ksclient,
+			}
+			got, err := d.GetDevOpsProjectByGenerateName(tt.args.workspace, tt.args.projectName)
+			tt.verify(got, err, t)
+		})
+	}
+}