From 1c0c0bd4d6b0706f2322a2b882c61fb1ff21b5cb Mon Sep 17 00:00:00 2001 From: johnniang Date: Tue, 31 Aug 2021 14:29:25 +0800 Subject: [PATCH] Make PipelineRun parameterized support 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) + }) + } +}