CEL Expressions
expr expressions use the Common Expression Language (CEL)
The CEL playground lets you test CEL expressions
---
apiVersion: canaries.flanksource.com/v1
kind: Canary
metadata:
name: currency-converter-display-cel
spec:
http:
- name: USD
url: https://api.frankfurter.app/latest?from=USD&to=GBP,EUR,ILS,ZAR
display:
expr: "'$1 = €' + string(json.rates.EUR) + ', £' + string(json.rates.GBP) + ', ₪' + string(json.rates.ILS)"
Values in CEL represent any of the following:
| Type | Description |
|---|---|
int | 64-bit signed integers |
uint | 64-bit unsigned integers |
double | 64-bit IEEE floating-point numbers |
bool | Booleans (true or false) |
string | Strings of Unicode code points |
bytes | Byte sequences |
list | Lists of values |
map | Associative arrays with int, uint, bool, or string keys |
null_type | The value null |
| message names | Protocol buffer messages |
type | Values representing the types in the first column |
Standard Operators
Arithmetic Operators
| Operator | Description | Example |
|---|---|---|
+ | Addition (also string/list concatenation) | 2 + 3 → 5"hello" + " world" → "hello world"[1, 2] + [3, 4] → [1, 2, 3, 4] |
- | Subtraction (also negation) | 5 - 3 → 2-5 → -5 |
* | Multiplication | 3 * 4 → 12 |
/ | Division | 10 / 2 → 5 |
% | Remainder (modulo) | 10 % 3 → 1 |
Comparison Operators
| Operator | Description | Example |
|---|---|---|
== | Equal | 5 == 5 → true |
!= | Not equal | 5 != 3 → true |
< | Less than | 3 < 5 → true |
<= | Less than or equal | 5 <= 5 → true |
> | Greater than | 5 > 3 → true |
>= | Greater than or equal | 5 >= 5 → true |
Logical Operators
| Operator | Description | Example |
|---|---|---|
&& | Logical AND | true && false → false |
|| | Logical OR | true || false → true |
! | Logical NOT | !true → false |
? : | Ternary conditional | true ? "yes" : "no" → "yes" |
Type Conversion Functions
| Function | Description | Example |
|---|---|---|
bool() | Convert to boolean | bool("true") → true |
bytes() | Convert to bytes | bytes("hello") → b'hello' |
double() | Convert to double | double(5) → 5.0 |
duration() | Convert to duration | duration("1h") → 1 hour duration |
int() | Convert to integer | int(5.7) → 5 |
string() | Convert to string | string(123) → "123" |
timestamp() | Convert to timestamp | timestamp("2023-01-01T00:00:00Z") |
uint() | Convert to unsigned integer | uint(5) → 5u |
type() | Get the type of a value | type(5) → int |
dyn() | Create a dynamic value | dyn({"key": "value"}) |
Built-in Functions
Type Checking
// Get the type of any value
type(5) // "int"
type("hello") // "string"
type([1, 2, 3]) // "list"
type({"key": "value"}) // "map"
Handling null types and missing keys
When dealing with CEL objects, we might get errors where a key doesn't exist or if you're chaining keys, then one of the keys in the middle is missing.
// Assume we have an obj with value: {'a': {'b': 'c'}}
o.a.b => c
o.a.d // Error, attribute 'd' doesn't exist
o.a.?d.orValue('fallback value') => 'fallback value'
You can read more about or and orValue below.
matchQuery
matchQuery matches a given resource against a search query.
Syntax:
matchQuery(r, s)
// Where
// r = resource
// s = search query
Example:
matchQuery(.config, "type=Kubernetes::Pod")
matchQuery(.config, "type=Kubernetes::Pod tags.cluster=homelab")
matchLabel
matchLabel matches a map's key against one or more patterns. It's particularly useful for matching against labels in Kubernetes objects or config items.
Syntax:
matchLabel(labels, key, patterns)
// Where
// labels = map of labels
// key = the label key to check
// patterns = comma-separated patterns to match against
Pattern Syntax:
- Use
*for wildcards (e.g.,us-*matchesus-east-1,us-west-2) - Use
!for exclusion (e.g.,!productionmatches any value exceptproduction) - Use
!*to match when the label doesn't exist - Multiple patterns are evaluated as OR conditions
Example:
// Match labels with wildcards
matchLabel(config.labels, "region", "us-*") // true if region starts with "us-"
matchLabel(config.labels, "env", "prod,staging") // true if env is "prod" OR "staging"
// Exclusion patterns
matchLabel(config.labels, "env", "!production") // true if env is NOT "production"
matchLabel(config.labels, "optional", "!*") // true if "optional" label doesn't exist
// Complex matching
matchLabel(config.tags, "cluster", "*-prod,*-staging") // true if cluster ends with "-prod" OR "-staging"
aws
aws.arnToMap
Takes in an AWS ARN, parses it, and returns a map.
aws.arnToMap("arn:aws:sns:eu-west-1:123:MMS-Topic") //
// map[string]string{
// "service": string,
// "region": string,
// "account": string,
// "resource": string,
// }
aws.fromAWSMap
aws.fromAWSMap takes a list of map[string]string and merges them into a single map. The input map has the field "Name".
aws.fromAWSMap(x).hello" == "world" // `true`
// Where
// x = [
// { Name: 'hello', Value: 'world' },
// { Name: 'John', Value: 'Doe' },
// ];
base64
base64.encode
base64.encode encodes the given byte slice to a Base64 encoded string.
base64.decode("aGVsbG8=") // return b'hello'
base64.decode
base64.decode decodes the given base64 encoded string back to its original form.
base64.decode("aGVsbG8=") // return b'hello'
collections
.keys
The keys method on a map returns a list of keys.
Syntax:
e.keys()
// Where:
// `e` is a map .
Examples:
{"first": "John", "last": "Doe"}.keys() // ["first", "last"]
.merge
The merge method on a map takes a second map to merge.
Syntax:
e.merge(x)
// Where:
// `e` is the map you want to merge to.
// `x` is the second map to merge from.
Examples:
{"first": "John"}.merge({"last": "Doe"}) // {"first": "John", "last": "Doe"}
.omit
The omit method on a map takes a list of keys to remove.
Syntax:
e.omit(x)
// Where:
// `e` is a list .
// `x` is a list of keys to omit.
Examples:
{"first": "John", "last": "Doe"}.omit(["first"]) // {"last": "Doe"}
.sort
The sort method on a list returns a sorted list.
Syntax:
e.sort()
// Where:
// `e` is a list .
Examples:
[3, 2, 1].sort() // [1, 2, 3]
['c', 'b', 'a'].sort() // ['a', 'b', 'c']
.distinct
The distinct method on a list returns a new list with duplicate elements removed.
[1, 2, 2, 3, 3, 3].distinct() // [1, 2, 3]
["a", "b", "a", "c"].distinct() // ["a", "b", "c"]
.flatten
The flatten method recursively flattens nested lists.
[[1, 2], [3, 4]].flatten() // [1, 2, 3, 4]
[1, [2, [3, 4]]].flatten() // [1, 2, 3, 4]
.reverse
The reverse method returns a new list with elements in reverse order.
[1, 2, 3, 4].reverse() // [4, 3, 2, 1]
["a", "b", "c"].reverse() // ["c", "b", "a"]
.range
The range function generates a list of integers.
// Generate integers from 0 to n-1
range(5) // [0, 1, 2, 3, 4]
// Generate integers from start to end-1
range(2, 5) // [2, 3, 4]
// Generate integers with a step
range(0, 10, 2) // [0, 2, 4, 6, 8]
.uniq
The uniq method on a list returns a list of unique items.
Syntax:
e.uniq()
// Where:
// `e` is a list .
Examples:
[1,2,3,3,3,].uniq().sum() // 10
["a", "b", "b"].uniq().join() // "ab"
.values
The values method on a map returns a list of items.
Syntax:
e.values()
// Where:
// `e` is a map .
Examples:
{'a': 1, 'b': 2}.values().sum() // 3
keyValToMap
keyValToMap converts a string in key=value,key2=value2 format into a map.
keyValToMap("a=b,c=d") // {"a": "b", "c": "d"}
keyValToMap("env=prod,region=us-east-1") // {"env": "prod", "region": "us-east-1"}
mapToKeyVal
mapToKeyVal converts a map into a string in key=value,key2=value2 format.
{"a": "b", "c": "d"}.mapToKeyVal() // "a=b,c=d"
{"env": "prod", "region": "us-east-1"}.mapToKeyVal() // "env=prod,region=us-east-1"
all
The all macro tests whether a predicate holds for all elements of a list e or keys of a map e. It returns a boolean value based on the evaluation. If any predicate evaluates to false, the macro evaluates to false, ignoring errors from other predicates.
Syntax:
e.all(x, p)
// Where:
// `e` is the list or a map.
// `x` represents each element of the list.
// `p` is the condition applied to each entry.
Examples:
// Checking if all elements of a list are greater than 0:
[1, 2, 3].all(e, e > 0) // true
// Ensure that the all the map keys begin with the letter "a"
{"a": "apple", "b": "banana", "c": "coconut"}.all(k, k.startsWith("a")) // false
exists
The exists macro checks if there is at least one element in a list that satisfies a given condition. It returns a boolean value based on the evaluation.
Syntax:
e.exists(x, p)
// Where:
// `e` is the list you're checking.
// `x` represents each element of the list.
// `p` is the condition applied to each entry.
Example:
// Checking if any element of a list is equal to 2:
[1, 2, 3].exists(e, e == 2) // true
exists_one
The exists_one macro checks if there is exactly one element in a list that satisfies a given condition. It returns a boolean value based on the evaluation.
Syntax:
e.exists_one(x, p)
// Where:
// `e` is the list you're checking.
// `x` represents each element of the list.
// `p` is the condition applied to each entry.
Example:
[1, 2, 3].exists_one(e, e > 1) // false
[1, 2, 3].exists_one(e, e == 2) // true
filter
The filter macro creates a new list containing only the elements or entries of an existing list that satisfy the given condition.
Syntax:
e.filter(x, p)
Where:
eis the list you are filtering.xrepresents each element of the list.pis the predicate expression applied to each entry.
Examples:
// Filtering a list to include only numbers greater than 2:
[1, 2, 3, 4].filter(e, e > 2) // [3, 4]
fold
The fold macro combines all elements of a collection, such as a list or a map, using a binary function. It's a powerful tool for aggregating or reducing data.
Syntax:
//For lists:
list.fold(e, acc, <binary_function>)
//For maps:
map.fold(k, v, acc, <binary_function>)
Where:
listis the list you're folding.mapis the map you're folding.erepresents each element of the list.krepresents each key of the map.vrepresents each value of the map.accis the accumulator, which holds the intermediate results.<binary_function>is the function applied to each entry and the accumulator.
Examples:
[1, 2, 3].fold(e, acc, acc + e) // 6
// Concatenating all values of a map:
{"a": "apple", "b": "banana"}.fold(k, v, acc, acc + v) // "applebanana"
has
The has macro tests whether a field is available. It's particularly useful for protobuf messages where fields can be absent rather than set to a default value. It's especially useful for distinguishing between a field being set to its default value and a field being unset. For instance, in a protobuf message, an unset integer field is indistinguishable from that field set to 0 without the has macro.
Syntax
has(x.y): boolean
// Where
// `x` is a message or a map and
// `y` (string) is the field you're checking for.
Example:
If you have a message person with a potential field name, you can check for its presence with:
has(person.name) // true if 'name' is present, false otherwise
in
The membership test operator checks whether an element is a member of a collection, such as a list or a map. It's worth noting that the in operator doesn't check for value membership in maps, only key membership.
Syntax:
"apple" in ["apple", "banana"] // => true
3 in [1, 2, 4] // => false
map
The map macro creates a new list by transforming a list e by taking each element x to the function given by the expression t, which can use the variable x.
Syntax:
e.map(x, t)
// Where:
// `e` is the list you are transforming.
// `x` represents each element of the list.
// `t` is the transformation function applied to each entry.
e.map(x, p, t)
// Where:
// `e` is the list you're transforming.
// `p` filter before the value is transformed
// `x` represents each element of the list.
// `t` is the transformation function applied to each entry.
Examples:
// Transforming each element of a list by multiplying it by 2:
[1, 2, 3].map(e, e * 2) // [2, 4, 6]
[(1, 2, 3)].map(x, x > 1, x + 1) // [3, 4]
or
If the value on the left-hand side is none-type, the optional value on the right hand side is returned. If the value on the left-hand set is valued, then it is returned. This operation is short-circuiting and evaluates as many links in the or chain as are needed to return a non-empty optional value.
obj.?field.or(m[?key]) // if obj.field is none, m.key is returned
l[?index].or(obj.?field.subfield).or(obj.?other) // first non-none of l[index], obj.field.subfield, obj.other is returned
orValue
When creating map or list, if a field may be optionally set based on its presence, then placing a ? before the field name or key ensures the type on the right-hand side must be optional(T) where T is the type of the field or key-value.
// The following returns a map with the key expression set only if the subfield is present, otherwise a default value is returned
{'a': 'x', 'b': 'y', 'c': 'z'}.?c.orValue('empty') // Returns z since `c` exists
{'a': 'x', 'b': 'y'}.?c.orValue('empty') // Returns 'empty' since `c` doesn't exist
// We can use the same for list types
[1, 2, 3][?2].orValue(5) // Return 3 since 2nd index has a value
[1, 2][?2].orValue(5) // Return 5 since 2nd index doesn't exist
size
size determines the number of elements in a collection or the number of Unicode characters in a string.
Syntax
(string) -> int string length
(bytes) -> int bytes length
(list(A)) -> int list size
(map(A, B)) -> int map size
"apple".size() // 5
b"abc".size() // 3
["apple", "banana", "cherry"].size() // 3
{"a": 1, "b": 2}.size(); // 2
slice
Returns a new sub-list using the indices provided.
[1, 2, 3, 4].slice(1, 3) // return [2, 3]
[(1, 2, 3, 4)].slice(2, 4) // return [3 ,4]
sets
sets.contains
Returns whether the first list argument contains all elements in the second list argument. The list may contain elements of any type and standard CEL equality is used to determine whether a value exists in both lists. If the second list is empty, the result always returns true.
sets.contains(list(T), list(T)) -> bool
Examples:
sets.contains([], []) // true
sets.contains([], [1]) // false
sets.contains([1, 2, 3, 4], [2, 3]) // true
sets.contains([1, 2.0, 3u], [1.0, 2u, 3]) // true
sets.equivalent
Returns whether the first and second list are set equivalent. Lists are set equivalent if for every item in the first list, there is an element in the second which is equal. The lists may not be of the same size as they do not guarantee the elements within them are unique, so size doesn't factor into the computation.
sets.equivalent(list(T), list(T)) -> bool
Examples:
sets.equivalent([], []) // true
sets.equivalent([1], [1, 1]) // true
sets.equivalent([1], [1u, 1.0]) // true
sets.equivalent([1, 2, 3], [3u, 2.0, 1]) // true
sets.intersects
Returns whether the first list has at least one element whose value is equal to an element in the second list. If either list is empty, the result is false.
sets.intersects([1], []) // false
sets.intersects([1], [1, 2]) // true
sets.intersects(
[[1], [2, 3]],
[
[1, 2],
[2, 3.0]
]
) // true
csv
CSV
CSV converts a CSV formatted array into a two-dimensional array, where each element is a row string.
CSV(["Alice,30", "Bob,31"])[0][0] // "Alice"
crypto
crypto.SHA1|256|384|512
The crypto.SHA* functions are used to compute the SHA hash of the input data.
crypto.SHA1("hello") // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c"
crypto.SHA256("hello") // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
dates
timestamp
timestamp represent a point in time. It's typically used in conjunction with other functions to extract or manipulate time-related data.
// Creating a timestamp for January 1st, 2023:
timestamp("2023-01-01T00:00:00Z")
// Creating another timestamp:
timestamp("2023-07-04T12:00:00Z")
.getDate
getDate extract the date part from a timestamp. It returns a string representation of the date.
// Extracting the date from a timestamp:
"2023-01-01T12:34:56Z".getDate() // "2023-01-01"
// Getting the date from another timestamp:
"2023-07-04T00:00:00Z".getDate() // "2023-07-04"
.get[DatePart]
| Function | Description | Example |
|---|---|---|
{date>.getDayOfMonth() | A integer value representing the day of the month, with the first day being 1. | 1 - 31 |
<date>.getDayOfWeek() | Returns an integer value representing the day of the week, where Sunday is 0 and Saturday is 6. | 0 - 6 |
<date>.getDayOfYear() | an integer value representing the day of the year, with January 1st being day 1. | 1 - 366 |
<date>.getDayOfMonth() | the full year (4 digits for 4-digit years) of the specified timestamp. | |
<date>.getHours() | the full year (4 digits for 4-digit years) of the specified timestamp. | 0- 23 |
<date>.getMilliseconds() | 0 -999 | |
<date>.getMinutes() | ||
<date>.getMonth() | 0 -11 | |
<date>.getSeconds() | 0 - 59 | 0 - 59 |
<date>.getHours() |
duration
duration parses a string into a new duration.
The format is an integer followed by a unit: s for seconds, m for minutes, h for hours, and d for days.
// Creating a duration of 5 hours:
duration("5h") // Represents a duration of 5 hours
duration("30m") // Represents a duration of 30 minutes
duration("7d") // Represents a duration of 7 days
Durations can also be crated using arithmetic:
| Field | Description |
|---|---|
| time.Unix(epoch) | converts a UNIX time (seconds since the UNIX epoch) into a time.Time object |
| time.Nanosecond | converts to a time.Duration |
| time.Microsecond | |
| time.Millisecond | |
| time.Second | |
| time.Minute | |
| time.Hour |
time.ZoneName
time.ZoneName returns the name of the local system's time zone. It doesn't require any parameters and is useful for retrieving the time zone information.
// Retrieving the local time zone name:
time.ZoneName() // Might evaluate to "PST" if the local time zone is Pacific Standard Time