Repository: kubesphere/ks-devops
Pull request #233 (2021-09-13 16:08:40)

Provide PipelineRuns creation API
### What this PR dose

- Provide PipelineRuns creation API which will create a PipelineRun CR only

### Why we need it

This PR is to prepare for PipelineRun feature in APIServer. At the same time, this PR is also a part of #41.

### Special notes

Docker image for test: `johnniang/devops-apiserver:create-pipelinerun`


### TODO

> This TODO will be done in the further PR.

- [ ] Add some tests against methods: `getScm`, `getPipelineRef` and `createPipelineRun`.

### API usage

- Create PipelineRun for multi-branch Pipeline
```bash
curl --location --request POST 'ip:30880/kapis/devops.kubesphere.io/v1alpha4/namespaces/demo-devopsproject/pipelines/demo-multi-branch-pipeline/pipelineruns?branch=master' \
--header 'Content-Type: application/json' \
--data-raw '{}'
```
```json
{
 "metadata": {
  "name": "good-runs-9b5ls",
  "generateName": "good-runs-",
  "namespace": "defaultfbbh5",
  "uid": "29e6a3bd-f5de-4e79-a136-a2404bcc7b78",
  "resourceVersion": "26069",
  "generation": 1,
  "creationTimestamp": "2021-09-13T22:43:24Z",
  "ownerReferences": [
   {
    "apiVersion": "devops.kubesphere.io/v1alpha3",
    "kind": "Pipeline",
    "name": "good",
    "uid": "5f5ba5c4-e04e-4eb4-9812-52f40f88b8d5",
    "controller": true,
    "blockOwnerDeletion": true
   }
  ],
  "managedFields": [...]
 },
 "spec": {
  "pipelineRef": {
   "kind": "Pipeline",
   "namespace": "defaultfbbh5",
   "name": "good"
  },
  "pipelineSpec": {
   "type": "pipeline",
   "pipeline": {
    "name": "good",
    "discarder": {
     "days_to_keep": "7",
     "num_to_keep": "10"
    },
    "disable_concurrent": true,
    "jenkinsfile": "pipeline {\n  agent {\n    node {\n      label 'maven'\n    }\n  }\n  stages {\n    stage('Clone') {\n      steps {\n        git(url: 'https://github.com/kubesphere-sigs/demo-java', changelog: true, poll: false)\n      }\n    }\n    stage('Build \u0026 Test') {\n      steps {\n        container('maven') {\n          sh 'mvn package test'\n        }\n      }\n    }\n    stage('Code Scan') {\n      steps {\n        withSonarQubeEnv('sonar') {\n          container('maven') {\n            sh '''mvn --version\nmvn sonar:sonar \\\\\n  -Dsonar.projectKey=test \\\\\n  -Dsonar.host.url=http://139.198.9.130:30687/ \\\\\n  -Dsonar.login=b3e146cdb76ecef5ffb12743779cd78e69a4b5c5'''\n          }\n\n        }\n\n        waitForQualityGate 'false'\n      }\n    }\n    stage('Build Image') {\n      steps {\n        container('maven') {\n          withCredentials([usernamePassword(credentialsId : 'docker' ,passwordVariable : 'PASS' ,usernameVariable : 'USER' ,)]) {\n            sh '''docker login -u $USER -p $PASS\ncat \u003c\u003cEOM \u003eDockerfile\nFROM kubesphere/java-8-centos7:v2.1.0\nCOPY target/demo-java-1.0.0.jar demo.jar\nCOPY target/lib demo-lib\nEXPOSE 8080\nENTRYPOINT [\"java\", \"-jar\", \"demo.jar\"]\nEOM\ndocker build . -t surenpi/java-demo\ndocker push surenpi/java-demo'''\n          }\n        }\n      }\n    }\n  }\n}\n"
   }
  }
 },
 "status": {}
}
```

- Create PipelineRun with parameters for multi-branch Pipeline
```bash
curl --location --request POST 'ip:30880/kapis/devops.kubesphere.io/v1alpha4/namespaces/demo-devopsproject/pipelines/demo-multi-branch-pipeline/pipelineruns?branch=dev' \
--header 'Content-Type: application/json' \
--data-raw '{
    "parameters": [{
        "name": "aname",
        "value": "avalue"
    }]
}'
```

/kind feature

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

