Introduce CEL EnvSets for managing safe rollout of new CEL features, libraries and expression variables

Kubernetes-commit: e740f8340eedc89baccd120329b454a860385e2d
This commit is contained in:
Joe Betz 2023-04-28 14:16:56 -04:00 committed by Kubernetes Publisher
parent 055116923f
commit f32e391a45
25 changed files with 1191 additions and 646 deletions

View File

@ -18,12 +18,13 @@ package cel
import (
"fmt"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"sync"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/version"
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
)
@ -36,104 +37,6 @@ const (
RequestResourceAuthorizerVarName = "authorizer.requestResource"
)
var (
initEnvsOnce sync.Once
initEnvs envs
initEnvsErr error
)
func getEnvs() (envs, error) {
initEnvsOnce.Do(func() {
requiredVarsEnv, err := buildRequiredVarsEnv()
if err != nil {
initEnvsErr = err
return
}
initEnvs, err = buildWithOptionalVarsEnvs(requiredVarsEnv)
if err != nil {
initEnvsErr = err
return
}
})
return initEnvs, initEnvsErr
}
// This is a similar code as in k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go
// If any changes are made here, consider to make the same changes there as well.
func buildBaseEnv() (*cel.Env, error) {
var opts []cel.EnvOption
opts = append(opts, cel.HomogeneousAggregateLiterals())
// Validate function declarations once during base env initialization,
// so they don't need to be evaluated each time a CEL rule is compiled.
// This is a relatively expensive operation.
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
opts = append(opts, library.ExtensionLibs...)
return cel.NewEnv(opts...)
}
func buildRequiredVarsEnv() (*cel.Env, error) {
baseEnv, err := buildBaseEnv()
if err != nil {
return nil, err
}
var propDecls []cel.EnvOption
reg := apiservercel.NewRegistry(baseEnv)
requestType := BuildRequestType()
rt, err := apiservercel.NewRuleTypes(requestType.TypeName(), requestType, reg)
if err != nil {
return nil, err
}
if rt == nil {
return nil, nil
}
opts, err := rt.EnvOptions(baseEnv.TypeProvider())
if err != nil {
return nil, err
}
propDecls = append(propDecls, cel.Variable(ObjectVarName, cel.DynType))
propDecls = append(propDecls, cel.Variable(OldObjectVarName, cel.DynType))
propDecls = append(propDecls, cel.Variable(RequestVarName, requestType.CelType()))
opts = append(opts, propDecls...)
env, err := baseEnv.Extend(opts...)
if err != nil {
return nil, err
}
return env, nil
}
type envs map[OptionalVariableDeclarations]*cel.Env
func buildEnvWithVars(baseVarsEnv *cel.Env, options OptionalVariableDeclarations) (*cel.Env, error) {
var opts []cel.EnvOption
if options.HasParams {
opts = append(opts, cel.Variable(ParamsVarName, cel.DynType))
}
if options.HasAuthorizer {
opts = append(opts, cel.Variable(AuthorizerVarName, library.AuthorizerType))
opts = append(opts, cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
return baseVarsEnv.Extend(opts...)
}
func buildWithOptionalVarsEnvs(requiredVarsEnv *cel.Env) (envs, error) {
envs := make(envs, 4) // since the number of variable combinations is small, pre-build a environment for each
for _, hasParams := range []bool{false, true} {
for _, hasAuthorizer := range []bool{false, true} {
opts := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer}
env, err := buildEnvWithVars(requiredVarsEnv, opts)
if err != nil {
return nil, err
}
envs[opts] = env
}
}
return envs, nil
}
// BuildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that
// converts the native type definition to apiservercel.DeclType once such a utility becomes available.
// The 'uid' field is omitted since it is not needed for in-process admission review.
@ -188,40 +91,43 @@ type CompilationResult struct {
ExpressionAccessor ExpressionAccessor
}
// Compiler provides a CEL expression compiler configured with the desired admission related CEL variables and
// environment mode.
type Compiler interface {
CompileCELExpression(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) CompilationResult
}
type compiler struct {
varEnvs variableDeclEnvs
}
func NewCompiler(env *environment.EnvSet) Compiler {
return &compiler{varEnvs: mustBuildEnvs(env)}
}
type variableDeclEnvs map[OptionalVariableDeclarations]*environment.EnvSet
// CompileCELExpression returns a compiled CEL expression.
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations, perCallLimit uint64) CompilationResult {
var env *cel.Env
envs, err := getEnvs()
if err != nil {
func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, envType environment.Type) CompilationResult {
resultError := func(errorString string, errType apiservercel.ErrorType) CompilationResult {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: "compiler initialization failed: " + err.Error(),
},
ExpressionAccessor: expressionAccessor,
}
}
env, ok := envs[optionalVars]
if !ok {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: fmt.Sprintf("compiler initialization failed: failed to load environment for %v", optionalVars),
Type: errType,
Detail: errorString,
},
ExpressionAccessor: expressionAccessor,
}
}
env, err := c.varEnvs[options].Env(envType)
if err != nil {
return resultError(fmt.Sprintf("unexpected error loading CEL environment: %v", err), apiservercel.ErrorTypeInternal)
}
ast, issues := env.Compile(expressionAccessor.GetExpression())
if issues != nil {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: "compilation failed: " + issues.String(),
},
ExpressionAccessor: expressionAccessor,
}
return resultError("compilation failed: "+issues.String(), apiservercel.ErrorTypeInvalid)
}
found := false
returnTypes := expressionAccessor.ReturnTypes()
@ -239,43 +145,61 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars Op
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
}
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: reason,
},
ExpressionAccessor: expressionAccessor,
}
return resultError(reason, apiservercel.ErrorTypeInvalid)
}
_, err = cel.AstToCheckedExpr(ast)
if err != nil {
// should be impossible since env.Compile returned no issues
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInternal,
Detail: "unexpected compilation error: " + err.Error(),
},
ExpressionAccessor: expressionAccessor,
}
return resultError("unexpected compilation error: "+err.Error(), apiservercel.ErrorTypeInternal)
}
prog, err := env.Program(ast,
cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
cel.CostLimit(perCallLimit),
)
if err != nil {
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: "program instantiation failed: " + err.Error(),
},
ExpressionAccessor: expressionAccessor,
}
return resultError("program instantiation failed: "+err.Error(), apiservercel.ErrorTypeInternal)
}
return CompilationResult{
Program: prog,
ExpressionAccessor: expressionAccessor,
}
}
func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
requestType := BuildRequestType()
envs := make(variableDeclEnvs, 4) // since the number of variable combinations is small, pre-build a environment for each
for _, hasParams := range []bool{false, true} {
for _, hasAuthorizer := range []bool{false, true} {
var envOpts []cel.EnvOption
if hasParams {
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
}
if hasAuthorizer {
envOpts = append(envOpts,
cel.Variable(AuthorizerVarName, library.AuthorizerType),
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
}
envOpts = append(envOpts,
cel.Variable(ObjectVarName, cel.DynType),
cel.Variable(OldObjectVarName, cel.DynType),
cel.Variable(RequestVarName, requestType.CelType()))
extended, err := baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: []*apiservercel.DeclType{
requestType,
},
},
)
if err != nil {
panic(fmt.Sprintf("environment misconfigured: %v", err))
}
envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer}] = extended
}
}
return envs
}

View File

