Repository: kubesphere/ks-devops
Pull request #958 (2023-06-06 07:05:03)

Bugfix 971 add check job name api
<!-- Thanks for sending a pull request! Here are some tips for you:

1. If you want **faster** PR reviews, read how: https://github.com/kubesphere/community/blob/master/developer-guide/development/the-pr-author-guide-to-getting-through-code-review.md
2. In case you want to know how your PR got reviewed, read: https://github.com/kubesphere/community/blob/master/developer-guide/development/code-review-guide.md
3. Here are some coding conventions followed by the KubeSphere community: https://github.com/kubesphere/community/blob/master/developer-guide/development/coding-conventions.md
4. Additional open-source best practice: https://github.com/LinuxSuRen/open-source-best-practice
-->

### What type of PR is this?
<!-- 
Add one of the following kinds:
/kind bug
/kind cleanup
/kind documentation
/kind feature
/kind design
/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 api-change

### What this PR does / why we need it:

### Which issue(s) this PR fixes:
<!--
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
_If PR is about `failing-tests or flakes`, please post the related issues/tests in a comment and do not use `Fixes`_*
Please leave it or change # to be None if there is no corresponding issue that exists
-->
Fixes #https://github.com/kubesphere/issues/issues/971

### Special notes for reviewers:
<!--
You can use the following command to let the DevOps SIG members help you to review your PR.
/cc @kubesphere/sig-devops 
And please avoid cc any individual.
-->
Please check the following list before waiting reviewers:

- [ ] Already committed the CRD files to [the Helm Chart](https://github.com/kubesphere-sigs/ks-devops-helm-chart/) if you created some new CRDs
- [ ] Already [added the permission](https://github.com/kubesphere/ks-installer/blob/9e063b085a0e43fdb3d0d9e3e7f4149146f14b9c/roles/ks-core/prepare/files/ks-init/role-templates.yaml) for the new API
- [ ] Already added the RBAC markers for the new controllers

### Does this PR introduce a user-facing change??
<!--
If no, just write "None" in the release-note block below.
If yes, a release note is required:
Enter your extended-release note in the block below. If the PR requires additional action from users switching to the new release, include the string "action required".

Please keep the note be same as your PR title if you believe it should be in the release notes.
-->
```release-note
add apis to check if the name of DevOps-Project or Pipeline exist
```


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

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **zheng1** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @zheng1` in a comment when ready.

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["zheng1"]} -->
yudong2015: /cherrypick release-3.4
ks-ci-bot: @yudong2015: once the present PR merges, I will cherry-pick it on top of release-3.4 in a new PR and assign it to you.

<details>

In response to [this](https://github.com/kubesphere/ks-devops/pull/958#issuecomment-1584052106):

>/cherrypick release-3.4


Instructions for interacting with me using PR comments are available [here](https://git.k8s.io/community/contributors/guide/pull-requests.md).  If you have questions or suggestions related to my behavior, please file an issue against the [kubernetes/test-infra](https://github.com/kubernetes/test-infra/issues/new?title=Prow%20issue:) repository.
</details>
yudong2015: /lgtm
/approve
ks-ci-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/958#issuecomment-1584052536" title="Approved">yudong2015</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)~~ [yudong2015]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->

Patch:
diff --git a/pkg/client/devops/fake/fakedevops.go b/pkg/client/devops/fake/fakedevops.go
index 12787ef..8ca956b 100644
--- a/pkg/client/devops/fake/fakedevops.go
+++ b/pkg/client/devops/fake/fakedevops.go
@@ -169,6 +169,10 @@ func (d *Devops) GetPipeline(projectName, pipelineName string, httpParameters *d
 	return nil, nil
 }
 
+func (d *Devops) CheckPipelineName(projectName string, httpParameters *devops.HttpParameters) (map[string]interface{}, error) {
+	return nil, nil
+}
+
 func (d *Devops) ListPipelines(httpParameters *devops.HttpParameters) (*devops.PipelineList, error) {
 	return nil, nil
 }
diff --git a/pkg/client/devops/jclient/pipeline.go b/pkg/client/devops/jclient/pipeline.go
index f1a5215..8656c39 100644
--- a/pkg/client/devops/jclient/pipeline.go
+++ b/pkg/client/devops/jclient/pipeline.go
@@ -225,3 +225,7 @@ func (j *JenkinsClient) CheckScriptCompile(projectName, pipelineName string, htt
 func (j *JenkinsClient) CheckCron(projectName string, httpParameters *devops.HttpParameters) (*devops.CheckCronRes, error) {
 	return j.jenkins.CheckCron(projectName, httpParameters)
 }
+
+func (j *JenkinsClient) CheckPipelineName(projectName string, httpParameters *devops.HttpParameters) (map[string]interface{}, error) {
+	return j.jenkins.CheckPipelineName(projectName, httpParameters)
+}
diff --git a/pkg/client/devops/jenkins/jenkins.go b/pkg/client/devops/jenkins/jenkins.go
index 9d2d2f9..cf13385 100644
--- a/pkg/client/devops/jenkins/jenkins.go
+++ b/pkg/client/devops/jenkins/jenkins.go
@@ -216,6 +216,16 @@ func (j *Jenkins) Poll() (int, error) {
 	return resp.StatusCode, nil
 }
 
+func (j *Jenkins) CheckPipelineName(projectName string, httpParameters *devops.HttpParameters) (map[string]interface{}, error) {
+	PipelineOjb := &Pipeline{
+		HttpParameters: httpParameters,
+		Jenkins:        j,
+		Path:           fmt.Sprintf(CheckPipelineName+httpParameters.Url.RawQuery, projectName),
+	}
+	res, err := PipelineOjb.CheckPipelineName()
+	return res, err
+}
+
 func (j *Jenkins) GetPipeline(projectName, pipelineName string, httpParameters *devops.HttpParameters) (*devops.Pipeline, error) {
 	PipelineOjb := &Pipeline{
 		HttpParameters: httpParameters,
diff --git a/pkg/client/devops/jenkins/pipeline.go b/pkg/client/devops/jenkins/pipeline.go
index b742bbc..e43706f 100644
--- a/pkg/client/devops/jenkins/pipeline.go
+++ b/pkg/client/devops/jenkins/pipeline.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"regexp"
 	"strconv"
 	"strings"
 	"time"
@@ -84,8 +85,36 @@ const (
 	CheckCronUrl         = "/job/%s/descriptorByName/hudson.triggers.TimerTrigger/checkSpec?%s"
 
 	cronJobLayout = "Monday, January 2, 2006 15:04:05 PM"
+
+	CheckPipelineName = "/job/%s/checkJobName?"
 )
 
+func (p *Pipeline) CheckPipelineName() (map[string]interface{}, error) {
+	res, err := p.Jenkins.SendPureRequest(p.Path, p.HttpParameters)
+	if err != nil {
+		return nil, err
+	}
+
+	/*
+		if the name exist , the res will be
+		 <div class=error><img src='/static/7abb227e/images/none.gif' height=16 width=1>A job already exists with the name ‘devopsd6b97’</div>
+	*/
+	pattern := `>[^<]+<`
+
+	reg := regexp.MustCompile(pattern)
+
+	result := make(map[string]interface{})
+
+	match := reg.FindString(string(res))
+	if match != "" {
+		result["exist"] = true
+	} else {
+		result["exist"] = false
+	}
+
+	return result, nil
+}
+
 func (p *Pipeline) GetPipeline() (*devops.Pipeline, error) {
 	res, err := p.Jenkins.SendPureRequest(p.Path, p.HttpParameters)
 	if err != nil {
diff --git a/pkg/client/devops/pipeline.go b/pkg/client/devops/pipeline.go
index 5b61123..5180423 100644
--- a/pkg/client/devops/pipeline.go
+++ b/pkg/client/devops/pipeline.go
@@ -1127,6 +1127,7 @@ type HttpParameters struct {
 
 type PipelineOperator interface {
 	// Pipelinne operator interface
+	CheckPipelineName(projectName string, httpParameters *HttpParameters) (map[string]interface{}, error)
 	GetPipeline(projectName, pipelineName string, httpParameters *HttpParameters) (*Pipeline, error)
 	ListPipelines(httpParameters *HttpParameters) (*PipelineList, error)
 	GetPipelineRun(projectName, pipelineName, runId string, httpParameters *HttpParameters) (*PipelineRun, error)
diff --git a/pkg/kapis/devops/v1alpha2/devops.go b/pkg/kapis/devops/v1alpha2/devops.go
index 0dc9d43..abcd5ac 100644
--- a/pkg/kapis/devops/v1alpha2/devops.go
+++ b/pkg/kapis/devops/v1alpha2/devops.go
@@ -45,6 +45,18 @@ import (
 
 const jenkinsHeaderPre = "X-"
 
+func (h *ProjectPipelineHandler) CheckPipelineName(req *restful.Request, resp *restful.Response) {
+	jobName := req.PathParameter("devops")
+	res, err := h.devopsOperator.CheckPipelineName(jobName, req.Request)
+	if err != nil {
+		parseErr(err, resp)
+		return
+	}
+
+	resp.Header().Set(restful.HEADER_ContentType, restful.MIME_JSON)
+	resp.WriteAsJson(res)
+}
+
 func (h *ProjectPipelineHandler) GetPipeline(req *restful.Request, resp *restful.Response) {
 	projectName := req.PathParameter("devops")
 	pipelineName := req.PathParameter("pipeline")
diff --git a/pkg/kapis/devops/v1alpha2/register.go b/pkg/kapis/devops/v1alpha2/register.go
index aa07a38..39c967f 100644
--- a/pkg/kapis/devops/v1alpha2/register.go
+++ b/pkg/kapis/devops/v1alpha2/register.go
@@ -107,6 +107,13 @@ func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops
 	if projectPipelineEnable {
 		projectPipelineHandler := NewProjectPipelineHandler(devopsClient, k8sClient)
 
+		// match Jenkins api "/job/{devops}/checkJobName?value="
+		webservice.Route(webservice.GET("/devops/{devops}/checkPipelineName").
+			To(projectPipelineHandler.CheckPipelineName).
+			Doc("check the job name does it exist").
+			Param(webservice.PathParameter("value", "the name of the new job")).
+			Returns(http.StatusOK, api.StatusOK, nil))
+
 		webservice.Route(webservice.GET("/devops/{devops}/credentials/{credential}/usage").
 			To(projectPipelineHandler.GetProjectCredentialUsage).
 			Doc("Get the specified credential usage of the DevOps project").
diff --git a/pkg/kapis/devops/v1alpha3/handler.go b/pkg/kapis/devops/v1alpha3/handler.go
index 77035fe..24f2240 100644
--- a/pkg/kapis/devops/v1alpha3/handler.go
+++ b/pkg/kapis/devops/v1alpha3/handler.go
@@ -17,6 +17,7 @@ limitations under the License.
 package v1alpha3
 
 import (
+	"fmt"
 	"github.com/emicklei/go-restful"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
@@ -393,3 +394,26 @@ func (h *devopsHandler) getDevOps(request *restful.Request) (operator devops.Dev
 	}
 	return
 }
+
+func (h *devopsHandler) CheckDevopsName(request *restful.Request, response *restful.Response) {
+	workspace := request.PathParameter("workspace")
+	devopsName := request.QueryParameter("devopsName")
+	generateNameFlag := request.QueryParameter("generateName")
+
+	var result map[string]interface{}
+	if client, err := h.getDevOps(request); err == nil {
+
+		switch generateNameFlag {
+		case "true":
+			result, err = client.CheckDevopsProject(workspace, devopsName)
+			if err != nil {
+				errorHandle(request, response, result, err)
+			}
+			errorHandle(request, response, result, nil)
+		default:
+			errorHandle(request, response, nil, fmt.Errorf("generateNameFlag can not be false"))
+		}
+	} else {
+		kapis.HandleBadRequest(response, request, err)
+	}
+}
diff --git a/pkg/kapis/devops/v1alpha3/register.go b/pkg/kapis/devops/v1alpha3/register.go
index 5809977..b4c5dc4 100644
--- a/pkg/kapis/devops/v1alpha3/register.go
+++ b/pkg/kapis/devops/v1alpha3/register.go
@@ -234,6 +234,12 @@ func registerRoutersForWorkspace(handler *devopsHandler, ws *restful.WebService)
 		Doc("Get the devopsproject of the specified workspace for the current user").
 		Returns(http.StatusOK, api.StatusOK, v1alpha3.DevOpsProject{}).
 		Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectTag}))
+
+	// /workspaces/{workspace}/devops/checkDevopsName?devopsName=xxx&generateName=true
+	ws.Route(ws.GET("/workspaces/{workspace}/devops/checkDevopsName").
+		To(handler.CheckDevopsName).
+		Doc("check the devops project name does it exist").
+		Returns(http.StatusOK, api.StatusOK, nil))
 }
 
 func registerRoutersForCI(handler *devopsHandler, ws *restful.WebService) {
diff --git a/pkg/models/devops/devops.go b/pkg/models/devops/devops.go
index 84021e5..2d83194 100644
--- a/pkg/models/devops/devops.go
+++ b/pkg/models/devops/devops.go
@@ -59,6 +59,7 @@ type DevopsOperator interface {
 	DeleteDevOpsProject(workspace string, projectName string) error
 	UpdateDevOpsProject(workspace string, project *v1alpha3.DevOpsProject) (*v1alpha3.DevOpsProject, error)
 	ListDevOpsProject(workspace string, limit, offset int) (api.ListResult, error)
+	CheckDevopsProject(workspace, projectName string) (map[string]interface{}, error)
 
 	CreatePipelineObj(projectName string, pipeline *v1alpha3.Pipeline) (*v1alpha3.Pipeline, error)
 	GetPipelineObj(projectName string, pipelineName string) (*v1alpha3.Pipeline, error)
@@ -73,6 +74,7 @@ type DevopsOperator interface {
 	UpdateCredentialObj(projectName string, secret *v1.Secret) (*v1.Secret, error)
 	ListCredentialObj(projectName string, query *query.Query) (api.ListResult, error)
 
+	CheckPipelineName(projectName string, req *http.Request) (map[string]interface{}, error)
 	GetPipeline(projectName, pipelineName string, req *http.Request) (*devops.Pipeline, error)
 	ListPipelines(req *http.Request) (*devops.PipelineList, error)
 	GetPipelineRun(projectName, pipelineName, runId string, req *http.Request) (*devops.PipelineRun, error)
@@ -191,6 +193,30 @@ func (d devopsOperator) CreateDevOpsProject(workspace string, project *v1alpha3.
 	return d.ksclient.DevopsV1alpha3().DevOpsProjects().Create(d.context, project, metav1.CreateOptions{})
 }
 
+// CheckDevopsProject check the devops is not exist
+func (d devopsOperator) CheckDevopsProject(workspace, projectName string) (map[string]interface{}, error) {
+	var list *v1alpha3.DevOpsProjectList
+	var err error
+
+	result := make(map[string]interface{})
+	result["exist"] = false
+
+	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 {
+				result["exist"] = true
+				break
+			}
+		}
+		return result, nil
+	} else {
+		return result, err
+	}
+}
+
 // 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) {
@@ -443,6 +469,10 @@ func (d devopsOperator) ListCredentialObj(projectName string, query *query.Query
 	return *resourcesV1alpha3.DefaultList(result, query, resourcesV1alpha3.DefaultCompare(), resourcesV1alpha3.DefaultFilter()), nil
 }
 
+func (d devopsOperator) CheckPipelineName(projectName string, req *http.Request) (map[string]interface{}, error) {
+	return d.devopsClient.CheckPipelineName(projectName, convertToHttpParameters(req))
+}
+
 // others
 func (d devopsOperator) GetPipeline(projectName, pipelineName string, req *http.Request) (*devops.Pipeline, error) {
 	return d.devopsClient.GetPipeline(projectName, pipelineName, convertToHttpParameters(req))


Test patch:
diff --git a/pkg/kapis/devops/v1alpha2/register_test.go b/pkg/kapis/devops/v1alpha2/register_test.go
index 11dc0d2..2188a0b 100644
--- a/pkg/kapis/devops/v1alpha2/register_test.go
+++ b/pkg/kapis/devops/v1alpha2/register_test.go
@@ -92,6 +92,12 @@ func TestAPIsExist(t *testing.T) {
 			method: http.MethodGet,
 			uri:    "/devops/fake/pipelines/fake",
 		},
+	}, {
+		name: "check pipeline name if exist",
+		args: args{
+			method: http.MethodGet,
+			uri:    "/devops/fake/checkPipelineName?value=fake1",
+		},
 	}, {
 		name: "search API",
 		args: args{
diff --git a/pkg/models/devops/devops_test.go b/pkg/models/devops/devops_test.go
index e5fd1bd..2f78e87 100644
--- a/pkg/models/devops/devops_test.go
+++ b/pkg/models/devops/devops_test.go
@@ -604,3 +604,72 @@ func Test_devopsOperator_GetJenkinsAgentLabels(t *testing.T) {
 		})
 	}
 }
+
+func Test_devopsOperator_CheckDevopsName(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 map[string]interface{}, 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 map[string]interface{}, resultErr error, t *testing.T) {
+			assert.Nil(t, resultErr)
+			assert.NotNil(t, result)
+			assert.Equal(t, true, result["exist"])
+		},
+	}, {
+		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: "ws",
+					},
+				},
+			}),
+		},
+		args: args{
+			workspace:   "ws",
+			projectName: "fake1",
+		},
+		verify: func(result map[string]interface{}, resultErr error, t *testing.T) {
+			assert.Nil(t, resultErr)
+			assert.NotNil(t, result)
+			assert.Equal(t, false, result["exist"])
+		},
+	}}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			d := devopsOperator{
+				ksclient: tt.fields.ksclient,
+			}
+			got, err := d.CheckDevopsProject(tt.args.workspace, tt.args.projectName)
+			tt.verify(got, err, t)
+		})
+	}
+}