Skip to content

Commit

Permalink
Add an interceptor to that can filter based on CEL expressions.
Browse files Browse the repository at this point in the history
This adds a cel interceptor, that uses a a CEL expression to filter request bodies.

This implements issue tektoncd#49.
  • Loading branch information
bigkevmcd authored and tekton-robot committed Jan 7, 2020
1 parent 2e8809d commit 9ace5c1
Show file tree
Hide file tree
Showing 168 changed files with 45,793 additions and 6 deletions.
49 changes: 46 additions & 3 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion Gopkg.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ required = [
"knative.dev/caching/pkg/apis/caching",
"github.com/tektoncd/plumbing/scripts",
"github.com/knative/test-infra/tools/dep-collector",
"github.com/google/go-github/github"
"github.com/google/go-github/github",
"github.com/google/cel-go/cel",
"github.com/google/cel-go/checker/decls",
"github.com/google/cel-go/common/types",
"github.com/google/cel-go/common/types/ref",
"github.com/google/cel-go/interpreter/functions",
"google.golang.org/genproto/googleapis/api/expr/v1alpha1",
]

[[constraint]]
Expand Down
84 changes: 84 additions & 0 deletions docs/cel_expressions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# CEL expression extensions

The CEL expression is configured to expose parts of the request, and some custom
functions to make matching easier.

### List of extensions

The body from the `http.Request` value is decoded to JSON and exposed, and the
headers are also available.

<table style="width=100%" border="1">
<tr>
<th>Symbol</th>
<th>Type</th>
<th>Description</th>
<th>Example</th>
</tr>
<tr>
<th>
body
</th>
<td>
map(string, dynamic)
</td>
<td>
This is the decoded JSON body from the incoming http.Request exposed as a map of string keys to any value types.
</td>
<td>
<pre>body.value == 'test'</pre>
</td>
</tr>
<tr>
<th>
header
</th>
<td>
map(string, list(string))
</td>
<td>
This is the request Header.
</td>
<td>
<pre>header['X-Test'][0] == 'test-value'</pre>
</td>
</tr>
</table>

NOTE: The header value is a Go `http.Header`, which is [defined](https://golang.org/pkg/net/http/#Header) as:

```go
type Header map[string][]string
```

i.e. the header is a mapping of strings, to arrays of strings, see the `match`
function on headers below for an extension that makes looking up headers easier.


### List of extension functions

This lists custom functions that can be used from CEL expressions in the
CEL interceptor.

<table style="width=100%" border="1">
<tr>
<th>Symbol</th>
<th>Type</th>
<th>Description</th>
<th>Example</th>
</tr>
<tr>
<th>
match
</th>
<td>
header.(string, string) -> bool
</td>
<td>
Uses the canonical header matching from Go's http.Request to match the header against the value.
</td>
<td>
<pre>header.match('x-test', 'test-value')</pre>
</td>
</tr>
</table>
29 changes: 29 additions & 0 deletions docs/eventlisteners.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,32 @@ spec:
template:
name: pipeline-template
```

### CEL Interceptors

CEL interceptors parse expressions to filter requests based on JSON bodies and request headers, using the
[CEL](https://github.com/google/cel-go) expression language.

Supported features include case-insensitive matching on request headers.

<!-- FILE: examples/eventlisteners/cel-eventlistener-interceptor.yaml -->
```YAML
apiVersion: tekton.dev/v1alpha1
kind: EventListener
metadata:
name: cel-listener-interceptor
spec:
serviceAccountName: tekton-triggers-example-sa
triggers:
- name: cel-trig
interceptor:
cel:
filter: "headers.match('X-GitHub-Event', 'push')"
bindings:
- name: pipeline-binding
template:
name: pipeline-template
```

The `expression` must return a `true` value, otherwise the request will be filtered
out.
15 changes: 15 additions & 0 deletions examples/eventlisteners/cel-eventlistener-interceptor.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: tekton.dev/v1alpha1
kind: EventListener
metadata:
name: cel-listener-interceptor
spec:
serviceAccountName: tekton-triggers-example-sa
triggers:
- name: cel-trig
interceptor:
cel:
filter: "headers.match('X-GitHub-Event', 'push')"
bindings:
- name: pipeline-binding
template:
name: pipeline-template
6 changes: 6 additions & 0 deletions pkg/apis/triggers/v1alpha1/event_listener_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ type EventInterceptor struct {
Webhook *WebhookInterceptor `json:"webhook,omitempty"`
GitHub *GitHubInterceptor `json:"github,omitempty"`
GitLab *GitLabInterceptor `json:"gitlab,omitempty"`
CEL *CELInterceptor `json:"cel,omitempty"`
}

// WebhookInterceptor provides a webhook to intercept and pre-process events
Expand All @@ -105,6 +106,11 @@ type GitLabInterceptor struct {
EventTypes []string `json:"eventTypes,omitempty"`
}

// CELInterceptor provides a webhook to intercept and pre-process events
type CELInterceptor struct {
Filter string `json:"filter,omitempty"`
}

// SecretRef contains the information required to reference a single secret string
// This is needed because the other secretRef types are not cross-namespace and do not
// actually contain the "SecretName" field, which allows us to access a single secret value.
Expand Down
9 changes: 7 additions & 2 deletions pkg/apis/triggers/v1alpha1/event_listener_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ func (s *EventListenerSpec) validate(ctx context.Context, el *EventListener) *ap
}

func (i *EventInterceptor) validate(ctx context.Context, namespace string) *apis.FieldError {
// Validate at least one
if i.Webhook == nil && i.GitHub == nil && i.GitLab == nil {
if i.Webhook == nil && i.GitHub == nil && i.GitLab == nil && i.CEL == nil {
return apis.ErrMissingField("interceptor")
}

Expand Down Expand Up @@ -126,5 +125,11 @@ func (i *EventInterceptor) validate(ctx context.Context, namespace string) *apis
// if i.GitLab != nil {
//
// }

if i.CEL != nil {
if i.CEL.Filter == "" {
return apis.ErrMissingField("interceptor.cel.filter")
}
}
return nil
}
23 changes: 23 additions & 0 deletions pkg/apis/triggers/v1alpha1/event_listener_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ func Test_EventListenerValidate(t *testing.T) {
bldr.EventListenerTriggerInterceptor("svc", "v1", "Service", "namespace"),
),
bldr.EventListenerTrigger("tb", "tt", "v1alpha1"))),
}, {
name: "Valid EventListener with CEL interceptor",
el: bldr.EventListener("name", "namespace",
bldr.EventListenerSpec(
bldr.EventListenerTrigger("tb", "tt", "v1alpha1",
bldr.EventListenerCELInterceptor("body.value == 'test'")))),
}}

for _, test := range tests {
Expand Down Expand Up @@ -236,6 +242,23 @@ func TestEventListenerValidate_error(t *testing.T) {
}},
},
},
}, {
name: "CEL interceptor with no filter",
el: &v1alpha1.EventListener{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: v1alpha1.EventListenerSpec{
Triggers: []v1alpha1.EventListenerTrigger{{
Bindings: []*v1alpha1.EventListenerBinding{{Name: "tb"}},
Template: v1alpha1.EventListenerTemplate{Name: "tt"},
Interceptor: &v1alpha1.EventInterceptor{
CEL: &v1alpha1.CELInterceptor{},
},
}},
},
},
}}

for _, test := range tests {
Expand Down
21 changes: 21 additions & 0 deletions pkg/apis/triggers/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 9ace5c1

Please sign in to comment.