@ -17,12 +17,15 @@ limitations under the License.
package cel
import (
"math/rand"
"strings"
"testing"
celgo "github.com/google/cel-go/cel"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
)
func TestCompileValidatingPolicyExpression(t *testing.T) {
@ -32,6 +35,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
hasParams bool
hasAuthorizer bool
errorExpressions map[string]string
envType environment.Type
}{
{
name: "invalid syntax",
@ -117,25 +121,57 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
"authorizer.group('') != null": "undeclared reference to 'authorizer'",
},
},
{
name: "compile with storage environment should recognize functions available only in the storage environment",
expressions: []string{
"test() == true",
},
envType: environment.StoredExpressions,
},
{
name: "compile with supported environment should not recognize functions available only in the storage environment",
errorExpressions: map[string]string{
"test() == true": "undeclared reference to 'test'",
},
envType: environment.NewExpressions,
},
}
// Include the test library, which includes the test() function in the storage environment during test
base := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())
extended, err := base.Extend(environment.VersionedOptions{
IntroducedVersion: version.MajorMinor(1, 999),
EnvOptions: []celgo.EnvOption{library.Test()},
})
if err != nil {
t.Fatal(err)
}
compiler := NewCompiler(extended)
for _, tc := range cases {
envType := tc.envType
if envType == "" {
envType = environment.NewExpressions
}
t.Run(tc.name, func(t *testing.T) {
for _, expr := range tc.expressions {
t.Run(expr, func(t *testing.T) {
t.Run("expression", func(t *testing.T) {
result := CompileCELExpression(&fakeValidationCondition{
options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
result := compiler.CompileCELExpression(&fakeValidationCondition{
Expression: expr,
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
}, options, envType)
if result.Error != nil {
t.Errorf("Unexpected error: %v", result.Error)
}
})
t.Run("auditAnnotation.valueExpression", func(t *testing.T) {
// Test audit annotation compilation by casting the result to a string
result := CompileCELExpression(&fakeAuditAnnotationCondition{
options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
result := compiler.CompileCELExpression(&fakeAuditAnnotationCondition{
ValueExpression: "string(" + expr + ")",
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
}, options, envType)
if result.Error != nil {
t.Errorf("Unexpected error: %v", result.Error)
}
@ -145,9 +181,10 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
for expr, expectErr := range tc.errorExpressions {
t.Run(expr, func(t *testing.T) {
t.Run("expression", func(t *testing.T) {
result := CompileCELExpression(&fakeValidationCondition{
options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
result := compiler.CompileCELExpression(&fakeValidationCondition{
Expression: expr,
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
}, options, envType)
if result.Error == nil {
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
return
@ -158,9 +195,10 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
})
t.Run("auditAnnotation.valueExpression", func(t *testing.T) {
// Test audit annotation compilation by casting the result to a string
result := CompileCELExpression(&fakeAuditAnnotationCondition{
options := OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}
result := compiler.CompileCELExpression(&fakeAuditAnnotationCondition{
ValueExpression: "string(" + expr + ")",
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
}, options, envType)
if result.Error == nil {
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
return
@ -175,6 +213,21 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
}
}
func BenchmarkCompile(b *testing.B) {
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
b.ResetTimer()
for i := 0; i < b.N; i++ {
options := OptionalVariableDeclarations{HasParams: rand.Int()%2 == 0, HasAuthorizer: rand.Int()%2 == 0}
result := compiler.CompileCELExpression(&fakeValidationCondition{
Expression: "object.foo < object.bar",
}, options, environment.StoredExpressions)
if result.Error != nil {
b.Fatal(result.Error)
}
}
}
type fakeValidationCondition struct {
Expression string
}

View File

@ -32,15 +32,17 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/library"
)
// filterCompiler implement the interface FilterCompiler.
type filterCompiler struct {
compiler Compiler
}
func NewFilterCompiler() FilterCompiler {
return &filterCompiler{}
func NewFilterCompiler(env *environment.EnvSet) FilterCompiler {
return &filterCompiler{compiler: NewCompiler(env)}
}
type evaluationActivation struct {
@ -75,13 +77,13 @@ func (a *evaluationActivation) Parent() interpreter.Activation {
}
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, perCallLimit uint64) Filter {
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) Filter {
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
if expressionAccessor == nil {
continue
}
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
}
return NewFilter(compilationResults)
}

View File

@ -28,6 +28,8 @@ import (
celtypes "github.com/google/cel-go/common/types"
"github.com/stretchr/testify/require"
"k8s.io/utils/pointer"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -38,7 +40,7 @@ import (
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/utils/pointer"
"k8s.io/apiserver/pkg/cel/environment"
)
type condition struct {
@ -91,8 +93,8 @@ func TestCompile(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var c filterCompiler
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, celconfig.PerCallLimit)
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))}
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.NewExpressions)
if e == nil {
t.Fatalf("unexpected nil validator")
}
@ -643,11 +645,20 @@ func TestFilter(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := filterCompiler{}
if tc.testPerCallLimit == 0 {
tc.testPerCallLimit = celconfig.PerCallLimit
}
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}, tc.testPerCallLimit)
env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend(
environment.VersionedOptions{
IntroducedVersion: environment.DefaultCompatibilityVersion(),
ProgramOptions: []celgo.ProgramOption{celgo.CostLimit(tc.testPerCallLimit)},
},
)
if err != nil {
t.Fatal(err)
}
c := NewFilterCompiler(env)
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}, environment.NewExpressions)
if f == nil {
t.Fatalf("unexpected nil validator")
}
@ -789,8 +800,8 @@ func TestRuntimeCELCostBudget(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := filterCompiler{}
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: false}, celconfig.PerCallLimit)
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))}
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: false}, environment.NewExpressions)
if f == nil {
t.Fatalf("unexpected nil validator")
}

View File

@ -27,6 +27,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
)
type ExpressionAccessor interface {
@ -57,8 +58,7 @@ type OptionalVariableDeclarations struct {
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
type FilterCompiler interface {
// Compile is used for the cel expression compilation
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input.
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, perCallLimit uint64) Filter
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter
}
// OptionalVariableBindings provides expression bindings for optional CEL variables.

View File

@ -48,6 +48,7 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/warning"
dynamicfake "k8s.io/client-go/dynamic/fake"
@ -210,7 +211,7 @@ func (f *fakeCompiler) HasSynced() bool {
func (f *fakeCompiler) Compile(
expressions []cel.ExpressionAccessor,
options cel.OptionalVariableDeclarations,
perCallLimit uint64,
envType environment.Type,
) cel.Filter {
if len(expressions) > 0 && expressions[0] != nil {
key := expressions[0].GetExpression()
@ -708,9 +709,9 @@ func must3[T any, I any](val T, _ I, err error) T {
return val
}
////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////
// Functionality Tests
////////////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////////
func TestPluginNotReady(t *testing.T) {
reset()
@ -1031,7 +1032,7 @@ func TestReconfigureBinding(t *testing.T) {
// Expect validation to fail the third time due to validation failure
require.ErrorContains(t, err, `Denied`, "expected a true policy failure, not a configuration error")
//require.Equal(t, []*unstructured.Unstructured{fakeParams, fakeParams2}, passedParams, "expected call to `Validate` to cause call to evaluator")
// require.Equal(t, []*unstructured.Unstructured{fakeParams, fakeParams2}, passedParams, "expected call to `Validate` to cause call to evaluator")
require.Equal(t, 2, numCompiles, "expect changing binding causes a recompile")
}
@ -1162,7 +1163,7 @@ func TestRemoveBinding(t *testing.T) {
),
`Denied`)
//require.Equal(t, []*unstructured.Unstructured{fakeParams}, passedParams)
// require.Equal(t, []*unstructured.Unstructured{fakeParams}, passedParams)
require.NoError(t, tracker.Delete(bindingsGVR, denyBinding.Namespace, denyBinding.Name))
require.NoError(t, waitForReconcileDeletion(testContext, controller, denyBinding))
}

View File

@ -43,6 +43,7 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/apiserver/pkg/warning"
"k8s.io/client-go/dynamic"
@ -140,7 +141,7 @@ func NewAdmissionController(
client,
dynamicClient,
typeChecker,
cel.NewFilterCompiler(),
cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())),
NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),

View File

@ -36,8 +36,8 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
"k8s.io/client-go/informers"
@ -512,13 +512,13 @@ func (c *policyController) latestPolicyData() []policyData {
for i := range matchConditions {
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
}
matcher = matchconditions.NewMatcher(c.filterCompiler.Compile(matchExpressionAccessors, optionalVars, celconfig.PerCallLimit), c.authz, failurePolicy, "validatingadmissionpolicy", definitionInfo.lastReconciledValue.Name)
matcher = matchconditions.NewMatcher(c.filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), c.authz, failurePolicy, "validatingadmissionpolicy", definitionInfo.lastReconciledValue.Name)
}
bindingInfo.validator = c.newValidator(
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, environment.StoredExpressions),
matcher,
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, celconfig.PerCallLimit),
c.filterCompiler.Compile(convertV1Alpha1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, celconfig.PerCallLimit),
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
c.filterCompiler.Compile(convertV1Alpha1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
failurePolicy,
c.authz,
)

View File

@ -21,20 +21,20 @@ import (
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
"k8s.io/api/admissionregistration/v1alpha1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/version"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/apiserver/pkg/cel/openapi"
"k8s.io/apiserver/pkg/cel/openapi/resolver"
"k8s.io/klog/v2"
@ -128,7 +128,7 @@ func (c *TypeChecker) CheckExpressions(expressions []string, hasParams bool, pol
for i, gvk := range gvks {
s := schemas[i]
issues, err := c.checkExpression(exp, hasParams, typeOverwrite{
object: common.SchemaDeclType(s, true),
object: common.SchemaDeclType(s, true).MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)),
params: paramsDeclType,
})
// save even if no issues are found, for the sake of formatting.
@ -144,6 +144,10 @@ func (c *TypeChecker) CheckExpressions(expressions []string, hasParams bool, pol
return allWarnings
}
func generateUniqueTypeName(kind string) string {
return fmt.Sprintf("%s%d", kind, time.Now().Nanosecond())
}
// formatWarning converts the resulting issues and possible error during
// type checking into a human-readable string
func (c *TypeChecker) formatWarning(results []typeCheckingResult) string {
@ -169,7 +173,7 @@ func (c *TypeChecker) declType(gvk schema.GroupVersionKind) (*apiservercel.DeclT
if err != nil {
return nil, err
}
return common.SchemaDeclType(&openapi.Schema{Schema: s}, true), nil
return common.SchemaDeclType(&openapi.Schema{Schema: s}, true).MaybeAssignTypeName(generateUniqueTypeName(gvk.Kind)), nil
}
func (c *TypeChecker) paramsType(policy *v1alpha1.ValidatingAdmissionPolicy) schema.GroupVersionKind {
@ -314,122 +318,51 @@ func sortGVKList(list []schema.GroupVersionKind) []schema.GroupVersionKind {
}
func buildEnv(hasParams bool, types typeOverwrite) (*cel.Env, error) {
baseEnv, err := getBaseEnv()
if err != nil {
return nil, err
}
reg := apiservercel.NewRegistry(baseEnv)
baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())
requestType := plugincel.BuildRequestType()
var varOpts []cel.EnvOption
var rts []*apiservercel.RuleTypes
var declTypes []*apiservercel.DeclType
// request, hand-crafted type
rt, opts, err := createRuleTypesAndOptions(reg, requestType, plugincel.RequestVarName)
if err != nil {
return nil, err
}
rts = append(rts, rt)
varOpts = append(varOpts, opts...)
declTypes = append(declTypes, requestType)
varOpts = append(varOpts, createVariableOpts(requestType, plugincel.RequestVarName)...)
// object and oldObject, same type, type(s) resolved from constraints
rt, opts, err = createRuleTypesAndOptions(reg, types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)
if err != nil {
return nil, err
}
rts = append(rts, rt)
varOpts = append(varOpts, opts...)
declTypes = append(declTypes, types.object)
varOpts = append(varOpts, createVariableOpts(types.object, plugincel.ObjectVarName, plugincel.OldObjectVarName)...)
// params, defined by ParamKind
if hasParams {
rt, opts, err := createRuleTypesAndOptions(reg, types.params, plugincel.ParamsVarName)
if err != nil {
return nil, err
}
rts = append(rts, rt)
varOpts = append(varOpts, opts...)
if hasParams && types.params != nil {
declTypes = append(declTypes, types.params)
varOpts = append(varOpts, createVariableOpts(types.params, plugincel.ParamsVarName)...)
}
opts, err = ruleTypesOpts(rts, baseEnv.TypeProvider())
env, err := baseEnv.Extend(
environment.VersionedOptions{
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: varOpts,
DeclTypes: declTypes,
},
)
if err != nil {
return nil, err
}
opts = append(opts, varOpts...) // add variables after ruleTypes.
env, err := baseEnv.Extend(opts...)
if err != nil {
return nil, err
}
return env, nil
return env.Env(environment.StoredExpressions)
}
// createRuleTypeAndOptions creates the cel RuleTypes and a slice of EnvOption
// createVariableOpts creates a slice of EnvOption
// that can be used for creating a CEL env containing variables of declType.
// declType can be nil, in which case the variables will be of DynType.
func createRuleTypesAndOptions(registry *apiservercel.Registry, declType *apiservercel.DeclType, variables ...string) (*apiservercel.RuleTypes, []cel.EnvOption, error) {
func createVariableOpts(declType *apiservercel.DeclType, variables ...string) []cel.EnvOption {
opts := make([]cel.EnvOption, 0, len(variables))
// untyped, use DynType
if declType == nil {
for _, v := range variables {
opts = append(opts, cel.Variable(v, cel.DynType))
}
return nil, opts, nil
}
// create a RuleType for the given type
rt, err := apiservercel.NewRuleTypes(declType.TypeName(), declType, registry)
if err != nil {
return nil, nil, err
}
if rt == nil {
return nil, nil, nil
t := cel.DynType
if declType != nil {
t = declType.CelType()
}
for _, v := range variables {
opts = append(opts, cel.Variable(v, declType.CelType()))
opts = append(opts, cel.Variable(v, t))
}
return rt, opts, nil
return opts
}
func ruleTypesOpts(ruleTypes []*apiservercel.RuleTypes, underlyingTypeProvider ref.TypeProvider) ([]cel.EnvOption, error) {
var providers []ref.TypeProvider // may be unused, too small to matter
var adapters []ref.TypeAdapter
for _, rt := range ruleTypes {
if rt != nil {
withTP, err := rt.WithTypeProvider(underlyingTypeProvider)
if err != nil {
return nil, err
}
providers = append(providers, withTP)
adapters = append(adapters, withTP)
}
}
var tp ref.TypeProvider
var ta ref.TypeAdapter
switch len(providers) {
case 0:
return nil, nil
case 1:
tp = providers[0]
ta = adapters[0]
default:
tp = &apiservercel.CompositedTypeProvider{Providers: providers}
ta = &apiservercel.CompositedTypeAdapter{Adapters: adapters}
}
return []cel.EnvOption{cel.CustomTypeProvider(tp), cel.CustomTypeAdapter(ta)}, nil
}
func getBaseEnv() (*cel.Env, error) {
typeCheckingBaseEnvInit.Do(func() {
var opts []cel.EnvOption
opts = append(opts, cel.HomogeneousAggregateLiterals())
// Validate function declarations once during base env initialization,
// so they don't need to be evaluated each time a CEL rule is compiled.
// This is a relatively expensive operation.
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
opts = append(opts, library.ExtensionLibs...)
typeCheckingBaseEnv, typeCheckingBaseEnvError = cel.NewEnv(opts...)
})
return typeCheckingBaseEnv, typeCheckingBaseEnvError
}
var typeCheckingBaseEnv *cel.Env
var typeCheckingBaseEnvError error
var typeCheckingBaseEnvInit sync.Once

View File

@ -36,6 +36,7 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/environment"
)
var _ cel.Filter = &fakeCelFilter{}
@ -928,8 +929,8 @@ func TestContextCanceled(t *testing.T) {
fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
fc := cel.NewFilterCompiler()
f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, celconfig.PerCallLimit)
fc := cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions)
v := validator{
failPolicy: &fail,
celMatcher: &fakeCELMatcher{matches: true},

View File

@ -26,8 +26,8 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/rest"
)
@ -140,7 +140,7 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler
HasParams: false,
HasAuthorizer: true,
},
celconfig.PerCallLimit,
environment.StoredExpressions,
), authorizer, m.FailurePolicy, "validating", m.Name)
})
return m.compiledMatcher
@ -268,7 +268,7 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil
HasParams: false,
HasAuthorizer: true,
},
celconfig.PerCallLimit,
environment.StoredExpressions,
), authorizer, v.FailurePolicy, "validating", v.Name)
})
return v.compiledMatcher

