在golang中使用cel的用法詳解
什么是CEL?
CEL 是一種非圖靈完備的表達(dá)式語言
,旨在快速、可移植且執(zhí)行安全。CEL 可以單獨(dú)使用,也可以嵌入到其他的產(chǎn)品中。
CEL被設(shè)計(jì)成一種可以安全地執(zhí)行用戶代碼的語言。雖然盲目調(diào)用用戶的python代碼是危險(xiǎn)的,但您可以安全地執(zhí)行用戶的CEL代碼。由于CEL防止了會(huì)降低其性能的行為,因此它的評估安全性在納秒到微秒之間;它非常適合性能關(guān)鍵型應(yīng)用程序。eval()
CEL 計(jì)算表達(dá)式,類似于單行函數(shù)或 lambda
表達(dá)式。雖然 CEL 通常用于布爾決策,但它也可用于構(gòu)造更復(fù)雜的對象,如 JSON
或 protobuf
消息。
關(guān)鍵概念
應(yīng)用
CEL是通用的,已用于各種應(yīng)用程序,從路由RPC到定義安全策略。CEL是可擴(kuò)展的,與應(yīng)用程序無關(guān),并針對一次編譯、多次評估的工作流進(jìn)行了優(yōu)化。 許多服務(wù)和應(yīng)用程序評估聲明性配置。例如,基于角色的訪問控制(RBAC)是一種聲明性配置,它在給定角色和一組用戶的情況下生成訪問決策。如果聲明性配置是80%的用例,那么當(dāng)用戶需要更強(qiáng)的表達(dá)能力時(shí),CEL是一個(gè)有用的工具,可以將剩余的20%取整。
編譯
表達(dá)式是針對環(huán)境編譯的。編譯步驟生成protobuf
形式的抽象語法樹(AST)。編譯后的表達(dá)式通常會(huì)存儲(chǔ)起來以備將來使用,從而使求值盡可能快。單個(gè)編譯表達(dá)式可以使用許多不同的輸入進(jìn)行求值。
抽象語法樹:
在計(jì)算機(jī)科學(xué)中,抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。之所以說語法是“抽象”的,是因?yàn)檫@里的語法并不會(huì)表示出真實(shí)語法中出現(xiàn)的每個(gè)細(xì)節(jié)。比如,嵌套括號被隱含在樹的結(jié)構(gòu)中,并沒有以節(jié)點(diǎn)的形式呈現(xiàn);而類似于 if-condition-then
這樣的條件跳轉(zhuǎn)語句,可以使用帶有三個(gè)分支的節(jié)點(diǎn)來表示。
和抽象語法樹相對的是具體語法樹(通常稱作分析樹)。一般的,在源代碼的翻譯和編譯過程中,語法分析器創(chuàng)建出分析樹,然后從分析樹生成AST。一旦AST被創(chuàng)建出來,在后續(xù)的處理過程中,比如語義分析階段,會(huì)添加一些信息。
表達(dá)式
用戶定義表達(dá)式;服務(wù)和應(yīng)用程序定義了它運(yùn)行的環(huán)境。函數(shù)簽名聲明輸入,并在CEL
表達(dá)式之外編寫。CEL
可用的函數(shù)庫是自動(dòng)導(dǎo)入的。 在下面的示例中,表達(dá)式采用一個(gè)請求對象,該請求包括一個(gè)聲明令牌。該表達(dá)式返回一個(gè)布爾值,指示聲明令牌是否仍然有效。
// 通過檢查“exp”聲明來檢查JSON Web令牌是否已過期。 // // Args: // claims - authentication claims. // now - timestamp indicating the current system time. // 如果令牌已過期,則返回:true // timestamp(claims["exp"]) < now
環(huán)境
環(huán)境是由服務(wù)定義的。嵌入CEL
的服務(wù)和應(yīng)用程序聲明表達(dá)式環(huán)境。環(huán)境是可以在表達(dá)式中使用的變量和函數(shù)的集合。 CEL
類型檢查器使用基于原型的聲明來確保表達(dá)式中的所有標(biāo)識符和函數(shù)引用都得到了正確的聲明和使用。
解析表達(dá)式的三個(gè)階段
處理表達(dá)式有三個(gè)階段:解析
、檢查
和求值
。CEL
最常見的模式是控制平面在配置時(shí)解析和檢查表達(dá)式,并存儲(chǔ)AST
。
在運(yùn)行時(shí),數(shù)據(jù)平面會(huì)重復(fù)檢索和評估AST。CEL
針對運(yùn)行時(shí)效率進(jìn)行了優(yōu)化,但不應(yīng)在延遲關(guān)鍵的代碼路徑中進(jìn)行解析和檢查。
CEL使用ANTLR lexer/parser
語法從人類可讀的表達(dá)式解析為抽象語法樹。解析階段發(fā)出一個(gè)基于原型的抽象語法樹,其中AST中的每個(gè)Expr
節(jié)點(diǎn)都包含一個(gè)整數(shù)id,用于索引解析和檢查期間生成的元數(shù)據(jù)。解析過程中生成的syntax.proto
忠實(shí)地表示了以字符串形式鍵入的內(nèi)容的抽象表示。
一旦解析了表達(dá)式,就可以根據(jù)環(huán)境對其進(jìn)行檢查,以確保表達(dá)式中的所有變量和函數(shù)標(biāo)識符都已聲明并正確使用。類型檢查器生成一個(gè)checked.proto
,其中包括類型、變量和函數(shù)解析元數(shù)據(jù),可以顯著提高評估效率。
評估CEL需要3件事:
任何自定義擴(kuò)展的函數(shù)綁定
變量綁定
AST評估
函數(shù)和變量綁定應(yīng)該與用于編譯AST的綁定相匹配。這些輸入中的任何一個(gè)都可以在多個(gè)評估中重復(fù)使用,例如在多組變量綁定中評估AST,或者在多個(gè)AST中使用相同的變量,或者在進(jìn)程的整個(gè)生命周期中使用函數(shù)綁定(常見情況)。
CEL 適合您的項(xiàng)目嗎?
由于 CEL 以納秒到微秒為單位評估 AST 的表達(dá)式,因此 CEL 的理想用例是具有性能關(guān)鍵路徑的應(yīng)用程序。不應(yīng)在關(guān)鍵路徑中將 CEL 代碼編譯到 AST 中;理想的應(yīng)用程序是經(jīng)常執(zhí)行配置且修改頻率相對較低的應(yīng)用程序。
例如,對服務(wù)的每個(gè) HTTP 請求執(zhí)行安全策略是 CEL 的理想用例,因?yàn)榘踩呗院苌俑模⑶?CEL 對響應(yīng)時(shí)間的影響可以忽略不計(jì)。在這種情況下,CEL 將返回一個(gè)布爾值(無論是否允許該請求),但它可能會(huì)返回更復(fù)雜的消息。
在 golang 中如何使用 CEL
一下代碼我們使用 golang 的 cel 包 github.com/google/cel-go/cel
使用 cel 進(jìn)行字符串拼接:
字符串 str = "Hello world! I'm " + name + "."
中存在變量 name
,在我們的程序中,這個(gè) name 是一個(gè)變量,需要在程序中替換為具體的值,比如:張三
步驟如下:
1、先初始化 env,也就是我們上面說的需要配置執(zhí)行的環(huán)境
2、在環(huán)境中綁定變量 name 以及類型
3、env.Compile(str)
就是做了我們上面所說的編譯并解析表達(dá)式,返回 str
所對應(yīng)的ast
4、程序求值。我們將 name 需要的具體值傳到 program 中執(zhí)行 values := map[string]interface{}{"name": "CEL"}
5、獲取到最后的結(jié)果
func Test_exprReplacement(t *testing.T) { var str = `"Hello world! I'm " + name + "."` env, err := cel.NewEnv( cel.Variable("name", cel.StringType), // 參數(shù)類型綁定 ) if err != nil { t.Fatal(err) } ast, iss := env.Compile(str) // 編譯,校驗(yàn),執(zhí)行 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"} // 傳給內(nèi)部程序并返回執(zhí)行的結(jié)果 out, detail, err := program.Eval(values) if err != nil { t.Fatal(err) } fmt.Println(detail) fmt.Println(out) }
測試結(jié)果:
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
計(jì)算一個(gè)表達(dá)式的邏輯結(jié)果:
返回表達(dá)式 var str = 100 + 200 >= 300
的執(zhí)行結(jié)果:
執(zhí)行步驟跟上面的一樣,這里就省略了。
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) }
輸出結(jié)果:
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
執(zhí)行一個(gè)有函數(shù)的表達(dá)式會(huì)是咋樣的呢?
先定一一個(gè)函數(shù):
type Integer interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } // 求所有傳入?yún)?shù)的和 func Add[T Integer](param1, param2 T, ints ...T) T { sum := param1 + param2 for _, v := range ints { sum += v } return sum }
執(zhí)行有函數(shù)的表達(dá)式: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) }
執(zhí)行步驟跟上面的一樣:
- 首先需要申明傳入函數(shù)參數(shù)的類型
age1
,age2
age3
- 申明
Add
函數(shù),并申明函數(shù)的參數(shù)類型,以及返回結(jié)果 env.Compile(str)
字符串的編譯、校驗(yàn)、執(zhí)行,返回ast
prog.Eval
傳入?yún)?shù)執(zhí)行并返回結(jié)果
執(zhí)行結(jié)果:
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
場景:
一般情況下項(xiàng)目中都很少會(huì)去執(zhí)行一個(gè) CEL 的表達(dá)式,我們都會(huì)按照固定好的邏輯去編寫代碼。
目前低代碼盛行的時(shí)代,項(xiàng)目中的功能都可以自定義,這樣一個(gè)功能就需要足夠的靈活,將一些程序執(zhí)行的邏輯交給用戶去控制。
比如我們目前的項(xiàng)目中:一個(gè)復(fù)雜的流程具體要怎么執(zhí)行,需要誰去審批,需要在那一步的時(shí)候跳過等這些都是可以靈活配置每一個(gè)節(jié)點(diǎn)的邏輯條件,滿足條件的就去執(zhí)行節(jié)點(diǎn)流程,不滿足的就去跳過執(zhí)行下一個(gè)流程處理。這時(shí)候配置條件就可以使用 cel 的表達(dá)式去配置,通過表單中的多個(gè)字段的值組成一個(gè) bool 條件。
比如請假流程:請假天數(shù) <= 3
流程需要走到部門領(lǐng)導(dǎo)審批, 請假天數(shù) > 3
流程需要部門領(lǐng)導(dǎo)審批完成后繼續(xù)流轉(zhuǎn)到部門領(lǐng)導(dǎo)的上級審批。
這樣一個(gè)條件中請假天數(shù)
是一個(gè)表單中某個(gè)字段的值,這樣配置條件就很靈活。這就是 cel
在我們項(xiàng)目中實(shí)際使用的例子的一部分。
cel 在 k8s中的使用
CEL 的每個(gè) Kubernetes API 字段都在 API 文檔中聲明了字段可使用哪些變量。例如,在 CustomResourceDefinitions 的 x-kubernetes-validations[i].rules
字段中,self
和 oldSelf
變量可用, 并且分別指代要由 CEL 表達(dá)式驗(yàn)證的自定義資源數(shù)據(jù)的前一個(gè)狀態(tài)和當(dāng)前狀態(tài)。 其他 Kubernetes API 字段可能聲明不同的變量。請查閱 API 字段的 API 文檔以了解該字段可使用哪些變量。
K8S 中 CEL 表達(dá)式示例:
規(guī)則 | 用途 |
---|---|
self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas | 驗(yàn)證定義副本的三個(gè)字段被正確排序 |
'Available' in self.stateCounts | 驗(yàn)證映射中存在主鍵為 'Available' 的條目 |
(self.list1.size() == 0) != (self.list2.size() == 0) | 驗(yàn)證兩個(gè)列表中有一個(gè)非空,但不是兩個(gè)都非空 |
self.envars.filter(e, e.name = 'MY_ENV').all(e, e.value.matches('^[a-zA-Z]*$') | 驗(yàn)證 listMap 條目的 'value' 字段,其主鍵字段 'name' 是 'MY_ENV' |
has(self.expired) && self.created + self.ttl < self.expired | 驗(yàn)證 'expired' 日期在 'create' 日期加上 'ttl' 持續(xù)時(shí)間之后 |
self.health.startsWith('ok') | 驗(yàn)證 'health' 字符串字段具有前綴 'ok' |
self.widgets.exists(w, w.key == 'x' && w.foo < 10) | 驗(yàn)證具有鍵 'x' 的 listMap 項(xiàng)的 'foo' 屬性小于 10 |
type(self) == string ? self == '99%' : self == 42 | 驗(yàn)證 int-or-string 字段是否同時(shí)具備 int 和 string 的屬性 |
self.metadata.name == 'singleton' | 驗(yàn)證某對象的名稱與特定的值匹配(使其成為一個(gè)特例) |
self.set1.all(e, !(e in self.set2)) | 驗(yàn)證兩個(gè) listSet 不相交 |
self.names.size() == self.details.size() && self.names.all(n, n in self.details) | 驗(yàn)證 'details' 映射是由 'names' listSet 中的各項(xiàng)鍵入的 |
思考
cel 的執(zhí)行流程都是固定的,不管是簡單的字符串還是內(nèi)嵌函數(shù)的執(zhí)行。那是不是我們就可以基于 go-cel
的功能來封裝一次,將相同的邏輯代碼抽取出來。這樣使用的時(shí)候就不需要每執(zhí)行一個(gè) cel 的表達(dá)式就去寫一遍實(shí)現(xiàn)了呢?
我們分析下相同點(diǎn)和不同點(diǎn):
相同點(diǎn):
cel.NewEnv
初始化 envenv.Compile
檢測,編譯 cel 表達(dá)式env.Program
ast 執(zhí)行prog.Eval
執(zhí)行并返回結(jié)果
不同的地方就是需要明確的標(biāo)明變量的類型,以及返回值(函數(shù)),而且參數(shù)個(gè)數(shù)不能多,也不能少,prog.Eval
傳入的實(shí)參只能多不能少,少了就會(huì)報(bào)錯(cuò)。
如果我們將需要傳遞的參數(shù)以及類型提前解析出來并動(dòng)態(tài)的傳入以上幾個(gè)步驟中,那我們的封裝是有意義的。代碼量也減少很多。哪如何抽取動(dòng)態(tài)參數(shù)以及類型呢?
以上就是在golang中使用cel的用法詳解的詳細(xì)內(nèi)容,更多關(guān)于golang中使用cel的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang?中實(shí)現(xiàn)?Set的思路詳解
本文介紹了Go中兩種set的實(shí)現(xiàn)原理,并在此基礎(chǔ)介紹了對應(yīng)于它們的兩個(gè)包簡單使用,本文介紹的非常詳細(xì),需要的朋友參考下吧2024-01-01Go語言中slice作為參數(shù)傳遞時(shí)遇到的一些“坑”
這篇文章主要給大家介紹了關(guān)于Go語言中slice作為參數(shù)傳遞時(shí)遇到的一些“坑”,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-03-03Golang語言JSON解碼函數(shù)Unmarshal的使用
本文主要介紹了Golang語言JSON解碼函數(shù)Unmarshal的使用,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01一鍵定位Golang線上服務(wù)內(nèi)存泄露的秘籍
內(nèi)存泄露?別讓它拖垮你的Golang線上服務(wù)!快速掌握Go語言服務(wù)內(nèi)存泄漏排查秘籍,從此問題無處遁形,一文讀懂如何精準(zhǔn)定位與有效解決Golang應(yīng)用中的內(nèi)存問題,立即閱讀,讓性能飛升!2024-01-01go語言 xorm框架 postgresql 的用法及詳細(xì)注解
這篇文章主要介紹了go語言 xorm框架 postgresql 的用法及詳細(xì)注解,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12