Skip to content

Commit

Permalink
Support JSONPath in TriggerBindings
Browse files Browse the repository at this point in the history
This commit switches TriggerBinding params to use
JSONPath instead of GJSON. To keep the commit small,
removing existing GJSON will be part of a separate commit.

Signed-off-by: Dibyo Mukherjee <[email protected]>
  • Loading branch information
dibyom committed Dec 3, 2019
1 parent 04ea5b2 commit 16969a1
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 55 deletions.
52 changes: 25 additions & 27 deletions docs/triggerbindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,46 +33,44 @@ Each parameter has a `name` and a `value`.

## Event Variable Interpolation

In order to parse generic events as efficiently as possible,
[GJSON](https://github.com/tidwall/gjson) is used internally. As a result, the
binding [path syntax](https://github.com/tidwall/gjson#path-syntax) differs
slightly from standard JSON. As of now, the following patterns are supported
within `TriggerBinding` parameter value interpolation: -
`\$\(body(\.[[:alnum:]/_\-\.\\]+|\.#\([[:alnum:]=<>%!"\*_-]+\)#??)*\)` -
`\$\(header(\.[[:alnum:]_\-]+)?\)`
TriggerBindings can access values from the HTTP JSON body and the headers using
JSONPath expressions. The expressions have to be wrapped in `$()` but can omit
the curly braces `{}` and the leading `.`.

These are all valid expressions:
```shell script
$(body.key1)
$(.body.key)
$({.body.key1})
$({body.key)
```

These are invalid expressions:
```shell script
{.body.key1} # INVALID - Not wrapped in $()
$({body) # INVALID - Ending curly brace absent
```


### Body
### Examples

HTTP Post request body data can be referenced using variable interpolation. Text
in the form of `$(body.X.Y.Z)` is replaced by the body data at JSON path
`X.Y.Z`.
``` shell script
`$(body)` is replaced by the entire body.

The following are some example variable interpolation replacements:
``` $(body)
-> "{\"key1\": \"value1\", \"key2\": {\"key3\": \"value3\"}, \"key4\":
[\"value4\", \"value5\"]}"
$(body) -> "{"key1": "value1", "key2": {"key3": "value3"}, "key4": ["value4", "value5"]}"

$(body.key1) -> "value1"

$(body.key2) -> "{\"key3\": \"value3\"}"
$(body.key2) -> "{"key3": "value3"}"

$(body.key2.key3) -> "value3"

$(body.key4.0) -> "value4"
```

### Header
$(body.key4[0]) -> "value4"

HTTP Post request header data can be referenced using variable interpolation.
Text in the form of `$(header.X)` is replaced by the event's header named `X`.
# $(header) is replaced by all of the headers from the event.

`$(header)` is replaced by all of the headers from the event.

The following are some example variable interpolation replacements:
```
$(header) -> "{\"One\":[\"one\"], \"Two\":[\"one\",\"two\",\"three\"]}"
$(header) -> "{"One":["one"], "Two":["one","two","three"]}"

$(header.One) -> "one"

Expand Down
55 changes: 47 additions & 8 deletions pkg/template/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,26 +169,65 @@ func getHeaderValue(header map[string][]string, headerName string) (string, erro
func ResolveParams(bindings []*triggersv1.TriggerBinding, body []byte, header map[string][]string, params []pipelinev1.ParamSpec) ([]pipelinev1.Param, error) {
out, err := MergeBindingParams(bindings)
if err != nil {
return nil, xerrors.Errorf("error merging trigger params: %v", err)
return nil, xerrors.Errorf("Failed to mergeBindingParams: %q", err)
}
out, err = ApplyBodyToParams(body, out)
event, err := NewEvent(body, header)
if err != nil {
return nil, xerrors.Errorf("error applying body to trigger params: %s", err)
return nil, xerrors.Errorf("Failed to create Event: %q", err)
}
out, err = ApplyHeaderToParams(header, out)
out, err = ApplyEventValuesToParams(event, out)
if err != nil {
return nil, xerrors.Errorf("error applying header to trigger params: %s", err)
return nil, xerrors.Errorf("Failed to ApplyEventValuesToParams: %q", err)
}

return MergeInDefaultParams(out, params), nil
}

// ResolveResources resolves a templated resource by replacing params with their values.
func ResolveResources(template *triggersv1.TriggerTemplate, params []pipelinev1.Param) []json.RawMessage {
resources := make([]json.RawMessage, len(template.Spec.ResourceTemplates))
uid := UID()
for i := range template.Spec.ResourceTemplates {
resources[i] = ApplyParamsToResourceTemplate(params, template.Spec.ResourceTemplates[i].RawMessage)
resources[i] = ApplyUIDToResourceTemplate(resources[i], uid)
resources[i] = ApplyUIDToResourceTemplate(resources[i], UID())
}
return resources
}


// Event represents a HTTP event that Triggers processes
type Event struct {
Header map[string]string `json:"header"`
Body interface{} `json:"body"`
}

// NewEvent returns a new Event from HTTP headers and body
func NewEvent(body []byte, headers map[string][]string) (Event, error) {
var data interface{}
if len(body) > 0 {
if err := json.Unmarshal(body, &data); err != nil {
return Event{}, xerrors.Errorf("failed to unmarshal request body: %q", err)
}
}
joinedHeaders := make(map[string]string, len(headers))
for k, v := range headers {
joinedHeaders[k] = strings.Join(v, ",")
}

return Event{
Header: joinedHeaders,
Body: data,
}, nil
}


// ApplyEventValuesToParams returns a slice of Params with the JSONPath variables replaced
// with values from the event body and headers.
func ApplyEventValuesToParams(ec Event, params []pipelinev1.Param) ([]pipelinev1.Param, error) {
for idx, p := range params {
val, err := ParseJSONPath(ec, p.Value.StringVal)
if err != nil {
return nil, xerrors.Errorf("failed to replace JSONPath value for param %s: %s: %w", p.Name, p.Value, err)
}
params[idx].Value = pipelinev1.ArrayOrString{Type: pipelinev1.ParamTypeString, StringVal: val}
}
return params, nil
}
34 changes: 15 additions & 19 deletions pkg/template/event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,17 +661,17 @@ func Test_NewResources(t *testing.T) {
args args
want []json.RawMessage
}{{
name: "empty",
args: args{
body: json.RawMessage{},
header: map[string][]string{},
binding: ResolvedTrigger{
TriggerTemplate: bldr.TriggerTemplate("tt", "namespace"),
TriggerBindings: []*triggersv1.TriggerBinding{bldr.TriggerBinding("tb", "namespace")},
name: "empty",
args: args{
body: []byte{},
header: map[string][]string{},
binding: ResolvedTrigger {
TriggerTemplate: bldr.TriggerTemplate("tt", "namespace"),
TriggerBindings: []*triggersv1.TriggerBinding{bldr.TriggerBinding("tb", "namespace")},
},
},
},
want: []json.RawMessage{},
}, {
want: []json.RawMessage{},
}, {
name: "one resource template",
args: args{
body: json.RawMessage(`{"foo": "bar"}`),
Expand Down Expand Up @@ -799,7 +799,7 @@ func Test_NewResources(t *testing.T) {
},
want: []json.RawMessage{
json.RawMessage(`{"rt1": "bar-cbhtc", "cbhtc": "cbhtc"}`),
json.RawMessage(`{"rt2": "default2-cbhtc"}`),
json.RawMessage(`{"rt2": "default2-bsvjp"}`),
json.RawMessage(`{"rt3": "rt3"}`),
},
}, {
Expand Down Expand Up @@ -866,8 +866,7 @@ func Test_NewResources_error(t *testing.T) {
header map[string][]string
elParams []pipelinev1.Param
binding ResolvedTrigger
}{
{
}{{
name: "bodypath not found in body",
body: json.RawMessage(`{"foo": "bar"}`),
binding: ResolvedTrigger{
Expand All @@ -884,8 +883,7 @@ func Test_NewResources_error(t *testing.T) {
),
},
},
},
{
}, {
name: "header not found in event",
body: json.RawMessage(`{"foo": "bar"}`),
header: map[string][]string{"One": {"one"}},
Expand All @@ -903,8 +901,7 @@ func Test_NewResources_error(t *testing.T) {
),
},
},
},
{
}, {
name: "merge params error",
elParams: []pipelinev1.Param{
{
Expand All @@ -926,8 +923,7 @@ func Test_NewResources_error(t *testing.T) {
),
},
},
},
{
}, {
name: "conflicting bindings",
binding: ResolvedTrigger{
TriggerTemplate: bldr.TriggerTemplate("tt", "namespace",
Expand Down
2 changes: 1 addition & 1 deletion pkg/template/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type ResolvedTrigger struct {
type getTriggerBinding func(name string, options metav1.GetOptions) (*triggersv1.TriggerBinding, error)
type getTriggerTemplate func(name string, options metav1.GetOptions) (*triggersv1.TriggerTemplate, error)

// ResolveBindings takes in a trigger containing object refs to bindings and
// ResolveTrigger takes in a trigger containing object refs to bindings and
// templates and resolves them to their underlying values.
func ResolveTrigger(trigger triggersv1.EventListenerTrigger, getTB getTriggerBinding, getTT getTriggerTemplate) (ResolvedTrigger, error) {
tb := make([]*triggersv1.TriggerBinding, 0, len(trigger.Bindings))
Expand Down

0 comments on commit 16969a1

Please sign in to comment.