View File

@ -35,6 +35,7 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/cel/environment"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
@ -97,7 +98,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
namespaceMatcher: &namespace.Matcher{},
objectMatcher: &object.Matcher{},
dispatcher: dispatcherFactory(&cm),
filterCompiler: cel.NewFilterCompiler(),
filterCompiler: cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())),
}, nil
}

View File

@ -1,119 +0,0 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"github.com/google/cel-go/common/types/ref"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
var _ ref.TypeProvider = (*CompositedTypeProvider)(nil)
var _ ref.TypeAdapter = (*CompositedTypeAdapter)(nil)
// CompositedTypeProvider is the provider that tries each of the underlying
// providers in order, and returns result of the first successful attempt.
type CompositedTypeProvider struct {
// Providers contains the underlying type providers.
// If Providers is empty, the CompositedTypeProvider becomes no-op provider.
Providers []ref.TypeProvider
}
// EnumValue finds out the numeric value of the given enum name.
// The result comes from first provider that returns non-nil.
func (c *CompositedTypeProvider) EnumValue(enumName string) ref.Val {
for _, p := range c.Providers {
val := p.EnumValue(enumName)
if val != nil {
return val
}
}
return nil
}
// FindIdent takes a qualified identifier name and returns a Value if one
// exists. The result comes from first provider that returns non-nil.
func (c *CompositedTypeProvider) FindIdent(identName string) (ref.Val, bool) {
for _, p := range c.Providers {
val, ok := p.FindIdent(identName)
if ok {
return val, ok
}
}
return nil, false
}
// FindType finds the Type given a qualified type name, or return false
// if none of the providers finds the type.
// If any of the providers find the type, the first provider that returns true
// will be the result.
func (c *CompositedTypeProvider) FindType(typeName string) (*exprpb.Type, bool) {
for _, p := range c.Providers {
typ, ok := p.FindType(typeName)
if ok {
return typ, ok
}
}
return nil, false
}
// FindFieldType returns the field type for a checked type value. Returns
// false if none of the providers can find the type.
// If multiple providers can find the field, the result is taken from
// the first that does.
func (c *CompositedTypeProvider) FindFieldType(messageType string, fieldName string) (*ref.FieldType, bool) {
for _, p := range c.Providers {
ft, ok := p.FindFieldType(messageType, fieldName)
if ok {
return ft, ok
}
}
return nil, false
}
// NewValue creates a new type value from a qualified name and map of field
// name to value.
// If multiple providers can create the new type, the first that returns
// non-nil will decide the result.
func (c *CompositedTypeProvider) NewValue(typeName string, fields map[string]ref.Val) ref.Val {
for _, p := range c.Providers {
v := p.NewValue(typeName, fields)
if v != nil {
return v
}
}
return nil
}
// CompositedTypeAdapter is the adapter that tries each of the underlying
// type adapter in order until the first successfully conversion.
type CompositedTypeAdapter struct {
// Adapters contains underlying type adapters.
// If Adapters is empty, the CompositedTypeAdapter becomes a no-op adapter.
Adapters []ref.TypeAdapter
}
// NativeToValue takes the value and convert it into a ref.Val
// The result comes from the first TypeAdapter that returns non-nil.
func (c *CompositedTypeAdapter) NativeToValue(value interface{}) ref.Val {
for _, a := range c.Adapters {
v := a.NativeToValue(value)
if v != nil {
return v
}
}
return nil
}

