Repository: kubesphere/ks-devops
Pull request #959 (2023-06-09 06:38:57)

[release-3.4] Bugfix 971 add check job name api
This is an automated cherry-pick of #958

```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/release-3.4/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: /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/959#issuecomment-1584059882" 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/release-3.4/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)
+		})
+	}
+}