Apply resource constraints to ValidatingAdmissionPolicy.

This commit is contained in:
Cici Huang 2023-02-14 06:37:57 +00:00
parent 64259b43b8
commit 244c63a2e6
14 changed files with 241 additions and 48 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

@ -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

@ -32,6 +32,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"
@ -446,7 +447,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

@ -28,6 +28,7 @@ import (
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
@ -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, runtimeCELCostBudget 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,37 @@
/*
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 apiservercel "k8s.io/apiserver/pkg/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
MaxRequestSizeBytes = apiservercel.DefaultMaxRequestSizeBytes
)

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