117
pkg/cel/environment/base.go Normal file
View File

@ -0,0 +1,117 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package environment
import (
"fmt"
"strconv"
"sync"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/ext"
"golang.org/x/sync/singleflight"
"k8s.io/apimachinery/pkg/util/version"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/cel/library"
)
// DefaultCompatibilityVersion returns a default compatibility version for use with EnvSet
// that guarantees compatibility with CEL features/libraries/parameters understood by
// an n-1 version
//
// This default will be set to no more than n-1 the current Kubernetes major.minor version.
//
// Note that a default version number less than n-1 indicates a wider range of version
// compatibility than strictly required for rollback. A wide range of compatibility is
// desirable because it means that CEL expressions are portable across a wider range
// of Kubernetes versions.
func DefaultCompatibilityVersion() *version.Version {
return version.MajorMinor(1, 27)
}
var baseOpts = []VersionedOptions{
{
// CEL epoch was actually 1.23, but we artificially set it to 1.0 because these
// options should always be present.
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: []cel.EnvOption{
cel.HomogeneousAggregateLiterals(),
// Validate function declarations once during base env initialization,
// so they don't need to be evaluated each time a CEL rule is compiled.
// This is a relatively expensive operation.
cel.EagerlyValidateDeclarations(true),
cel.DefaultUTCTimeZone(true),
ext.Strings(),
library.URLs(),
library.Regex(),
library.Lists(),
},
ProgramOptions: []cel.ProgramOption{
cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
cel.CostLimit(celconfig.PerCallLimit),
},
},
{
IntroducedVersion: version.MajorMinor(1, 27),
EnvOptions: []cel.EnvOption{
library.Authz(),
},
},
{
IntroducedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
cel.CrossTypeNumericComparisons(true),
// TODO: Add CEL Optionals once we bump cel-go
},
},
}
// MustBaseEnvSet returns the common CEL base environments for Kubernetes for Version, or panics
// if the version is nil, or does not have major and minor components.
//
// The returned environment contains function libraries, language settings, optimizations and
// runtime cost limits appropriate CEL as it is used in Kubernetes.
//
// The returned environment contains no CEL variable definitions or custom type declarations and
// should be extended to construct environments with the appropriate variable definitions,
// type declarations and any other needed configuration.
func MustBaseEnvSet(ver *version.Version) *EnvSet {
if ver == nil {
panic("version must be non-nil")
}
if len(ver.Components()) < 2 {
panic(fmt.Sprintf("version must contain an major and minor component, but got: %s", ver.String()))
}
key := strconv.FormatUint(uint64(ver.Major()), 10) + "." + strconv.FormatUint(uint64(ver.Minor()), 10)
if entry, ok := baseEnvs.Load(key); ok {
return entry.(*EnvSet)
}
entry, _, _ := baseEnvsSingleflight.Do(key, func() (interface{}, error) {
entry := mustNewEnvSet(ver, baseOpts)
baseEnvs.Store(key, entry)
return entry, nil
})
return entry.(*EnvSet)
}
var (
baseEnvs = sync.Map{}
baseEnvsSingleflight = &singleflight.Group{}
)

View File

@ -0,0 +1,43 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package environment
import (
"testing"
"k8s.io/apimachinery/pkg/util/version"
)
// BenchmarkLoadBaseEnv is expected to be very fast, because a
// a cached environment is loaded for each MustBaseEnvSet call.
func BenchmarkLoadBaseEnv(b *testing.B) {
ver := DefaultCompatibilityVersion()
MustBaseEnvSet(ver)
b.ResetTimer()
for i := 0; i < b.N; i++ {
MustBaseEnvSet(ver)
}
}
// BenchmarkLoadBaseEnvDifferentVersions is expected to be relatively slow, because a
// a new environment must be created for each MustBaseEnvSet call.
func BenchmarkLoadBaseEnvDifferentVersions(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
MustBaseEnvSet(version.MajorMinor(1, uint(i)))
}
}

View File

