From aac855b206e45a5419fa6282650e2d541bb6fd87 Mon Sep 17 00:00:00 2001 From: John Niang Date: Wed, 1 Sep 2021 20:46:30 +0800 Subject: [PATCH] Make PipelineRun parameterized support (#187) Fix wrong format of PipelineRun template Fix unquoted error of parameter value Refine parameter flag Remove unused parameter type Add some tests against PipelineRun template parsing Remove unused method --- kubectl-plugin/pipeline/run.go | 58 ++++++++---- kubectl-plugin/pipeline/run_test.go | 136 ++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 18 deletions(-) create mode 100644 kubectl-plugin/pipeline/run_test.go diff --git a/kubectl-plugin/pipeline/run.go b/kubectl-plugin/pipeline/run.go index 6360cb7..02055c2 100644 --- a/kubectl-plugin/pipeline/run.go +++ b/kubectl-plugin/pipeline/run.go @@ -31,13 +31,15 @@ func newPipelineRunCmd(client dynamic.Interface) (cmd *cobra.Command) { flags.StringVarP(&opt.namespace, "namespace", "n", "", "The namespace of target Pipeline") flags.BoolVarP(&opt.batch, "batch", "b", false, "Run pipeline as batch mode") + flags.StringToStringVarP(&opt.parameters, "parameters", "P", map[string]string{}, "The parameters that you want to pass, example of single parameter: name=value") return } type pipelineRunOpt struct { - pipeline string - namespace string - batch bool + pipeline string + namespace string + batch bool + parameters map[string]string // inner fields client dynamic.Interface @@ -56,24 +58,18 @@ func (o *pipelineRunOpt) preRunE(cmd *cobra.Command, args []string) (err error) } func (o *pipelineRunOpt) runE(_ *cobra.Command, _ []string) (err error) { - var tpl *template.Template - if tpl, err = template.New("pipelineRunTpl").Parse(pipelineRunTpl); err != nil { - err = fmt.Errorf("failed to parse template:'%s', error: %v", pipelineRunTpl, err) - return - } - - var buf bytes.Buffer - if err = tpl.Execute(&buf, map[string]string{ - "name": o.pipeline, - "namespace": o.namespace, - }); err != nil { - err = fmt.Errorf("failed render pipeline template, error: %v", err) - return + pipelineRunYaml, err := parsePipelineRunTpl(map[string]interface{}{ + "name": o.pipeline, + "namespace": o.namespace, + "parameters": o.parameters, + }) + if err != nil { + return err } var pipelineRunObj *unstructured.Unstructured - if pipelineRunObj, err = types.GetObjectFromYaml(buf.String()); err != nil { - err = fmt.Errorf("failed to unmarshal yaml to DevOpsProject object, %v", err) + if pipelineRunObj, err = types.GetObjectFromYaml(pipelineRunYaml); err != nil { + err = fmt.Errorf("failed to unmarshal yaml to Pipelinerun object, %v", err) return } @@ -131,6 +127,25 @@ func (o *pipelineRunOpt) getPipelineNameList() (names []string, err error) { return } +func parsePipelineRunTpl(data map[string]interface{}) (pipelineRunYaml string, err error) { + var tpl *template.Template + if tpl, err = template.New("pipelineRunTpl").Parse(pipelineRunTpl); err != nil { + err = fmt.Errorf("failed to parse template:'%s', error: %v", pipelineRunTpl, err) + return + } + + if err != nil { + return + } + + var buf bytes.Buffer + if err = tpl.Execute(&buf, data); err != nil { + err = fmt.Errorf("failed to render pipeline template, error: %v", err) + return + } + return buf.String(), nil +} + var pipelineRunTpl = ` apiVersion: devops.kubesphere.io/v1alpha4 kind: PipelineRun @@ -140,4 +155,11 @@ metadata: spec: pipelineRef: name: {{.name}} + {{- if .parameters }} + parameters: + {{- range $name, $value := .parameters }} + - name: {{ $name | printf "%q" }} + value: {{ $value | printf "%q" }} + {{- end }} + {{- end }} ` diff --git a/kubectl-plugin/pipeline/run_test.go b/kubectl-plugin/pipeline/run_test.go new file mode 100644 index 0000000..bdaad92 --- /dev/null +++ b/kubectl-plugin/pipeline/run_test.go @@ -0,0 +1,136 @@ +package pipeline + +import ( + "bytes" + "fmt" + "github.com/stretchr/testify/assert" + "html/template" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/yaml" + "testing" +) + +func TestPipelineRunTplParse(t *testing.T) { + tpl := template.New("PipelineRunTpl") + tpl, err := tpl.Parse(pipelineRunTpl) + if err != nil { + t.Errorf("failed to parse PipelineRun template, err = %v", err) + } + var buf bytes.Buffer + err = tpl.Execute(&buf, map[string]interface{}{ + "name": "fake_name", + "namespace": "fake_ns", + "parameters": nil, + }) + if err != nil { + t.Errorf("failed to execute PipelineRun template, err = %v", err) + } + fmt.Println(buf.String()) +} + +// getNestedString comes from k8s.io/apimachinery@v0.19.4/pkg/apis/meta/v1/unstructured/helpers.go:277 +func getNestedString(obj map[string]interface{}, fields ...string) string { + val, found, err := unstructured.NestedString(obj, fields...) + if !found || err != nil { + return "" + } + return val +} + +func getNestSlice(obj map[string]interface{}, fields ...string) []interface{} { + val, found, err := unstructured.NestedSlice(obj, fields...) + if !found || err != nil { + return nil + } + return val +} + +func Test_parsePipelineRunTpl(t *testing.T) { + type args struct { + data map[string]interface{} + } + tests := []struct { + name string + args args + pipelineRunAssert func(obj *unstructured.Unstructured) + wantErr bool + }{{ + name: "Without parameters", + args: args{ + data: map[string]interface{}{ + "name": "fake_name", + "namespace": "fake_namespace", + }, + }, + pipelineRunAssert: func(obj *unstructured.Unstructured) { + assert.Equal(t, "fake_name", obj.GetGenerateName()) + assert.Equal(t, "fake_namespace", obj.GetNamespace()) + assert.Equal(t, "fake_name", getNestedString(obj.Object, "spec", "pipelineRef", "name")) + assert.Equal(t, 0, len(getNestSlice(obj.Object, "spec", "parameters"))) + }, + }, { + name: "With nil parameters", + args: args{ + data: map[string]interface{}{ + "name": "fake_name", + "namespace": "fake_namespace", + "parameters": nil, + }, + }, + pipelineRunAssert: func(obj *unstructured.Unstructured) { + assert.Equal(t, "fake_name", obj.GetGenerateName()) + assert.Equal(t, "fake_namespace", obj.GetNamespace()) + assert.Equal(t, "fake_name", getNestedString(obj.Object, "spec", "pipelineRef", "name")) + assert.Equal(t, 0, len(getNestSlice(obj.Object, "spec", "parameters"))) + }, + }, { + name: "With empty parameters", + args: args{ + data: map[string]interface{}{ + "name": "fake_name", + "namespace": "fake_namespace", + "parameters": map[string]string{}, + }, + }, + pipelineRunAssert: func(obj *unstructured.Unstructured) { + assert.Equal(t, "fake_name", obj.GetGenerateName()) + assert.Equal(t, "fake_namespace", obj.GetNamespace()) + assert.Equal(t, "fake_name", getNestedString(obj.Object, "spec", "pipelineRef", "name")) + assert.Equal(t, 0, len(getNestSlice(obj.Object, "spec", "parameters"))) + }, + }, { + name: "With one parameter", + args: args{ + data: map[string]interface{}{ + "name": "fake_name", + "namespace": "fake_namespace", + "parameters": map[string]string{ + "a": "b", + }, + }, + }, + pipelineRunAssert: func(obj *unstructured.Unstructured) { + assert.Equal(t, "fake_name", obj.GetGenerateName()) + assert.Equal(t, "fake_namespace", obj.GetNamespace()) + assert.Equal(t, "fake_name", getNestedString(obj.Object, "spec", "pipelineRef", "name")) + assert.Equal(t, 1, len(getNestSlice(obj.Object, "spec", "parameters"))) + assert.Equal(t, map[string]interface{}{"name": "a", "value": "b"}, getNestSlice(obj.Object, "spec", "parameters")[0]) + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPipelineRunYaml, err := parsePipelineRunTpl(tt.args.data) + if (err != nil) != tt.wantErr { + t.Errorf("parsePipelineRunTpl() error = %v, wantErr %v", err, tt.wantErr) + return + } + obj := unstructured.Unstructured{} + err = yaml.Unmarshal([]byte(gotPipelineRunYaml), &obj) + if (err != nil) != tt.wantErr { + t.Errorf("parsePipelineRunTpl() error = %v, wantErr %v", err, tt.wantErr) + return + } + tt.pipelineRunAssert(&obj) + }) + } +}