diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go new file mode 100644 index 00000000000..9771ae9ae4b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/activation.go @@ -0,0 +1,190 @@ +/* +Copyright 2024 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 ( + "context" + "fmt" + "github.com/google/cel-go/interpreter" + "math" + "time" + + admissionv1 "k8s.io/api/admission/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/cel" + "k8s.io/apiserver/pkg/cel/library" +) + +// newActivation creates an activation for CEL admission plugins from the given request, admission chain and +// variable binding information. +func newActivation(compositionCtx CompositionContext, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace) (*evaluationActivation, error) { + oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject) + if err != nil { + return nil, fmt.Errorf("failed to prepare oldObject variable for evaluation: %w", err) + } + objectVal, err := objectToResolveVal(versionedAttr.VersionedObject) + if err != nil { + return nil, fmt.Errorf("failed to prepare object variable for evaluation: %w", err) + } + var paramsVal, authorizerVal, requestResourceAuthorizerVal any + if inputs.VersionedParams != nil { + paramsVal, err = objectToResolveVal(inputs.VersionedParams) + if err != nil { + return nil, fmt.Errorf("failed to prepare params variable for evaluation: %w", err) + } + } + + if inputs.Authorizer != nil { + authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer) + requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr) + } + + requestVal, err := convertObjectToUnstructured(request) + if err != nil { + return nil, fmt.Errorf("failed to prepare request variable for evaluation: %w", err) + } + namespaceVal, err := objectToResolveVal(namespace) + if err != nil { + return nil, fmt.Errorf("failed to prepare namespace variable for evaluation: %w", err) + } + va := &evaluationActivation{ + object: objectVal, + oldObject: oldObjectVal, + params: paramsVal, + request: requestVal.Object, + namespace: namespaceVal, + authorizer: authorizerVal, + requestResourceAuthorizer: requestResourceAuthorizerVal, + } + + // composition is an optional feature that only applies for ValidatingAdmissionPolicy and MutatingAdmissionPolicy. + if compositionCtx != nil { + va.variables = compositionCtx.Variables(va) + } + return va, nil +} + +type evaluationActivation struct { + object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{} +} + +// ResolveName returns a value from the activation by qualified name, or false if the name +// could not be found. +func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) { + switch name { + case ObjectVarName: + return a.object, true + case OldObjectVarName: + return a.oldObject, true + case ParamsVarName: + return a.params, true // params may be null + case RequestVarName: + return a.request, true + case NamespaceVarName: + return a.namespace, true + case AuthorizerVarName: + return a.authorizer, a.authorizer != nil + case RequestResourceAuthorizerVarName: + return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil + case VariableVarName: // variables always present + return a.variables, true + default: + return nil, false + } +} + +// Parent returns the parent of the current activation, may be nil. +// If non-nil, the parent will be searched during resolve calls. +func (a *evaluationActivation) Parent() interpreter.Activation { + return nil +} + +// Evaluate runs a compiled CEL admission plugin expression using the provided activation and CEL +// runtime cost budget. +func (a *evaluationActivation) Evaluate(ctx context.Context, compositionCtx CompositionContext, compilationResult CompilationResult, remainingBudget int64) (EvaluationResult, int64, error) { + var evaluation = EvaluationResult{} + if compilationResult.ExpressionAccessor == nil { // in case of placeholder + return evaluation, remainingBudget, nil + } + + evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor + if compilationResult.Error != nil { + evaluation.Error = &cel.Error{ + Type: cel.ErrorTypeInvalid, + Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error), + Cause: compilationResult.Error, + } + return evaluation, remainingBudget, nil + } + if compilationResult.Program == nil { + evaluation.Error = &cel.Error{ + Type: cel.ErrorTypeInternal, + Detail: "unexpected internal error compiling expression", + } + return evaluation, remainingBudget, nil + } + t1 := time.Now() + evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, a) + // budget may be spent due to lazy evaluation of composited variables + if compositionCtx != nil { + compositionCost := compositionCtx.GetAndResetCost() + if compositionCost > remainingBudget { + return evaluation, -1, &cel.Error{ + Type: cel.ErrorTypeInvalid, + Detail: "validation failed due to running out of cost budget, no further validation rules will be run", + Cause: cel.ErrOutOfBudget, + } + } + remainingBudget -= compositionCost + } + elapsed := time.Since(t1) + evaluation.Elapsed = elapsed + if evalDetails == nil { + return evaluation, -1, &cel.Error{ + Type: cel.ErrorTypeInternal, + Detail: 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 evaluation, -1, &cel.Error{ + Type: cel.ErrorTypeInvalid, + Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()), + Cause: cel.ErrOutOfBudget, + } + } else { + if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget { + return evaluation, -1, &cel.Error{ + Type: cel.ErrorTypeInvalid, + Detail: "validation failed due to running out of cost budget, no further validation rules will be run", + Cause: cel.ErrOutOfBudget, + } + } + remainingBudget -= int64(*rtCost) + } + } + if err != nil { + evaluation.Error = &cel.Error{ + Type: cel.ErrorTypeInvalid, + Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err), + } + } else { + evaluation.EvalResult = evalResult + } + return evaluation, remainingBudget, nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go index 06035f6b9e9..8088c332566 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go @@ -24,8 +24,10 @@ import ( "k8s.io/apimachinery/pkg/util/version" celconfig "k8s.io/apiserver/pkg/apis/cel" apiservercel "k8s.io/apiserver/pkg/cel" + "k8s.io/apiserver/pkg/cel/common" "k8s.io/apiserver/pkg/cel/environment" "k8s.io/apiserver/pkg/cel/library" + "k8s.io/apiserver/pkg/cel/mutation" ) const ( @@ -186,7 +188,7 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op found := false returnTypes := expressionAccessor.ReturnTypes() for _, returnType := range returnTypes { - if ast.OutputType() == returnType || cel.AnyType == returnType { + if ast.OutputType().IsExactType(returnType) || cel.AnyType.IsExactType(returnType) { found = true break } @@ -194,9 +196,9 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op if !found { var reason string if len(returnTypes) == 1 { - reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String()) + reason = fmt.Sprintf("must evaluate to %v but got %v", returnTypes[0].String(), ast.OutputType().String()) } else { - reason = fmt.Sprintf("must evaluate to one of %v", returnTypes) + reason = fmt.Sprintf("must evaluate to one of %v but got %v", returnTypes, ast.OutputType().String()) } return resultError(reason, apiservercel.ErrorTypeInvalid, nil) @@ -226,46 +228,78 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs { envs := make(variableDeclEnvs, 8) // 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 err error for _, strictCost := 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(NamespaceVarName, namespaceType.CelType()), - 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{ - namespaceType, - requestType, - }, - }, - ) + decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost} + envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl) if err != nil { - panic(fmt.Sprintf("environment misconfigured: %v", err)) + panic(err) } - if strictCost { - extended, err = extended.Extend(environment.StrictCostOpt) - if err != nil { - panic(fmt.Sprintf("environment misconfigured: %v", err)) - } - } - envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}] = extended + } + // We only need this ObjectTypes where strict cost is true + decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: true, HasPatchTypes: true} + envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl) + if err != nil { + panic(err) } } } return envs } + +func createEnvForOpts(baseEnv *environment.EnvSet, namespaceType *apiservercel.DeclType, requestType *apiservercel.DeclType, opts OptionalVariableDeclarations) (*environment.EnvSet, error) { + var envOpts []cel.EnvOption + envOpts = append(envOpts, + cel.Variable(ObjectVarName, cel.DynType), + cel.Variable(OldObjectVarName, cel.DynType), + cel.Variable(NamespaceVarName, namespaceType.CelType()), + cel.Variable(RequestVarName, requestType.CelType())) + if opts.HasParams { + envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType)) + } + if opts.HasAuthorizer { + envOpts = append(envOpts, + cel.Variable(AuthorizerVarName, library.AuthorizerType), + cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType)) + } + + 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{ + namespaceType, + requestType, + }, + }, + ) + if err != nil { + return nil, fmt.Errorf("environment misconfigured: %w", err) + } + if opts.StrictCost { + extended, err = extended.Extend(environment.StrictCostOpt) + if err != nil { + return nil, fmt.Errorf("environment misconfigured: %w", err) + } + } + + if opts.HasPatchTypes { + extended, err = extended.Extend(hasPatchTypes) + if err != nil { + return nil, fmt.Errorf("environment misconfigured: %w", err) + } + } + return extended, nil +} + +var hasPatchTypes = environment.VersionedOptions{ + // Feature epoch was actually 1.32, but we artificially set it to 1.0 because these + // options should always be present. + IntroducedVersion: version.MajorMinor(1, 0), + EnvOptions: []cel.EnvOption{ + common.ResolverEnvOption(&mutation.DynamicTypeResolver{}), + library.JSONPatch(), // for jsonPatch.escape() function + }, +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go index 9c449ecda2f..bf8715a1442 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition.go @@ -36,15 +36,27 @@ import ( const VariablesTypeName = "kubernetes.variables" +// CompositedCompiler compiles expressions with variable composition. type CompositedCompiler struct { Compiler - FilterCompiler + ConditionCompiler + MutatingCompiler CompositionEnv *CompositionEnv } -type CompositedFilter struct { - Filter +// CompositedConditionEvaluator provides evaluation of a condition expression with variable composition. +// The expressions must return a boolean. +type CompositedConditionEvaluator struct { + ConditionEvaluator + + compositionEnv *CompositionEnv +} + +// CompositedEvaluator provides evaluation of a single expression with variable composition. +// The types that may returned by the expression is determined at compilation time. +type CompositedEvaluator struct { + MutatingEvaluator compositionEnv *CompositionEnv } @@ -64,11 +76,13 @@ func NewCompositedCompilerFromTemplate(context *CompositionEnv) *CompositedCompi CompiledVariables: map[string]CompilationResult{}, } compiler := NewCompiler(context.EnvSet) - filterCompiler := NewFilterCompiler(context.EnvSet) + conditionCompiler := &conditionCompiler{compiler} + mutation := &mutatingCompiler{compiler} return &CompositedCompiler{ - Compiler: compiler, - FilterCompiler: filterCompiler, - CompositionEnv: context, + Compiler: compiler, + ConditionCompiler: conditionCompiler, + MutatingCompiler: mutation, + CompositionEnv: context, } } @@ -85,11 +99,20 @@ func (c *CompositedCompiler) CompileAndStoreVariable(variable NamedExpressionAcc return result } -func (c *CompositedCompiler) Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter { - filter := c.FilterCompiler.Compile(expressions, optionalDecls, envType) - return &CompositedFilter{ - Filter: filter, - compositionEnv: c.CompositionEnv, +func (c *CompositedCompiler) CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator { + condition := c.ConditionCompiler.CompileCondition(expressions, optionalDecls, envType) + return &CompositedConditionEvaluator{ + ConditionEvaluator: condition, + compositionEnv: c.CompositionEnv, + } +} + +// CompileEvaluator compiles an mutatingEvaluator for the given expression, options and environment. +func (c *CompositedCompiler) CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator { + mutation := c.MutatingCompiler.CompileMutatingEvaluator(expression, optionalDecls, envType) + return &CompositedEvaluator{ + MutatingEvaluator: mutation, + compositionEnv: c.CompositionEnv, } } @@ -160,9 +183,9 @@ func (c *compositionContext) Variables(activation any) ref.Val { return lazyMap } -func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) { +func (f *CompositedConditionEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) { ctx = f.compositionEnv.CreateContext(ctx) - return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget) + return f.ConditionEvaluator.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget) } func (c *compositionContext) reportCost(cost int64) { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go index 53d9b861284..13a373faabf 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/composition_test.go @@ -223,8 +223,8 @@ func TestCompositedPolicies(t *testing.T) { t.Fatal(err) } compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions) - validations := []ExpressionAccessor{&condition{Expression: tc.expression}} - f := compiler.Compile(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions) + validations := []ExpressionAccessor{&testCondition{Expression: tc.expression}} + f := compiler.CompileCondition(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions) versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) if err != nil { t.Fatal(err) diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition.go new file mode 100644 index 00000000000..f28401f3e1b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition.go @@ -0,0 +1,216 @@ +/* +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 ( + "context" + "reflect" + + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/cel/environment" +) + +// conditionCompiler implement the interface ConditionCompiler. +type conditionCompiler struct { + compiler Compiler +} + +func NewConditionCompiler(env *environment.EnvSet) ConditionCompiler { + return &conditionCompiler{compiler: NewCompiler(env)} +} + +// CompileCondition compiles the cel expressions defined in the ExpressionAccessors into a ConditionEvaluator +func (c *conditionCompiler) CompileCondition(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) ConditionEvaluator { + compilationResults := make([]CompilationResult, len(expressionAccessors)) + for i, expressionAccessor := range expressionAccessors { + if expressionAccessor == nil { + continue + } + compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode) + } + return NewCondition(compilationResults) +} + +// condition implements the ConditionEvaluator interface +type condition struct { + compilationResults []CompilationResult +} + +func NewCondition(compilationResults []CompilationResult) ConditionEvaluator { + return &condition{ + compilationResults, + } +} + +func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) { + if obj == nil || reflect.ValueOf(obj).IsNil() { + return &unstructured.Unstructured{Object: nil}, nil + } + ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + return &unstructured.Unstructured{Object: ret}, nil +} + +func objectToResolveVal(r runtime.Object) (interface{}, error) { + if r == nil || reflect.ValueOf(r).IsNil() { + return nil, nil + } + v, err := convertObjectToUnstructured(r) + if err != nil { + return nil, err + } + return v.Object, nil +} + +// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations +// errors per evaluation are returned on the Evaluation object +// 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 (c *condition) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) { + // TODO: replace unstructured with ref.Val for CEL variables when native type support is available + evaluations := make([]EvaluationResult, len(c.compilationResults)) + var err error + + // if this activation supports composition, we will need the compositionCtx. It may be nil. + compositionCtx, _ := ctx.(CompositionContext) + + activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace) + if err != nil { + return nil, -1, err + } + + remainingBudget := runtimeCELCostBudget + for i, compilationResult := range c.compilationResults { + evaluations[i], remainingBudget, err = activation.Evaluate(ctx, compositionCtx, compilationResult, remainingBudget) + if err != nil { + return nil, -1, err + } + } + + return evaluations, remainingBudget, nil +} + +// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154 +func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest { + // Attempting to use same logic as webhook for constructing resource + // GVK, GVR, subresource + // Use the GVK, GVR that the matcher decided was equivalent to that of the request + // https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210 + gvk := equivalentKind + gvr := equivalentGVR + subresource := attr.GetSubresource() + + requestGVK := attr.GetKind() + requestGVR := attr.GetResource() + requestSubResource := attr.GetSubresource() + + aUserInfo := attr.GetUserInfo() + var userInfo authenticationv1.UserInfo + if aUserInfo != nil { + userInfo = authenticationv1.UserInfo{ + Extra: make(map[string]authenticationv1.ExtraValue), + Groups: aUserInfo.GetGroups(), + UID: aUserInfo.GetUID(), + Username: aUserInfo.GetName(), + } + // Convert the extra information in the user object + for key, val := range aUserInfo.GetExtra() { + userInfo.Extra[key] = authenticationv1.ExtraValue(val) + } + } + + dryRun := attr.IsDryRun() + + return &admissionv1.AdmissionRequest{ + Kind: metav1.GroupVersionKind{ + Group: gvk.Group, + Kind: gvk.Kind, + Version: gvk.Version, + }, + Resource: metav1.GroupVersionResource{ + Group: gvr.Group, + Resource: gvr.Resource, + Version: gvr.Version, + }, + SubResource: subresource, + RequestKind: &metav1.GroupVersionKind{ + Group: requestGVK.Group, + Kind: requestGVK.Kind, + Version: requestGVK.Version, + }, + RequestResource: &metav1.GroupVersionResource{ + Group: requestGVR.Group, + Resource: requestGVR.Resource, + Version: requestGVR.Version, + }, + RequestSubResource: requestSubResource, + Name: attr.GetName(), + Namespace: attr.GetNamespace(), + Operation: admissionv1.Operation(attr.GetOperation()), + UserInfo: userInfo, + // Leave Object and OldObject unset since we don't provide access to them via request + DryRun: &dryRun, + Options: runtime.RawExtension{ + Object: attr.GetOperationOptions(), + }, + } +} + +// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation. +// If the namespace is nil, CreateNamespaceObject returns nil +func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace { + if namespace == nil { + return nil + } + + return &v1.Namespace{ + Status: namespace.Status, + Spec: namespace.Spec, + ObjectMeta: metav1.ObjectMeta{ + Name: namespace.Name, + GenerateName: namespace.GenerateName, + Namespace: namespace.Namespace, + UID: namespace.UID, + ResourceVersion: namespace.ResourceVersion, + Generation: namespace.Generation, + CreationTimestamp: namespace.CreationTimestamp, + DeletionTimestamp: namespace.DeletionTimestamp, + DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds, + Labels: namespace.Labels, + Annotations: namespace.Annotations, + Finalizers: namespace.Finalizers, + }, + } +} + +// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator +func (c *condition) CompilationErrors() []error { + compilationErrors := []error{} + for _, result := range c.compilationResults { + if result.Error != nil { + compilationErrors = append(compilationErrors, result.Error) + } + } + return compilationErrors +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go similarity index 93% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go index f6e6f8fcf91..1684aa3cc65 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/condition_test.go @@ -28,8 +28,6 @@ import ( celtypes "github.com/google/cel-go/common/types" "github.com/stretchr/testify/require" - pointer "k8s.io/utils/ptr" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -48,17 +46,18 @@ import ( genericfeatures "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" + pointer "k8s.io/utils/ptr" ) -type condition struct { +type testCondition struct { Expression string } -func (c *condition) GetExpression() string { - return c.Expression +func (tc *testCondition) GetExpression() string { + return tc.Expression } -func (v *condition) ReturnTypes() []*celgo.Type { +func (tc *testCondition) ReturnTypes() []*celgo.Type { return []*celgo.Type{celgo.BoolType} } @@ -71,10 +70,10 @@ func TestCompile(t *testing.T) { { name: "invalid syntax", validation: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "1 < 'asdf'", }, - &condition{ + &testCondition{ Expression: "1 < 2", }, }, @@ -85,13 +84,13 @@ func TestCompile(t *testing.T) { { name: "valid syntax", validation: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "1 < 2", }, - &condition{ + &testCondition{ Expression: "object.spec.string.matches('[0-9]+')", }, - &condition{ + &testCondition{ Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'", }, }, @@ -100,13 +99,13 @@ func TestCompile(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))} - e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions) + c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))} + e := c.CompileCondition(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions) if e == nil { t.Fatalf("unexpected nil validator") } validations := tc.validation - CompilationResults := e.(*filter).compilationResults + CompilationResults := e.(*condition).compilationResults require.Equal(t, len(validations), len(CompilationResults)) meets := make([]bool, len(validations)) @@ -131,7 +130,7 @@ func TestCompile(t *testing.T) { } } -func TestFilter(t *testing.T) { +func TestCondition(t *testing.T) { simpleLabelSelector, err := labels.NewRequirement("apple", selection.Equals, []string{"banana"}) if err != nil { panic(err) @@ -205,7 +204,7 @@ func TestFilter(t *testing.T) { { name: "valid syntax for object", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "has(object.subsets) && object.subsets.size() < 2", }, }, @@ -220,7 +219,7 @@ func TestFilter(t *testing.T) { { name: "valid syntax for metadata", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "object.metadata.name == 'endpoints1'", }, }, @@ -235,10 +234,10 @@ func TestFilter(t *testing.T) { { name: "valid syntax for oldObject", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "oldObject == null", }, - &condition{ + &testCondition{ Expression: "object != null", }, }, @@ -256,7 +255,7 @@ func TestFilter(t *testing.T) { { name: "valid syntax for request", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "request.operation == 'CREATE'", }, }, @@ -271,7 +270,7 @@ func TestFilter(t *testing.T) { { name: "valid syntax for configMap", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "request.namespace != params.data.fakeString", }, }, @@ -287,7 +286,7 @@ func TestFilter(t *testing.T) { { name: "test failure", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "object.subsets.size() > 2", }, }, @@ -310,10 +309,10 @@ func TestFilter(t *testing.T) { { name: "test failure with multiple validations", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "has(object.subsets)", }, - &condition{ + &testCondition{ Expression: "object.subsets.size() > 2", }, }, @@ -332,10 +331,10 @@ func TestFilter(t *testing.T) { { name: "test failure policy with multiple failed validations", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "oldObject != null", }, - &condition{ + &testCondition{ Expression: "object.subsets.size() > 2", }, }, @@ -354,10 +353,10 @@ func TestFilter(t *testing.T) { { name: "test Object null in delete", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "oldObject != null", }, - &condition{ + &testCondition{ Expression: "object == null", }, }, @@ -376,7 +375,7 @@ func TestFilter(t *testing.T) { { name: "test runtime error", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "oldObject.x == 100", }, }, @@ -392,7 +391,7 @@ func TestFilter(t *testing.T) { { name: "test against crd param", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "object.subsets.size() < params.spec.testSize", }, }, @@ -408,10 +407,10 @@ func TestFilter(t *testing.T) { { name: "test compile failure", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "fail to compile test", }, - &condition{ + &testCondition{ Expression: "object.subsets.size() > params.spec.testSize", }, }, @@ -430,7 +429,7 @@ func TestFilter(t *testing.T) { { name: "test pod", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "object.spec.nodeName == 'testnode'", }, }, @@ -446,7 +445,7 @@ func TestFilter(t *testing.T) { { name: "test deny paramKind without paramRef", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "params != null", }, }, @@ -461,7 +460,7 @@ func TestFilter(t *testing.T) { { name: "test allow paramKind without paramRef", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "params == null", }, }, @@ -477,10 +476,10 @@ func TestFilter(t *testing.T) { { name: "test authorizer allow resource check", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.group('').resource('endpoints').check('create').allowed()", }, - &condition{ + &testCondition{ Expression: "authorizer.group('').resource('endpoints').check('create').errored()", }, }, @@ -503,7 +502,7 @@ func TestFilter(t *testing.T) { { name: "test authorizer error using fieldSelector with 1.30 compatibility", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()", }, }, @@ -535,7 +534,7 @@ func TestFilter(t *testing.T) { { name: "test authorizer allow resource check with all fields", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()", }, }, @@ -567,7 +566,7 @@ func TestFilter(t *testing.T) { { name: "test authorizer allow resource check with parse failures", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo badoperator bar').labelSelector('apple badoperator banana').subresource('status').namespace('test').name('backend').check('create').allowed()", }, }, @@ -595,7 +594,7 @@ func TestFilter(t *testing.T) { { name: "test authorizer allow resource check with all fields, without gate", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()", }, }, @@ -621,7 +620,7 @@ func TestFilter(t *testing.T) { { name: "test authorizer not allowed resource check one incorrect field", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()", }, @@ -646,7 +645,7 @@ func TestFilter(t *testing.T) { { name: "test authorizer reason", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.group('').resource('endpoints').check('create').reason() == 'fake reason'", }, }, @@ -661,13 +660,13 @@ func TestFilter(t *testing.T) { { name: "test authorizer error", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.group('').resource('endpoints').check('create').errored()", }, - &condition{ + &testCondition{ Expression: "authorizer.group('').resource('endpoints').check('create').error() == 'fake authz error'", }, - &condition{ + &testCondition{ Expression: "authorizer.group('').resource('endpoints').check('create').allowed()", }, }, @@ -688,7 +687,7 @@ func TestFilter(t *testing.T) { { name: "test authorizer allow path check", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.path('/healthz').check('get').allowed()", }, }, @@ -706,7 +705,7 @@ func TestFilter(t *testing.T) { { name: "test authorizer decision is denied path check", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.path('/healthz').check('get').allowed() == false", }, }, @@ -721,7 +720,7 @@ func TestFilter(t *testing.T) { { name: "test request resource authorizer allow check", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.requestResource.check('custom-verb').allowed()", }, }, @@ -745,7 +744,7 @@ func TestFilter(t *testing.T) { { name: "test subresource request resource authorizer allow check", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.requestResource.check('custom-verb').allowed()", }, }, @@ -769,7 +768,7 @@ func TestFilter(t *testing.T) { { name: "test serviceAccount authorizer allow check", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "authorizer.serviceAccount('default', 'test-serviceaccount').group('').resource('endpoints').namespace('default').name('endpoints1').check('custom-verb').allowed()", }, }, @@ -796,7 +795,7 @@ func TestFilter(t *testing.T) { { name: "test perCallLimit exceed", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "object.subsets.size() < params.spec.testSize", }, }, @@ -813,28 +812,28 @@ func TestFilter(t *testing.T) { { name: "test namespaceObject", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "namespaceObject.metadata.name == 'test'", }, - &condition{ + &testCondition{ Expression: "'env' in namespaceObject.metadata.labels && namespaceObject.metadata.labels.env == 'test'", }, - &condition{ + &testCondition{ Expression: "('fake' in namespaceObject.metadata.labels) && namespaceObject.metadata.labels.fake == 'test'", }, - &condition{ + &testCondition{ Expression: "namespaceObject.spec.finalizers[0] == 'kubernetes'", }, - &condition{ + &testCondition{ Expression: "namespaceObject.status.phase == 'Active'", }, - &condition{ + &testCondition{ Expression: "size(namespaceObject.metadata.managedFields) == 1", }, - &condition{ + &testCondition{ Expression: "size(namespaceObject.metadata.ownerReferences) == 1", }, - &condition{ + &testCondition{ Expression: "'env' in namespaceObject.metadata.annotations", }, }, @@ -891,14 +890,14 @@ func TestFilter(t *testing.T) { if err != nil { t.Fatal(err) } - c := NewFilterCompiler(env) - f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, environment.NewExpressions) + c := NewConditionCompiler(env) + f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, environment.NewExpressions) if f == nil { t.Fatalf("unexpected nil validator") } validations := tc.validations - CompilationResults := f.(*filter).compilationResults + CompilationResults := f.(*condition).compilationResults require.Equal(t, len(validations), len(CompilationResults)) versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) @@ -960,10 +959,10 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "expression exceed RuntimeCELCostBudget at fist expression", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "has(object.subsets) && object.subsets.size() < 2", }, - &condition{ + &testCondition{ Expression: "has(object.subsets)", }, }, @@ -975,10 +974,10 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "expression exceed RuntimeCELCostBudget at last expression", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "has(object.subsets) && object.subsets.size() < 2", }, - &condition{ + &testCondition{ Expression: "object.subsets.size() > 2", }, }, @@ -991,10 +990,10 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "test RuntimeCELCostBudge is not exceed", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "oldObject != null", }, - &condition{ + &testCondition{ Expression: "object.subsets.size() > 2", }, }, @@ -1008,10 +1007,10 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "test RuntimeCELCostBudge exactly covers", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "oldObject != null", }, - &condition{ + &testCondition{ Expression: "object.subsets.size() > 2", }, }, @@ -1025,13 +1024,13 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "test RuntimeCELCostBudge exactly covers then constant", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "oldObject != null", }, - &condition{ + &testCondition{ Expression: "object.subsets.size() > 2", }, - &condition{ + &testCondition{ Expression: "true", // zero cost }, }, @@ -1045,7 +1044,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "Extended library cost: authz check", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, }, @@ -1059,7 +1058,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "Extended library cost: isSorted()", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "[1,2,3,4].isSorted()", }, }, @@ -1072,7 +1071,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "Extended library cost: url", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'", }, }, @@ -1085,7 +1084,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "Extended library cost: split", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "size('abc 123 def 123'.split(' ')) > 0", }, }, @@ -1098,7 +1097,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "Extended library cost: join", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0", }, }, @@ -1111,7 +1110,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "Extended library cost: find", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "size('abc 123 def 123'.find('123')) > 0", }, }, @@ -1124,7 +1123,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "Extended library cost: quantity", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")", }, }, @@ -1137,10 +1136,10 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at fist expression", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, - &condition{ + &testCondition{ Expression: "has(object.subsets)", }, }, @@ -1154,10 +1153,10 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at last expression", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, }, @@ -1171,10 +1170,10 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge is not exceed", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, }, @@ -1190,10 +1189,10 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge exactly covers", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()", }, }, @@ -1209,7 +1208,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: per call limit exceeds", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "!authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed()", }, }, @@ -1224,7 +1223,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: isSorted()", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "[1,2,3,4].isSorted()", }, }, @@ -1238,7 +1237,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: url", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'", }, }, @@ -1252,7 +1251,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: split", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "size('abc 123 def 123'.split(' ')) > 0", }, }, @@ -1266,7 +1265,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: join", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0", }, }, @@ -1280,7 +1279,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: find", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "size('abc 123 def 123'.find('123')) > 0", }, }, @@ -1294,7 +1293,7 @@ func TestRuntimeCELCostBudget(t *testing.T) { { name: "With StrictCostEnforcementForVAP enabled: Extended library cost: quantity", validations: []ExpressionAccessor{ - &condition{ + &testCondition{ Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")", }, }, @@ -1309,13 +1308,13 @@ func TestRuntimeCELCostBudget(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))} - f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions) + c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))} + f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions) if f == nil { t.Fatalf("unexpected nil validator") } validations := tc.validations - CompilationResults := f.(*filter).compilationResults + CompilationResults := f.(*condition).compilationResults require.Equal(t, len(validations), len(CompilationResults)) versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) @@ -1476,7 +1475,7 @@ func TestCompilationErrors(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - e := filter{ + e := condition{ compilationResults: tc.results, } compilationErrors := e.CompilationErrors() diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go deleted file mode 100644 index 216a474d23e..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go +++ /dev/null @@ -1,361 +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 ( - "context" - "fmt" - "math" - "reflect" - "time" - - "github.com/google/cel-go/interpreter" - - admissionv1 "k8s.io/api/admission/v1" - authenticationv1 "k8s.io/api/authentication/v1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "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(env *environment.EnvSet) FilterCompiler { - return &filterCompiler{compiler: NewCompiler(env)} -} - -type evaluationActivation struct { - object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{} -} - -// ResolveName returns a value from the activation by qualified name, or false if the name -// could not be found. -func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) { - switch name { - case ObjectVarName: - return a.object, true - case OldObjectVarName: - return a.oldObject, true - case ParamsVarName: - return a.params, true // params may be null - case RequestVarName: - return a.request, true - case NamespaceVarName: - return a.namespace, true - case AuthorizerVarName: - return a.authorizer, a.authorizer != nil - case RequestResourceAuthorizerVarName: - return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil - case VariableVarName: // variables always present - return a.variables, true - default: - return nil, false - } -} - -// Parent returns the parent of the current activation, may be nil. -// If non-nil, the parent will be searched during resolve calls. -func (a *evaluationActivation) Parent() interpreter.Activation { - return nil -} - -// Compile compiles the cel expressions defined in the ExpressionAccessors into a 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] = c.compiler.CompileCELExpression(expressionAccessor, options, mode) - } - return NewFilter(compilationResults) -} - -// filter implements the Filter interface -type filter struct { - compilationResults []CompilationResult -} - -func NewFilter(compilationResults []CompilationResult) Filter { - return &filter{ - compilationResults, - } -} - -func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) { - if obj == nil || reflect.ValueOf(obj).IsNil() { - return &unstructured.Unstructured{Object: nil}, nil - } - ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) - if err != nil { - return nil, err - } - return &unstructured.Unstructured{Object: ret}, nil -} - -func objectToResolveVal(r runtime.Object) (interface{}, error) { - if r == nil || reflect.ValueOf(r).IsNil() { - return nil, nil - } - v, err := convertObjectToUnstructured(r) - if err != nil { - return nil, err - } - return v.Object, nil -} - -// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations -// errors per evaluation are returned on the Evaluation object -// 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(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, 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 - - oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject) - if err != nil { - return nil, -1, err - } - objectVal, err := objectToResolveVal(versionedAttr.VersionedObject) - if err != nil { - return nil, -1, err - } - var paramsVal, authorizerVal, requestResourceAuthorizerVal any - if inputs.VersionedParams != nil { - paramsVal, err = objectToResolveVal(inputs.VersionedParams) - if err != nil { - return nil, -1, err - } - } - - if inputs.Authorizer != nil { - authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer) - requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr) - } - - requestVal, err := convertObjectToUnstructured(request) - if err != nil { - return nil, -1, err - } - namespaceVal, err := objectToResolveVal(namespace) - if err != nil { - return nil, -1, err - } - va := &evaluationActivation{ - object: objectVal, - oldObject: oldObjectVal, - params: paramsVal, - request: requestVal.Object, - namespace: namespaceVal, - authorizer: authorizerVal, - requestResourceAuthorizer: requestResourceAuthorizerVal, - } - - // composition is an optional feature that only applies for ValidatingAdmissionPolicy. - // check if the context allows composition - var compositionCtx CompositionContext - var ok bool - if compositionCtx, ok = ctx.(CompositionContext); ok { - va.variables = compositionCtx.Variables(va) - } - - remainingBudget := runtimeCELCostBudget - for i, compilationResult := range f.compilationResults { - var evaluation = &evaluations[i] - if compilationResult.ExpressionAccessor == nil { // in case of placeholder - continue - } - evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor - if compilationResult.Error != nil { - evaluation.Error = &cel.Error{ - Type: cel.ErrorTypeInvalid, - Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error), - Cause: compilationResult.Error, - } - continue - } - if compilationResult.Program == nil { - evaluation.Error = &cel.Error{ - Type: cel.ErrorTypeInternal, - Detail: fmt.Sprintf("unexpected internal error compiling expression"), - } - continue - } - t1 := time.Now() - evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, va) - // budget may be spent due to lazy evaluation of composited variables - if compositionCtx != nil { - compositionCost := compositionCtx.GetAndResetCost() - if compositionCost > remainingBudget { - return nil, -1, &cel.Error{ - Type: cel.ErrorTypeInvalid, - Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"), - Cause: cel.ErrOutOfBudget, - } - } - remainingBudget -= compositionCost - } - elapsed := time.Since(t1) - evaluation.Elapsed = elapsed - if evalDetails == nil { - return nil, -1, &cel.Error{ - Type: cel.ErrorTypeInternal, - Detail: 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, -1, &cel.Error{ - Type: cel.ErrorTypeInvalid, - Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()), - Cause: cel.ErrOutOfBudget, - } - } else { - if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget { - return nil, -1, &cel.Error{ - Type: cel.ErrorTypeInvalid, - Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"), - Cause: cel.ErrOutOfBudget, - } - } - remainingBudget -= int64(*rtCost) - } - } - if err != nil { - evaluation.Error = &cel.Error{ - Type: cel.ErrorTypeInvalid, - Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err), - } - } else { - evaluation.EvalResult = evalResult - } - } - - return evaluations, remainingBudget, nil -} - -// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154 -func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest { - // Attempting to use same logic as webhook for constructing resource - // GVK, GVR, subresource - // Use the GVK, GVR that the matcher decided was equivalent to that of the request - // https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210 - gvk := equivalentKind - gvr := equivalentGVR - subresource := attr.GetSubresource() - - requestGVK := attr.GetKind() - requestGVR := attr.GetResource() - requestSubResource := attr.GetSubresource() - - aUserInfo := attr.GetUserInfo() - var userInfo authenticationv1.UserInfo - if aUserInfo != nil { - userInfo = authenticationv1.UserInfo{ - Extra: make(map[string]authenticationv1.ExtraValue), - Groups: aUserInfo.GetGroups(), - UID: aUserInfo.GetUID(), - Username: aUserInfo.GetName(), - } - // Convert the extra information in the user object - for key, val := range aUserInfo.GetExtra() { - userInfo.Extra[key] = authenticationv1.ExtraValue(val) - } - } - - dryRun := attr.IsDryRun() - - return &admissionv1.AdmissionRequest{ - Kind: metav1.GroupVersionKind{ - Group: gvk.Group, - Kind: gvk.Kind, - Version: gvk.Version, - }, - Resource: metav1.GroupVersionResource{ - Group: gvr.Group, - Resource: gvr.Resource, - Version: gvr.Version, - }, - SubResource: subresource, - RequestKind: &metav1.GroupVersionKind{ - Group: requestGVK.Group, - Kind: requestGVK.Kind, - Version: requestGVK.Version, - }, - RequestResource: &metav1.GroupVersionResource{ - Group: requestGVR.Group, - Resource: requestGVR.Resource, - Version: requestGVR.Version, - }, - RequestSubResource: requestSubResource, - Name: attr.GetName(), - Namespace: attr.GetNamespace(), - Operation: admissionv1.Operation(attr.GetOperation()), - UserInfo: userInfo, - // Leave Object and OldObject unset since we don't provide access to them via request - DryRun: &dryRun, - Options: runtime.RawExtension{ - Object: attr.GetOperationOptions(), - }, - } -} - -// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation. -// If the namespace is nil, CreateNamespaceObject returns nil -func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace { - if namespace == nil { - return nil - } - - return &v1.Namespace{ - Status: namespace.Status, - Spec: namespace.Spec, - ObjectMeta: metav1.ObjectMeta{ - Name: namespace.Name, - GenerateName: namespace.GenerateName, - Namespace: namespace.Namespace, - UID: namespace.UID, - ResourceVersion: namespace.ResourceVersion, - Generation: namespace.Generation, - CreationTimestamp: namespace.CreationTimestamp, - DeletionTimestamp: namespace.DeletionTimestamp, - DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds, - Labels: namespace.Labels, - Annotations: namespace.Annotations, - Finalizers: namespace.Finalizers, - }, - } -} - -// CompilationErrors returns a list of all the errors from the compilation of the evaluator -func (e *filter) CompilationErrors() []error { - compilationErrors := []error{} - for _, result := range e.compilationResults { - if result.Error != nil { - compilationErrors = append(compilationErrors, result.Error) - } - } - return compilationErrors -} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go index ae61dc826c4..a9e35a226f1 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go @@ -63,12 +63,15 @@ type OptionalVariableDeclarations struct { HasAuthorizer bool // StrictCost specifies if the CEL cost limitation is strict for extended libraries as well as native libraries. StrictCost bool + // HasPatchTypes specifies if JSONPatch, Object, Object.metadata and similar types are available in CEL. These can be used + // to initialize the typed objects in CEL required to create patches. + HasPatchTypes bool } -// 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, envType environment.Type) Filter +// ConditionCompiler contains a function to assist with converting types and values to/from CEL-typed values. +type ConditionCompiler interface { + // CompileCondition is used for the cel expression compilation + CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator } // OptionalVariableBindings provides expression bindings for optional CEL variables. @@ -82,16 +85,38 @@ type OptionalVariableBindings struct { Authorizer authorizer.Authorizer } -// Filter contains a function to evaluate compiled CEL-typed values +// ConditionEvaluator contains the result of compiling a CEL expression +// that evaluates to a condition. This is used both for validation and pre-conditions. // It expects the inbound object to already have been converted to the version expected // by the underlying CEL code (which is indicated by the match criteria of a policy definition). // versionedParams may be nil. -type Filter interface { +type ConditionEvaluator interface { // ForInput converts compiled CEL-typed values into evaluated CEL-typed value. // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. - // If cost budget is calculated, the filter should return the remaining budget. + // If cost budget is calculated, the condition should return the remaining budget. ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) - // CompilationErrors returns a list of errors from the compilation of the evaluator + // CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator + CompilationErrors() []error +} + +// MutatingCompiler contains a function to assist with converting types and values to/from CEL-typed values. +type MutatingCompiler interface { + // CompileMutatingEvaluator is used for the cel expression compilation + CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator +} + +// MutatingEvaluator contains the result of compiling a CEL expression +// that evaluates to a mutation. +// It expects the inbound object to already have been converted to the version expected +// by the underlying CEL code (which is indicated by the match criteria of a policy definition). +// versionedParams may be nil. +type MutatingEvaluator interface { + // ForInput converts compiled CEL-typed values into a CEL-typed value representing a mutation. + // runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input. + // If cost budget is calculated, the condition should return the remaining budget. + ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error) + + // CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator CompilationErrors() []error } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/mutation.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/mutation.go new file mode 100644 index 00000000000..8c609b94410 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/mutation.go @@ -0,0 +1,73 @@ +/* +Copyright 2024 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 ( + "context" + + admissionv1 "k8s.io/api/admission/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/cel/environment" +) + +// mutatingCompiler provides a MutatingCompiler implementation. +type mutatingCompiler struct { + compiler Compiler +} + +// CompileMutatingEvaluator compiles a CEL expression for admission plugins and returns an MutatingEvaluator for executing the +// compiled CEL expression. +func (p *mutatingCompiler) CompileMutatingEvaluator(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) MutatingEvaluator { + compilationResult := p.compiler.CompileCELExpression(expressionAccessor, options, mode) + return NewMutatingEvaluator(compilationResult) +} + +type mutatingEvaluator struct { + compilationResult CompilationResult +} + +func NewMutatingEvaluator(compilationResult CompilationResult) MutatingEvaluator { + return &mutatingEvaluator{compilationResult} +} + +// ForInput evaluates the compiled CEL expression and returns an evaluation result +// errors per evaluation are returned in the evaluation result +// 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 (p *mutatingEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error) { + // if this activation supports composition, we will need the compositionCtx. It may be nil. + compositionCtx, _ := ctx.(CompositionContext) + + activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace) + if err != nil { + return EvaluationResult{}, -1, err + } + evaluation, remainingBudget, err := activation.Evaluate(ctx, compositionCtx, p.compilationResult, runtimeCELCostBudget) + if err != nil { + return evaluation, -1, err + } + return evaluation, remainingBudget, nil + +} + +// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator +func (p *mutatingEvaluator) CompilationErrors() (compilationErrors []error) { + if p.compilationResult.Error != nil { + return []error{p.compilationResult.Error} + } + return nil +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go index f23580cc09f..7ae29d5bb44 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go @@ -50,7 +50,7 @@ type WebhookAccessor interface { GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error) // GetCompiledMatcher gets the compiled matcher object - GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher + GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher // GetName gets the webhook Name field. Note that the name is scoped to the webhook // configuration and does not provide a globally unique identity, if a unique identity is @@ -132,7 +132,7 @@ func (m *mutatingWebhookAccessor) GetType() string { return "admit" } -func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher { +func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher { m.compileMatcher.Do(func() { expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions)) for i, matchCondition := range m.MutatingWebhook.MatchConditions { @@ -145,7 +145,7 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) { strictCost = true } - m.compiledMatcher = matchconditions.NewMatcher(compiler.Compile( + m.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition( expressions, cel.OptionalVariableDeclarations{ HasParams: false, @@ -265,7 +265,7 @@ func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Cli return v.client, v.clientErr } -func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher { +func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher { v.compileMatcher.Do(func() { expressions := make([]cel.ExpressionAccessor, len(v.ValidatingWebhook.MatchConditions)) for i, matchCondition := range v.ValidatingWebhook.MatchConditions { @@ -278,7 +278,7 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) { strictCost = true } - v.compiledMatcher = matchconditions.NewMatcher(compiler.Compile( + v.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition( expressions, cel.OptionalVariableDeclarations{ HasParams: false, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go index 9b6ac5a8199..8db7d3ced94 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "io" + "k8s.io/apiserver/pkg/cel/environment" "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" @@ -56,7 +57,7 @@ type Webhook struct { namespaceMatcher *namespace.Matcher objectMatcher *object.Matcher dispatcher Dispatcher - filterCompiler cel.FilterCompiler + filterCompiler cel.ConditionCompiler authorizer authorizer.Authorizer } @@ -101,7 +102,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory namespaceMatcher: &namespace.Matcher{}, objectMatcher: &object.Matcher{}, dispatcher: dispatcherFactory(&cm), - filterCompiler: cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))), + filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))), }, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go index 6c12323862d..f9780108ecf 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook_test.go @@ -76,7 +76,7 @@ type fakeWebhookAccessor struct { matchResult bool } -func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher { +func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher { return &fakeMatcher{ throwError: f.throwError, matchResult: f.matchResult, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go index 21dd28f6c24..b9b4c9169e8 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher.go @@ -54,14 +54,14 @@ var _ Matcher = &matcher{} // matcher evaluates compiled cel expressions and determines if they match the given request or not type matcher struct { - filter celplugin.Filter + filter celplugin.ConditionEvaluator failPolicy v1.FailurePolicyType matcherType string matcherKind string objectName string } -func NewMatcher(filter celplugin.Filter, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher { +func NewMatcher(filter celplugin.ConditionEvaluator, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher { var f v1.FailurePolicyType if failPolicy == nil { f = v1.Fail diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go index 20d9aebfb7d..153ebb3b5af 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions/matcher_test.go @@ -35,7 +35,7 @@ import ( "k8s.io/apiserver/pkg/admission/plugin/cel" ) -var _ cel.Filter = &fakeCelFilter{} +var _ cel.ConditionEvaluator = &fakeCelFilter{} type fakeCelFilter struct { evaluations []cel.EvaluationResult