@ -0,0 +1,274 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package environment
import (
"fmt"
"math"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/version"
apiservercel "k8s.io/apiserver/pkg/cel"
)
// Type defines the different types of CEL environments used in Kubernetes.
// CEL environments are used to compile and evaluate CEL expressions.
// Environments include:
// - Function libraries
// - Variables
// - Types (both core CEL types and Kubernetes types)
// - Other CEL environment and program options
type Type string
const (
// NewExpressions is used to validate new or modified expressions in
// requests that write expressions to API resources.
//
// This environment type is compatible with a specific Kubernetes
// major/minor version. To ensure safe rollback, this environment type
// may not include all the function libraries, variables, type declarations, and CEL
// language settings available in the StoredExpressions environment type.
//
// NewExpressions must be used to validate (parse, compile, type check)
// all new or modified CEL expressions before they are written to storage.
NewExpressions Type = "NewExpressions"
// StoredExpressions is used to compile and run CEL expressions that have been
// persisted to storage.
//
// This environment type is compatible with CEL expressions that have been
// persisted to storage by all known versions of Kubernetes. This is the most
// permissive environment available.
//
// StoredExpressions is appropriate for use with CEL expressions in
// configuration files.
StoredExpressions Type = "StoredExpressions"
)
// EnvSet manages the creation and extension of CEL environments. Each EnvSet contains
// both an NewExpressions and StoredExpressions environment. EnvSets are created
// and extended using VersionedOptions so that the EnvSet can prepare environments according
// to what options were introduced at which versions.
//
// Each EnvSet is given a compatibility version when it is created, and prepares the
// NewExpressions environment to be compatible with that version. The EnvSet also
// prepares StoredExpressions to be compatible with all known versions of Kubernetes.
type EnvSet struct {
// compatibilityVersion is the version that all configuration in
// the NewExpressions environment is compatible with.
compatibilityVersion *version.Version
// newExpressions is an environment containing only configuration
// in this EnvSet that is enabled at this compatibilityVersion.
newExpressions *cel.Env
// storedExpressions is an environment containing the latest configuration
// in this EnvSet.
storedExpressions *cel.Env
}
func newEnvSet(compatibilityVersion *version.Version, opts []VersionedOptions) (*EnvSet, error) {
base, err := cel.NewEnv()
if err != nil {
return nil, err
}
baseSet := EnvSet{compatibilityVersion: compatibilityVersion, newExpressions: base, storedExpressions: base}
return baseSet.Extend(opts...)
}
func mustNewEnvSet(ver *version.Version, opts []VersionedOptions) *EnvSet {
envSet, err := newEnvSet(ver, opts)
if err != nil {
panic(fmt.Sprintf("Default environment misconfigured: %v", err))
}
return envSet
}
// NewExpressionsEnv returns the NewExpressions environment Type for this EnvSet.
// See NewExpressions for details.
func (e *EnvSet) NewExpressionsEnv() *cel.Env {
return e.newExpressions
}
// StoredExpressionsEnv returns the StoredExpressions environment Type for this EnvSet.
// See StoredExpressions for details.
func (e *EnvSet) StoredExpressionsEnv() *cel.Env {
return e.storedExpressions
}
// Env returns the CEL environment for the given Type.
func (e *EnvSet) Env(envType Type) (*cel.Env, error) {
switch envType {
case NewExpressions:
return e.newExpressions, nil
case StoredExpressions:
return e.storedExpressions, nil
default:
return nil, fmt.Errorf("unsupported environment type: %v", envType)
}
}
// VersionedOptions provides a set of CEL configuration options as well as the version the
// options were introduced and, optionally, the version the options were removed.
type VersionedOptions struct {
// IntroducedVersion is the version at which these options were introduced.
// The NewExpressions environment will only include options introduced at or before the
// compatibility version of the EnvSet.
//
// For example, to configure a CEL environment with an "object" variable bound to a
// resource kind, first create a DeclType from the groupVersionKind of the resource and then
// populate a VersionedOptions with the variable and the type:
//
// schema := schemaResolver.ResolveSchema(groupVersionKind)
// objectType := apiservercel.SchemaDeclType(schema, true)
// ...
// VersionOptions{
// IntroducedVersion: version.MajorMinor(1, 26),
// DeclTypes: []*apiservercel.DeclType{ objectType },
// EnvOptions: []cel.EnvOption{ cel.Variable("object", objectType.CelType()) },
// },
//
// To create an DeclType from a CRD, use a structural schema. For example:
//
// schema := structuralschema.NewStructural(crdJSONProps)
// objectType := apiservercel.SchemaDeclType(schema, true)
//
// Required.
IntroducedVersion *version.Version
// RemovedVersion is the version at which these options were removed.
// The NewExpressions environment will not include options removed at or before the
// compatibility version of the EnvSet.
//
// All option removals must be backward compatible; the removal must either be paired
// with a compatible replacement introduced at the same version, or the removal must be non-breaking.
// The StoredExpressions environment will not include removed options.
//
// A function library may be upgraded by setting the RemovedVersion of the old library
// to the same value as the IntroducedVersion of the new library. The new library must
// be backward compatible with the old library.
//
// For example:
//
// VersionOptions{
// IntroducedVersion: version.MajorMinor(1, 26), RemovedVersion: version.MajorMinor(1, 27),
// EnvOptions: []cel.EnvOption{ libraries.Example(libraries.ExampleVersion(1)) },
// },
// VersionOptions{
// IntroducedVersion: version.MajorMinor(1, 27),
// EnvOptions: []EnvOptions{ libraries.Example(libraries.ExampleVersion(2)) },
// },
//
// Optional.
RemovedVersion *version.Version
// EnvOptions provides CEL EnvOptions. This may be used to add a cel.Variable, a
// cel.Library, or to enable other CEL EnvOptions such as language settings.
//
// If an added cel.Variable has an OpenAPI type, the type must be included in DeclTypes.
EnvOptions []cel.EnvOption
// ProgramOptions provides CEL ProgramOptions. This may be used to set a cel.CostLimit,
// enable optimizations, and set other program level options that should be enabled
// for all programs using this environment.
ProgramOptions []cel.ProgramOption
// DeclTypes provides OpenAPI type declarations to register with the environment.
//
// If cel.Variables added to EnvOptions refer to a OpenAPI type, the type must be included in
// DeclTypes.
DeclTypes []*apiservercel.DeclType
}
// Extend returns an EnvSet based on this EnvSet but extended with given VersionedOptions.
// This EnvSet is not mutated.
// The returned EnvSet has the same compatibility version as the EnvSet that was extended.
//
// Extend is an expensive operation and each call to Extend that adds DeclTypes increases
// the depth of a chain of resolvers. For these reasons, calls to Extend should be kept
// to a minimum.
//
// Some best practices:
//
// - Minimize calls Extend when handling API requests. Where possible, call Extend
// when initializing components.
// - If an EnvSets returned by Extend can be used to compile multiple CEL programs,
// call Extend once and reuse the returned EnvSets.
// - Prefer a single call to Extend with a full list of VersionedOptions over
// making multiple calls to Extend.
func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
if len(options) > 0 {
newExprOpts, err := e.filterAndBuildOpts(e.newExpressions, e.compatibilityVersion, options)
if err != nil {
return nil, err
}
p, err := e.newExpressions.Extend(newExprOpts)
if err != nil {
return nil, err
}
storedExprOpt, err := e.filterAndBuildOpts(e.storedExpressions, version.MajorMinor(math.MaxUint, math.MaxUint), options)
if err != nil {
return nil, err
}
s, err := e.storedExpressions.Extend(storedExprOpt)
if err != nil {
return nil, err
}
return &EnvSet{compatibilityVersion: e.compatibilityVersion, newExpressions: p, storedExpressions: s}, nil
}
return e, nil
}
func (e *EnvSet) filterAndBuildOpts(base *cel.Env, compatVer *version.Version, opts []VersionedOptions) (cel.EnvOption, error) {
var envOpts []cel.EnvOption
var progOpts []cel.ProgramOption
var declTypes []*apiservercel.DeclType
for _, opt := range opts {
if compatVer.AtLeast(opt.IntroducedVersion) && (opt.RemovedVersion == nil || compatVer.LessThan(opt.RemovedVersion)) {
envOpts = append(envOpts, opt.EnvOptions...)
progOpts = append(progOpts, opt.ProgramOptions...)
declTypes = append(declTypes, opt.DeclTypes...)
}
}
if len(declTypes) > 0 {
provider := apiservercel.NewDeclTypeProvider(declTypes...)
providerOpts, err := provider.EnvOptions(base.TypeProvider())
if err != nil {
return nil, err
}
envOpts = append(envOpts, providerOpts...)
}
combined := cel.Lib(&envLoader{
envOpts: envOpts,
progOpts: progOpts,
})
return combined, nil
}
type envLoader struct {
envOpts []cel.EnvOption
progOpts []cel.ProgramOption
}
func (e *envLoader) CompileOptions() []cel.EnvOption {
return e.envOpts
}
func (e *envLoader) ProgramOptions() []cel.ProgramOption {
return e.progOpts
}

View File