This pull-request has been approved by: *<a href="https://github.com/kubesphere/ks-devops/pull/233#" title="Author self-approved">JohnNiang</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **linuxsuren** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @linuxsuren` in a comment when ready.

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

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

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

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["linuxsuren"]} -->
codecov[bot]: # [Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/233?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) Report
> Merging [#233](https://codecov.io/gh/kubesphere/ks-devops/pull/233?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (2e79b02) into [master](https://codecov.io/gh/kubesphere/ks-devops/commit/4a8b4609f40de66f448607212333ff4dfbdaadab?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) (4a8b460) will **increase** coverage by `0.07%`.
> The diff coverage is `14.70%`.

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

```diff
@@            Coverage Diff            @@
##           master    #233      +/-   ##
=========================================
+ Coverage    7.15%   7.22%   +0.07%     
=========================================
  Files          83      83              
  Lines       21039   21107      +68     
=========================================
+ Hits         1506    1526      +20     
- Misses      19405   19455      +50     
+ Partials      128     126       -2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `7.22% <14.70%> (+0.07%)` | :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/233?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere) | Coverage Δ | |
|---|---|---|
| [pkg/client/devops/pipeline.go](https://codecov.io/gh/kubesphere/ks-devops/pull/233/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2NsaWVudC9kZXZvcHMvcGlwZWxpbmUuZ28=) | `100.00% <ø> (ø)` | |
| [pkg/kapis/devops/v1alpha4/handler.go](https://codecov.io/gh/kubesphere/ks-devops/pull/233/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhNC9oYW5kbGVyLmdv) | `0.00% <0.00%> (ø)` | |
| [pkg/kapis/devops/v1alpha4/register.go](https://codecov.io/gh/kubesphere/ks-devops/pull/233/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhNC9yZWdpc3Rlci5nbw==) | `0.00% <0.00%> (ø)` | |
| [pkg/kapis/devops/v1alpha4/util.go](https://codecov.io/gh/kubesphere/ks-devops/pull/233/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-cGtnL2thcGlzL2Rldm9wcy92MWFscGhhNC91dGlsLmdv) | `83.33% <100.00%> (+11.90%)` | :arrow_up: |
| [...trollers/devopsproject/devopsproject\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/233/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvZGV2b3BzcHJvamVjdC9kZXZvcHNwcm9qZWN0X2NvbnRyb2xsZXIuZ28=) | `43.00% <0.00%> (+2.59%)` | :arrow_up: |
| [controllers/pipeline/pipeline\_controller.go](https://codecov.io/gh/kubesphere/ks-devops/pull/233/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere#diff-Y29udHJvbGxlcnMvcGlwZWxpbmUvcGlwZWxpbmVfY29udHJvbGxlci5nbw==) | `42.53% <0.00%> (+3.73%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kubesphere/ks-devops/pull/233?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/233?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Last update [4a8b460...2e79b02](https://codecov.io/gh/kubesphere/ks-devops/pull/233?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=kubesphere).


Patch:
diff --git a/.gitignore b/.gitignore
index f42517a..9881b0e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,5 +23,8 @@ bin
 *.swo
 *~
 
+.vscode
+__debug_bin
+
 kubesphere.yaml
 */**/*.xml
diff --git a/pkg/client/devops/pipeline.go b/pkg/client/devops/pipeline.go
index 8d3f302..802ee95 100644
--- a/pkg/client/devops/pipeline.go
+++ b/pkg/client/devops/pipeline.go
@@ -546,14 +546,16 @@ type PipelineBranchItem struct {
 	} `json:"pullRequest,omitempty"`
 }
 
-// RunPipeline
+type Parameter struct {
+	Name  string      `json:"name,omitempty" description:"name"`
+	Value interface{} `json:"value,omitempty" description:"value"`
+}
+
 type RunPayload struct {
-	Parameters []struct {
-		Name  string      `json:"name,omitempty" description:"name"`
-		Value interface{} `json:"value,omitempty" description:"value"`
-	} `json:"parameters,omitempty"`
+	Parameters []Parameter `json:"parameters,omitempty"`
 }
 
+// RunPipeline
 type RunPipeline struct {
 	Class string `json:"_class,omitempty" description:"It’s a fully qualified name and is an identifier of the producer of this resource's capability."`
 	Links struct {
diff --git a/pkg/kapis/devops/v1alpha4/handler.go b/pkg/kapis/devops/v1alpha4/handler.go
index 729d8a4..dfc2d06 100644
--- a/pkg/kapis/devops/v1alpha4/handler.go
+++ b/pkg/kapis/devops/v1alpha4/handler.go
@@ -2,14 +2,17 @@ package v1alpha4
 
 import (
 	"context"
+	"io"
+	"strconv"
+
 	"github.com/emicklei/go-restful"
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha4"
 	"kubesphere.io/devops/pkg/apiserver/query"
+	"kubesphere.io/devops/pkg/client/devops"
 	resourcesV1alpha3 "kubesphere.io/devops/pkg/models/resources/v1alpha3"
 	"sigs.k8s.io/controller-runtime/pkg/client"
-	"strconv"
 )
 
 type apiHandlerOption struct {
@@ -26,48 +29,81 @@ func newAPIHandler(o apiHandlerOption) *apiHandler {
 	return &apiHandler{o}
 }
 
-func (h *apiHandler) listPipelineRuns() restful.RouteFunction {
-	return func(request *restful.Request, response *restful.Response) {
-		nsName := request.PathParameter("namespace")
-		pipName := request.PathParameter("pipeline")
-		branchName := request.QueryParameter("branch")
-		backward, err := strconv.ParseBool(request.QueryParameter("backward"))
-		if err != nil {
-			// by default, we have to guarantee backward compatibility
-			backward = true
-		}
+func (h *apiHandler) listPipelineRuns(request *restful.Request, response *restful.Response) {
+	nsName := request.PathParameter("namespace")
+	pipName := request.PathParameter("pipeline")
+	branchName := request.QueryParameter("branch")
+	backward, err := strconv.ParseBool(request.QueryParameter("backward"))
+	if err != nil {
+		// by default, we have to guarantee backward compatibility
+		backward = true
+	}
+
+	queryParam := query.ParseQueryParameter(request)
+
+	// validate the Pipeline
+	var pip v1alpha3.Pipeline
+	err = h.client.Get(context.Background(), client.ObjectKey{Namespace: nsName, Name: pipName}, &pip)
+	if err != nil {
+		api.HandleError(request, response, err)
+		return
+	}
+
+	// build label selector
+	labelSelector, err := buildLabelSelector(queryParam, pip.Name, branchName)
+	if err != nil {
+		api.HandleError(request, response, err)
+		return
+	}
 
-		queryParam := query.ParseQueryParameter(request)
+	var prs v1alpha4.PipelineRunList
+	// fetch PipelineRuns
+	if err := h.client.List(context.Background(), &prs,
+		client.InNamespace(pip.Namespace),
+		client.MatchingLabelsSelector{Selector: labelSelector}); err != nil {
+		api.HandleError(request, response, err)
+		return
+	}
 
-		// validate the Pipeline
-		var pip v1alpha3.Pipeline
-		err = h.client.Get(context.Background(), client.ObjectKey{Namespace: nsName, Name: pipName}, &pip)
-		if err != nil {
-			api.HandleError(request, response, err)
-			return
-		}
+	var listHandler resourcesV1alpha3.ListHandler
+	if backward {
+		listHandler = backwardListHandler{}
+	}
+	apiResult := resourcesV1alpha3.ToListResult(convertPipelineRunsToObject(prs.Items), queryParam, listHandler)
+	_ = response.WriteAsJson(apiResult)
+}
 
-		// build label selector
-		labelSelector, err := buildLabelSelector(queryParam, pip.Name, branchName)
-		if err != nil {
-			api.HandleError(request, response, err)
-			return
-		}
+func (h *apiHandler) createPipelineRuns(request *restful.Request, response *restful.Response) {
+	nsName := request.PathParameter("namespace")
+	pipName := request.PathParameter("pipeline")
+	branch := request.QueryParameter("branch")
+	payload := devops.RunPayload{}
+	if err := request.ReadEntity(&payload); err != nil && err != io.EOF {
+		api.HandleBadRequest(response, request, err)
+		return
+	}
+	// validate the Pipeline
+	var pipeline v1alpha3.Pipeline
+	if err := h.client.Get(context.Background(), client.ObjectKey{Namespace: nsName, Name: pipName}, &pipeline); err != nil {
+		api.HandleError(request, response, err)
+		return
+	}
 
-		var prs v1alpha4.PipelineRunList
-		// fetch PipelineRuns
-		if err := h.client.List(context.Background(), &prs,
-			client.InNamespace(pip.Namespace),
-			client.MatchingLabelsSelector{Selector: labelSelector}); err != nil {
-			api.HandleError(request, response, err)
-			return
-		}
+	var (
+		scm *v1alpha4.SCM
+		err error
+	)
+	if scm, err = getScm(&pipeline.Spec, branch); err != nil {
+		api.HandleBadRequest(response, request, err)
+		return
+	}
 
-		var listHandler resourcesV1alpha3.ListHandler
-		if backward {
-			listHandler = backwardListHandler{}
-		}
-		apiResult := resourcesV1alpha3.ToListResult(convertPipelineRunsToObject(prs.Items), queryParam, listHandler)
-		_ = response.WriteAsJson(apiResult)
+	// create PipelineRun
+	pr := createPipelineRun(&pipeline, &payload, scm)
+	if err := h.client.Create(context.Background(), pr); err != nil {
+		api.HandleError(request, response, err)
+		return
 	}
+
+	_ = response.WriteEntity(pr)
 }
diff --git a/pkg/kapis/devops/v1alpha4/register.go b/pkg/kapis/devops/v1alpha4/register.go
index e94bd90..ec68dcb 100644
--- a/pkg/kapis/devops/v1alpha4/register.go
+++ b/pkg/kapis/devops/v1alpha4/register.go
@@ -1,11 +1,13 @@
 package v1alpha4
 
 import (
+	"net/http"
+
 	"github.com/emicklei/go-restful"
 	"kubesphere.io/devops/pkg/api"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha4"
 	"kubesphere.io/devops/pkg/apiserver/runtime"
-	"net/http"
+	"kubesphere.io/devops/pkg/client/devops"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -31,7 +33,7 @@ func AddToContainer(o Option) {
 
 func addToContainer(o Option, ws *restful.WebService, handler *apiHandler) {
 	ws.Route(ws.GET("/namespaces/{namespace}/pipelines/{pipeline}/pipelineruns").
-		To(handler.listPipelineRuns()).
+		To(handler.listPipelineRuns).
 		Doc("Get all runs of the specified pipeline").
 		Param(ws.PathParameter("namespace", "Namespace of the pipeline")).
 		Param(ws.PathParameter("pipeline", "Name of the pipeline")).
@@ -43,5 +45,14 @@ func addToContainer(o Option, ws *restful.WebService, handler *apiHandler) {
 			DefaultValue("true")).
 		Returns(http.StatusOK, api.StatusOK, v1alpha4.PipelineRunList{}),
 	)
+	ws.Route(ws.POST("/namespaces/{namespace}/pipelines/{pipeline}/pipelineruns").
+		To(handler.createPipelineRuns).
+		Doc("Create a PipelineRun for the specified pipeline").
+		Param(ws.PathParameter("namespace", "Namespace of the pipeline")).
+		Param(ws.PathParameter("pipeline", "Name of the pipeline")).
+		Param(ws.QueryParameter("branch", "The name of SCM reference, only for multi-branch pipeline")).
+		Reads(devops.RunPayload{}).
+		Returns(http.StatusCreated, api.StatusOK, v1alpha4.PipelineRun{}),
+	)
 	o.Container.Add(ws)
 }
diff --git a/pkg/kapis/devops/v1alpha4/util.go b/pkg/kapis/devops/v1alpha4/util.go
index db9dd4d..3627b99 100644
--- a/pkg/kapis/devops/v1alpha4/util.go
+++ b/pkg/kapis/devops/v1alpha4/util.go
@@ -1,11 +1,18 @@
 package v1alpha4
 
 import (
+	"errors"
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/selection"
+	"kubesphere.io/devops/pkg/api/devops/v1alpha3"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha4"
 	"kubesphere.io/devops/pkg/apiserver/query"
+	"kubesphere.io/devops/pkg/client/devops"
 )
 
 func buildLabelSelector(queryParam *query.Query, pipelineName, branchName string) (labels.Selector, error) {
@@ -34,3 +41,61 @@ func convertPipelineRunsToObject(prs []v1alpha4.PipelineRun) []runtime.Object {
 	}
 	return result
 }
+
+func convertParameters(payload *devops.RunPayload) []v1alpha4.Parameter {
+	if payload == nil {
+		return nil
+	}
+	var parameters []v1alpha4.Parameter
+	for _, parameter := range payload.Parameters {
+		if parameter.Name == "" || parameter.Value == "" {
+			continue
+		}
+		parameters = append(parameters, v1alpha4.Parameter{
+			Name:  parameter.Name,
+			Value: fmt.Sprint(parameter.Value),
+		})
+	}
+	return parameters
+}
+
+func getScm(ps *v1alpha3.PipelineSpec, branch string) (*v1alpha4.SCM, error) {
+	var scm *v1alpha4.SCM
+	if ps.Type == v1alpha3.MultiBranchPipelineType {
+		if branch == "" {
+			return nil, errors.New("missing branch name for running a multi-branch Pipeline")
+		}
+		// TODO validate if the branch dose exist
+		// we can not determine what is reference type here. So we set reference name only for now
+		scm = &v1alpha4.SCM{
+			RefName: branch,
+			RefType: "",
+		}
+	}
+	return scm, nil
+}
+
+func getPipelineRef(pipeline *v1alpha3.Pipeline) *corev1.ObjectReference {
+	return &corev1.ObjectReference{
+		Kind:      pipeline.Kind,
+		Name:      pipeline.GetName(),
+		Namespace: pipeline.GetNamespace(),
+	}
+}
+
+func createPipelineRun(pipeline *v1alpha3.Pipeline, payload *devops.RunPayload, scm *v1alpha4.SCM) *v1alpha4.PipelineRun {
+	controllerRef := metav1.NewControllerRef(pipeline, pipeline.GroupVersionKind())
+	return &v1alpha4.PipelineRun{
+		ObjectMeta: metav1.ObjectMeta{
+			GenerateName:    pipeline.GetName() + "-",
+			Namespace:       pipeline.GetNamespace(),
+			OwnerReferences: []metav1.OwnerReference{*controllerRef},
+		},
+		Spec: v1alpha4.PipelineRunSpec{
+			PipelineRef:  getPipelineRef(pipeline),
+			PipelineSpec: &pipeline.Spec,
+			Parameters:   convertParameters(payload),
+			SCM:          scm,
+		},
+	}
+}


Test patch:
diff --git a/pkg/kapis/devops/v1alpha4/util_test.go b/pkg/kapis/devops/v1alpha4/util_test.go
index 1c21210..11e1257 100644
--- a/pkg/kapis/devops/v1alpha4/util_test.go
+++ b/pkg/kapis/devops/v1alpha4/util_test.go
@@ -7,6 +7,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime"
 	"kubesphere.io/devops/pkg/api/devops/v1alpha4"
 	"kubesphere.io/devops/pkg/apiserver/query"
+	"kubesphere.io/devops/pkg/client/devops"
 	"reflect"
 	"testing"
 )
@@ -109,3 +110,81 @@ func Test_convertPipelineRunsToObject(t *testing.T) {
 		})
 	}
 }
+
+func Test_convertParameters(t *testing.T) {
+	type args struct {
+		payload *devops.RunPayload
+	}
+	tests := []struct {
+		name string
+		args args
+		want []v1alpha4.Parameter
+	}{{
+		name: "Nil payload",
+		args: args{
+			payload: nil,
+		},
+		want: nil,
+	}, {
+		name: "Nil parameters",
+		args: args{
+			payload: &devops.RunPayload{
+				Parameters: nil,
+			},
+		},
+		want: nil,
+	}, {
+		name: "Single parameter",
+		args: args{
+			payload: &devops.RunPayload{
+				Parameters: []devops.Parameter{{
+					Name:  "aname",
+					Value: "avalue",
+				}},
+			},
+		},
+		want: []v1alpha4.Parameter{{
+			Name:  "aname",
+			Value: "avalue",
+		}},
+	}, {
+		name: "Empty parameter",
+		args: args{
+			payload: &devops.RunPayload{
+				Parameters: []devops.Parameter{{
+					Name:  "",
+					Value: "",
+				}},
+			},
+		},
+		want: nil,
+	}, {
+		name: "Two parameters",
+		args: args{
+			payload: &devops.RunPayload{
+				Parameters: []devops.Parameter{{
+					Name:  "aname",
+					Value: "avalue",
+				}, {
+					Name:  "bname",
+					Value: "bvalue",
+				}},
+			},
+		},
+		want: []v1alpha4.Parameter{{
+			Name:  "aname",
+			Value: "avalue",
+		}, {
+			Name:  "bname",
+			Value: "bvalue",
+		}},
+	},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := convertParameters(tt.args.payload); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("convertParameters() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}