Skip to content

Commit 9796a2e

Browse files
committed
feat: [klient/cel] add CEL-based assertions and conformance profiles
1 parent 08a5ab3 commit 9796a2e

20 files changed

Lines changed: 2357 additions & 6 deletions

cel/cel.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
// Package cel provides CEL-based assertion utilities for testing Kubernetes
18+
// objects. It wraps cel-go with the variables and library set that the
19+
// Kubernetes API server registers for admission CEL, so the same expressions
20+
// that appear in a ValidatingAdmissionPolicy can appear in a test assertion.
21+
package cel
22+
23+
import (
24+
"fmt"
25+
"sync"
26+
27+
"github.com/google/cel-go/cel"
28+
"github.com/google/cel-go/common/types"
29+
"github.com/google/cel-go/common/types/ref"
30+
)
31+
32+
// Env selects which CEL variable shape the Evaluator exposes.
33+
type Env int
34+
35+
const (
36+
// EnvAdmission exposes object, oldObject, request, params,
37+
// namespaceObject, and authorizer (matching ValidatingAdmissionPolicy).
38+
EnvAdmission Env = iota
39+
// EnvCRD exposes self and oldSelf (matching x-kubernetes-validations).
40+
EnvCRD
41+
)
42+
43+
// DefaultCostLimit matches the API server's per-expression CEL cost limit.
44+
const DefaultCostLimit uint64 = 10_000_000
45+
46+
// Evaluator compiles and evaluates CEL expressions against variable bindings.
47+
// An Evaluator is safe for concurrent use; compiled programs are cached so
48+
// the same expression is not re-compiled across assertions.
49+
type Evaluator struct {
50+
env *cel.Env
51+
cache sync.Map // expression -> cel.Program
52+
opts options
53+
}
54+
55+
type options struct {
56+
env Env
57+
libSet librarySet
58+
costLimit uint64
59+
customVars []cel.EnvOption
60+
}
61+
62+
// Option configures an Evaluator.
63+
type Option func(*options)
64+
65+
// WithEnvironment selects the CEL variable shape. Defaults to EnvAdmission.
66+
func WithEnvironment(env Env) Option {
67+
return func(o *options) { o.env = env }
68+
}
69+
70+
// WithLibraries restricts the Kubernetes CEL libraries wired into the
71+
// evaluator. By default every library registered for admission CEL is wired
72+
// in; passing WithLibraries replaces that set with the provided subset.
73+
func WithLibraries(libs ...Library) Option {
74+
return func(o *options) {
75+
o.libSet = newLibrarySet(libs)
76+
}
77+
}
78+
79+
// WithCostLimit overrides the per-expression CEL cost limit. The default is
80+
// DefaultCostLimit, matching the API server. Passing 0 disables the limit.
81+
func WithCostLimit(limit uint64) Option {
82+
return func(o *options) { o.costLimit = limit }
83+
}
84+
85+
// WithVariables is an escape hatch for advanced use: the supplied EnvOptions
86+
// are appended after the default variables and libraries are registered.
87+
func WithVariables(decls ...cel.EnvOption) Option {
88+
return func(o *options) { o.customVars = append(o.customVars, decls...) }
89+
}
90+
91+
// NewEvaluator returns an Evaluator configured for the admission CEL
92+
// environment with every standard Kubernetes CEL library wired in.
93+
func NewEvaluator(opts ...Option) (*Evaluator, error) {
94+
o := options{
95+
env: EnvAdmission,
96+
libSet: allLibraries(),
97+
costLimit: DefaultCostLimit,
98+
}
99+
for _, opt := range opts {
100+
opt(&o)
101+
}
102+
103+
envOpts := make([]cel.EnvOption, 0, 16)
104+
envOpts = append(envOpts, variableDecls(o.env)...)
105+
envOpts = append(envOpts, o.libSet.envOptions()...)
106+
envOpts = append(envOpts, o.customVars...)
107+
108+
env, err := cel.NewEnv(envOpts...)
109+
if err != nil {
110+
return nil, fmt.Errorf("cel: build env: %w", err)
111+
}
112+
return &Evaluator{env: env, opts: o}, nil
113+
}
114+
115+
// Eval compiles expr and evaluates it against b, returning the raw result.
116+
// Compiled programs are cached per-expression inside the Evaluator.
117+
func (e *Evaluator) Eval(expr string, b Bindings) (ref.Val, error) {
118+
prg, err := e.programFor(expr)
119+
if err != nil {
120+
return nil, err
121+
}
122+
out, _, err := prg.Eval(map[string]any(b))
123+
if err != nil {
124+
return nil, fmt.Errorf("cel: eval %q: %w", expr, err)
125+
}
126+
return out, nil
127+
}
128+
129+
// Assert returns nil iff expr evaluates to boolean true. Non-boolean results
130+
// and compile/evaluation errors are surfaced as errors.
131+
func (e *Evaluator) Assert(expr string, b Bindings) error {
132+
out, err := e.Eval(expr, b)
133+
if err != nil {
134+
return err
135+
}
136+
bv, ok := out.(types.Bool)
137+
if !ok {
138+
return fmt.Errorf("cel: expression %q returned %T, want bool", expr, out)
139+
}
140+
if !bool(bv) {
141+
return fmt.Errorf("cel: assertion failed: %s", expr)
142+
}
143+
return nil
144+
}
145+
146+
// programFor returns a cached compiled program for expr, compiling on first
147+
// use. Multiple goroutines racing on the same expression may each compile,
148+
// but only one result is retained; cel.Program is concurrency-safe.
149+
func (e *Evaluator) programFor(expr string) (cel.Program, error) {
150+
if p, ok := e.cache.Load(expr); ok {
151+
prg, ok := p.(cel.Program)
152+
if !ok {
153+
return nil, fmt.Errorf("cel: cached value for %q is %T, want cel.Program", expr, p)
154+
}
155+
return prg, nil
156+
}
157+
ast, iss := e.env.Compile(expr)
158+
if iss != nil && iss.Err() != nil {
159+
return nil, fmt.Errorf("cel: compile %q: %w", expr, iss.Err())
160+
}
161+
progOpts := []cel.ProgramOption{cel.EvalOptions(cel.OptOptimize)}
162+
if e.opts.costLimit > 0 {
163+
progOpts = append(progOpts, cel.CostLimit(e.opts.costLimit))
164+
}
165+
prg, err := e.env.Program(ast, progOpts...)
166+
if err != nil {
167+
return nil, fmt.Errorf("cel: program %q: %w", expr, err)
168+
}
169+
actual, _ := e.cache.LoadOrStore(expr, prg)
170+
stored, ok := actual.(cel.Program)
171+
if !ok {
172+
return nil, fmt.Errorf("cel: cached value for %q is %T, want cel.Program", expr, actual)
173+
}
174+
return stored, nil
175+
}

0 commit comments

Comments
 (0)