@ -0,0 +1,326 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package environment
import (
"context"
"fmt"
"testing"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
)
type envTypeAndVersion struct {
version *version.Version
envType Type
}
func TestBaseEnvironment(t *testing.T) {
widgetsType := apiservercel.NewObjectType("Widget",
map[string]*apiservercel.DeclField{
"x": {
Name: "x",
Type: apiservercel.StringType,
},
})
cases := []struct {
name string
typeVersionCombinations []envTypeAndVersion
validExpressions []string
invalidExpressions []string
activation any
opts []VersionedOptions
}{
{
name: "core settings enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 23), NewExpressions},
{version.MajorMinor(1, 23), StoredExpressions},
},
validExpressions: []string{
"[1, 2, 3].indexOf(2) == 1", // lists
"'abc'.contains('bc')", //strings
"isURL('http://example.com')", // urls
"'a 1 b 2'.find('[0-9]') == '1'", // regex
},
},
{
name: "authz disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 26), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"authorizer.path('/healthz').check('get').allowed()"},
activation: map[string]any{"authorizer": library.NewAuthorizerVal(nil, fakeAuthorizer{decision: authorizer.DecisionAllow})},
opts: []VersionedOptions{
{IntroducedVersion: version.MajorMinor(1, 27), EnvOptions: []cel.EnvOption{cel.Variable("authorizer", library.AuthorizerType)}},
},
},
{
name: "authz enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
{version.MajorMinor(1, 26), StoredExpressions},
},
validExpressions: []string{"authorizer.path('/healthz').check('get').allowed()"},
activation: map[string]any{"authorizer": library.NewAuthorizerVal(nil, fakeAuthorizer{decision: authorizer.DecisionAllow})},
opts: []VersionedOptions{
{IntroducedVersion: version.MajorMinor(1, 27), EnvOptions: []cel.EnvOption{cel.Variable("authorizer", library.AuthorizerType)}},
},
},
{
name: "cross numeric comparisons disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"1.5 > 1"},
},
{
name: "cross numeric comparisons enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
{version.MajorMinor(1, 27), StoredExpressions},
},
validExpressions: []string{"1.5 > 1"},
},
{
name: "user defined variable disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"fizz == 'buzz'"},
activation: map[string]any{"fizz": "buzz"},
opts: []VersionedOptions{
{IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{cel.Variable("fizz", cel.StringType)}},
},
},
{
name: "user defined variable enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
{version.MajorMinor(1, 27), StoredExpressions},
},
validExpressions: []string{"fizz == 'buzz'"},
activation: map[string]any{"fizz": "buzz"},
opts: []VersionedOptions{
{IntroducedVersion: version.MajorMinor(1, 28), EnvOptions: []cel.EnvOption{cel.Variable("fizz", cel.StringType)}},
},
},
{
name: "declared type enabled before removed",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
// always disabled for StoredExpressions
},
validExpressions: []string{"widget.x == 'buzz'"},
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
RemovedVersion: version.MajorMinor(1, 29),
DeclTypes: []*apiservercel.DeclType{widgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("widget", cel.ObjectType("Widget")),
},
},
},
},
{
name: "declared type disabled after removed",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 29), NewExpressions},
{version.MajorMinor(1, 29), StoredExpressions},
},
invalidExpressions: []string{"widget.x == 'buzz'"},
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
RemovedVersion: version.MajorMinor(1, 29),
DeclTypes: []*apiservercel.DeclType{widgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("widget", cel.ObjectType("Widget")),
},
},
},
},
{
name: "declared type disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
// always enabled for StoredExpressions
},
invalidExpressions: []string{"widget.x == 'buzz'"},
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
DeclTypes: []*apiservercel.DeclType{widgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("widget", widgetsType.CelType()),
},
},
},
},
{
name: "declared type enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
{version.MajorMinor(1, 27), StoredExpressions},
},
validExpressions: []string{"widget.x == 'buzz'"},
activation: map[string]any{"widget": map[string]any{"x": "buzz"}},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 28),
DeclTypes: []*apiservercel.DeclType{widgetsType},
EnvOptions: []cel.EnvOption{
cel.Variable("widget", widgetsType.CelType()),
},
},
},
},
{
name: "library version 0 enabled, version 1 disabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 27), NewExpressions},
// version 1 always enabled for StoredExpressions
},
validExpressions: []string{"test() == true"},
invalidExpressions: []string{"testV1() == true"},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 27),
RemovedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
library.Test(library.TestVersion(0)),
},
},
{
IntroducedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
library.Test(library.TestVersion(1)),
},
},
},
},
{
name: "library version 0 disabled, version 1 enabled",
typeVersionCombinations: []envTypeAndVersion{
{version.MajorMinor(1, 28), NewExpressions},
{version.MajorMinor(1, 26), StoredExpressions},
{version.MajorMinor(1, 27), StoredExpressions},
{version.MajorMinor(1, 28), StoredExpressions},
},
validExpressions: []string{"test() == false", "testV1() == true"},
opts: []VersionedOptions{
{
IntroducedVersion: version.MajorMinor(1, 27),
RemovedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
library.Test(library.TestVersion(0)),
},
},
{
IntroducedVersion: version.MajorMinor(1, 28),
EnvOptions: []cel.EnvOption{
library.Test(library.TestVersion(1)),
},
},
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
activation := tc.activation
if activation == nil {
activation = map[string]any{}
}
for _, tv := range tc.typeVersionCombinations {
t.Run(fmt.Sprintf("version=%s,envType=%s", tv.version.String(), tv.envType), func(t *testing.T) {
envSet := MustBaseEnvSet(tv.version)
if tc.opts != nil {
var err error
envSet, err = envSet.Extend(tc.opts...)
if err != nil {
t.Errorf("unexpected error extending environment %v", err)
}
}
envType := NewExpressions
if len(tv.envType) > 0 {
envType = tv.envType
}
validationEnv, err := envSet.Env(envType)
if err != nil {
t.Fatal(err)
}
for _, valid := range tc.validExpressions {
if ok, err := isValid(validationEnv, valid, activation); !ok {
if err != nil {
t.Errorf("expected expression to be valid but got %v", err)
}
t.Error("expected expression to return true")
}
}
for _, invalid := range tc.invalidExpressions {
if ok, _ := isValid(validationEnv, invalid, activation); ok {
t.Errorf("expected invalid expression to result in error")
}
}
})
}
})
}
}
func isValid(env *cel.Env, expr string, activation any) (bool, error) {
ast, issues := env.Compile(expr)
if len(issues.Errors()) > 0 {
return false, issues.Err()
}
prog, err := env.Program(ast)
if err != nil {
return false, err
}
result, _, err := prog.Eval(activation)
if err != nil {
return false, err
}
return result.Value() == true, nil
}
type fakeAuthorizer struct {
decision authorizer.Decision
reason string
err error
}
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
return f.decision, f.reason, f.err
}

View File

@ -362,7 +362,13 @@ func TestAuthzLibrary(t *testing.T) {
func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate, expectRuntimeCost uint64) {
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
env, err := cel.NewEnv(append(k8sExtensionLibs, ext.Strings())...)
env, err := cel.NewEnv(
ext.Strings(),
URLs(),
Regex(),
Lists(),
Authz(),
)
if err != nil {
t.Fatalf("%v", err)
}

View File

@ -1,35 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package library
import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/ext"
"github.com/google/cel-go/interpreter"
)
// ExtensionLibs declares the set of CEL extension libraries available everywhere CEL is used in Kubernetes.
var ExtensionLibs = append(k8sExtensionLibs, ext.Strings())
var k8sExtensionLibs = []cel.EnvOption{
URLs(),
Regex(),
Lists(),
Authz(),
}
var ExtensionLibRegexOptimizations = []*interpreter.RegexOptimization{FindRegexOptimization, FindAllRegexOptimization}

View File

