CEL expression extensions
The CEL expression is configured to expose parts of the request, and some custom functions to make matching easier.
In addition to the custom function extension listed below, you can craft any valid CEL expression as defined by the cel-spec language definition
String functions
The upstream CEL implementation provides extensions to the CEL specification for manipulating strings.
For example:
'refs/heads/main'.split('/') // result = list ['refs', 'heads', 'main']
['refs', 'heads', 'main'].join('/') // result = string 'refs/heads/main'
'my place'.replace('my ',' ') // result = string 'place'
'this that another'.replace('th ',' ', 2) // result = 'is at another'
The replace
overload allows an optional limit on replacements.
Notes on numbers in CEL expressions
One thing to be aware of is how numeric values are treated in CEL expressions, JSON numbers are decoded to CEL double values.
For example:
{
"count": 2,
"measure": 1.7
}
In the JSON above, both numbers are parsed as CEL double (Go float64
) values.
This means that if you want to do integer arithmetic, you’ll need to use explicit conversion functions.
From the CEL specification:
Note that currently there are no automatic arithmetic conversions for the numeric types (int, uint, and double).
You can either explicitly convert the number, or add another double value e.g.
interceptors:
- ref:
name: cel
params:
- name: overlays
value:
- key: count_plus_1
expression: "body.count + 1.0"
- key: count_plus_2
expression: "int(body.count) + 2"
- key: measure_times_3
expression: "body.measure * 3.0"
These will be serialised back to JSON appropriately:
{
"count_plus_1": 2,
"count_plus_2": 3,
"measure_times_3": 5.1
}
Error messages in conversions
The following example will generate an error with the JSON example.
interceptors:
- ref:
name: cel
params:
- name: overlays
value:
- key: bad_measure_times_3
expression: "body.measure * 3"
bad_measure_times_3 will fail with
failed to evaluate overlay expression 'body.measure * 3': no such overload
because there’s no automatic conversion.
CEL expression examples
Matching on an element in an array.
CEL provides several macros which can operate on JSON objects.
If you have a JSON body like this:
{
"labels": [
{
"name": "test-a"
},
{
"name":"test-b"
}
]
}
You can use this in filters in the following ways:
filter: body.labels.exists(x, x.name == 'test-b')
is truefilter: body.labels.exists(x, x.name == 'test-c')
is falsefilter: body.labels.exists_one(x, x.name.endsWith('-b'))
is truefilter: body.labels.exists_one(x, x.name.startsWith('test-'))
is falsefilter: body.labels.all(x, x.name.startsWith('test-'))
is truefilter: body.labels.all(x, x.name.endsWith('-b'))
is false
You can also parse additional data from each of the labels:
- name: overlays
value:
- key: suffixes
expression: "body.labels.map(x, x.name.substring(x.name.lastIndexOf('-')+1))"
This yields an array of ["a", "b"]
in the suffixes
extension key.
- name: overlays
value:
- key: filtered
expression: "body.labels.filter(x, x.name.endsWith('-b'))"
This would add an extensions key filtered
with only one of the labels.
[
{
"name": "test-b"
}
]
cel-go extensions
All the functionality from the cel-go project’s CEL extension is available in your CEL expressions.
The following extensions are available:
cel-go Bytes
The cel-go project function base64.decode
returns a CEL Bytes
value.
To compare this to a string, you will need to convert it to a Bytes type:
base64.decode(body.b64value) == b'hello' # compare to Bytes literal
base64.decode(body.b64value) == bytes('hello') # convert to bytes.
Returning Bytes
If you decode a base64 string with the cel-go base64 decoder, the result will be a set of base64 decoded bytes. To ensure the result is encoded as a string you will need to explicitly convert it to a CEL string.
interceptors:
- ref:
name: "cel"
params:
- name: "overlays"
value:
- key: base64_decoded
expression: "string(base64.decode(body.b64Value))"
This will correctly appear in the extension as the decoded version.
List of extensions
The body from the http.Request
value is decoded to JSON and exposed, and the
headers are also available.
Symbol | Type | Description | Example |
---|---|---|---|
body | map(string, dynamic) | This is the decoded JSON body from the incoming http.Request exposed as a map of string keys to any value types. |
body.value == 'test' |
header | map(string, list(string)) | This is the request Header. |
header['X-Test'][0] == 'test-value' |
requestURL | string | This is the URL for the incoming HTTP request. |
requestURL.parseURL().path |
NOTE: The header value is a Go http.Header
, which is
defined as:
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.
Symbol | Type | Description | Example | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
match | header.match(string, string) -> bool | Uses the canonical header matching from Go's http.Request to match the header against the value. |
header.match('x-test', 'test-value') |
canonical | header.canonical(string) -> string | Uses the canonical header matching from Go's http.Request to get the provided header name. |
header.canonical('x-test') |
||||||||||||||
truncate |
<string>.truncate(uint) -> string |
Truncates a string to no more than the specified length. |
body.commit.sha.truncate(5) |
||||||||||||||||||
split |
<string>.split(string) -> list(string) |
Splits a string on the provided separator value. |
body.ref.split('/') |
||||||||||||||||||
join |
<list(string)>.join(string) -> string |
Joins a list of strings on the provided separator value. |
['body', 'refs', 'main'].join('/') |
||||||||||||||||||
decodeb64 deprecated: please use base64.decode |
<string>.decodeb64() -> string |
Decodes a base64 encoded string. |
body.message.data.decodeb64() |
||||||||||||||||||
compareSecret |
<string>.compareSecret(string, string, string) -> bool |
Constant-time comparison of strings against secrets, this will fetch the secret using the combination of namespace/name and compare the token key to the string using a cryptographic constant-time comparison.. The event-listener service account must have access to the secret. The parameters to the function are 1. the key within the secret, 2. the secret name, and 3. the namespace for the secret (optional, defaults to the namespace of the EventListener). |
header.canonical('X-Secret-Token').compareSecret('', 'secret-name', 'namespace') |
||||||||||||||||||
compareSecret |
<string>.compareSecret(string, string) -> bool |
This is almost identical to the version above, but only requires two arguments, the namespace is assumed to be the namespace for the event-listener. |
header.canonical('X-Secret-Token').compareSecret('key', 'secret-name') |
||||||||||||||||||
parseJSON() |
<string>.parseJSON() -> map<string, dyn> |
This parses a string that contains a JSON body into a map which which can be subsequently used in other expressions. |
'{"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() |
<string>.parseURL() -> map<string, dyn> |
This parses a string that contains a URL into a map with keys for the elements of the URL. The resulting map will contain the following keys for this URL "https://user:pass@example.com/test/path?s=testing#first"
|
'https://example.com/test?query=testing'.parseURL().query['query'] == "testing" |
||||||||||||||||||
marshalJSON() |
<jsonObjectOrList>.marshalJSON() -> <string> |
Returns the JSON encoding of 'jsonObjectOrList' as a string. |
{"testing":"value"}.marshalJSON() == "{\"testing\": \"value\"}" |
||||||||||||||||||
first() |
<jsonArray>.first() -> <jsonObject> |
Returns the first element in the array. |
[1, 2, 3, 4, 5].first() == 1 |
||||||||||||||||||
last() |
<jsonArray>.last() -> <jsonObject> |
Returns the last element in the array. |
[1, 2, 3, 4, 5].last() == 5 |
||||||||||||||||||
translate() |
<string>.translate(string, string) -> <string> |
Uses a regular expression to replace characters from the source string with characters from the replacements. |
"This is $an Invalid5String ".translate("[^a-z0-9]+", "") == "hisisannvalid5tring" "This is $an Invalid5String ".translate("[^a-z0-9]+", "ABC") == "ABChisABCisABCanABCnvalid5ABCtring" |
||||||||||||||||||
lowerAscii() |
<string>.lowerAscii() -> <string> |
Returns a new string where all ASCII characters are lower-cased. |
"TacoCat".lowerAscii() == "tacocat" "TacoCÆt Xii".lowerAscii() == "tacocÆt xii" |
||||||||||||||||||
upperAscii() |
<string>.upperAscii() -> <string> |
Returns a new string where all ASCII characters are upper-cased. |
"TacoCat".upperAscii() == "TACOCAT" "TacoCÆt Xii".upperAscii() == "TACOCÆT XII" |
Troubleshooting CEL expressions
You can use the cel-eval
tool to evaluate your CEL expressions against a specific HTTP request.
To install the cel-eval
tool use the following command:
$ go install github.com/tektoncd/triggers/cmd/cel-eval@latest
Below is an example of using the tool to evaluate a CEL expression:
$ cat testdata/expression.txt
body.test.nested == "value"
$ cat testdata/http.txt
POST /foo HTTP/1.1
Content-Length: 29
Content-Type: application/json
X-Header: tacocat
{"test": {"nested": "value"}}
$ cel-eval -e testdata/expression.txt -r testdata/http.txt
true
Feedback
Was this page helpful?