1. Pengantar
Apa itu CEL?
CEL adalah bahasa ekspresi non-Turing lengkap yang dirancang agar cepat, portabel, dan aman untuk dieksekusi. CEL dapat digunakan sendiri, atau disematkan ke dalam produk yang lebih besar.
CEL dirancang sebagai bahasa yang aman untuk mengeksekusi kode pengguna. Meskipun berbahaya untuk memanggil eval() secara membabi buta pada kode python pengguna, Anda dapat mengeksekusi kode CEL pengguna dengan aman. Selain itu, karena CEL mencegah perilaku yang akan membuatnya kurang berperforma, CEL dievaluasi dengan aman dalam urutan nanodetik hingga mikrodetik; CEL ideal untuk aplikasi yang sangat penting performanya.
CEL mengevaluasi ekspresi, yang mirip dengan fungsi satu baris atau ekspresi lambda. Meskipun umumnya digunakan untuk keputusan boolean, CEL juga dapat digunakan untuk membuat objek yang lebih kompleks seperti pesan JSON atau protobuf.
Apakah CEL tepat untuk project Anda?
Karena CEL mengevaluasi ekspresi dari AST dalam nanodetik hingga mikrodetik, kasus penggunaan yang ideal untuk CEL adalah aplikasi dengan jalur yang penting untuk performa. Kompilasi kode CEL ke dalam AST tidak boleh dilakukan di jalur penting; aplikasi yang ideal adalah aplikasi yang konfigurasinya sering dieksekusi dan relatif jarang dimodifikasi.
Misalnya, menjalankan kebijakan keamanan dengan setiap permintaan HTTP ke layanan adalah kasus penggunaan yang ideal untuk CEL karena kebijakan keamanan jarang berubah, dan CEL akan memberikan dampak yang dapat diabaikan pada waktu respons. Dalam hal ini, CEL menampilkan nilai boolean, apakah permintaan harus diizinkan atau tidak, tetapi dapat menampilkan pesan yang lebih kompleks.
Apa yang dibahas dalam Codelab ini?
Langkah pertama codelab ini membahas motivasi penggunaan CEL dan Konsep Intinya. Bagian lainnya dikhususkan untuk Latihan pengodean yang mencakup kasus penggunaan umum. Untuk melihat bahasa, semantik, dan fitur secara lebih mendalam, lihat Definisi Bahasa CEL di GitHub dan Dokumen Go CEL.
Codelab ini ditujukan bagi developer yang ingin mempelajari CEL untuk menggunakan layanan yang sudah mendukung CEL. Codelab ini tidak membahas cara mengintegrasikan CEL ke dalam project Anda sendiri.
Yang akan Anda pelajari
- Konsep inti dari CEL
- Hello, World: Menggunakan CEL untuk mengevaluasi String
- Membuat variabel
- Memahami short-circuiting CEL dalam operasi AND/OR Logis
- Cara menggunakan CEL untuk membuat JSON
- Cara menggunakan CEL untuk membuat Protobuf
- Membuat Makro
- Cara menyesuaikan ekspresi CEL
Yang Anda butuhkan
Prasyarat
Codelab ini dibangun berdasarkan pemahaman dasar tentang Protocol Buffers dan Go Lang.
Jika Anda belum memahami Protocol Buffers, latihan pertama akan memberi Anda gambaran tentang cara kerja CEL, tetapi karena contoh yang lebih canggih menggunakan Protocol Buffers sebagai input ke CEL, contoh tersebut mungkin lebih sulit dipahami. Pertimbangkan untuk mempelajari salah satu tutorial ini terlebih dahulu. Perhatikan bahwa Protocol Buffers tidak diperlukan untuk menggunakan CEL, tetapi digunakan secara ekstensif dalam codelab ini.
Anda dapat menguji apakah go telah diinstal dengan menjalankan:
go --help
2. Konsep utama
Aplikasi
CEL bersifat serbaguna, dan telah digunakan untuk beragam aplikasi, mulai dari merutekan RPC, hingga menentukan kebijakan keamanan. CEL dapat diperluas, agnostik terhadap aplikasi, dan dioptimalkan untuk alur kerja kompilasi sekali, evaluasi berkali-kali.
Banyak layanan dan aplikasi mengevaluasi konfigurasi deklaratif. Misalnya, Role-Based Access Control (RBAC) adalah konfigurasi deklaratif yang menghasilkan keputusan akses berdasarkan peran dan sekumpulan pengguna. Jika konfigurasi deklaratif adalah kasus penggunaan 80%, maka CEL adalah alat yang berguna untuk melengkapi 20% sisanya saat pengguna memerlukan kemampuan ekspresif yang lebih besar.
Kompilasi
Ekspresi dikompilasi terhadap lingkungan. Langkah kompilasi menghasilkan Abstract Syntax Tree (AST) dalam bentuk protobuf. Ekspresi yang dikompilasi umumnya disimpan untuk digunakan di masa mendatang agar evaluasi tetap secepat mungkin. Satu ekspresi yang dikompilasi dapat dievaluasi dengan banyak input yang berbeda.
Ekspresi
Pengguna menentukan ekspresi; layanan dan aplikasi menentukan lingkungan tempat ekspresi berjalan. Tanda tangan fungsi mendeklarasikan input, dan ditulis di luar ekspresi CEL. Library fungsi yang tersedia untuk CEL diimpor secara otomatis.
Dalam contoh berikut, ekspresi mengambil objek permintaan, dan permintaan menyertakan token klaim. Ekspresi menampilkan boolean yang menunjukkan apakah token klaim masih valid.
// Check whether a JSON Web Token has expired by inspecting the 'exp' claim.
//
// Args:
// claims - authentication claims.
// now - timestamp indicating the current system time.
// Returns: true if the token has expired.
//
timestamp(claims["exp"]) < now
Lingkungan
Lingkungan ditentukan oleh layanan. Layanan dan aplikasi yang menyematkan CEL mendeklarasikan lingkungan ekspresi. Lingkungan adalah kumpulan variabel dan fungsi yang dapat digunakan dalam ekspresi.
Deklarasi berbasis proto digunakan oleh pemeriksa jenis CEL untuk memastikan bahwa semua referensi fungsi dan ID dalam ekspresi dideklarasikan dan digunakan dengan benar.
Tiga fase penguraian ekspresi
Ada tiga fase dalam memproses ekspresi: mengurai, memeriksa, dan mengevaluasi. Pola paling umum untuk CEL adalah agar bidang kontrol mengurai dan memeriksa ekspresi pada waktu konfigurasi, serta menyimpan AST.

