Introduce CEL EnvSets for managing safe rollout of new CEL features, libraries and expression variables
Kubernetes-commit: e740f8340eedc89baccd120329b454a860385e2d
This commit is contained in:
parent
055116923f
commit
f32e391a45
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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{}
|
||||
)
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
156
pkg/cel/types.go
156
pkg/cel/types.go
|
@ -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 (
|
||||
|
|
Loading…
Reference in New Issue