什么是CEL?
CEL 是一种非图灵完备的表达式言语
,旨在快速、可移植且履行安全。CEL 能够单独运用,也能够嵌入到其他的产品中。
CEL被规划成一种能够安全地履行用户代码的言语。尽管盲目调用用户的python代码是风险的,但您能够安全地履行用户的CEL代码。由于CEL防止了会下降其功用的行为,因而它的评价安全性在纳秒到微秒之间;它十分合适功用要害型应用程序。eval()
CEL 核算表达式,类似于单行函数或 lambda
表达式。尽管 CEL 一般用于布尔决议计划,但它也可用于结构更杂乱的目标,如 JSON
或 protobuf
音讯。
要害概念
应用
CEL是通用的,已用于各种应用程序,从路由RPC到界说安全策略。CEL是可扩展的,与应用程序无关,并针对一次编译、屡次评价的工作流进行了优化。 许多服务和应用程序评价声明性装备。例如,依据人物的拜访操控(RBAC)是一种声明性装备,它在给定人物和一组用户的情况下生成拜访决议计划。假如声明性装备是80%的用例,那么当用户需求更强的表达能力时,CEL是一个有用的工具,能够将剩下的20%取整。
编译
表达式是针对环境编译的。编译过程生成protobuf
方式的笼统语法树(AST)。编译后的表达式一般会存储起来以备将来运用,从而使求值尽可能快。单个编译表达式能够运用许多不同的输入进行求值。
笼统语法树:
在核算机科学中,笼统语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种笼统表明。它以树状的方式表现编程言语的语法结构,树上的每个节点都表明源代码中的一种结构。之所以说语法是“笼统”的,是由于这里的语法并不会表明出实在语法中出现的每个细节。比方,嵌套括号被隐含在树的结构中,并没有以节点的方式出现;而类似于 if-condition-then
这样的条件跳转语句,能够运用带有三个分支的节点来表明。
和笼统语法树相对的是详细语法树(一般称作剖析树)。一般的,在源代码的翻译和编译过程中,语法剖析器创建出剖析树,然后从剖析树生成AST。一旦AST被创建出来,在后续的处理过程中,比方语义剖析阶段,会添加一些信息。
表达式
用户界说表达式;服务和应用程序界说了它运转的环境。函数签名声明输入,并在CEL
表达式之外编写。CEL
可用的函数库是自动导入的。
在下面的示例中,表达式选用一个恳求目标,该恳求包含一个声明令牌。该表达式回来一个布尔值,指示声明令牌是否依然有效。
// 通过查看“exp”声明来查看JSON Web令牌是否已过期。
//
// Args:
// claims - authentication claims.
// now - timestamp indicating the current system time.
// 假如令牌已过期,则回来:true
//
timestamp(claims["exp"]) < now
环境
环境是由服务界说的。嵌入CEL
的服务和应用程序声明表达式环境。环境是能够在表达式中运用的变量和函数的集合。
CEL
类型查看器运用依据原型的声明来确保表达式中的一切标识符和函数引证都得到了正确的声明和运用。
解析表达式的三个阶段
处理表达式有三个阶段:解析
、查看
和求值
。CEL
最常见的形式是操控平面在装备时解析和查看表达式,并存储AST
。
在运转时,数据平面会重复检索和评价AST。CEL
针对运转时功率进行了优化,但不应在延迟要害的代码途径中进行解析和查看。
CEL运用ANTLR lexer/parser
语法从人类可读的表达式解析为笼统语法树。解析阶段发出一个依据原型的笼统语法树,其间AST中的每个Expr
节点都包含一个整数id,用于索引解析和查看期间生成的元数据。解析过程中生成的syntax.proto
忠诚地表明晰以字符串方式键入的内容的笼统表明。
一旦解析了表达式,就能够依据环境对其进行查看,以确保表达式中的一切变量和函数标识符都已声明并正确运用。类型查看器生成一个checked.proto
,其间包含类型、变量和函数解析元数据,能够显著进步评价功率。
评价CEL需求3件事:
-
任何自界说扩展的函数绑定
-
变量绑定
-
AST评价
函数和变量绑定应该与用于编译AST的绑定相匹配。这些输入中的任何一个都能够在多个评价中重复运用,例如在多组变量绑定中评价AST,或者在多个AST中运用相同的变量,或者在进程的整个生命周期中运用函数绑定(常见情况)。
CEL 合适您的项目吗?
由于 CEL 以纳秒到微秒为单位评价 AST 的表达式,因而 CEL 的理想用例是具有功用要害途径的应用程序。不应在要害途径中将 CEL 代码编译到 AST 中;理想的应用程序是常常履行装备且修正频率相对较低的应用程序。
例如,对服务的每个 HTTP 恳求履行安全策略是 CEL 的理想用例,由于安全策略很少更改,并且 CEL 对响应时刻的影响能够忽略不计。在这种情况下,CEL 将回来一个布尔值(无论是否答应该恳求),但它可能会回来更杂乱的音讯。
在 golang 中怎样运用 CEL
一下代码咱们运用 golang 的 cel 包 github.com/google/cel-go/cel
运用 cel 进行字符串拼接:
字符串 str = "Hello world! I'm " name "."
中存在变量 name
,在咱们的程序中,这个 name 是一个变量,需求在程序中替换为详细的值,比方:张三
过程如下:
1、先初始化 env,也便是咱们上面说的需求装备履行的环境
2、在环境中绑定变量 name 以及类型
3、env.Compile(str)
便是做了咱们上面所说的编译并解析表达式,回来 str
所对应的ast
4、程序求值。咱们将 name 需求的详细值传到 program 中履行 values := map[string]interface{}{"name": "CEL"}
5、获取到最后的成果
func Test_exprReplacement(t *testing.T) {
var str = `"Hello world! I'm " name "."`
env, err := cel.NewEnv(
cel.Variable("name", cel.StringType), // 参数类型绑定
)
if err != nil {
t.Fatal(err)
}
ast, iss := env.Compile(str) // 编译,校验,履行 str
if iss.Err() != nil {
t.Fatal(iss.Err())
}
program, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
// 初始化 name 变量的值
values := map[string]interface{}{"name": "CEL"}
// 传给内部程序并回来履行的成果
out, detail, err := program.Eval(values)
if err != nil {
t.Fatal(err)
}
fmt.Println(detail)
fmt.Println(out)
}
测试成果:
Running tool: /usr/local/go/bin/go test -timeout 10s -run ^Test_exprReplacement$ github.com/demo007x/goexpr -v
=== RUN Test_exprReplacement
Hello world! I'm CEL.
<nil>
--- PASS: Test_exprReplacement (0.00s)
PASS
ok github.com/demo007x/goexpr 0.010s
核算一个表达式的逻辑成果:
回来表达式 var str = 100 200 >= 300
的履行成果:
履行过程跟上面的相同,这里就省略了。
func Test_LogicExpr1(t *testing.T) {
var str = `100 200 >= 300`
env, err := cel.NewEnv()
if err != nil {
t.Fatal(err)
}
ast, iss := env.Compile(str)
if iss.Err() != nil {
t.Fatal(iss.Err())
}
prog, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
out, detail, err := prog.Eval(map[string]interface{}{})
if err != nil {
t.Fatal(err)
}
fmt.Println(out)
fmt.Println(detail)
}
输出成果:
Running tool: /usr/local/go/bin/go test -timeout 10s -run ^Test_LogicExpr1$ github.com/demo007x/goexpr -v
=== RUN Test_LogicExpr1
true
<nil>
--- PASS: Test_LogicExpr1 (0.00s)
PASS
ok github.com/demo007x/goexpr 0.010s
履行一个有函数的表达式会是咋样的呢?
先定一一个函数:
type Integer interface {
~int | ~int8 | ~int16 | ~int32 | ~int64
}
// 求一切传入参数的和
func Add[T Integer](param1, param2 T, ints ...T) T {
sum := param1 param2
for _, v := range ints {
sum = v
}
return sum
}
履行有函数的表达式:6 == Add(age1, age2, age3)
func Test_add(t *testing.T) {
str := `6 == Add(age1, age2, age3)`
env, _ := cel.NewEnv(
cel.Variable("age1", cel.IntType),
cel.Variable("age2", cel.IntType),
cel.Variable("age3", cel.IntType),
cel.Function("Add", cel.Overload(
"Add",
[]*cel.Type{cel.IntType, cel.IntType, cel.IntType},
cel.IntType,
cel.FunctionBinding(func(vals ...ref.Val) ref.Val {
var xx []int64
for _, v := range vals {
xx = append(xx, v.Value().(int64))
}
return types.Int(Add[int64](xx[0], xx[1], xx[2:]...))
}),
)),
)
ast, iss := env.Compile(str)
if iss.Err() != nil {
t.Fatal(iss.Err())
}
prog, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
val, detail, err := prog.Eval(map[string]interface{}{"age1": 1, "age2": 2, "age3": 3})
if err != nil {
t.Fatal(err)
}
fmt.Print(detail)
fmt.Println(val)
}
履行过程跟上面的相同:
- 首先需求声明传入函数参数的类型
age1
,age2
age3
- 声明
Add
函数,并声明函数的参数类型,以及回来成果 -
env.Compile(str)
字符串的编译、校验、履行,回来ast
-
prog.Eval
传入参数履行并回来成果
履行成果:
Running tool: /usr/local/go/bin/go test -timeout 10s -run ^Test_add$ github.com/demo007x/goexpr -v
=== RUN Test_add
<nil>true
--- PASS: Test_add (0.00s)
PASS
ok github.com/demo007x/goexpr 0.014s
场景:
一般情况下项目中都很少会去履行一个 CEL 的表达式,咱们都会依照固定好的逻辑去编写代码。
现在低代码盛行的时代,项目中的功用都能够自界说,这样一个功用就需求足够的灵活,将一些程序履行的逻辑交给用户去操控。
比方咱们现在的项目中:一个杂乱的流程详细要怎样履行,需求谁去审批,需求在那一步的时分越过等这些都是能够灵活装备每一个节点的逻辑条件,满意条件的就去履行节点流程,不满意的就去越过履行下一个流程处理。这时分装备条件就能够运用 cel 的表达式去装备,通过表单中的多个字段的值组成一个 bool 条件。
比方请假流程:请假天数 <= 3
流程需求走到部分领导审批, 请假天数 > 3
流程需求部分领导审批完成后持续流转到部分领导的上级审批。
这样一个条件中请假天数
是一个表单中某个字段的值,这样装备条件就很灵活。这便是 cel
在咱们项目中实际运用的比如的一部分。
cel 在 k8s中的运用
CEL 的每个 Kubernetes API 字段都在 API 文档中声明晰字段可运用哪些变量。例如,在 CustomResourceDefinitions 的 x-kubernetes-validations[i].rules
字段中,self
和 oldSelf
变量可用, 并且分别指代要由 CEL 表达式验证的自界说资源数据的前一个状况和当前状况。 其他 Kubernetes API 字段可能声明不同的变量。请查阅 API 字段的 API 文档以了解该字段可运用哪些变量。
K8S 中 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 的履行流程都是固定的,不管是简略的字符串还是内嵌函数的履行。那是不是咱们就能够依据 go-cel
的功用来封装一次,将相同的逻辑代码抽取出来。这样运用的时分就不需求每履行一个 cel 的表达式就去写一遍完成了呢?
咱们剖析下相同点和不同点:
相同点:
-
cel.NewEnv
初始化 env -
env.Compile
检测,编译 cel 表达式 -
env.Program
ast 履行 -
prog.Eval
履行并回来成果
不同的当地便是需求明确的标明变量的类型,以及回来值(函数),并且参数个数不能多,也不能少,prog.Eval
传入的实参只能多不能少,少了就会报错。
假如咱们将需求传递的参数以及类型提前解析出来并动态的传入以上几个过程中,那咱们的封装是有意义的。代码量也削减很多。哪怎样抽取动态参数以及类型呢?
ps: 掘友们有没有好的方法能够一起谈论