Saat runtime, bidang data mengambil dan mengevaluasi AST berulang kali. CEL dioptimalkan untuk efisiensi runtime, tetapi parsing dan pemeriksaan tidak boleh dilakukan di jalur kode yang penting untuk latensi.

CEL diuraikan dari ekspresi yang dapat dibaca manusia ke hierarki sintaksis abstrak menggunakan tata bahasa penguraian / leksikal ANTLR. Fase penguraian memancarkan hierarki sintaksis abstrak berbasis proto yang setiap node Expr di AST-nya berisi ID bilangan bulat yang digunakan untuk mengindeks metadata yang dihasilkan selama penguraian dan pemeriksaan. syntax.proto yang dihasilkan selama penguraian secara akurat merepresentasikan representasi abstrak dari apa yang diketik dalam bentuk string ekspresi.
Setelah diuraikan, ekspresi dapat diperiksa terhadap lingkungan untuk memastikan semua variabel dan ID fungsi dalam ekspresi telah dideklarasikan dan digunakan dengan benar. Pemeriksa jenis menghasilkan checked.proto yang mencakup metadata resolusi jenis, variabel, dan fungsi yang dapat meningkatkan efisiensi evaluasi secara drastis.
Evaluator CEL memerlukan 3 hal:
- Pengikatan fungsi untuk ekstensi kustom
- Binding variabel
- AST yang akan dievaluasi
Binding fungsi dan variabel harus cocok dengan yang digunakan untuk mengompilasi AST. Input ini dapat digunakan kembali di beberapa evaluasi, seperti AST yang dievaluasi di banyak set binding variabel, atau variabel yang sama yang digunakan terhadap banyak AST, atau binding fungsi yang digunakan selama masa aktif proses (kasus umum).
3. Siapkan
Kode untuk codelab ini ada di folder codelab di repo cel-go. Solusi ini tersedia di codelab/solution folder dari repositori yang sama.
Clone dan cd ke dalam repo:
git clone https://github.com/google/cel-go.git
cd cel-go/codelab
Jalankan kode menggunakan go run:
go run .
Anda akan melihat output berikut:
=== Exercise 1: Hello World ===
=== Exercise 2: Variables ===
=== Exercise 3: Logical AND/OR ===
=== Exercise 4: Customization ===
=== Exercise 5: Building JSON ===
=== Exercise 6: Building Protos ===
=== Exercise 7: Macros ===
=== Exercise 8: Tuning ===
Di mana paket CEL?
Di editor Anda, buka codelab/codelab.go. Anda akan melihat fungsi utama yang mendorong eksekusi latihan dalam codelab ini, diikuti dengan tiga blok fungsi pembantu. Kumpulan helper pertama membantu fase evaluasi CEL:
- Fungsi
Compile: Mengurai dan memeriksa ekspresi input terhadap lingkungan - Fungsi
Eval: Mengevaluasi program yang dikompilasi berdasarkan input - Fungsi
Report: Mencetak hasil evaluasi dengan tampilan yang bagus
Selain itu, helper request dan auth telah disediakan untuk membantu pembuatan input untuk berbagai latihan.
Latihan ini akan merujuk ke paket berdasarkan nama paket singkatnya. Pemetaan dari paket ke lokasi sumber dalam repositori google/cel-go ada di bawah jika Anda ingin mempelajari detailnya:
Paket | Lokasi Sumber | Deskripsi |
cel | Antarmuka tingkat teratas | |
ref | Antarmuka referensi | |
tipe | Nilai jenis runtime |
4. Halo, Dunia!
Seperti semua bahasa pemrograman, kita akan mulai dengan membuat dan mengevaluasi "Hello World!".
Mengonfigurasi lingkungan
Di editor Anda, temukan deklarasi exercise1, dan isi kode berikut untuk menyiapkan lingkungan:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Will add the parse and check steps here
}
Aplikasi CEL mengevaluasi ekspresi terhadap Lingkungan. env, err := cel.NewEnv() mengonfigurasi lingkungan standar.
Lingkungan dapat disesuaikan dengan memberikan opsi cel.EnvOption ke panggilan. Opsi tersebut dapat menonaktifkan makro, mendeklarasikan variabel dan fungsi kustom, dll.
Lingkungan CEL standar mendukung semua jenis, operator, fungsi, dan makro yang ditentukan dalam spesifikasi bahasa.
Mengurai dan memeriksa ekspresi
Setelah lingkungan dikonfigurasi, ekspresi dapat diuraikan dan diperiksa. Tambahkan kode berikut ke fungsi Anda:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Check that the expression compiles and returns a String.
ast, iss := env.Parse(`"Hello, World!"`)
// Report syntactic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Check the output type is a string.
if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
glog.Exitf(
"Got %v, wanted %v output type",
checked.OutputType(), cel.StringType,
)
}
// Will add the planning step here
}
Nilai iss yang ditampilkan oleh panggilan Parse dan Check adalah daftar masalah yang mungkin berupa error. Jika iss.Err() tidak bernilai nol, ada error dalam sintaksis atau semantik, dan program tidak dapat dilanjutkan. Jika ekspresi terbentuk dengan baik, hasil panggilan ini adalah cel.Ast yang dapat dieksekusi.
Evaluasi ekspresi
Setelah ekspresi diuraikan dan diperiksa ke dalam cel.Ast, ekspresi dapat dikonversi menjadi program yang dapat dievaluasi yang pengikatan fungsinya dan mode evaluasinya dapat disesuaikan dengan opsi fungsional. Perhatikan, cel.Ast juga dapat dibaca dari proto menggunakan fungsi cel.CheckedExprToAst atau cel.ParsedExprToAst.
Setelah cel.Program direncanakan, cel.Program dapat dievaluasi terhadap input dengan memanggil Eval. Hasil Eval akan berisi hasil, detail evaluasi, dan status error.
Tambahkan perencanaan dan panggil eval:
// exercise1 evaluates a simple literal expression: "Hello, World!"
//
// Compile, eval, profit!
func exercise1() {
fmt.Println("=== Exercise 1: Hello World ===\n")
// Create the standard environment.
env, err := cel.NewEnv()
if err != nil {
glog.Exitf("env error: %v", err)
}
// Check that the expression compiles and returns a String.
ast, iss := env.Parse(`"Hello, World!"`)
// Report syntactic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Type-check the expression for correctness.
checked, iss := env.Check(ast)
// Report semantic errors, if present.
if iss.Err() != nil {
glog.Exit(iss.Err())
}
// Check the output type is a string.
if !reflect.DeepEqual(checked.OutputType(), cel.StringType) {
glog.Exitf(
"Got %v, wanted %v output type",
checked.OutputType(), cel.StringType)
}
// Plan the program.
program, err := env.Program(checked)
if err != nil {
glog.Exitf("program error: %v", err)
}
// Evaluate the program without any additional arguments.
eval(program, cel.NoVars())
fmt.Println()
}
Untuk mempersingkat, kita akan menghilangkan kasus error yang disertakan di atas dari latihan mendatang.
Jalankan kode
Pada command line, jalankan kembali kode:
go run .
Anda akan melihat output berikut, beserta placeholder untuk latihan berikutnya.
=== Exercise 1: Hello World ===
------ input ------
(interpreter.emptyActivation)
------ result ------
value: Hello, World! (types.String)
5. Menggunakan variabel dalam fungsi
Sebagian besar aplikasi CEL akan mendeklarasikan variabel yang dapat dirujuk dalam ekspresi. Deklarasi variabel menentukan nama dan jenis. Jenis variabel dapat berupa jenis bawaan CEL, jenis terkenal buffer protokol, atau jenis pesan protobuf apa pun selama deskriptornya juga diberikan ke CEL.
Tambahkan fungsi
Di editor Anda, temukan deklarasi exercise2, lalu tambahkan kode berikut:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a request of type google.rpc.context.AttributeContext.Request
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
// Add cel.EnvOptions values here.
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
Menjalankan ulang dan memahami error
Jalankan kembali program:
go run .
Anda akan melihat output berikut:
ERROR: <input>:1:1: undeclared reference to 'request' (in container '')
| request.auth.claims.group == 'admin'
| ^
Pemeriksa jenis menghasilkan error untuk objek permintaan yang dengan mudah menyertakan cuplikan sumber tempat terjadinya error.
6. Deklarasikan variabel
Tambahkan EnvOptions
Di editor, mari kita perbaiki error yang dihasilkan dengan memberikan deklarasi untuk objek permintaan sebagai pesan berjenis google.rpc.context.AttributeContext.Request seperti berikut:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
Menjalankan ulang dan memahami error
Menjalankan program lagi:
go run .
Anda akan melihat error berikut:
ERROR: <input>:1:8: [internal] unexpected failed resolution of 'google.rpc.context.AttributeContext.Request'
| request.auth.claims.group == 'admin'
| .......^
Untuk menggunakan variabel yang merujuk ke pesan protobuf, pemeriksa jenis juga perlu mengetahui deskriptor jenis.
Gunakan cel.Types untuk menentukan deskriptor permintaan dalam fungsi Anda:
// exercise2 shows how to declare and use variables in expressions.
//
// Given a `request` of type `google.rpc.context.AttributeContext.Request`
// determine whether a specific auth claim is set.
func exercise2() {
fmt.Println("=== Exercise 2: Variables ===\n")
env, err := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
if err != nil {
glog.Exit(err)
}
ast := compile(env, `request.auth.claims.group == 'admin'`, cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a request object that sets the proper group claim.
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
Berhasil dijalankan ulang.
Jalankan lagi program:
go run .
Anda akan melihat berikut ini:
=== Exercise 2: Variables ===
request.auth.claims.group == 'admin'
------ input ------
request = time: <
seconds: 1569255569
>
auth: <
principal: "user:me@acme.co"
claims: <
fields: <
key: "group"
value: <
string_value: "admin"
>
>
>
>
------ result ------
value: true (types.Bool)
Untuk meninjau, kita baru saja mendeklarasikan variabel untuk error, menetapkan deskriptor jenis, lalu mereferensikan variabel dalam evaluasi ekspresi.
7. Logika AND/OR
Salah satu fitur CEL yang lebih unik adalah penggunaan operator logis komutatif. Setiap sisi cabang bersyarat dapat mempersingkat evaluasi, bahkan jika terjadi error atau input sebagian.
Dengan kata lain, CEL menemukan urutan evaluasi yang memberikan hasil jika memungkinkan, dengan mengabaikan error atau bahkan data yang hilang yang mungkin terjadi dalam urutan evaluasi lainnya. Aplikasi dapat mengandalkan properti ini untuk meminimalkan biaya evaluasi, menunda pengumpulan input yang mahal jika hasil dapat dicapai tanpa input tersebut.
Kita akan menambahkan contoh AND/OR, lalu mencobanya dengan input yang berbeda untuk memahami cara evaluasi short-circuit CEL.
Membuat fungsi
Di editor Anda, tambahkan konten berikut ke latihan 3:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper
// user, and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
}
Selanjutnya, sertakan pernyataan OR ini yang akan menampilkan benar jika pengguna adalah anggota grup admin, atau memiliki ID email tertentu:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks if the `request.auth.claims.group`
// value is equal to admin or the `request.auth.principal` is
// `user:me@acme.co`. Issue two requests, one that specifies the proper
// user, and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
}
Terakhir, tambahkan kasus eval yang mengevaluasi pengguna dengan set klaim kosong:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
}
Jalankan kode dengan set klaim kosong
Dengan menjalankan kembali program, Anda akan melihat output baru berikut:
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
seconds: 1569302377
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: true (types.Bool)
Memperbarui kasus evaluasi
Selanjutnya, perbarui kasus evaluasi untuk meneruskan pokok yang berbeda dengan set klaim kosong:
// exercise3 demonstrates how CEL's commutative logical operators work.
//
// Construct an expression which checks whether the request.auth.claims.group
// value is equal to admin or the request.auth.principal is
// user:me@acme.co. Issue two requests, one that specifies the proper user,
// and one that specifies an unexpected user.
func exercise3() {
fmt.Println("=== Exercise 3: Logical AND/OR ===\n")
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
)
ast := compile(env,
`request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'`,
cel.BoolType)
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("other:me@acme.co", emptyClaims), time.Now()))
}
Menjalankan kode dengan waktu
Menjalankan kembali program,
go run .
Anda akan melihat error berikut:
=== Exercise 3: Logical AND/OR ===
request.auth.claims.group == 'admin'
|| request.auth.principal == 'user:me@acme.co'
------ input ------
request = time: <
seconds: 1588989833
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: true (types.Bool)
------ input ------
request = time: <
seconds: 1588989833
>
auth: <
principal: "other:me@acme.co"
claims: <
>
>
------ result ------
error: no such key: group
Dalam protobuf, kita tahu kolom dan jenis yang diharapkan. Dalam nilai peta dan json, kita tidak tahu apakah kunci akan ada. Karena tidak ada nilai default yang aman untuk kunci yang tidak ada, CEL akan menampilkan error secara default.
8. Fungsi Kustom
Meskipun CEL mencakup banyak fungsi bawaan, ada kalanya fungsi kustom berguna. Misalnya, fungsi kustom dapat digunakan untuk meningkatkan pengalaman pengguna untuk kondisi umum atau mengekspos status yang sensitif terhadap konteks
Dalam latihan ini, kita akan mempelajari cara mengekspos fungsi untuk menggabungkan pemeriksaan yang umum digunakan.
Memanggil fungsi kustom
Pertama, buat kode untuk menyiapkan penggantian bernama contains yang menentukan apakah kunci ada dalam peta dan memiliki nilai tertentu. Biarkan placeholder untuk definisi fungsi dan pengikatan fungsi:
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The
// custom map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Add the custom function declaration and binding here with cel.Function()
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan and provide the 'contains' function impl.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(
program,
request(auth("user:me@acme.co", emptyClaims), time.Now()),
)
fmt.Println()
}
Jalankan kode dan pahami errornya
Dengan menjalankan kembali kode, Anda akan melihat error berikut:
ERROR: <input>:1:29: found no matching overload for 'contains' applied to 'map(string, dyn).(string, string)'
| request.auth.claims.contains('group', 'admin')
| ............................^
Untuk memperbaiki error, kita perlu menambahkan fungsi contains ke daftar deklarasi yang saat ini mendeklarasikan variabel permintaan.
Deklarasikan jenis berparameter dengan menambahkan 3 baris berikut. (Ini serumit kelebihan beban fungsi apa pun untuk CEL)
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Add the custom function declaration and binding here with cel.Function()
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
Tambahkan fungsi kustom
Selanjutnya, kita akan menambahkan fungsi contains baru yang akan menggunakan jenis yang diberi parameter:
// exercise4 demonstrates how to extend CEL with custom functions.
// Declare a `contains` member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
// Env declaration.
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Declare the custom contains function and its implementation.
cel.Function("contains",
cel.MemberOverload(
"map_contains_key_value",
[]*cel.Type{mapAB, typeParamA, typeParamB},
cel.BoolType,
// Provide the implementation using cel.FunctionBinding()
),
),
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan and provide the 'contains' function impl.
// Output: false
program, _ := env.Program(ast)
emptyClaims := map[string]string{}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
fmt.Println()
}
Jalankan program untuk memahami error
Lakukan latihan. Anda akan melihat error berikut tentang fungsi runtime yang tidak ada:
------ result ------
error: no such overload: contains
Berikan implementasi fungsi ke deklarasi NewEnv menggunakan fungsi cel.FunctionBinding():
// exercise4 demonstrates how to extend CEL with custom functions.
//
// Declare a contains member function on map types that returns a boolean
// indicating whether the map contains the key-value pair.
func exercise4() {
fmt.Println("=== Exercise 4: Customization ===\n")
// Determine whether an optional claim is set to the proper value. The custom
// map.contains(key, value) function is used as an alternative to:
// key in map && map[key] == value
// Useful components of the type-signature for 'contains'.
typeParamA := cel.TypeParamType("A")
typeParamB := cel.TypeParamType("B")
mapAB := cel.MapType(typeParamA, typeParamB)
// Env declaration.
env, _ := cel.NewEnv(
cel.Types(&rpcpb.AttributeContext_Request{}),
// Declare the request.
cel.Variable("request",
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
),
// Declare the custom contains function and its implementation.
cel.Function("contains",
cel.MemberOverload(
"map_contains_key_value",
[]*cel.Type{mapAB, typeParamA, typeParamB},
cel.BoolType,
cel.FunctionBinding(mapContainsKeyValue)),
),
)
ast := compile(env,
`request.auth.claims.contains('group', 'admin')`,
cel.BoolType)
// Construct the program plan.
// Output: false
program, err := env.Program(ast)
if err != nil {
glog.Exit(err)
}
eval(program, request(auth("user:me@acme.co", emptyClaims), time.Now()))
claims := map[string]string{"group": "admin"}
eval(program, request(auth("user:me@acme.co", claims), time.Now()))
fmt.Println()
}
Program sekarang akan berhasil dijalankan:
=== Exercise 4: Custom Functions ===
request.auth.claims.contains('group', 'admin')
------ input ------
request = time: <
seconds: 1569302377
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: false (types.Bool)
Apa yang terjadi jika klaim sudah ada?
Sebagai nilai tambah, coba tetapkan klaim admin pada input untuk memverifikasi bahwa kelebihan beban contains juga menampilkan nilai benar (true) saat klaim ada. Anda akan melihat output berikut:
=== Exercise 4: Customization ===
request.auth.claims.contains('group','admin')
------ input ------
request = time: <
seconds: 1588991010
>
auth: <
principal: "user:me@acme.co"
claims: <
>
>
------ result ------
value: false (types.Bool)
------ input ------
request = time: <
seconds: 1588991010
>
auth: <
principal: "user:me@acme.co"
claims: <
fields: <
key: "group"
value: <
string_value: "admin"
>
>
>
>
------ result ------
value: true (types.Bool)
Sebelum melanjutkan, ada baiknya memeriksa fungsi mapContainsKeyValue itu sendiri:
// mapContainsKeyValue implements the custom function:
// map.contains(key, value) -> bool.
func mapContainsKeyValue(args ...ref.Val) ref.Val {
// The declaration of the function ensures that only arguments which match
// the mapContainsKey signature will be provided to the function.
m := args[0].(traits.Mapper)
// CEL has many interfaces for dealing with different type abstractions.
// The traits.Mapper interface unifies field presence testing on proto
// messages and maps.
key := args[1]
v, found := m.Find(key)
// If not found and the value was non-nil, the value is an error per the
// `Find` contract. Propagate it accordingly. Such an error might occur with
// a map whose key-type is listed as 'dyn'.
if !found {
if v != nil {
return types.ValOrErr(v, "unsupported key type")
}
// Return CEL False if the key was not found.
return types.False
}
// Otherwise whether the value at the key equals the value provided.
return v.Equal(args[2])
}
Untuk memberikan kemudahan ekstensi yang paling besar, tanda tangan untuk fungsi kustom mengharapkan argumen jenis ref.Val. Sebagai gantinya, kemudahan ekstensi menambah beban pada pelaksana untuk memastikan semua jenis nilai ditangani dengan benar. Jika jenis atau jumlah argumen input tidak cocok dengan deklarasi fungsi, error no such overload akan ditampilkan.
cel.FunctionBinding() menambahkan penjaga jenis runtime untuk memastikan bahwa kontrak runtime cocok dengan deklarasi yang diperiksa jenisnya di lingkungan.
9. Membangun JSON
CEL juga dapat menghasilkan output non-boolean, seperti JSON. Tambahkan kode berikut ke fungsi Anda:
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
// Declare the 'now' variable as a Timestamp.
// cel.Variable("now", cel.TimestampType),
)
// Note the quoted keys in the CEL map literal. For proto messages the
// field names are unquoted as they represent well-defined identifiers.
ast := compile(env, `
{'sub': 'serviceAccount:delegate@acme.co',
'aud': 'my-project',
'iss': 'auth.acme.com:12350',
'iat': now,
'nbf': now,
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
}}`,
cel.MapType(cel.StringType, cel.DynType))
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]interface{}{
"now": &tpb.Timestamp{Seconds: time.Now().Unix()},
},
)
fmt.Printf("------ type conversion ------\n%v\n", out)
fmt.Println()
}
Menjalankan kode
Dengan menjalankan kembali kode, Anda akan melihat error berikut:
ERROR: <input>:5:11: undeclared reference to 'now' (in container '')
| 'iat': now,
| ..........^
... and more ...
Tambahkan deklarasi untuk variabel now jenis cel.TimestampType ke cel.NewEnv() dan jalankan lagi:
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
env, _ := cel.NewEnv(
cel.Variable("now", cel.TimestampType),
)
// Note the quoted keys in the CEL map literal. For proto messages the
// field names are unquoted as they represent well-defined identifiers.
ast := compile(env, `
{'sub': 'serviceAccount:delegate@acme.co',
'aud': 'my-project',
'iss': 'auth.acme.com:12350',
'iat': now,
'nbf': now,
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
}}`,
cel.MapType(cel.StringType, cel.DynType))
// Hint:
// Convert `out` to JSON using the valueToJSON() helper method.
// The valueToJSON() function calls ConvertToNative(&structpb.Value{})
// to adapt the CEL value to a protobuf JSON representation and then
// uses the jsonpb.Marshaler utilities on the output to render the JSON
// string.
program, _ := env.Program(ast)
out, _, _ := eval(
program,
map[string]interface{}{
"now": time.Now(),
},
)
fmt.Printf("------ type conversion ------\n%v\n", out)
fmt.Println()
}
Jalankan kembali kode, dan kode akan berhasil:
=== Exercise 5: Building JSON ===
{'aud': 'my-project',
'exp': now + duration('300s'),
'extra_claims': {
'group': 'admin'
},
'iat': now,
'iss': 'auth.acme.com:12350',
'nbf': now,
'sub': 'serviceAccount:delegate@acme.co'
}
------ input ------
now = seconds: 1569302377
------ result ------
value: &{0xc0002eaf00 map[aud:my-project exp:{0xc0000973c0} extra_claims:0xc000097400 iat:{0xc000097300} iss:auth.acme.com:12350 nbf:{0xc000097300} sub:serviceAccount:delegate@acme.co] {0x8c8f60 0xc00040a6c0 21}} (*types.baseMap)
------ type conversion ------
&{0xc000313510 map[aud:my-project exp:{0xc000442510} extra_claims:0xc0004acdc0 iat:{0xc000442450} iss:auth.acme.com:12350 nbf:{0xc000442450} sub:serviceAccount:delegate@acme.co] {0x9d0ce0 0xc0004424b0 21}}
Program berjalan, tetapi nilai output out harus dikonversi secara eksplisit ke JSON. Representasi CEL internal dalam kasus ini dapat dikonversi ke JSON karena hanya merujuk pada jenis yang dapat didukung JSON atau yang memiliki pemetaan Proto ke JSON yang sudah dikenal.
// exercise5 covers how to build complex objects as CEL literals.
//
// Given the input now, construct a JWT with an expiry of 5 minutes.
func exercise5() {
fmt.Println("=== Exercise 5: Building JSON ===\n")
...
fmt.Printf("------ type conversion ------\n%v\n", valueToJSON(out))
fmt.Println()
}
Setelah jenis dikonversi menggunakan fungsi helper valueToJSON dalam file codelab.go, Anda akan melihat output tambahan berikut:
------ type conversion ------
{
"aud": "my-project",
"exp": "2019-10-13T05:54:29Z",
"extra_claims": {
"group": "admin"
},
"iat": "2019-10-13T05:49:29Z",
"iss": "auth.acme.com:12350",
"nbf": "2019-10-13T05:49:29Z",
"sub": "serviceAccount:delegate@acme.co"
}
10. Membangun Protos
CEL dapat membuat pesan protobuf untuk semua jenis pesan yang dikompilasi ke dalam aplikasi. Tambahkan fungsi untuk membuat google.rpc.context.AttributeContext.Request dari jwt input
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Types(requestType),
// Add cel.Variable() option for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
)
// Compile the Request message construction expression and validate that
// the resulting expression type matches the fully qualified message name.
//
// Note: the field names within the proto message types are not quoted as they
// are well-defined names composed of valid identifier characters. Also, note
// that when building nested proto objects, the message name needs to prefix
// the object construction.
ast := compile(env, `
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}`,
cel.ObjectType("google.rpc.context.AttributeContext.Request"),
)
program, _ := env.Program(ast)
// Construct the message. The result is a ref.Val that returns a dynamic
// proto message.
out, _, _ := eval(
program,
map[string]interface{}{
"jwt": map[string]interface{}{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string]string{
"group": "admin",
},
},
"now": time.Now(),
},
)
// Hint: Unwrap the CEL value to a proto. Make sure to use the
// `ConvertToNative(reflect.TypeOf(requestType))` to convert the dynamic proto
// message to the concrete proto message type expected.
fmt.Printf("------ type unwrap ------\n%v\n", out)
fmt.Println()
}
Menjalankan kode
Dengan menjalankan kembali kode, Anda akan melihat error berikut:
ERROR: <input>:2:10: undeclared reference to 'Request' (in container '')
| Request{
| .........^
Pada dasarnya, penampung setara dengan namespace atau paket, tetapi bahkan dapat sekecil nama pesan protobuf. Penampung CEL menggunakan aturan penyelesaian namespace yang sama dengan Protobuf dan C++ untuk menentukan tempat deklarasi nama variabel, fungsi, atau jenis tertentu.
Mengingat penampung google.rpc.context.AttributeContext, pemeriksa jenis dan evaluator akan mencoba nama ID berikut untuk semua variabel, jenis, dan fungsi:
google.rpc.context.AttributeContext.<id>google.rpc.context.<id>google.rpc.<id>google.<id><id>
Untuk nama absolut, awali referensi variabel, jenis, atau fungsi dengan titik di depannya. Dalam contoh, ekspresi .<id> hanya akan menelusuri ID <id> tingkat teratas tanpa memeriksa terlebih dahulu dalam penampung.
Coba tentukan opsi cel.Container("google.rpc.context.AttributeContext") ke lingkungan CEL dan jalankan lagi:
// exercise6 describes how to build proto message types within CEL.
//
// Given an input jwt and time now construct a
// google.rpc.context.AttributeContext.Request with the time and auth
// fields populated according to the go/api-attributes specification.
func exercise6() {
fmt.Println("=== Exercise 6: Building Protos ===\n")
// Construct an environment and indicate that the container for all references
// within the expression is `google.rpc.context.AttributeContext`.
requestType := &rpcpb.AttributeContext_Request{}
env, _ := cel.NewEnv(
// Add cel.Container() option for 'google.rpc.context.AttributeContext'
cel.Container("google.rpc.context.AttributeContext.Request"),
cel.Types(requestType),
// Later, add cel.Variable() options for 'jwt' as a map(string, Dyn) type
// and for 'now' as a timestamp.
// cel.Variable("now", cel.TimestampType),
// cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
)
...
}
Output Anda akan terlihat seperti berikut:
ERROR: <input>:4:16: undeclared reference to 'jwt' (in container 'google.rpc.context.AttributeContext')
| principal: jwt.iss + '/' + jwt.sub,
| ...............^
... dan banyak lagi error...
Selanjutnya, deklarasikan variabel jwt dan now, lalu program akan berjalan seperti yang diharapkan:
=== Exercise 6: Building Protos ===
Request{
auth: Auth{
principal: jwt.iss + '/' + jwt.sub,
audiences: [jwt.aud],
presenter: 'azp' in jwt ? jwt.azp : "",
claims: jwt
},
time: now
}
------ input ------
jwt = {
"aud": "my-project",
"extra_claims": {
"group": "admin"
},
"iss": "auth.acme.com:12350",
"sub": "serviceAccount:delegate@acme.co"
}
now = seconds: 1588993027
------ result ------
value: &{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false} (*types.protoObj)
----- type unwrap ----
&{0xc000485270 0xc000274180 {0xa6ee80 0xc000274180 22} 0xc0004be140 0xc0002bf700 false}
Sebagai kredit tambahan, Anda juga harus membuka jenis dengan memanggil out.Value() pada pesan untuk melihat bagaimana perubahan hasilnya.
11. Makro
Makro dapat digunakan untuk memanipulasi program CEL pada waktu penguraian. Makro mencocokkan tanda tangan panggilan dan memanipulasi panggilan input serta argumennya untuk menghasilkan AST subekspresi baru.
Makro dapat digunakan untuk menerapkan logika kompleks di AST yang tidak dapat ditulis langsung di CEL. Misalnya, makro has memungkinkan pengujian kehadiran kolom. Makro pemahaman seperti exists dan all menggantikan panggilan fungsi dengan iterasi terbatas pada daftar atau peta input. Kedua konsep tersebut tidak mungkin dilakukan pada tingkat sintaksis, tetapi dapat dilakukan melalui perluasan makro.
Tambahkan dan jalankan latihan berikutnya:
// exercise7 introduces macros for dealing with repeated fields and maps.
//
// Determine whether the jwt.extra_claims has at least one key that starts
// with the group prefix, and ensure that all group-like keys have list
// values containing only strings that end with '@acme.co`.
func exercise7() {
fmt.Println("=== Exercise 7: Macros ===\n")
env, _ := cel.NewEnv(
cel.ClearMacros(),
cel.Variable("jwt", cel.MapType(cel.StringType, cel.DynType)),
)
ast := compile(env,
`jwt.extra_claims.exists(c, c.startsWith('group'))
&& jwt.extra_claims
.filter(c, c.startsWith('group'))
.all(c, jwt.extra_claims[c]
.all(g, g.endsWith('@acme.co')))`,
cel.BoolType)
program, _ := env.Program(ast)
// Evaluate a complex-ish JWT with two groups that satisfy the criteria.
// Output: true.
eval(program,
map[string]interface{}{
"jwt": map[string]interface{}{
"sub": "serviceAccount:delegate@acme.co",
"aud": "my-project",
"iss": "auth.acme.com:12350",
"extra_claims": map[string][]string{
"group1": {"admin@acme.co", "analyst@acme.co"},
"labels": {"metadata", "prod", "pii"},
"groupN": {"forever@acme.co"},
},
},
})
fmt.Println()
}
Anda akan melihat error berikut:
ERROR: <input>:1:25: undeclared reference to 'c' (in container '')
| jwt.extra_claims.exists(c, c.startsWith('group'))
| ........................^
... dan banyak lagi ...
Error ini terjadi karena makro belum diaktifkan. Untuk mengaktifkan makro, hapus cel.ClearMacros(), lalu jalankan lagi:
=== Exercise 7: Macros ===
jwt.extra_claims.exists(c, c.startsWith('group'))
&& jwt.extra_claims
.filter(c, c.startsWith('group'))
.all(c, jwt.extra_claims[c]
.all(g, g.endsWith('@acme.co')))
------ input ------
jwt = {
"aud": "my-project",
"extra_claims": {
"group1": [
"admin@acme.co",
"analyst@acme.co"
],
"groupN": [
"forever@acme.co"
],
"labels": [
"metadata",
"prod",
"pii"
]
},
"iss": "auth.acme.com:12350",
"sub": "serviceAccount:delegate@acme.co"
}
------ result ------
value: true (types.Bool)
Berikut adalah makro yang saat ini didukung:
Makro | Tanda Tangan | Deskripsi |
semua | r.all(var, cond) | Menguji apakah cond dievaluasi sebagai benar untuk semua var dalam rentang r. |
ada | r.exists(var, cond) | Menguji apakah cond bernilai benar untuk var apa pun dalam rentang r. |
exists_one | r.exists_one(var, cond) | Uji apakah cond dievaluasi benar untuk hanya satu var dalam rentang r. |
filter | r.filter(var, cond) | Untuk daftar, buat daftar baru tempat setiap elemen var dalam rentang r memenuhi kondisi cond. Untuk peta, buat daftar baru dengan setiap var kunci dalam rentang r memenuhi kondisi cond. |
peta | r.map(var, expr) | Buat daftar baru yang setiap var dalam rentang r diubah oleh expr. |
r.map(var, cond, expr) | Sama seperti peta dua argumen, tetapi dengan filter cond bersyarat sebelum nilai diubah. | |
memiliki | has(a.b) | Uji kehadiran b pada nilai a : Untuk peta, definisi pengujian json. Untuk proto, menguji nilai primitif non-default atau kolom pesan yang ditetapkan. |
Jika argumen rentang r adalah jenis map, var akan menjadi kunci peta, dan untuk nilai jenis list, var akan menjadi nilai elemen daftar. Makro all, exists, exists_one, filter, dan map melakukan penulisan ulang AST yang melakukan iterasi for-each yang dibatasi oleh ukuran input.
Pemahaman terbatas memastikan bahwa program CEL tidak akan Turing-lengkap, tetapi dievaluasi dalam waktu super-linear sehubungan dengan input. Gunakan makro ini seperlunya atau tidak sama sekali. Penggunaan komprehensi yang berat biasanya merupakan indikator yang baik bahwa fungsi kustom akan memberikan pengalaman pengguna dan performa yang lebih baik.
12. Tuning mesin
Saat ini ada beberapa fitur yang eksklusif untuk CEL-Go, tetapi menunjukkan rencana mendatang untuk penerapan CEL lainnya. Latihan berikut menunjukkan berbagai rencana program untuk AST yang sama:
// exercise8 covers features of CEL-Go which can be used to improve
// performance and debug evaluation behavior.
//
// Turn on the optimization, exhaustive eval, and state tracking
// ProgramOption flags to see the impact on evaluation behavior.
func exercise8() {
fmt.Println("=== Exercise 8: Tuning ===\n")
// Declare the x and 'y' variables as input into the expression.
env, _ := cel.NewEnv(
cel.Variable("x", cel.IntType),
cel.Variable("y", cel.UintType),
)
ast := compile(env,
`x in [1, 2, 3, 4, 5] && type(y) == uint`,
cel.BoolType)
// Try the different cel.EvalOptions flags when evaluating this AST for
// the following use cases:
// - cel.OptOptimize: optimize the expression performance.
// - cel.OptExhaustiveEval: turn off short-circuiting.
// - cel.OptTrackState: track state and compute a residual using the
// interpreter.PruneAst function.
program, _ := env.Program(ast)
eval(program, cel.NoVars())
fmt.Println()
}
Optimize
Jika tanda pengoptimalan diaktifkan, CEL akan meluangkan waktu tambahan untuk membuat literal daftar dan peta terlebih dahulu dan mengoptimalkan panggilan tertentu seperti operator in agar menjadi pengujian keanggotaan set yang sebenarnya:
// Turn on optimization.
trueVars := map[string]interface{}{"x": int64(4), "y": uint64(2)}
program, _ := env.Program(ast, cel.EvalOptions(cel.OptOptimize))
// Try benchmarking with the optimization flag on and off.
eval(program, trueVars)
Jika program yang sama dievaluasi berkali-kali terhadap input yang berbeda, pengoptimalan adalah pilihan yang baik. Namun, jika program hanya akan dievaluasi satu kali, pengoptimalan hanya akan menambah overhead.
Evaluasi Menyeluruh (Exhaustive Eval)
Evaluasi Lengkap dapat berguna untuk men-debug perilaku evaluasi ekspresi karena memberikan insight tentang nilai yang diamati di setiap langkah dalam evaluasi ekspresi.
// Turn on exhaustive eval to see what the evaluation state looks like.
// The input is structure to show a false on the first branch, and true
// on the second.
falseVars := map[string]interface{}{"x": int64(6), "y": uint64(2)}
program, _ = env.Program(ast, cel.EvalOptions(cel.OptExhaustiveEval))
eval(program, falseVars)
Anda akan melihat daftar status evaluasi ekspresi untuk setiap ID ekspresi:
------ eval states ------
1: 6 (types.Int)
2: false (types.Bool)
3: &{0xc0000336b0 [1 2 3 4 5] {0x89f020 0xc000434760 151}} (*types.baseList)
4: 1 (types.Int)
5: 2 (types.Int)
6: 3 (types.Int)
7: 4 (types.Int)
8: 5 (types.Int)
9: uint (*types.Type)
10: 2 (types.Uint)
11: true (types.Bool)
12: uint (*types.Type)
13: false (types.Bool)
ID ekspresi 2 sesuai dengan hasil operator in di cabang pertama, dan ID ekspresi 11 sesuai dengan operator == di cabang kedua. Dalam evaluasi normal, ekspresi akan dihentikan setelah 2 dihitung. Jika y bukan uint, status akan menampilkan dua alasan mengapa ekspresi akan gagal, bukan hanya satu.
13. Apa yang dibahas?
Jika Anda memerlukan mesin ekspresi, pertimbangkan untuk menggunakan CEL. CEL sangat ideal untuk project yang perlu menjalankan konfigurasi pengguna dengan performa yang sangat penting.
Dalam latihan sebelumnya, kami harap Anda sudah terbiasa meneruskan data ke CEL dan mendapatkan kembali output atau keputusan.
Kami harap Anda memahami jenis operasi yang dapat Anda lakukan, mulai dari keputusan boolean hingga membuat pesan JSON dan Protobuffer.
Kami harap Anda memahami cara menggunakan ekspresi dan fungsinya. Kami juga memahami cara umum untuk memperpanjangnya.