diff --git a/docs/cel_expressions.md b/docs/cel_expressions.md index 47b11a9db..d39c7ef9a 100644 --- a/docs/cel_expressions.md +++ b/docs/cel_expressions.md @@ -276,6 +276,20 @@ interceptor.
'{"testing":"value"}'.parseJSON().testing == "value"
+ + + parseYAML() + + +
<string>.parseYAML() -> map<string, dyn>
+ + + This parses a string that contains a YAML body into a map which which can be subsequently used in other expressions. + + +
'key1: value1\nkey2: value2\n'.parseYAML().key1 == "value"
+ + parseURL() diff --git a/go.mod b/go.mod index b3f7e4ebc..5240a2556 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 // indirect knative.dev/caching v0.0.0-20200228235451-13d271455c74 knative.dev/pkg v0.0.0-20200207155214-fef852970f43 - sigs.k8s.io/yaml v1.2.0 // indirect + sigs.k8s.io/yaml v1.2.0 ) // Knative deps (release-0.12) diff --git a/pkg/interceptors/cel/cel_test.go b/pkg/interceptors/cel/cel_test.go index f02f2ca5e..37529b047 100644 --- a/pkg/interceptors/cel/cel_test.go +++ b/pkg/interceptors/cel/cel_test.go @@ -329,6 +329,7 @@ func TestExpressionEvaluation(t *testing.T) { }, "b64value": "ZXhhbXBsZQ==", "json_body": `{"testing": "value"}`, + "yaml_body": "key1: value1\nkey2: value2\nkey3: value3\n", "testURL": "https://user:password@site.example.com/path/to?query=search#first", "multiURL": "https://user:password@site.example.com/path/to?query=search&query=results", } @@ -426,6 +427,11 @@ func TestExpressionEvaluation(t *testing.T) { expr: "body.json_body.parseJSON().testing == 'value'", want: types.Bool(true), }, + { + name: "parse YAML body in a string", + expr: "body.yaml_body.parseYAML().key1 == 'value1'", + want: types.Bool(true), + }, { name: "parse URL", expr: "body.testURL.parseURL().path == '/path/to'", @@ -477,8 +483,10 @@ func TestExpressionEvaluation(t *testing.T) { func TestExpressionEvaluation_Error(t *testing.T) { testSHA := "ec26c3e57ca3a959ca5aad62de7213c562f8c821" jsonMap := map[string]interface{}{ - "value": "testing", - "sha": testSHA, + "value": "testing", + "sha": testSHA, + "valid_yaml": "key1: value1\nkey2: value2\n", + "invalid_yaml": "key1: value1key2: value2\n", "pull_request": map[string]interface{}{ "commits": []string{}, }, @@ -547,6 +555,21 @@ func TestExpressionEvaluation_Error(t *testing.T) { expr: "body.pull_request.parseJSON().test == 'test'", want: "unexpected type 'map' passed to parseJSON", }, + { + name: "parseYAML decoding non-string", + expr: "body.pull_request.parseYAML().key1 == 'value1'", + want: "unexpected type 'map' passed to parseYAML", + }, + { + name: "unknown key", + expr: "body.valid_yaml.parseYAML().key3 == 'value3'", + want: "no such key: key3", + }, + { + name: "invalid YAML body", + expr: "body.invalid_yaml.parseYAML().key1 == 'value1'", + want: "failed to decode 'key1: value1key2: value2\n' in parseYAML:", + }, } for _, tt := range tests { t.Run(tt.name, func(rt *testing.T) { diff --git a/pkg/interceptors/cel/triggers.go b/pkg/interceptors/cel/triggers.go index aa10d290d..4eaa85200 100644 --- a/pkg/interceptors/cel/triggers.go +++ b/pkg/interceptors/cel/triggers.go @@ -31,6 +31,7 @@ import ( "github.com/google/cel-go/interpreter/functions" "github.com/tektoncd/triggers/pkg/interceptors" "k8s.io/client-go/kubernetes" + "sigs.k8s.io/yaml" triggersv1 "github.com/tektoncd/triggers/pkg/apis/triggers/v1alpha1" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" @@ -137,6 +138,16 @@ import ( // Examples: // // 'https://example.com/testing'.parseURL().host == 'example.com' +// +// parseYAML +// +// Parses a YAML string into a map of strings to dynamic values +// +// .parseYAML() -> map +// +// Examples: +// +// body.field.parseYAML().item // Triggers creates and returns a new cel.Lib with the triggers extensions. func Triggers(request *http.Request, ns string, k kubernetes.Interface) cel.EnvOption { @@ -171,6 +182,9 @@ func (triggersLib) CompileOptions() []cel.EnvOption { decls.NewFunction("parseJSON", decls.NewInstanceOverload("parseJSON_string", []*exprpb.Type{decls.String}, mapStrDyn)), + decls.NewFunction("parseYAML", + decls.NewInstanceOverload("parseYAML_string", + []*exprpb.Type{decls.String}, mapStrDyn)), decls.NewFunction("parseURL", decls.NewInstanceOverload("parseURL_string", []*exprpb.Type{decls.String}, mapStrDyn)), @@ -197,6 +211,9 @@ func (t triggersLib) ProgramOptions() []cel.ProgramOption { &functions.Overload{ Operator: "parseJSON", Unary: parseJSONString}, + &functions.Overload{ + Operator: "parseYAML", + Unary: parseYAMLString}, &functions.Overload{ Operator: "parseURL", Unary: parseURLString}, @@ -322,6 +339,19 @@ func parseJSONString(val ref.Val) ref.Val { return types.NewDynamicMap(types.NewRegistry(), decodedVal) } +func parseYAMLString(val ref.Val) ref.Val { + str, ok := val.(types.String) + if !ok { + return types.ValOrErr(str, "unexpected type '%v' passed to parseYAML", val.Type()) + } + decodedVal := map[string]interface{}{} + err := yaml.Unmarshal([]byte(str), &decodedVal) + if err != nil { + return types.NewErr("failed to decode '%v' in parseYAML: %w", str, err) + } + return types.NewDynamicMap(types.NewRegistry(), decodedVal) +} + func parseURLString(val ref.Val) ref.Val { str, ok := val.(types.String) if !ok {