Skip to content

Commit

Permalink
Add CEL function to parse YAML
Browse files Browse the repository at this point in the history
Added a CEL function named parseYAML that can parse an YAML string into a map of strings to dynamic values

Syntax: <string>.parseYAML() -> map<string, dyn>
  • Loading branch information
chetan-rns authored and tekton-robot committed Jun 26, 2020
1 parent fa3a902 commit 5348837
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 3 deletions.
14 changes: 14 additions & 0 deletions docs/cel_expressions.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,20 @@ interceptor.
<pre>'{"testing":"value"}'.parseJSON().testing == "value"</pre>
</td>
</tr>
<tr>
<th>
parseYAML()
</th>
<td>
<pre>&lt;string&gt;.parseYAML() -> map&lt;string, dyn&gt;</pre>
</td>
<td>
This parses a string that contains a YAML body into a map which which can be subsequently used in other expressions.
</td>
<td>
<pre>'key1: value1\nkey2: value2\n'.parseYAML().key1 == "value"</pre>
</td>
</tr>
<tr>
<th>
parseURL()
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 25 additions & 2 deletions pkg/interceptors/cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down Expand Up @@ -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'",
Expand Down Expand Up @@ -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{},
},
Expand Down Expand Up @@ -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) {
Expand Down
30 changes: 30 additions & 0 deletions pkg/interceptors/cel/triggers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
//
// <string>.parseYAML() -> map<string, dyn>
//
// 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 {
Expand Down Expand Up @@ -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)),
Expand All @@ -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},
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 5348837

Please sign in to comment.