Kubernetes 中的通用表达式语言

通用表达式语言 (Common Expression Language, CEL) 用于声明 Kubernetes API 的验证规则、策略规则和其他限制或条件。

CEL 表达式在 API 服务器中直接进行处理, 这使得 CEL 成为许多可扩展性用例的便捷替代方案,而无需使用类似 Webhook 这种进程外机制。 只要控制平面的 API 服务器组件保持可用状态,你的 CEL 表达式就会继续执行。

语言概述

CEL 语言的语法直观简单, 类似于 C、C++、Java、JavaScript 和 Go 中的表达式。

CEL 的设计目的是嵌入应用程序中。每个 CEL “程序” 都是一个单独的表达式,其评估结果为单个值。 CEL 表达式通常是短小的 “一行式”,可以轻松嵌入到 Kubernetes API 资源的字符串字段中。

对 CEL 程序的输入是各种 “变量”。包含 CEL 的每个 Kubernetes API 字段都在 API 文档中声明了字段可使用哪些变量。例如,在 CustomResourceDefinition 的 x-kubernetes-validations[i].rules 字段中,selfoldSelf 变量可用, 并且分别指代要由 CEL 表达式验证的自定义资源数据的前一个状态和当前状态。 其他 Kubernetes API 字段可能声明不同的变量。请查阅 API 字段的 API 文档以了解该字段可使用哪些变量。

CEL 表达式示例:

CEL 表达式例子和每个表达式的用途
规则用途
self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas验证定义副本的三个字段被正确排序
‘Available’ in self.stateCounts验证映射中存在主键为 ‘Available’ 的条目
(self.list1.size() == 0) != (self.list2.size() == 0)验证两个列表中有一个非空,但不是两个都非空
self.envars.filter(e, e.name = ‘MY_ENV’).all(e, e.value.matches(‘^[a-zA-Z]*$’)验证 listMap 条目的 ‘value’ 字段,其主键字段 ‘name’ 是 ‘MY_ENV’
has(self.expired) && self.created + self.ttl < self.expired验证 ‘expired’ 日期在 ‘create’ 日期加上 ‘ttl’ 持续时间之后
self.health.startsWith(‘ok’)验证 ‘health’ 字符串字段具有前缀 ‘ok’
self.widgets.exists(w, w.key == ‘x’ && w.foo < 10)验证具有键 ‘x’ 的 listMap 项的 ‘foo’ 属性小于 10
type(self) == string ? self == ‘99%’ : self == 42验证 int-or-string 字段是否同时具备 int 和 string 的属性
self.metadata.name == ‘singleton’验证某对象的名称与特定的值匹配(使其成为一个特例)
self.set1.all(e, !(e in self.set2))验证两个 listSet 不相交
self.names.size() == self.details.size() && self.names.all(n, n in self.details)验证 ‘details’ 映射是由 ‘names’ listSet 中的各项键入的

CEL 选项、语言特性和库

CEL 配置了以下选项、库和语言特性,这些特性是在所列的 Kubernetes 版本中引入的:

CEL 选项、库或语言特性包含的内容可用性
标准宏hasallexistsexists_onemapfilter所有 Kubernetes 版本
标准函数参见官方标准定义列表所有 Kubernetes 版本
同质聚合字面量所有 Kubernetes 版本
默认 UTC 时区所有 Kubernetes 版本
迫切验证声明所有 Kubernetes 版本
扩展字符串库,v1charAtindexOflastIndexOflowerAsciiupperAsciireplacesplitjoinsubstringtrim所有 Kubernetes 版本
Kubernetes 列表库参见 Kubernetes 列表库所有 Kubernetes 版本
Kubernetes 正则表达式库参见 Kubernetes 正则表达式库所有 Kubernetes 版本
[Kubernetes URL 库]参见 Kubernetes URL 库所有 Kubernetes 版本
Kubernetes 鉴权组件库参见 Kubernetes 鉴权组件库所有 Kubernetes 版本
Kubernetes 数量库参见 Kubernetes 数量库Kubernetes v1.29+
CEL 可选类型参见 CEL 可选类型Kubernetes v1.29+
CEL CrossTypeNumericComparisons参见 CEL CrossTypeNumericComparisonsKubernetes v1.29+

CEL 函数、特性和语言设置支持 Kubernetes 控制平面回滚。 例如,CEL 可选值(Optional Values) 是在 Kubernetes 1.29 引入的,因此只有该版本或更新的 API 服务器才会接受使用 CEL Optional Values 的 CEL 表达式的写入请求。 但是,当集群回滚到 Kubernetes 1.28 时,已经存储在 API 资源中的使用了 “CEL Optional Values” 的 CEL 表达式将继续正确评估。

Kubernetes CEL 库

除了 CEL 社区库之外,Kubernetes 还包括在 Kubernetes 中使用 CEL 时所有可用的 CEL 库。

Kubernetes 列表库

列表库包括 indexOflastIndexOf,这两个函数的功能类似于同名的字符串函数。 这些函数返回提供的元素在列表中的第一个或最后一个位置索引。

列表库还包括 minmaxsumsum 可以用于所有数字类型以及持续时间类型。 minmax 可用于所有可比较的类型。

isSorted 也作为一个便捷的函数提供,并且支持所有可比较的类型。

例如:

使用列表库函数的 CEL 表达式例子
CEL 表达式用途
names.isSorted()验证名称列表是否按字母顺序排列
items.map(x, x.weight).sum() == 1.0验证对象列表的 “weight” 总和为 1.0
lowPriorities.map(x, x.priority).max() < highPriorities.map(x, x.priority).min()验证两组优先级不重叠
names.indexOf(‘should-be-first’) == 1如果是特定值,则使用列表中的第一个名称

更多信息请查阅 Go 文档: Kubernetes 列表库

Kubernetes 正则表达式库

除了 CEL 标准库提供的 matches 函数外,正则表达式库还提供了 findfindAll, 使得更多种类的正则表达式运算成为可能。

例如:

使用正则表达式库函数的 CEL 表达式例子
CEL 表达式用途
“abc 123”.find(‘[0-9]‘)找到字符串中的第一个数字
“1, 2, 3, 4”.findAll(‘[0-9]‘).map(x, int(x)).sum() < 100验证字符串中的数字之和小于 100

更多信息请查阅 Go 文档: Kubernetes 正则表达式库

Kubernetes URL 库

为了更轻松、更安全地处理 URL,添加了以下函数:

  • isURL(string) 按照 Go 的 net/url 检查字符串是否是一个有效的 URL。该字符串必须是一个绝对 URL。
  • url(string) URL 将字符串转换为 URL,如果字符串不是一个有效的 URL,则返回错误。

一旦通过 url 函数解析,所得到的 URL 对象就具有 getSchemegetHostgetHostnamegetPortgetEscapedPathgetQuery 访问器。

例如:

使用 URL 库函数的 CEL 表达式例子
CEL 表达式用途
url(‘https://example.com:80/').getHost()获取 URL 的 ‘example.com:80’ 主机部分
url(‘https://example.com/path with spaces/‘).getEscapedPath()返回 ‘/path%20with%20spaces/‘

更多信息请查阅 Go 文档: Kubernetes URL 库

Kubernetes 鉴权组件库

在 API 中使用 CEL 表达式,可以使用类型为 Authorizer 的变量, 这个鉴权组件可用于对请求的主体(已认证用户)执行鉴权检查。

API 资源检查的过程如下:

  1. 指定要检查的组和资源:Authorizer.group(string).resource(string) ResourceCheck
  2. 可以调用以下任意组合的构建器函数(Builder Function),以进一步缩小鉴权检查范围。 注意这些函数将返回接收者的类型,并且可以串接起来:
    • ResourceCheck.subresource(string) ResourceCheck
    • ResourceCheck.namespace(string) ResourceCheck
    • ResourceCheck.name(string) ResourceCheck
  3. 调用 ResourceCheck.check(verb string) Decision 来执行鉴权检查。
  4. 调用 allowed() boolreason() string 来查验鉴权检查的结果。

对非资源访问的鉴权过程如下:

  1. 仅指定路径:Authorizer.path(string) PathCheck
  2. 调用 PathCheck.check(httpVerb string) Decision 来执行鉴权检查。
  3. 调用 allowed() boolreason() string 来查验鉴权检查的结果。

对于服务账号执行鉴权检查的方式:

  • Authorizer.serviceAccount(namespace string, name string) Authorizer
使用 URL 库函数的 CEL 表达式示例
CEL 表达式用途
authorizer.group(‘’).resource(‘pods’).namespace(‘default’).check(‘create’).allowed()如果主体(用户或服务账号)被允许在 default 名字空间中创建 Pod,返回 true。
authorizer.path(‘/healthz’).check(‘get’).allowed()检查主体(用户或服务账号)是否有权限向 /healthz API 路径发出 HTTP GET 请求。
authorizer.serviceAccount(‘default’, ‘myserviceaccount’).resource(‘deployments’).check(‘delete’).allowed()检查服务账号是否有权限删除 Deployment。

更多信息请参阅 Go 文档: Kubernetes Authz library

Kubernetes 数量库

Kubernetes 1.28 添加了对数量字符串(例如 1.5G、512k、20Mi)的操作支持。

  • isQuantity(string) 根据 Kubernetes 的 resource.Quantity, 检查字符串是否是有效的 Quantity。
  • quantity(string) Quantity 将字符串转换为 Quantity,如果字符串不是有效的数量,则会报错。

一旦通过 quantity 函数解析,得到的 Quantity 对象将具有以下成员函数库:

Quantity 的可用成员函数
成员函数CEL 返回值描述
isInteger()bool仅当 asInteger 可以被安全调用且不出错时,才返回 true
asInteger()int将当前值作为 int64 的表示返回,如果转换会导致溢出或精度丢失,则会报错
asApproximateFloat()float返回数量的 float64 表示,可能会丢失精度。如果数量的值超出了 float64 的范围,则返回 +Inf/-Inf
sign()int如果数量为正,则返回 1;如果数量为负,则返回 -1;如果数量为零,则返回 0
add(<Quantity>)Quantity返回两个数量的和
add(<int>)Quantity返回数量和整数的和
sub(<Quantity>)Quantity返回两个数量的差
sub(<int>)Quantity返回数量减去整数的差
isLessThan(<Quantity>)bool如果接收值小于操作数,则返回 true
isGreaterThan(<Quantity>)bool如果接收值大于操作数,则返回 true
compareTo(<Quantity>)int将接收值与操作数进行比较,如果它们相等,则返回 0;如果接收值大于操作数,则返回 1;如果接收值小于操作数,则返回 -1

例如:

使用 URL 库函数的 CEL 表达式示例
CEL 表达式用途
quantity(“500000G”).isInteger()测试转换为整数是否会报错
quantity(“50k”).asInteger()精确转换为整数
quantity(“9999999999999999999999999999999999999G”).asApproximateFloat()松散转换为浮点数
quantity(“50k”).add(“20k”)两个数量相加
quantity(“50k”).sub(20000)从数量中减去整数
quantity(“50k”).add(20).sub(quantity(“100k”)).sub(-50000)链式相加和减去整数和数量
quantity(“200M”).compareTo(quantity(“0.2G”))比较两个数量
quantity(“150Mi”).isGreaterThan(quantity(“100Mi”))测试数量是否大于接收值
quantity(“50M”).isLessThan(quantity(“100M”))测试数量是否小于接收值

类型检查

CEL 是一种逐渐类型化的语言

一些 Kubernetes API 字段包含完全经过类型检查的 CEL 表达式。 例如,CustomResourceDefinition 验证规则就是完全经过类型检查的。

一些 Kubernetes API 字段包含部分经过类型检查的 CEL 表达式。 部分经过类型检查的表达式是指一些变量是静态类型,而另一些变量是动态类型的表达式。 例如在 ValidatingAdmissionPolicy 的 CEL 表达式中,request 变量是有类型的,但 object 变量是动态类型的。 因此,包含 request.namex 的表达式将无法通过类型检查,因为 namex 字段未定义。 然而,即使对于 object 所引用的资源种类没有定义 namex 字段, object.namex 也会通过类型检查,因为 object 是动态类型。

在 CEL 中,has() 宏可用于检查动态类型变量的字段是否可访问,然后再尝试访问该字段的值。 例如:

  1. has(object.namex) ? object.namex == 'special' : request.name == 'special'

类型系统集成

表格显示了 OpenAPIv3 类型和 CEL 类型之间的关系
OpenAPIv3 类型CEL 类型
设置了 properties 的 ‘object’object / “message type” (type(<object>) 评估为 selfType<uniqueNumber>.path.to.object.from.self
设置了 AdditionalProperties 的 ‘object’map
设置了 x-kubernetes-embedded-type 的 ‘object’object / “message type”,’apiVersion’、’kind’、’metadata.name’ 和 ‘metadata.generateName’ 被隐式包含在模式中
设置了 x-kubernetes-preserve-unknown-fields 的 ‘object’object / “message type”,CEL 表达式中不可访问的未知字段
x-kubernetes-int-or-stringint 或 string 的并集,self.intOrString < 100 || self.intOrString == ‘50%’ 对于 50“50%”都评估为 true
‘array’list
设置了 x-kubernetes-list-type=map 的 ‘array’list,具有基于 Equality 和唯一键保证的 map
设置了 x-kubernetes-list-type=set 的 ‘array’list,具有基于 Equality 和唯一条目保证的 set
‘boolean’boolean
‘number’ (所有格式)double
‘integer’ (所有格式)int (64)
非等价 uint (64)
‘null’null_type
‘string’string
设置了 format=byte 的 ‘string’(以 base64 编码)bytes
设置了 format=date 的 ‘string’timestamp (google.protobuf.Timestamp)
设置了 format=datetime 的 ‘string’timestamp (google.protobuf.Timestamp)
设置了 format=duration 的 ‘string’duration (google.protobuf.Duration)

另见:CEL 类型OpenAPI 类型Kubernetes 结构化模式

x-kubernetes-list-typesetmap 的数组进行相等比较时会忽略元素顺序。 例如,如果这些数组代表 Kubernetes 的 set 值,则 [1, 2] == [2, 1]

使用 x-kubernetes-list-type 的数组进行串接时,使用 list 类型的语义:

  • setX + Y 执行并集操作,保留 X 中所有元素的数组位置, 将 Y 中非交集元素追加到 X 中,保留它们的部分顺序。
  • mapX + Y 执行合并操作,保留 X 中所有键的数组位置, 但是当 XY 的键集相交时,将 Y 中的值覆盖 X 中的值。 将 Y 中非交集键的元素附加到 X 中,保留它们的部分顺序。

转义

仅形如 [a-zA-Z_.-/][a-zA-Z0-9_.-/]* 的 Kubernetes 资源属性名可以从 CEL 中访问。 当在表达式中访问可访问的属性名时,会根据以下规则进行转义:

CEL 标识符转义规则表
转义序列等价的属性名
underscores
dot.
dash-
slash/
{keyword}__CEL 保留的 关键字

当你需要转义 CEL 的任一 保留的 关键字时,你需要使用下划线转义来完全匹配属性名 (例如,sprint 这个单词中的 int 不会被转义,也不需要被转义)。

转义示例:

转义的 CEL 标识符例子
属性名称带有转义的属性名称的规则
namespaceself.namespace > 0
x-propself.xdashprop > 0
redactdself.redactunderscores__d > 0
stringself.startsWith(‘kube’)

资源约束

CEL 不是图灵完备的,提供了多种生产安全控制手段来限制执行时间。 CEL 的资源约束特性提供了关于表达式复杂性的反馈,并帮助保护 API 服务器免受过度的资源消耗。 CEL 的资源约束特性用于防止 CEL 评估消耗过多的 API 服务器资源。

资源约束特性的一个关键要素是 CEL 定义的成本单位,它是一种跟踪 CPU 利用率的方式。 成本单位独立于系统负载和硬件。成本单位也是确定性的;对于任何给定的 CEL 表达式和输入数据, 由 CEL 解释器评估表达式将始终产生相同的成本。

CEL 的许多核心运算具有固定成本。例如比较(例如 <)这类最简单的运算成本为 1。 有些运算具有更高的固定成本,例如列表字面声明具有 40 个成本单位的固定基础成本。

调用本地代码实现的函数时,基于运算的时间复杂度估算其成本。 举例而言:matchfind 这类使用正则表达式的运算使用 length(regexString)*length(inputString) 的近似成本进行估算。 这个近似的成本反映了 Go 的 RE2 实现的最坏情况的时间复杂度。

运行时成本预算

所有由 Kubernetes 评估的 CEL 表达式都受到运行时成本预算的限制。 运行时成本预算是通过在解释 CEL 表达式时增加成本单元计数器来计算实际 CPU 利用率的估算值。 如果 CEL 解释器执行的指令太多,将超出运行时成本预算,表达式的执行将停止,并将出现错误。

一些 Kubernetes 资源定义了额外的运行时成本预算,用于限制多个表达式的执行。 如果所有表达式的成本总和超过预算,表达式的执行将停止,并将出现错误。 例如,自定义资源的验证具有针对验证自定义资源所评估的所有 验证规则每个验证 运行时成本预算。

估算的成本限制

对于某些 Kubernetes 资源,API 服务器还可能检查 CEL 表达式的最坏情况估计运行时间是否过于昂贵而无法执行。 如果是,则 API 服务器会拒绝包含 CEL 表达式的创建或更新操作,以防止 CEL 表达式被写入 API 资源。 此特性提供了更强的保证,即写入 API 资源的 CEL 表达式将在运行时进行评估,而不会超过运行时成本预算。