Updated February 17 2021 with CEL syntax changes

I’ve been working on TektonCD since November, it’s a set of primitives for executing Kubernetes containers in sequence, and provides resources like Git and PullRequests to make it easier to drive CI/CD type flows.

The biggest change I’ve landed is the CEL interceptor, which allows matching on specific events to trigger pipeline runs.

Tekton Triggers work by having EventListeners receive incoming webhook notifications, processing them using an Interceptor, and creating Kubernetes resources from templates if the interceptor allows it, with extraction of fields from the body of the webhook (there’s an assumption that the body is JSON).

TriggerFlow

The following YAML describes an EventListener that creates resources using a template dev-ci-build-from-pr-template, and a binding called dev-ci-build-from-pr-binding.

  
apiVersion: tekton.dev/v1alpha1
kind: EventListener
metadata:
  name: cicd-event-listener
spec:
  serviceAccountName: demo-sa
  triggers:
    - name: dev-ci-build-from-pr
      interceptor:
        header:
        - name: Pullrequest-Action
          value: opened,synchronize
        - name:  Pullrequest-Repo
          value: tektoncd/triggers
        objectRef:
          kind: Service
          name: demo-interceptor
          apiVersion: v1
          namespace: cicd-environment
      binding:
        - name: dev-ci-build-from-pr-binding
      template:
        name: dev-ci-build-from-pr-template

The binding looks like this:

apiVersion: tekton.dev/v1alpha1
kind: TriggerBinding
metadata:
  name: dev-ci-build-from-pr-binding
spec:
  params:
  - name: gitref
    value: $(body.pull_request.head.ref)

This means that there’s a gitref parameter that can be substituted into the template, and it’s extracted from the JSON body by walking the tree body.pull_request.head.ref

If you look at the example in the GitHub documentation, this would get the name of the source branch.

The template is basically a standard Kubernetes resource definition YAML, but with substitutable values.

Looking at the EventListener:

interceptor:
  header:
  - name: Pullrequest-Action
    value: opened,synchronize
  - name:  Pullrequest-Repo
    value: tektoncd/triggers
  objectRef:
    kind: Service
    name: demo-interceptor
    apiVersion: v1
    namespace: cicd-environment

This is the old v0.1.0 style interception format, this only provided for HTTP based Webhooks, the entire incoming hook request (headers and body) are resubmitted to an endpoint (identified by its service in the objectRef), and if the HTTP endpoint returns a 200 response, the template/bindings are applied.

The header section defines additional HTTP headers that can be added to the request that’s submitted to the endpoint, in this case, there are two, Pullrequest-Action and Pullrequest-Repo, with opened,synchronize and tektoncd/triggers respectively as the values.

What does the demo-interceptor do with these?

Well…that’s hard to know, I wrote it, so I have a good idea of what it’s doing, but it’s hard to reason about.

Triggers v0.2 has support for additional interceptors, including my contribution, the CEL interceptor.

Rewriting this using this new interceptor makes it a lot clearer:

interceptors:
  - cel:
      filter: >-
        (header.match('X-GitHub-Event', 'pull_request') &&
         body.action in ['opened', 'synchronize']) &&
         body.pull_request.head.repo.full_name == 'tektoncd/triggers'
bindings:
  - name: dev-ci-build-from-pr-binding
template:
  name: dev-ci-build-from-pr-template

CEL is a simple expression language, and while the filter above is long, it basically matches on the event type from the incoming HTTP headers, then looks into the JSON body to match on other elements, in this case, checking to see if this is a pull-request ‘opened’ or ‘synchronize’ event, and if the repo is the one we’re watching for.

Not only is it ‘shorter’, it’s easier to understand and thus validate.

Note: the match function in the expression is a wrapper around Header.Get.

This also allows a single EventListener to handle multiple events:

apiVersion: tekton.dev/v1alpha1
kind: EventListener
metadata:
  name: cicd-event-listener
spec:
  serviceAccountName: demo-sa
  triggers:
    - name: dev-ci-build-from-pr
      interceptors:
        - cel:
            filter: >-
              (header.match('X-GitHub-Event', 'pull_request') &&
               body.action in ['opened', 'synchronize']) &&
               body.pull_request.head.repo.full_name == 'tektoncd/triggers'
      bindings:
        - name: dev-ci-build-from-pr-binding
      template:
        name: dev-ci-build-from-pr-template
    - name: dev-cd-deploy-from-master
      interceptors:
        # Updated November 2020 to illustrate use of header.canonical instead of header.match
        - cel:
            filter: >-
              (header.canonical('X-GitHub-Event') == 'push' &&
               body.repository.full_name == 'tekton/triggers') &&
               body.ref.startsWith('refs/heads/master')
      bindings:
        - name: dev-cd-deploy-from-master-binding
      template:
        name: dev-cd-deploy-from-master-template

In this example, pull requests trigger one template (dev-ci-build-from-pr-template), and pushes trigger a second (dev-cd-deploy-from-master-template).

Update November 2020

There’s now a page of documentation for the CEL interceptor https://tekton.dev/docs/triggers/cel_expressions/

Eagle-eyed readers might notice that the demo-interceptor enriched the response body, adding an additional intercepted property, with a short_sha key, and trimmed version of the SHA (using the full 40 char version is a little unwieldy), and this functionality is missing?

Overlays are coming in v0.3, which will bring this behaviour back.

interceptors:
  - cel:
      filter: >-
        (header.match('X-GitHub-Event', 'pull_request') &&
         body.action in ['opened', 'synchronize']) &&
         body.pull_request.head.repo.full_name == 'tektoncd/triggers'
      overlays:
      - key: intercepted.short_sha
        expression: 'truncate(body.pull_request.head.sha, 7)'
bindings:
  - name: dev-ci-build-from-pr-binding
template:
  name: dev-ci-build-from-pr-template

This uses a custom function in CEL to truncate the string, and updates the JSON body using the ‘key’ to walk the tree and insert (or overwrite) the value, this can then be extracted in a template binding, for use in a template (common use is tagging Docker images to be able to tie an image to its git source).

Update November 2020

The CEL Syntax changed in v0.5.0, extension functions moved to resemble method calls on the keys being extracted.

overlays:
  - key: intercepted.short_sha
    expression: 'body.pull_request.head.sha.truncate(7)'
apiVersion: tekton.dev/v1alpha1
kind: TriggerBinding
metadata:
  name: dev-ci-build-from-pr-binding
spec:
  params:
  - name: short_sha
    value: $(body.intercepted.short_sha)

I’m hopeful of adding additional CEL expression functions to make working with these hooks a bit easier.

Update November 2020

In Triggers release v0.10.0 the CEL overlays changed slightly:

In the move to immutable bodies, the body is no longer modified directly by the CEL interceptor, the overlays are instead, placed into an extensions body, this means that you need to get the keys from extensions.

apiVersion: tekton.dev/v1alpha1
kind: TriggerBinding
metadata:
  name: dev-ci-build-from-pr-binding
spec:
  params:
  - name: short_sha
    value: $(extensions.intercepted.short_sha)