@ -20,22 +20,16 @@ import (
"testing"
"github.com/google/cel-go/cel"
"k8s.io/apimachinery/pkg/util/sets"
)
func TestLibraryCompatibility(t *testing.T) {
functionNames := map[string]struct{}{}
decls := map[cel.Library]map[string][]cel.FunctionOpt{
urlsLib: urlLibraryDecls,
listsLib: listsLibraryDecls,
regexLib: regexLibraryDecls,
authzLib: authzLibraryDecls,
}
if len(k8sExtensionLibs) != len(decls) {
t.Errorf("Expected the same number of libraries in the ExtensionLibs as are tested for compatibility")
}
for _, decl := range decls {
for name := range decl {
var libs []map[string][]cel.FunctionOpt
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls)
functionNames := sets.New[string]()
for _, lib := range libs {
for name := range lib {
functionNames[name] = struct{}{}
}
}
@ -43,19 +37,24 @@ func TestLibraryCompatibility(t *testing.T) {
// WARN: All library changes must follow
// https://github.com/kubernetes/enhancements/tree/master/keps/sig-api-machinery/2876-crd-validation-expression-language#function-library-updates
// and must track the functions here along with which Kubernetes version introduced them.
knownFunctions := []string{
knownFunctions := sets.New[string](
// Kubernetes 1.24:
"isSorted", "sum", "max", "min", "indexOf", "lastIndexOf", "find", "findAll", "url", "getScheme", "getHost", "getHostname",
"getPort", "getEscapedPath", "getQuery", "isURL",
// Kubernetes <1.27>:
"path", "group", "serviceAccount", "resource", "subresource", "namespace", "name", "check", "allowed", "denied", "reason",
"path", "group", "serviceAccount", "resource", "subresource", "namespace", "name", "check", "allowed", "reason",
// Kubernetes <1.??>:
}
for _, fn := range knownFunctions {
delete(functionNames, fn)
}
)
if len(functionNames) != 0 {
t.Errorf("Expected all functions in the libraries to be assigned to a kubernetes release, but found the unassigned function names: %v", functionNames)
// TODO: test celgo function lists
unexpected := functionNames.Difference(knownFunctions)
missing := knownFunctions.Difference(functionNames)
if len(unexpected) != 0 {
t.Errorf("Expected all functions in the libraries to be assigned to a kubernetes release, but found the unexpected function names: %v", unexpected)
}
if len(missing) != 0 {
t.Errorf("Expected all functions in the libraries to be assigned to a kubernetes release, but found the missing function names: %v", missing)
}
}

View File

@ -77,7 +77,9 @@ func (*regex) CompileOptions() []cel.EnvOption {
}
func (*regex) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
return []cel.ProgramOption{
cel.OptimizeRegex(FindRegexOptimization, FindAllRegexOptimization),
}
}
func find(strVal ref.Val, regexVal ref.Val) ref.Val {

79
pkg/cel/library/test.go Normal file
View File

@ -0,0 +1,79 @@
/*
Copyright 2023 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package library
import (
"math"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// Test provides a test() function that returns true.
func Test(options ...TestOption) cel.EnvOption {
t := &testLib{version: math.MaxUint32}
for _, o := range options {
t = o(t)
}
return cel.Lib(t)
}
type testLib struct {
version uint32
}
type TestOption func(*testLib) *testLib
func TestVersion(version uint32) func(lib *testLib) *testLib {
return func(sl *testLib) *testLib {
sl.version = version
return sl
}
}
func (t *testLib) CompileOptions() []cel.EnvOption {
var options []cel.EnvOption
if t.version == 0 {
options = append(options, cel.Function("test",
cel.Overload("test", []*cel.Type{}, cel.BoolType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
return types.True
}))))
}
if t.version >= 1 {
options = append(options, cel.Function("test",
cel.Overload("test", []*cel.Type{}, cel.BoolType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
// Return false here so tests can observe which version of the function is registered
// Actual function libraries must not break backward compatibility
return types.False
}))))
options = append(options, cel.Function("testV1",
cel.Overload("testV1", []*cel.Type{}, cel.BoolType,
cel.FunctionBinding(func(args ...ref.Val) ref.Val {
return types.True
}))))
}
return options
}
func (*testLib) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{}
}

View File

@ -21,12 +21,12 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter"
"k8s.io/apimachinery/pkg/util/version"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/common"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/environment"
"k8s.io/kube-openapi/pkg/validation/spec"
)
@ -103,43 +103,26 @@ func TestMultipleTypes(t *testing.T) {
// foo is an object with a string field "foo", an integer field "common", and a string field "confusion"
// bar is an object with a string field "bar", an integer field "common", and an integer field "confusion"
func buildTestEnv() (*cel.Env, error) {
var opts []cel.EnvOption
opts = append(opts, cel.HomogeneousAggregateLiterals())
opts = append(opts, cel.EagerlyValidateDeclarations(true), cel.DefaultUTCTimeZone(true))
opts = append(opts, library.ExtensionLibs...)
env, err := cel.NewEnv(opts...)
if err != nil {
return nil, err
}
reg := apiservercel.NewRegistry(env)
fooType := common.SchemaDeclType(simpleMapSchema("foo", spec.StringProperty()), true).MaybeAssignTypeName("fooType")
barType := common.SchemaDeclType(simpleMapSchema("bar", spec.Int64Property()), true).MaybeAssignTypeName("barType")
declType := common.SchemaDeclType(simpleMapSchema("foo", spec.StringProperty()), true)
fooRT, err := apiservercel.NewRuleTypes("fooType", declType, reg)
env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()).Extend(
environment.VersionedOptions{
IntroducedVersion: version.MajorMinor(1, 26),
EnvOptions: []cel.EnvOption{
cel.Variable("foo", fooType.CelType()),
cel.Variable("bar", barType.CelType()),
},
DeclTypes: []*apiservercel.DeclType{
fooType,
barType,
},
},
)
if err != nil {
return nil, err
}
fooRT, err = fooRT.WithTypeProvider(env.TypeProvider())
if err != nil {
return nil, err
}
fooType, _ := fooRT.FindDeclType("fooType")
declType = common.SchemaDeclType(simpleMapSchema("bar", spec.Int64Property()), true)
barRT, err := apiservercel.NewRuleTypes("barType", declType, reg)
if err != nil {
return nil, err
}
barRT, err = barRT.WithTypeProvider(env.TypeProvider())
if err != nil {
return nil, err
}
barType, _ := barRT.FindDeclType("barType")
opts = append(opts, cel.CustomTypeProvider(&apiservercel.CompositedTypeProvider{Providers: []ref.TypeProvider{fooRT, barRT}}))
opts = append(opts, cel.CustomTypeAdapter(&apiservercel.CompositedTypeAdapter{Adapters: []ref.TypeAdapter{fooRT, barRT}}))
opts = append(opts, cel.Variable("foo", fooType.CelType()))
opts = append(opts, cel.Variable("bar", barType.CelType()))
return env.Extend(opts...)
return env.Env(environment.NewExpressions)
}
func simpleMapSchema(fieldName string, confusionSchema *spec.Schema) common.Schema {

View File

@ -1,79 +0,0 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cel
import (
"sync"
"github.com/google/cel-go/cel"
)
// Resolver declares methods to find policy templates and related configuration objects.
type Resolver interface {
// FindType returns a DeclType instance corresponding to the given fully-qualified name, if
// present.
FindType(name string) (*DeclType, bool)
}
// NewRegistry create a registry for keeping track of environments and types
// from a base cel.Env expression environment.
func NewRegistry(stdExprEnv *cel.Env) *Registry {
return &Registry{
exprEnvs: map[string]*cel.Env{"": stdExprEnv},
types: map[string]*DeclType{
BoolType.TypeName(): BoolType,
BytesType.TypeName(): BytesType,
DoubleType.TypeName(): DoubleType,
DurationType.TypeName(): DurationType,
IntType.TypeName(): IntType,
NullType.TypeName(): NullType,
StringType.TypeName(): StringType,
TimestampType.TypeName(): TimestampType,
UintType.TypeName(): UintType,
ListType.TypeName(): ListType,
MapType.TypeName(): MapType,
},
}
}
// Registry defines a repository of environment, schema, template, and type definitions.
//
// Registry instances are concurrency-safe.
type Registry struct {
rwMux sync.RWMutex
exprEnvs map[string]*cel.Env
types map[string]*DeclType
}
// FindType implements the Resolver interface method.
func (r *Registry) FindType(name string) (*DeclType, bool) {
r.rwMux.RLock()
defer r.rwMux.RUnlock()
typ, found := r.types[name]
if found {
return typ, true
}
return typ, found
}
// SetType registers a DeclType descriptor by its fully qualified name.
func (r *Registry) SetType(name string, declType *DeclType) error {
r.rwMux.Lock()
defer r.rwMux.Unlock()
r.types[name] = declType
return nil
}

View File

@ -319,44 +319,53 @@ func (f *DeclField) EnumValues() []ref.Val {
return ev
}
// NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible.
func NewRuleTypes(kind string,
declType *DeclType,
res Resolver) (*RuleTypes, error) {
func allTypesForDecl(declTypes []*DeclType) map[string]*DeclType {
if declTypes == nil {
return nil
}
allTypes := map[string]*DeclType{}
for _, declType := range declTypes {
for k, t := range FieldTypeMap(declType.TypeName(), declType) {
allTypes[k] = t
}
}
return allTypes
}
// NewDeclTypeProvider returns an Open API Schema-based type-system which is CEL compatible.
func NewDeclTypeProvider(rootTypes ...*DeclType) *DeclTypeProvider {
// Note, if the schema indicates that it's actually based on another proto
// then prefer the proto definition. For expressions in the proto, a new field
// annotation will be needed to indicate the expected environment and type of
// the expression.
schemaTypes, err := newSchemaTypeProvider(kind, declType)
if err != nil {
return nil, err
allTypes := allTypesForDecl(rootTypes)
return &DeclTypeProvider{
registeredTypes: allTypes,
}
if schemaTypes == nil {
return nil, nil
}
return &RuleTypes{
ruleSchemaDeclTypes: schemaTypes,
resolver: res,
}, nil
}
// RuleTypes extends the CEL ref.TypeProvider interface and provides an Open API Schema-based
// DeclTypeProvider extends the CEL ref.TypeProvider interface and provides an Open API Schema-based
// type-system.
type RuleTypes struct {
ref.TypeProvider
ruleSchemaDeclTypes *schemaTypeProvider
typeAdapter ref.TypeAdapter
resolver Resolver
type DeclTypeProvider struct {
registeredTypes map[string]*DeclType
typeProvider ref.TypeProvider
typeAdapter ref.TypeAdapter
}
func (rt *DeclTypeProvider) EnumValue(enumName string) ref.Val {
return rt.typeProvider.EnumValue(enumName)
}
func (rt *DeclTypeProvider) FindIdent(identName string) (ref.Val, bool) {
return rt.typeProvider.FindIdent(identName)
}
// EnvOptions returns a set of cel.EnvOption values which includes the declaration set
// as well as a custom ref.TypeProvider.
//
// Note, the standard declaration set includes 'rule' which is defined as the top-level rule-schema
// type if one is configured.
//
// If the RuleTypes value is nil, an empty []cel.EnvOption set is returned.
func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
// If the DeclTypeProvider value is nil, an empty []cel.EnvOption set is returned.
func (rt *DeclTypeProvider) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
if rt == nil {
return []cel.EnvOption{}, nil
}
@ -367,13 +376,12 @@ func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
return []cel.EnvOption{
cel.CustomTypeProvider(rtWithTypes),
cel.CustomTypeAdapter(rtWithTypes),
cel.Variable("rule", rt.ruleSchemaDeclTypes.root.CelType()),
}, nil
}
// WithTypeProvider returns a new RuleTypes that sets the given TypeProvider
// If the original RuleTypes is nil, the returned RuleTypes is still nil.
func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
// WithTypeProvider returns a new DeclTypeProvider that sets the given TypeProvider
// If the original DeclTypeProvider is nil, the returned DeclTypeProvider is still nil.
func (rt *DeclTypeProvider) WithTypeProvider(tp ref.TypeProvider) (*DeclTypeProvider, error) {
if rt == nil {
return nil, nil
}
@ -382,13 +390,12 @@ func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
if ok {
ta = tpa
}
rtWithTypes := &RuleTypes{
TypeProvider: tp,
typeAdapter: ta,
ruleSchemaDeclTypes: rt.ruleSchemaDeclTypes,
resolver: rt.resolver,
rtWithTypes := &DeclTypeProvider{
typeProvider: tp,
typeAdapter: ta,
registeredTypes: rt.registeredTypes,
}
for name, declType := range rt.ruleSchemaDeclTypes.types {
for name, declType := range rt.registeredTypes {
tpType, found := tp.FindType(name)
expT, err := declType.ExprType()
if err != nil {
@ -396,7 +403,7 @@ func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
}
if found && !proto.Equal(tpType, expT) {
return nil, fmt.Errorf(
"type %s definition differs between CEL environment and rule", name)
"type %s definition differs between CEL environment and type provider", name)
}
}
return rtWithTypes, nil
@ -409,7 +416,7 @@ func (rt *RuleTypes) WithTypeProvider(tp ref.TypeProvider) (*RuleTypes, error) {
//
// Note, when the type name is based on the Open API Schema, the name will reflect the object path
// where the type definition appears.
func (rt *RuleTypes) FindType(typeName string) (*exprpb.Type, bool) {
func (rt *DeclTypeProvider) FindType(typeName string) (*exprpb.Type, bool) {
if rt == nil {
return nil, false
}
@ -421,11 +428,11 @@ func (rt *RuleTypes) FindType(typeName string) (*exprpb.Type, bool) {
}
return expT, found
}
return rt.TypeProvider.FindType(typeName)
return rt.typeProvider.FindType(typeName)
}
// FindDeclType returns the CPT type description which can be mapped to a CEL type.
func (rt *RuleTypes) FindDeclType(typeName string) (*DeclType, bool) {
func (rt *DeclTypeProvider) FindDeclType(typeName string) (*DeclType, bool) {
if rt == nil {
return nil, false
}
@ -438,10 +445,10 @@ func (rt *RuleTypes) FindDeclType(typeName string) (*DeclType, bool) {
// If, in the future an object instance rather than a type name were provided, the field
// resolution might more accurately reflect the expected type model. However, in this case
// concessions were made to align with the existing CEL interfaces.
func (rt *RuleTypes) FindFieldType(typeName, fieldName string) (*ref.FieldType, bool) {
func (rt *DeclTypeProvider) FindFieldType(typeName, fieldName string) (*ref.FieldType, bool) {
st, found := rt.findDeclType(typeName)
if !found {
return rt.TypeProvider.FindFieldType(typeName, fieldName)
return rt.typeProvider.FindFieldType(typeName, fieldName)
}
f, found := st.Fields[fieldName]
@ -471,48 +478,63 @@ func (rt *RuleTypes) FindFieldType(typeName, fieldName string) (*ref.FieldType,
// NativeToValue is an implementation of the ref.TypeAdapater interface which supports conversion
// of rule values to CEL ref.Val instances.
func (rt *RuleTypes) NativeToValue(val interface{}) ref.Val {
func (rt *DeclTypeProvider) NativeToValue(val interface{}) ref.Val {
return rt.typeAdapter.NativeToValue(val)
}
// TypeNames returns the list of type names declared within the RuleTypes object.
func (rt *RuleTypes) TypeNames() []string {
typeNames := make([]string, len(rt.ruleSchemaDeclTypes.types))
func (rt *DeclTypeProvider) NewValue(typeName string, fields map[string]ref.Val) ref.Val {
// TODO: implement for OpenAPI types to enable CEL object instantiation, which is needed
// for mutating admission.
return rt.typeProvider.NewValue(typeName, fields)
}
// TypeNames returns the list of type names declared within the DeclTypeProvider object.
func (rt *DeclTypeProvider) TypeNames() []string {
typeNames := make([]string, len(rt.registeredTypes))
i := 0
for name := range rt.ruleSchemaDeclTypes.types {
for name := range rt.registeredTypes {
typeNames[i] = name
i++
}
return typeNames
}
func (rt *RuleTypes) findDeclType(typeName string) (*DeclType, bool) {
declType, found := rt.ruleSchemaDeclTypes.types[typeName]
func (rt *DeclTypeProvider) findDeclType(typeName string) (*DeclType, bool) {
declType, found := rt.registeredTypes[typeName]
if found {
return declType, true
}
declType, found = rt.resolver.FindType(typeName)
if found {
return declType, true
}
return nil, false
declType = findScalar(typeName)
return declType, declType != nil
}
func newSchemaTypeProvider(kind string, declType *DeclType) (*schemaTypeProvider, error) {
if declType == nil {
return nil, nil
func findScalar(typename string) *DeclType {
switch typename {
case BoolType.TypeName():
return BoolType
case BytesType.TypeName():
return BytesType
case DoubleType.TypeName():
return DoubleType
case DurationType.TypeName():
return DurationType
case IntType.TypeName():
return IntType
case NullType.TypeName():
return NullType
case StringType.TypeName():
return StringType
case TimestampType.TypeName():
return TimestampType
case UintType.TypeName():
return UintType
case ListType.TypeName():
return ListType
case MapType.TypeName():
return MapType
default:
return nil
}
root := declType.MaybeAssignTypeName(kind)
types := FieldTypeMap(kind, root)
return &schemaTypeProvider{
root: root,
types: types,
}, nil
}
type schemaTypeProvider struct {
root *DeclType
types map[string]*DeclType
}
var (