Merge pull request #115747 from cici37/rc

Apply cost constraints to ValidatingAdmissionPolicy
This commit is contained in:
Kubernetes Prow Robot 2023-03-06 16:54:18 -08:00 committed by GitHub
commit 8c61473f1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 291 additions and 104 deletions

View File

@ -21,8 +21,6 @@ import (
"regexp"
"strings"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/api/validation/path"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -31,6 +29,8 @@ import (
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/util/webhook"
@ -740,7 +740,7 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
Expression: trimmedExpression,
Message: v.Message,
Reason: v.Reason,
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true})
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}, celconfig.PerCallLimit)
if result.Error != nil {
switch result.Error.Type {
case cel.ErrorTypeRequired:

View File

@ -35,6 +35,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/util/webhook"
@ -1026,7 +1027,7 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
} else if typeInfo == nil {
allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), fmt.Errorf("internal error: failed to retrieve type information for x-kubernetes-validations")))
} else {
compResults, err := cel.Compile(typeInfo.Schema, typeInfo.DeclType, cel.PerCallLimit)
compResults, err := cel.Compile(typeInfo.Schema, typeInfo.DeclType, celconfig.PerCallLimit)
if err != nil {
allErrs.CELErrors = append(allErrs.CELErrors, field.InternalError(fldPath.Child("x-kubernetes-validations"), err))
} else {

View File

@ -24,6 +24,7 @@ import (
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
func TestCelCostStability(t *testing.T) {
@ -1096,16 +1097,16 @@ func TestCelCostStability(t *testing.T) {
t.Run(testName, func(t *testing.T) {
t.Parallel()
s := withRule(*tt.schema, validRule)
celValidator := NewValidator(&s, true, PerCallLimit)
celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
if celValidator == nil {
t.Fatal("expected non nil validator")
}
ctx := context.TODO()
errs, remainingBudegt := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget)
errs, remainingBudegt := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
for _, err := range errs {
t.Errorf("unexpected error: %v", err)
}
rtCost := RuntimeCELCostBudget - remainingBudegt
rtCost := celconfig.RuntimeCELCostBudget - remainingBudegt
if rtCost != expectedCost {
t.Fatalf("runtime cost %d does not match expected runtime cost %d", rtCost, expectedCost)
}

View File

@ -27,6 +27,7 @@ import (
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
celconfig "k8s.io/apiserver/pkg/apis/cel"
apiservercel "k8s.io/apiserver/pkg/cel"
"k8s.io/apiserver/pkg/cel/library"
"k8s.io/apiserver/pkg/cel/metrics"
@ -40,22 +41,6 @@ const (
// OldScopedVarName is the variable name assigned to the existing value of the locally scoped data element of a
// CEL validation expression.
OldScopedVarName = "oldSelf"
// PerCallLimit specify the actual cost limit per CEL validation call
// current PerCallLimit gives roughly 0.1 second for each expression validation call
PerCallLimit = 1000000
// RuntimeCELCostBudget is the overall cost budget for runtime CEL validation cost per CustomResource
// current RuntimeCELCostBudget gives roughly 1 seconds for CR validation
RuntimeCELCostBudget = 10000000
// checkFrequency configures the number of iterations within a comprehension to evaluate
// before checking whether the function evaluation has been interrupted
checkFrequency = 100
// maxRequestSizeBytes is the maximum size of a request to the API server
// TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable
maxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
)
// CompilationResult represents the cel compilation result for one rule
@ -103,7 +88,7 @@ func getBaseEnv() (*cel.Env, error) {
// - nil Program, non-nil Error: Compilation resulted in an error
// - nil Program, nil Error: The provided rule was empty so compilation was not attempted
//
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit as input.
// 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 Compile(s *schema.Structural, declType *apiservercel.DeclType, perCallLimit uint64) ([]CompilationResult, error) {
t := time.Now()
defer func() {
@ -195,7 +180,7 @@ func compileRule(rule apiextensions.ValidationRule, env *cel.Env, perCallLimit u
cel.CostLimit(perCallLimit),
cel.CostTracking(estimator),
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),
cel.InterruptCheckFrequency(checkFrequency),
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
)
if err != nil {
compilationResult.Error = &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid, Detail: "program instantiation failed: " + err.Error()}
@ -274,5 +259,5 @@ func (c *sizeEstimator) EstimateCallCost(function, overloadID string, target *ch
// this function.
func maxCardinality(minSize int64) uint64 {
sz := minSize + 1 // assume at least one comma between elements
return uint64(maxRequestSizeBytes / sz)
return uint64(celconfig.MaxRequestSizeBytes / sz)
}

View File

@ -27,6 +27,7 @@ import (
apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
const (
@ -645,7 +646,7 @@ func TestCelCompilation(t *testing.T) {
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
compilationResults, err := Compile(&tt.input, model.SchemaDeclType(&tt.input, false), PerCallLimit)
compilationResults, err := Compile(&tt.input, model.SchemaDeclType(&tt.input, false), celconfig.PerCallLimit)
if err != nil {
t.Errorf("Expected no error, but got: %v", err)
}
@ -1081,7 +1082,7 @@ func genMapWithCustomItemRule(item *schema.Structural, rule string) func(maxProp
// if expectedCostExceedsLimit is non-zero. Typically, only expectedCost or expectedCostExceedsLimit is non-zero, not both.
func schemaChecker(schema *schema.Structural, expectedCost uint64, expectedCostExceedsLimit uint64, t *testing.T) func(t *testing.T) {
return func(t *testing.T) {
compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), PerCallLimit)
compilationResults, err := Compile(schema, model.SchemaDeclType(schema, false), celconfig.PerCallLimit)
if err != nil {
t.Errorf("Expected no error, got: %v", err)
}

View File

@ -64,7 +64,7 @@ type Validator struct {
// of the Structural schema and returns a custom resource validator that contains nested
// validators for all items, properties and additionalProperties that transitively contain validator rules.
// Returns nil if there are no validator rules in the Structural schema. May return a validator containing only errors.
// Adding perCallLimit as input arg for testing purpose only. Callers should always use const PerCallLimit as input
// Adding perCallLimit as input arg for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input
func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64) *Validator {
if !hasXValidations(s) {
return nil
@ -75,6 +75,7 @@ func NewValidator(s *schema.Structural, isResourceRoot bool, perCallLimit uint64
// validator creates a Validator for all x-kubernetes-validations at the level of the provided schema and lower and
// returns the Validator if any x-kubernetes-validations exist in the schema, or nil if no x-kubernetes-validations
// exist. declType is expected to be a CEL DeclType corresponding to the structural schema.
// 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 validator(s *schema.Structural, isResourceRoot bool, declType *cel.DeclType, perCallLimit uint64) *Validator {
compiledRules, err := Compile(s, declType, perCallLimit)
var itemsValidator, additionalPropertiesValidator *Validator

View File

@ -30,6 +30,7 @@ import (
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/model"
"k8s.io/apimachinery/pkg/util/validation/field"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
// TestValidationExpressions tests CEL integration with custom resource values and OpenAPIv3.
@ -1766,7 +1767,7 @@ func TestValidationExpressions(t *testing.T) {
t.Run(tests[i].name, func(t *testing.T) {
t.Parallel()
tt := tests[i]
tt.costBudget = RuntimeCELCostBudget
tt.costBudget = celconfig.RuntimeCELCostBudget
ctx := context.TODO()
for j := range tt.valid {
validRule := tt.valid[j]
@ -1777,7 +1778,7 @@ func TestValidationExpressions(t *testing.T) {
t.Run(testName, func(t *testing.T) {
t.Parallel()
s := withRule(*tt.schema, validRule)
celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), PerCallLimit)
celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit)
if celValidator == nil {
t.Fatal("expected non nil validator")
}
@ -1801,7 +1802,7 @@ func TestValidationExpressions(t *testing.T) {
}
t.Run(testName, func(t *testing.T) {
s := withRule(*tt.schema, rule)
celValidator := NewValidator(&s, true, PerCallLimit)
celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
if celValidator == nil {
t.Fatal("expected non nil validator")
}
@ -2015,7 +2016,7 @@ func TestValidationExpressionsAtSchemaLevels(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := context.TODO()
celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), PerCallLimit)
celValidator := validator(tt.schema, true, model.SchemaDeclType(tt.schema, true), celconfig.PerCallLimit)
if celValidator == nil {
t.Fatal("expected non nil validator")
}
@ -2082,7 +2083,7 @@ func TestCELValidationLimit(t *testing.T) {
t.Run(validRule, func(t *testing.T) {
t.Parallel()
s := withRule(*tt.schema, validRule)
celValidator := validator(&s, false, model.SchemaDeclType(&s, false), PerCallLimit)
celValidator := validator(&s, false, model.SchemaDeclType(&s, false), celconfig.PerCallLimit)
// test with cost budget exceeded
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, 0)
@ -2107,7 +2108,7 @@ func TestCELValidationLimit(t *testing.T) {
if celValidator == nil {
t.Fatal("expected non nil validator")
}
errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget)
errs, _ = celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
for _, err := range errs {
if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "no further validation rules will be run due to call cost exceeds limit for rule") {
found = true
@ -2150,11 +2151,11 @@ func TestCELValidationContextCancellation(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
ctx := context.TODO()
s := withRule(*tt.schema, tt.rule)
celValidator := NewValidator(&s, true, PerCallLimit)
celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
if celValidator == nil {
t.Fatal("expected non nil validator")
}
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget)
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
for _, err := range errs {
t.Errorf("unexpected error: %v", err)
}
@ -2163,7 +2164,7 @@ func TestCELValidationContextCancellation(t *testing.T) {
found := false
evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond)
cancel()
errs, _ = celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget)
errs, _ = celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
for _, err := range errs {
if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") {
found = true
@ -2208,7 +2209,7 @@ func TestCELMaxRecursionDepth(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.costBudget = RuntimeCELCostBudget
tt.costBudget = celconfig.RuntimeCELCostBudget
ctx := context.TODO()
for j := range tt.valid {
validRule := tt.valid[j]
@ -2216,7 +2217,7 @@ func TestCELMaxRecursionDepth(t *testing.T) {
t.Run(testName, func(t *testing.T) {
t.Parallel()
s := withRule(*tt.schema, validRule)
celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), PerCallLimit)
celValidator := validator(&s, tt.isRoot, model.SchemaDeclType(&s, tt.isRoot), celconfig.PerCallLimit)
if celValidator == nil {
t.Fatal("expected non nil validator")
}
@ -2240,7 +2241,7 @@ func TestCELMaxRecursionDepth(t *testing.T) {
}
t.Run(testName, func(t *testing.T) {
s := withRule(*tt.schema, rule)
celValidator := NewValidator(&s, true, PerCallLimit)
celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
if celValidator == nil {
t.Fatal("expected non nil validator")
}
@ -2285,12 +2286,12 @@ func BenchmarkCELValidationWithContext(b *testing.B) {
b.Run(tt.name, func(b *testing.B) {
ctx := context.TODO()
s := withRule(*tt.schema, tt.rule)
celValidator := NewValidator(&s, true, PerCallLimit)
celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
if celValidator == nil {
b.Fatal("expected non nil validator")
}
for i := 0; i < b.N; i++ {
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget)
errs, _ := celValidator.Validate(ctx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
for _, err := range errs {
b.Fatalf("validation failed: %v", err)
}
@ -2325,14 +2326,14 @@ func BenchmarkCELValidationWithCancelledContext(b *testing.B) {
b.Run(tt.name, func(b *testing.B) {
ctx := context.TODO()
s := withRule(*tt.schema, tt.rule)
celValidator := NewValidator(&s, true, PerCallLimit)
celValidator := NewValidator(&s, true, celconfig.PerCallLimit)
if celValidator == nil {
b.Fatal("expected non nil validator")
}
for i := 0; i < b.N; i++ {
evalCtx, cancel := context.WithTimeout(ctx, time.Microsecond)
cancel()
errs, _ := celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, RuntimeCELCostBudget)
errs, _ := celValidator.Validate(evalCtx, field.NewPath("root"), &s, tt.obj, nil, celconfig.RuntimeCELCostBudget)
//found := false
//for _, err := range errs {
// if err.Type == field.ErrorTypeInvalid && strings.Contains(err.Error(), "operation interrupted") {
@ -2379,7 +2380,7 @@ func BenchmarkCELValidationWithAndWithoutOldSelfReference(b *testing.B) {
},
},
}
validator := NewValidator(s, true, PerCallLimit)
validator := NewValidator(s, true, celconfig.PerCallLimit)
if validator == nil {
b.Fatal("expected non nil validator")
}
@ -2390,7 +2391,7 @@ func BenchmarkCELValidationWithAndWithoutOldSelfReference(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
errs, _ := validator.Validate(ctx, root, s, obj, obj, RuntimeCELCostBudget)
errs, _ := validator.Validate(ctx, root, s, obj, obj, celconfig.RuntimeCELCostBudget)
for _, err := range errs {
b.Errorf("unexpected error: %v", err)
}

View File

@ -32,6 +32,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
// ValidateDefaults checks that default values validate and are properly pruned.
@ -50,7 +51,7 @@ func ValidateDefaults(ctx context.Context, pth *field.Path, s *structuralschema.
}
}
allErr, error, _ := validate(ctx, pth, s, s, f, false, requirePrunedDefaults, cel.RuntimeCELCostBudget)
allErr, error, _ := validate(ctx, pth, s, s, f, false, requirePrunedDefaults, celconfig.RuntimeCELCostBudget)
return allErr, error
}
@ -91,7 +92,7 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
allErrs = append(allErrs, field.Invalid(pth.Child("default"), s.Default.Object, fmt.Sprintf("must result in valid metadata: %v", errs.ToAggregate())))
} else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if celValidator := cel.NewValidator(s, isResourceRoot, cel.PerCallLimit); celValidator != nil {
} else if celValidator := cel.NewValidator(s, isResourceRoot, celconfig.PerCallLimit); celValidator != nil {
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost)
remainingCost = rmCost
allErrs = append(allErrs, celErrs...)
@ -116,7 +117,7 @@ func validate(ctx context.Context, pth *field.Path, s *structuralschema.Structur
allErrs = append(allErrs, errs...)
} else if errs := apiservervalidation.ValidateCustomResource(pth.Child("default"), s.Default.Object, validator); len(errs) > 0 {
allErrs = append(allErrs, errs...)
} else if celValidator := cel.NewValidator(s, isResourceRoot, cel.PerCallLimit); celValidator != nil {
} else if celValidator := cel.NewValidator(s, isResourceRoot, celconfig.PerCallLimit); celValidator != nil {
celErrs, rmCost := celValidator.Validate(ctx, pth.Child("default"), s, s.Default.Object, s.Default.Object, remainingCost)
remainingCost = rmCost
allErrs = append(allErrs, celErrs...)

View File

@ -42,6 +42,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/sets"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
// TestRoundTrip checks the conversion to go-openapi types.
@ -608,7 +609,7 @@ func TestValidateCustomResource(t *testing.T) {
if err != nil {
t.Fatal(err)
}
celValidator := cel.NewValidator(structural, false, cel.PerCallLimit)
celValidator := cel.NewValidator(structural, false, celconfig.PerCallLimit)
for i, obj := range tt.objects {
var oldObject interface{}
if len(tt.oldObjects) == len(tt.objects) {
@ -617,14 +618,14 @@ func TestValidateCustomResource(t *testing.T) {
if errs := ValidateCustomResource(nil, obj, validator); len(errs) > 0 {
t.Errorf("unexpected validation error for %v: %v", obj, errs)
}
errs, _ := celValidator.Validate(context.TODO(), nil, structural, obj, oldObject, cel.RuntimeCELCostBudget)
errs, _ := celValidator.Validate(context.TODO(), nil, structural, obj, oldObject, celconfig.RuntimeCELCostBudget)
if len(errs) > 0 {
t.Errorf(errs.ToAggregate().Error())
}
}
for i, failingObject := range tt.failingObjects {
errs := ValidateCustomResource(nil, failingObject.object, validator)
celErrs, _ := celValidator.Validate(context.TODO(), nil, structural, failingObject.object, failingObject.oldObject, cel.RuntimeCELCostBudget)
celErrs, _ := celValidator.Validate(context.TODO(), nil, structural, failingObject.object, failingObject.oldObject, celconfig.RuntimeCELCostBudget)
errs = append(errs, celErrs...)
if len(errs) == 0 {
t.Errorf("missing error for %v", failingObject.object)

View File

@ -21,11 +21,11 @@ import (
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
"k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel"
structurallisttype "k8s.io/apiextensions-apiserver/pkg/apiserver/schema/listtype"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
type statusStrategy struct {
@ -113,7 +113,7 @@ func (a statusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Obj
if has, err := hasBlockingErr(errs); has {
errs = append(errs, err)
} else {
err, _ := celValidator.Validate(ctx, nil, a.customResourceStrategy.structuralSchemas[v], uNew.Object, oldObject, cel.RuntimeCELCostBudget)
err, _ := celValidator.Validate(ctx, nil, a.customResourceStrategy.structuralSchemas[v], uNew.Object, oldObject, celconfig.RuntimeCELCostBudget)
errs = append(errs, err...)
}
}

View File

@ -35,6 +35,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/features"
apiserverstorage "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
@ -61,7 +62,7 @@ func NewStrategy(typer runtime.ObjectTyper, namespaceScoped bool, kind schema.Gr
celValidators := map[string]*cel.Validator{}
if utilfeature.DefaultFeatureGate.Enabled(features.CustomResourceValidationExpressions) {
for name, s := range structuralSchemas {
v := cel.NewValidator(s, true, cel.PerCallLimit) // CEL programs are compiled and cached here
v := cel.NewValidator(s, true, celconfig.PerCallLimit) // CEL programs are compiled and cached here
if v != nil {
celValidators[name] = v
}
@ -178,7 +179,7 @@ func (a customResourceStrategy) Validate(ctx context.Context, obj runtime.Object
if has, err := hasBlockingErr(errs); has {
errs = append(errs, err)
} else {
err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], u.Object, nil, cel.RuntimeCELCostBudget)
err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], u.Object, nil, celconfig.RuntimeCELCostBudget)
errs = append(errs, err...)
}
}
@ -235,7 +236,7 @@ func (a customResourceStrategy) ValidateUpdate(ctx context.Context, obj, old run
if has, err := hasBlockingErr(errs); has {
errs = append(errs, err)
} else {
err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], uNew.Object, uOld.Object, cel.RuntimeCELCostBudget)
err, _ := celValidator.Validate(ctx, nil, a.structuralSchemas[v], uNew.Object, uOld.Object, celconfig.RuntimeCELCostBudget)
errs = append(errs, err...)
}
}

View File

@ -18,6 +18,7 @@ package cel
import (
"fmt"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"sync"
"github.com/google/cel-go/cel"
@ -33,8 +34,6 @@ const (
RequestVarName = "request"
AuthorizerVarName = "authorizer"
RequestResourceAuthorizerVarName = "authorizer.requestResource"
checkFrequency = 100
)
var (
@ -190,7 +189,8 @@ type CompilationResult struct {
}
// CompileCELExpression returns a compiled CEL expression.
func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations) CompilationResult {
// 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 {
@ -245,9 +245,10 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars Op
}
}
prog, err := env.Program(ast,
cel.EvalOptions(cel.OptOptimize),
cel.EvalOptions(cel.OptOptimize, cel.OptTrackCost),
cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...),
cel.InterruptCheckFrequency(checkFrequency),
cel.InterruptCheckFrequency(celconfig.CheckFrequency),
cel.CostLimit(perCallLimit),
)
if err != nil {
return CompilationResult{

View File

@ -19,6 +19,8 @@ package cel
import (
"strings"
"testing"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
func TestCompileValidatingPolicyExpression(t *testing.T) {
@ -120,7 +122,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
for _, expr := range tc.expressions {
result := CompileCELExpression(&fakeExpressionAccessor{
expr,
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true})
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true}, celconfig.PerCallLimit)
if result.Error != nil {
t.Errorf("Unexpected error: %v", result.Error)
}
@ -128,7 +130,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
for expr, expectErr := range tc.errorExpressions {
result := CompileCELExpression(&fakeExpressionAccessor{
expr,
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer})
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
if result.Error == nil {
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
continue

View File

@ -19,6 +19,7 @@ package cel
import (
"errors"
"fmt"
"math"
"reflect"
"time"
@ -74,13 +75,14 @@ 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) Filter {
// 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 (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, perCallLimit uint64) Filter {
if len(expressionAccessors) == 0 {
return nil
}
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
compilationResults[i] = CompileCELExpression(expressionAccessor, options)
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)
}
return NewFilter(compilationResults)
}
@ -120,7 +122,8 @@ func objectToResolveVal(r runtime.Object) (interface{}, error) {
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
// errors per evaluation are returned on the Evaluation object
func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings) ([]EvaluationResult, error) {
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
evaluations := make([]EvaluationResult, len(f.compilationResults))
var err error
@ -159,6 +162,7 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *a
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
remainingBudget := runtimeCELCostBudget
for i, compilationResult := range f.compilationResults {
var evaluation = &evaluations[i]
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
@ -171,9 +175,22 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *a
continue
}
t1 := time.Now()
evalResult, _, err := compilationResult.Program.Eval(va)
evalResult, evalDetails, err := compilationResult.Program.Eval(va)
elapsed := time.Since(t1)
evaluation.Elapsed = elapsed
if evalDetails == nil {
return nil, errors.New(fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()))
} else {
rtCost := evalDetails.ActualCost()
if rtCost == nil {
return nil, errors.New(fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()))
} else {
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
return nil, errors.New(fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"))
}
remainingBudget -= int64(*rtCost)
}
}
if err != nil {
evaluation.Error = errors.New(fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err))
} else {

View File

@ -27,6 +27,7 @@ import (
celtypes "github.com/google/cel-go/common/types"
"github.com/stretchr/testify/require"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
@ -87,7 +88,7 @@ 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})
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, celconfig.PerCallLimit)
if e == nil {
t.Fatalf("unexpected nil validator")
}
@ -144,13 +145,14 @@ func TestFilter(t *testing.T) {
var nilUnstructured *unstructured.Unstructured
cases := []struct {
name string
attributes admission.Attributes
params runtime.Object
validations []ExpressionAccessor
results []EvaluationResult
hasParamKind bool
authorizer authorizer.Authorizer
name string
attributes admission.Attributes
params runtime.Object
validations []ExpressionAccessor
results []EvaluationResult
hasParamKind bool
authorizer authorizer.Authorizer
testPerCallLimit uint64
}{
{
name: "valid syntax for object",
@ -616,12 +618,32 @@ func TestFilter(t *testing.T) {
APIVersion: "*",
}),
},
{
name: "test perCallLimit exceed",
validations: []ExpressionAccessor{
&condition{
Expression: "object.subsets.size() < params.spec.testSize",
},
},
attributes: newValidAttribute(nil, false),
results: []EvaluationResult{
{
Error: errors.New(fmt.Sprintf("operation cancelled: actual cost limit exceeded")),
},
},
hasParamKind: true,
params: crdParams,
testPerCallLimit: 1,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
c := filterCompiler{}
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil})
if tc.testPerCallLimit == 0 {
tc.testPerCallLimit = celconfig.PerCallLimit
}
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil}, tc.testPerCallLimit)
if f == nil {
t.Fatalf("unexpected nil validator")
}
@ -635,7 +657,7 @@ func TestFilter(t *testing.T) {
}
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
evalResults, err := f.ForInput(versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars)
evalResults, err := f.ForInput(versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, celconfig.RuntimeCELCostBudget)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -652,6 +674,111 @@ func TestFilter(t *testing.T) {
}
}
func TestRuntimeCELCostBudget(t *testing.T) {
configMapParams := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Data: map[string]string{
"fakeString": "fake",
},
}
cases := []struct {
name string
attributes admission.Attributes
params runtime.Object
validations []ExpressionAccessor
hasParamKind bool
authorizer authorizer.Authorizer
testRuntimeCELCostBudget int64
exceedBudget bool
}{
{
name: "expression exceed RuntimeCELCostBudget at fist expression",
validations: []ExpressionAccessor{
&condition{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
&condition{
Expression: "has(object.subsets)",
},
},
attributes: newValidAttribute(nil, false),
hasParamKind: false,
testRuntimeCELCostBudget: 1,
exceedBudget: true,
},
{
name: "expression exceed RuntimeCELCostBudget at last expression",
validations: []ExpressionAccessor{
&condition{
Expression: "has(object.subsets) && object.subsets.size() < 2",
},
&condition{
Expression: "object.subsets.size() > 2",
},
},
attributes: newValidAttribute(nil, false),
hasParamKind: true,
params: configMapParams,
testRuntimeCELCostBudget: 5,
exceedBudget: true,
},
{
name: "test RuntimeCELCostBudge is not exceed",
validations: []ExpressionAccessor{
&condition{
Expression: "oldObject != null",
},
&condition{
Expression: "object.subsets.size() > 2",
},
},
attributes: newValidAttribute(nil, false),
hasParamKind: true,
params: configMapParams,
exceedBudget: false,
},
}
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)
if f == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.validations
CompilationResults := f.(*filter).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
versionedAttr, err := generic.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
if err != nil {
t.Fatalf("unexpected error on conversion: %v", err)
}
if tc.testRuntimeCELCostBudget == 0 {
tc.testRuntimeCELCostBudget = celconfig.RuntimeCELCostBudget
}
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
evalResults, err := f.ForInput(versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, tc.testRuntimeCELCostBudget)
if tc.exceedBudget && err == nil {
t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil")
}
if tc.exceedBudget && !strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") {
t.Errorf("Expected RuntimeCELCostBudge exceeded error but got: %v", err)
}
if err != nil && !tc.exceedBudget {
t.Fatalf("unexpected error: %v", err)
}
if tc.exceedBudget && len(evalResults) != 0 {
t.Fatalf("unexpected result returned: %v", evalResults)
}
})
}
}
// newObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file.
func newObjectInterfacesForTest() admission.ObjectInterfaces {
scheme := runtime.NewScheme()

View File

@ -65,7 +65,8 @@ 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
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations) Filter
// 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
}
// OptionalVariableBindings provides expression bindings for optional CEL variables.
@ -84,7 +85,8 @@ type OptionalVariableBindings struct {
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
type Filter interface {
// ForInput converts compiled CEL-typed values into evaluated CEL-typed values
ForInput(versionedAttr *generic.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings) ([]EvaluationResult, error)
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
ForInput(versionedAttr *generic.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, error)
// CompilationErrors returns a list of errors from the compilation of the evaluator
CompilationErrors() []error

View File

@ -172,6 +172,7 @@ func (f *fakeCompiler) HasSynced() bool {
func (f *fakeCompiler) Compile(
expressions []cel.ExpressionAccessor,
options cel.OptionalVariableDeclarations,
perCallLimit uint64,
) cel.Filter {
key := expressions[0].GetExpression()
if fun, ok := f.CompileFuncs[key]; ok {
@ -208,7 +209,7 @@ type fakeFilter struct {
keyId string
}
func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings) ([]cel.EvaluationResult, error) {
func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, runtimeCELCostBudget int64) ([]cel.EvaluationResult, error) {
return []cel.EvaluationResult{}, nil
}
@ -220,10 +221,10 @@ var _ Validator = &fakeValidator{}
type fakeValidator struct {
*fakeFilter
ValidateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision
ValidateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision
}
func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision) {
func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision) {
//Key must be something that we can decipher from the inputs to Validate so using message which will be on the validationCondition object of evalResult
validateKey := definition.Spec.Validations[0].Expression
if validatorMap == nil {
@ -234,8 +235,8 @@ func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmiss
validatorMap[validateKey] = f
}
func (f *fakeValidator) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
return f.ValidateFunc(versionedAttr, versionedParams)
func (f *fakeValidator) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return f.ValidateFunc(versionedAttr, versionedParams, runtimeCELCostBudget)
}
var _ Matcher = &fakeMatcher{}
@ -715,7 +716,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
@ -775,7 +776,7 @@ func TestDefinitionDoesntMatch(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
@ -886,7 +887,7 @@ func TestReconfigureBinding(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
@ -993,7 +994,7 @@ func TestRemoveDefinition(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
@ -1060,7 +1061,7 @@ func TestRemoveBinding(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
@ -1168,7 +1169,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
@ -1234,7 +1235,7 @@ func TestEmptyParamSource(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
@ -1334,7 +1335,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
}
})
validator1.RegisterDefinition(&policy1, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator1.RegisterDefinition(&policy1, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
evaluations1.Add(1)
return []PolicyDecision{
{
@ -1351,7 +1352,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
}
})
validator2.RegisterDefinition(&policy2, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator2.RegisterDefinition(&policy2, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
evaluations2.Add(1)
return []PolicyDecision{
{
@ -1459,7 +1460,7 @@ func TestNativeTypeParam(t *testing.T) {
}
})
validator.RegisterDefinition(&nativeTypeParamPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
validator.RegisterDefinition(&nativeTypeParamPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
evaluations.Add(1)
if _, ok := versionedParams.(*v1.ConfigMap); ok {
return []PolicyDecision{

View File

@ -20,6 +20,7 @@ import (
"context"
"errors"
"fmt"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"sync"
"sync/atomic"
"time"
@ -320,7 +321,7 @@ func (c *celAdmissionController) Validate(
versionedAttr = va
}
decisions := bindingInfo.validator.Validate(versionedAttr, param)
decisions := bindingInfo.validator.Validate(versionedAttr, param, celconfig.RuntimeCELCostBudget)
for _, decision := range decisions {
switch decision.Action {

View File

@ -33,6 +33,7 @@ import (
celmetrics "k8s.io/apiserver/pkg/admission/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
@ -459,7 +460,7 @@ func (c *policyController) latestPolicyData() []policyData {
}
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
bindingInfo.validator = c.newValidator(
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars),
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
c.authz,
)

View File

@ -55,5 +55,6 @@ type Matcher interface {
// Validator is contains logic for converting ValidationEvaluation to PolicyDecisions
type Validator interface {
// Validate is used to take cel evaluations and convert into decisions
Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision
}

View File

@ -54,7 +54,8 @@ func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction {
}
// Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
var f v1.FailurePolicyType
if v.failPolicy == nil {
f = v1.Fail
@ -63,7 +64,7 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version
}
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
evalResults, err := v.filter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars)
evalResults, err := v.filter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, runtimeCELCostBudget)
if err != nil {
return []PolicyDecision{
{

View File

@ -32,6 +32,7 @@ import (
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
var _ cel.Filter = &fakeCelFilter{}
@ -41,7 +42,7 @@ type fakeCelFilter struct {
throwError bool
}
func (f *fakeCelFilter) ForInput(*generic.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings) ([]cel.EvaluationResult, error) {
func (f *fakeCelFilter) ForInput(*generic.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, int64) ([]cel.EvaluationResult, error) {
if f.throwError {
return nil, errors.New("test error")
}
@ -465,7 +466,7 @@ func TestValidate(t *testing.T) {
},
}
policyResults := v.Validate(fakeVersionedAttr, nil)
policyResults := v.Validate(fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget)
require.Equal(t, len(policyResults), len(tc.policyDecision))

View File

@ -0,0 +1,36 @@
/*
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
const (
// PerCallLimit specify the actual cost limit per CEL validation call
// current PerCallLimit gives roughly 0.1 second for each expression validation call
PerCallLimit = 1000000
// RuntimeCELCostBudget is the overall cost budget for runtime CEL validation cost per ValidatingAdmissionPolicyBinding or CustomResource
// current RuntimeCELCostBudget gives roughly 1 seconds for the validation
RuntimeCELCostBudget = 10000000
// CheckFrequency configures the number of iterations within a comprehension to evaluate
// before checking whether the function evaluation has been interrupted
CheckFrequency = 100
// MaxRequestSizeBytes is the maximum size of a request to the API server
// TODO(DangerOnTheRanger): wire in MaxRequestBodyBytes from apiserver/pkg/server/options/server_run_options.go to make this configurable
// Note that even if server_run_options.go becomes configurable in the future, this cost constant should be fixed and it should be the max allowed request size for the server
MaxRequestSizeBytes = int64(3 * 1024 * 1024)
)

View File

@ -16,9 +16,11 @@ limitations under the License.
package cel
import celconfig "k8s.io/apiserver/pkg/apis/cel"
const (
// DefaultMaxRequestSizeBytes is the size of the largest request that will be accepted
DefaultMaxRequestSizeBytes = int64(3 * 1024 * 1024)
DefaultMaxRequestSizeBytes = celconfig.MaxRequestSizeBytes
// MaxDurationSizeJSON
// OpenAPI duration strings follow RFC 3339, section 5.6 - see the comment on maxDatetimeSizeJSON

1
vendor/modules.txt vendored
View File

@ -1474,6 +1474,7 @@ k8s.io/apiserver/pkg/apis/audit
k8s.io/apiserver/pkg/apis/audit/install
k8s.io/apiserver/pkg/apis/audit/v1
k8s.io/apiserver/pkg/apis/audit/validation
k8s.io/apiserver/pkg/apis/cel
k8s.io/apiserver/pkg/apis/config
k8s.io/apiserver/pkg/apis/config/v1
k8s.io/apiserver/pkg/apis/config/validation