CEL Libraries
kro includes a rich set of CEL function libraries from three sources: kro's own custom libraries, the cel-go standard extensions, and Kubernetes apiserver libraries.
Library Overview
| Library | Provider | Docs |
|---|---|---|
| Hash | kro | kro, Go doc |
| JSON | kro | kro, Go doc |
| Random | kro | kro, Go doc |
| Maps | kro | kro, Go doc |
| Index Mutation | kro | kro, Go doc |
| Omit | kro | kro, Go doc |
| Strings | cel-go | kro, Go doc |
| Lists (cel-go) | cel-go | kro, Go doc |
| Encoders | cel-go | kro, Go doc |
| Two-Variable Comprehensions | cel-go | kro, Go doc |
| URLs | kubernetes | kro, Go doc, k8s.io |
| Regex | kubernetes | kro, Go doc, k8s.io |
| Quantity | kubernetes | kro, Go doc, k8s.io |
| Lists (k8s) | kubernetes | kro, Go doc, k8s.io |
| IP | kubernetes | kro, Go doc, k8s.io |
| CIDR | kubernetes | kro, Go doc, k8s.io |
| Semver | kubernetes | kro, Go doc, k8s.io |
kro Libraries
Hash
Cryptographic and non-cryptographic hash functions. All return raw bytes; use "%x".format(...) for hex or base64.encode() for base64.
| Function | Returns | Description |
|---|---|---|
hash.fnv64a(string) | bytes | FNV-1a 64-bit hash. Fast, non-cryptographic. Recommended for most use cases. |
hash.sha256(string) | bytes | SHA-256 cryptographic hash. |
hash.md5(string) | bytes | MD5 hash. |
Examples:
# SHA-256 as hex string (like sha256sum output)
checksum: ${"%x".format([hash.sha256(schema.spec.config)])}
# FNV-1a as hex (short, fast identifier)
id: ${"%x".format([hash.fnv64a(schema.spec.name + "-" + schema.spec.namespace)])}
# Base64-encoded hash (more compact)
encoded: ${base64.encode(hash.fnv64a(schema.metadata.uid))}JSON
Parse and serialize JSON strings.
| Function | Returns | Description |
|---|---|---|
json.unmarshal(string) | dyn | Parse a JSON string into a CEL value (map, list, string, number, bool, or null). |
json.marshal(dyn) | string | Serialize a CEL value to a JSON string. |
Examples:
# Parse a JSON string from a ConfigMap or user input
config: ${json.unmarshal(schema.spec.jsonConfig)}
# Access nested fields after parsing
dbHost: ${json.unmarshal(configmap.data.settings).database.host}
# Serialize structured data into a JSON string (e.g. for an annotation or env var)
configJson: ${json.marshal({"name": schema.spec.name, "replicas": schema.spec.replicas})}Random
Deterministic random generation seeded by a stable value (e.g. resource UID), so results are reproducible across reconcile loops.
| Function | Returns | Description |
|---|---|---|
random.seededString(int, string) | string | Random alphanumeric string of given length, seeded by the second argument. |
random.seededInt(int, int, string) | int | Random integer in [min, max), seeded by the third argument. |
Examples:
# Generate a stable random suffix
suffix: ${random.seededString(8, schema.metadata.uid)}
# Generate a stable random port
port: ${random.seededInt(30000, 32768, schema.metadata.uid)}Maps
Map manipulation functions.
| Function | Returns | Description |
|---|---|---|
<map>.merge(map) | map | Merge two maps. Keys from the second map overwrite keys in the first. Keys must be strings. |
Examples:
# Merge user labels with default labels
labels: ${{"app": schema.spec.name, "managed-by": "kro"}.merge(schema.spec.extraLabels)}
# Layer overrides on top of defaults
config: ${{"timeout": "30s", "retries": "3"}.merge(schema.spec.overrides)}Index Mutation
Pure list functions that return a new list without modifying the input.
| Function | Signature | Description |
|---|---|---|
lists.setAtIndex | list(T), int, T -> list(T) | Replace the element at index. Index must be in [0, size(list)). |
lists.insertAtIndex | list(T), int, T -> list(T) | Insert value before index. Use index == size(list) to append. Index must be in [0, size(list)]. |
lists.removeAtIndex | list(T), int -> list(T) | Remove the element at index. Index must be in [0, size(list)). |
Examples:
# Replace the second tag
tags: ${lists.setAtIndex(schema.spec.tags, 1, "new-tag")}
# Prepend an environment variable
envVars: ${lists.insertAtIndex(schema.spec.envVars, 0, "DEBUG=true")}
# Remove the first port
ports: ${lists.removeAtIndex(schema.spec.ports, 0)}
# Chain operations: swap first two elements
swapped: ${lists.setAtIndex(lists.setAtIndex(schema.spec.items, 0, schema.spec.items[1]), 1, schema.spec.items[0])}Omit
The omit() sentinel tells kro to remove a field from the rendered resource. This is useful for conditionally excluding fields.
| Function | Returns | Description |
|---|---|---|
omit() | omit | Returns a sentinel value that causes the field to be excluded from the output. |
Examples:
# Conditionally include a field
nodeSelector: ${schema.spec.pinToNode ? {"kubernetes.io/hostname": schema.spec.nodeName} : omit()}omit() is only allowed in resource template fields. It is rejected in includeWhen, readyWhen, and forEach expressions.
Runtime
The runtime variable builds and reads instance status conditions. It is only
available in the schema's status.conditions block.
| Function | Returns | Description |
|---|---|---|
runtime.newCondition({type, status, reason, message}) | Condition | Builds a condition. type and status are required; status must be True, False, or Unknown. |
runtime.condition(schema, type) | Condition | Reads kro's internal value for a built-in condition type (Ready, InstanceManaged, GraphResolved, ResourcesReady). |
Examples:
# Publish an application-level condition
- ${runtime.newCondition({type: 'AppReady', status: deployment.status.readyReplicas > 0 ? 'True' : 'False', reason: 'ReplicaCount', message: ''})}
# Compose kro's lifecycle signal with a domain check
- ${runtime.newCondition({type: 'Ready', status: runtime.condition(schema, 'ResourcesReady').status == 'True' && deployment.status.readyReplicas > 0 ? 'True' : 'False', reason: 'Health', message: ''})}See Custom Status Conditions for details.
cel-go Libraries
Strings
Extended string manipulation functions from cel-go/ext.
| Function | Returns | Description |
|---|---|---|
<string>.charAt(int) | string | Character at position. |
<string>.indexOf(string) | int | Index of first occurrence, or -1. |
<string>.indexOf(string, int) | int | Same, starting search from offset. |
<string>.lastIndexOf(string) | int | Index of last occurrence, or -1. |
<string>.lastIndexOf(string, int) | int | Same, with max search position. |
<string>.lowerAscii() | string | Lowercase ASCII characters. |
<string>.upperAscii() | string | Uppercase ASCII characters. |
<string>.replace(string, string) | string | Replace all occurrences. |
<string>.replace(string, string, int) | string | Replace up to N occurrences. |
<string>.split(string) | list(string) | Split by separator. |
<string>.split(string, int) | list(string) | Split with limit. |
<string>.substring(int) | string | Substring from position to end. |
<string>.substring(int, int) | string | Substring from start (inclusive) to end (exclusive). |
<string>.trim() | string | Remove leading and trailing whitespace. |
<string>.reverse() | string | Reverse the string. |
<list(string)>.join() | string | Concatenate list elements. |
<list(string)>.join(string) | string | Concatenate with separator. |
<string>.format(list) | string | Printf-style formatting (%s, %d, %f, %e, %b, %x, %X, %o). |
strings.quote(string) | string | Escape a string for safe printing. |
Examples:
# Printf-style formatting
roleArn: ${"arn:aws:iam::%s:role/%s".format([schema.spec.accountId, schema.spec.roleName])}
# Join list elements with a separator
labelStr: ${schema.spec.tags.join(",")}
# Split a domain name
parts: ${schema.spec.fqdn.split(".")}
# Substring extraction
prefix: ${schema.spec.name.substring(0, 5)}
# Case conversion
lower: ${schema.spec.input.lowerAscii()}
upper: ${schema.spec.input.upperAscii()}
# Search within strings
hasPort: ${schema.spec.endpoint.indexOf(":") >= 0}
# Replace characters
sanitized: ${schema.spec.name.replace("_", "-")}
# Trim whitespace
cleaned: ${schema.spec.input.trim()}
# Hex encoding via %x (useful for hash output)
checksum: ${"%x".format([hash.sha256(schema.spec.data)])}Lists (cel-go)
Extended list manipulation functions from cel-go/ext.
| Function | Returns | Description |
|---|---|---|
<list>.slice(int, int) | list | Sub-list from start (inclusive) to end (exclusive). |
<list>.flatten() | list | Flatten nested lists one level deep. |
<list>.flatten(int) | list | Flatten up to N levels. |
<list>.sort() | list | Sort comparable elements. |
<list>.sortBy(var, keyExpr) | list | Sort by a key expression. |
<list>.distinct() | list | Remove duplicate elements. |
<list>.reverse() | list | Reverse element order. |
lists.range(int) | list(int) | Generate [0, 1, ..., n-1]. |
Examples:
# Slice a list
subset: ${schema.spec.items.slice(0, 3)}
# Sort and deduplicate
tags: ${schema.spec.tags.sort().distinct()}
# Sort by a field
ordered: ${items.sortBy(i, i.data.priority)}
# Generate index range
indices: ${lists.range(schema.spec.count)}
# Flatten nested lists
flat: ${schema.spec.nestedPorts.flatten()}Encoders
Base64 encoding and decoding from cel-go/ext.
| Function | Returns | Description |
|---|---|---|
base64.encode(bytes) | string | Encode bytes to base64 string. |
base64.decode(string) | bytes | Decode base64 string to bytes. |
Examples:
# Encode a hash to base64
encoded: ${base64.encode(hash.sha256(schema.spec.data))}
# Encode a string as base64 (e.g. for a Kubernetes Secret)
secret: ${base64.encode(bytes(schema.spec.password))}
# Decode a base64-encoded string
decoded: ${string(base64.decode(schema.spec.encodedValue))}Two-Variable Comprehensions
Two-variable versions of list/map comprehension macros from cel-go/ext. These provide access to both the key/index and value in each iteration.
| Macro | Description |
|---|---|
<list|map>.all(k, v, predicate) | True if all entries satisfy the predicate. |
<list|map>.exists(k, v, predicate) | True if any entry satisfies the predicate. |
<list|map>.existsOne(k, v, predicate) | True if exactly one entry satisfies the predicate. |
<list|map>.transformList(k, v, transform) | Convert map/list to a list by evaluating transform for each entry. |
<list|map>.transformList(k, v, filter, transform) | Same with a filter predicate. |
<list|map>.transformMap(k, v, transform) | Transform values while preserving keys. |
<list|map>.transformMap(k, v, filter, transform) | Same with a filter predicate. |
<list|map>.transformMapEntry(k, v, transform) | Build new key-value pairs (transform must return a single-entry map). |
<list|map>.transformMapEntry(k, v, filter, transform) | Same with a filter predicate. |
Examples:
# Extract keys from a map
keys: ${{ "app": "nginx", "version": "1.19" }.transformList(k, v, k)}
# -> ["app", "version"]
# Transform map values
scaled: ${{ "cpu": 2, "memory": 4 }.transformMap(k, v, v * 2)}
# -> {"cpu": 4, "memory": 8}
# Filter and transform
filtered: ${{ "a": 1, "b": 5, "c": 3 }.transformMap(k, v, v > 1, v * 10)}
# -> {"b": 50, "c": 30}
# Swap keys and values
swapped: ${{ "us-east-1": "primary", "eu-west-1": "secondary" }.transformMapEntry(k, v, {v: k})}
# -> {"primary": "us-east-1", "secondary": "eu-west-1"}
# Build formatted strings from key-value pairs
envVars: ${{ "APP": "nginx", "PORT": "8080" }.transformList(k, v, k + "=" + v)}
# -> ["APP=nginx", "PORT=8080"]
# Two-variable all/exists on a map
allDifferent: ${{ "hello": "world", "taco": "bell" }.all(k, v, k != v)}Kubernetes Libraries
URLs
Parse and inspect URLs. The URL must be an absolute URI or an absolute path.
| Function | Returns | Description |
|---|---|---|
url(string) | URL | Parse a string into a URL. Errors if invalid. |
isURL(string) | bool | Returns true if the string is a valid URL. |
<URL>.getScheme() | string | Returns the URL scheme (e.g. https). Empty string if absent. |
<URL>.getHost() | string | Returns the host including port (e.g. example.com:80). IPv6 addresses are bracketed. |
<URL>.getHostname() | string | Returns the hostname without port. IPv6 addresses returned without brackets. |
<URL>.getPort() | string | Returns the port, or empty string if not specified. |
<URL>.getEscapedPath() | string | Returns the URL-escaped path. |
<URL>.getQuery() | map(string, list(string)) | Returns query parameters as a map. |
Examples:
# Extract the host from an endpoint URL
host: ${url(schema.spec.endpoint).getHostname()}
# Validate a URL
valid: ${isURL(schema.spec.callbackUrl)}
# Build a connection using URL parts
port: ${url(database.status.endpoint).getPort()}Regex
Regular expression matching and extraction via k8s.io/apiserver/pkg/cel/library.
| Function | Returns | Description |
|---|---|---|
<string>.find(string) | string | Returns the first regex match, or empty string. |
<string>.findAll(string) | list(string) | Returns all non-overlapping matches. |
<string>.findAll(string, int) | list(string) | Returns up to N matches. |
The built-in CEL <string>.matches(string) function is always available (it's part of the CEL standard library, not this extension). It returns true if the entire string matches the regex.
Examples:
# Validate a resource name pattern
valid: ${schema.spec.name.matches('^[a-z][a-z0-9-]*$')}
# Extract version numbers from a string
versions: ${"v1.2.3 and v4.5.6".findAll('[0-9]+\\.[0-9]+\\.[0-9]+')}
# Find first match
version: ${schema.spec.image.find('[0-9]+\\.[0-9]+\\.[0-9]+')}Quantity
Work with Kubernetes resource quantities (e.g. 100m, 1Gi, 500Mi).
| Function | Returns | Description |
|---|---|---|
quantity(string) | Quantity | Parse a Kubernetes quantity string. |
isQuantity(string) | bool | True if the string is a valid quantity. |
<Quantity>.add(Quantity|int) | Quantity | Sum of two quantities. |
<Quantity>.sub(Quantity|int) | Quantity | Difference of two quantities. |
<Quantity>.isLessThan(Quantity) | bool | Comparison. |
<Quantity>.isGreaterThan(Quantity) | bool | Comparison. |
<Quantity>.compareTo(Quantity) | int | Returns -1, 0, or 1. |
<Quantity>.asInteger() | int | Scaled value as integer. Errors on overflow or precision loss. |
<Quantity>.asApproximateFloat() | float | Approximate float value. |
<Quantity>.isInteger() | bool | True if the quantity can be represented as an integer without loss. |
<Quantity>.sign() | int | Returns 1, -1, or 0. |
Examples:
# Compare memory requests
needsMore: ${quantity(schema.spec.memoryRequest).isLessThan(quantity('1Gi'))}
# Arithmetic on quantities
total: ${quantity(schema.spec.cpuRequest).add(quantity('100m'))}
remaining: ${quantity('2Gi').sub(quantity(schema.spec.memoryRequest))}
# Validate a resource quantity
valid: ${isQuantity(schema.spec.cpuLimit)}
# Convert whole-number quantities to integer
megabytes: ${quantity(schema.spec.storageSize).asInteger()}Lists (k8s)
Kubernetes-specific list utility functions from k8s.io/apiserver/pkg/cel/library.
| Function | Returns | Description |
|---|---|---|
<list>.isSorted() | bool | True if elements are in sorted order. |
<list>.sum() | T | Sum of numeric or duration elements. Returns 0 for empty lists. |
<list>.min() | T | Minimum element. Errors on empty list. |
<list>.max() | T | Maximum element. Errors on empty list. |
<list>.indexOf(T) | int | Index of first occurrence, or -1. |
<list>.lastIndexOf(T) | int | Index of last occurrence, or -1. |
Examples:
# Check if ports are sorted
sorted: ${schema.spec.ports.isSorted()}
# Sum all replica counts
totalReplicas: ${schema.spec.replicaCounts.sum()}
# Find min/max
minPort: ${schema.spec.ports.min()}
maxPort: ${schema.spec.ports.max()}
# Find element index
idx: ${schema.spec.tags.indexOf("production")}IP
Parse and inspect IPv4 and IPv6 addresses. IPv4-mapped IPv6 addresses and addresses with zones are not allowed.
| Function | Returns | Description |
|---|---|---|
ip(string) | IP | Parse a string into an IP address. Errors if invalid. |
isIP(string) | bool | Returns true if the string is a valid IPv4 or IPv6 address. |
ip.isCanonical(string) | bool | Returns true if the string is in canonical IP form. |
<IP>.family() | int | Returns 4 or 6. |
<IP>.isLoopback() | bool | True for 127.x.x.x (v4) or ::1 (v6). |
<IP>.isGlobalUnicast() | bool | True for globally routable addresses. |
<IP>.isLinkLocalUnicast() | bool | True for link-local unicast (169.254.x.x / fe80::/10). |
<IP>.isLinkLocalMulticast() | bool | True for link-local multicast (224.0.0.x / ff00::/8). |
<IP>.isUnspecified() | bool | True for 0.0.0.0 or ::. |
string(<IP>) | string | Convert back to string. |
Examples:
# Validate an IP address from user input
ready: ${isIP(schema.spec.clusterIP)}
# Check IP family for dual-stack logic
family: ${string(ip(schema.spec.podIP).family())}
# Gate on global unicast
isRoutable: ${ip(schema.spec.address).isGlobalUnicast()}
# Canonical form check
canonical: ${ip.isCanonical(schema.spec.ipv6Addr)}CIDR
Parse and inspect CIDR subnet notation. Works with both IPv4 and IPv6.
| Function | Returns | Description |
|---|---|---|
cidr(string) | CIDR | Parse a CIDR string (e.g. 192.168.0.0/24). Errors if invalid. |
isCIDR(string) | bool | Returns true if the string is valid CIDR notation. |
<CIDR>.containsIP(string|IP) | bool | True if the CIDR contains the given IP. |
<CIDR>.containsCIDR(string|CIDR) | bool | True if the CIDR fully contains the given subnet. |
<CIDR>.ip() | IP | Returns the address part of the CIDR. |
<CIDR>.prefixLength() | int | Returns the prefix length in bits. |
<CIDR>.masked() | CIDR | Returns the CIDR in canonical masked form. |
string(<CIDR>) | string | Convert back to string. |
Examples:
# Check if a pod IP falls within the service CIDR
inRange: ${cidr(schema.spec.serviceCIDR).containsIP(pod.status.podIP)}
# Validate that a user-provided CIDR is valid
valid: ${isCIDR(schema.spec.subnetCIDR)}
# Extract prefix length
prefixLen: ${string(cidr(schema.spec.subnetCIDR).prefixLength())}
# Check subnet containment
isSubnet: ${cidr('10.0.0.0/8').containsCIDR(schema.spec.vpcCIDR)}Semver
Parse and compare semantic versions. Supports optional normalization to handle common non-strict formats like v1.0 or 01.02.03.
| Function | Returns | Description |
|---|---|---|
semver(string) | Semver | Parse a strict semver string (e.g. 1.2.3). |
semver(string, bool) | Semver | Parse with normalization (strips v prefix, fills missing parts, removes leading zeros). |
isSemver(string) | bool | Returns true if the string is a valid semver. |
isSemver(string, bool) | bool | Same with optional normalization. |
<Semver>.major() | int | Major version number. |
<Semver>.minor() | int | Minor version number. |
<Semver>.patch() | int | Patch version number. |
<Semver>.isGreaterThan(Semver) | bool | True if receiver > operand. |
<Semver>.isLessThan(Semver) | bool | True if receiver < operand. |
<Semver>.compareTo(Semver) | int | Returns -1, 0, or 1. |
Examples:
# Validate a version string
validVersion: ${isSemver(schema.spec.version)}
# Compare versions
ready: ${semver(schema.spec.version).compareTo(semver('2.0.0')) >= 0}
# Extract major version for image tag
majorTag: ${string(semver(schema.spec.appVersion).major())}
# Tolerant parsing with normalization (accepts "v1.0", "01.02.03", etc.)
valid: ${isSemver(schema.spec.version, true)}
parsed: ${semver(schema.spec.version, true).minor()}For the complete CEL language reference, see the CEL language definitions.