From e96ef311872ee6429a54e4580528717238a6816b Mon Sep 17 00:00:00 2001 From: Igor Velichkovich Date: Wed, 15 Feb 2023 16:08:59 -0600 Subject: [PATCH] refactor admission cel validator and compiler to be reusable --- .../validation/validation.go | 9 +- .../apiserver/pkg/admission/plugin/cel/OWNERS | 10 + .../compiler.go => cel/compile.go} | 23 +- .../compiler_test.go => cel/compile_test.go} | 20 +- .../pkg/admission/plugin/cel/filter.go | 244 +++++ .../pkg/admission/plugin/cel/filter_test.go | 584 ++++++++++++ .../pkg/admission/plugin/cel/interface.go | 68 ++ .../admission_test.go | 578 ++++++++---- .../validatingadmissionpolicy/controller.go | 37 +- .../controller_reconcile.go | 53 +- .../validatingadmissionpolicy/interface.go | 38 +- .../validatingadmissionpolicy/matcher.go | 78 ++ .../policy_decision.go | 10 +- .../validatingadmissionpolicy/validator.go | 312 ++----- .../validator_test.go | 832 ++++++++---------- vendor/modules.txt | 1 + 16 files changed, 1909 insertions(+), 988 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/OWNERS rename staging/src/k8s.io/apiserver/pkg/admission/plugin/{validatingadmissionpolicy/compiler.go => cel/compile.go} (90%) rename staging/src/k8s.io/apiserver/pkg/admission/plugin/{validatingadmissionpolicy/compiler_test.go => cel/compile_test.go} (91%) create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go create mode 100644 staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matcher.go diff --git a/pkg/apis/admissionregistration/validation/validation.go b/pkg/apis/admissionregistration/validation/validation.go index 78b5c8cc869..d3eccb17d0d 100644 --- a/pkg/apis/admissionregistration/validation/validation.go +++ b/pkg/apis/admissionregistration/validation/validation.go @@ -18,6 +18,7 @@ package validation import ( "fmt" + "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" "regexp" "strings" @@ -28,7 +29,7 @@ import ( "k8s.io/apimachinery/pkg/util/sets" utilvalidation "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" - plugincel "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" + plugincel "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/cel" "k8s.io/apiserver/pkg/util/webhook" "k8s.io/kubernetes/pkg/apis/admissionregistration" @@ -733,7 +734,11 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio if len(trimmedExpression) == 0 { allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified")) } else { - result := plugincel.CompileValidatingPolicyExpression(trimmedExpression, paramKind != nil) + result := plugincel.CompileCELExpression(&validatingadmissionpolicy.ValidationCondition{ + Expression: trimmedExpression, + Message: v.Message, + Reason: v.Reason, + }, paramKind != nil) if result.Error != nil { switch result.Error.Type { case cel.ErrorTypeRequired: diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/OWNERS b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/OWNERS new file mode 100644 index 00000000000..6a637d28d58 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/OWNERS @@ -0,0 +1,10 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +approvers: + - jpbetz + - cici37 + - alexzielenski +reviewers: + - jpbetz + - cici37 + - alexzielenski diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/compiler.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go similarity index 90% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/compiler.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go index 3767c0d9d1c..f6b2605ac71 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/compiler.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package cel import ( "sync" @@ -160,14 +160,15 @@ func buildRequestType() *apiservercel.DeclType { )) } -// CompilationResult represents a compiled ValidatingAdmissionPolicy validation expression. +// CompilationResult represents a compiled validations expression. type CompilationResult struct { - Program cel.Program - Error *apiservercel.Error + Program cel.Program + Error *apiservercel.Error + ExpressionAccessor ExpressionAccessor } -// CompileValidatingPolicyExpression returns a compiled vaalidating policy CEL expression. -func CompileValidatingPolicyExpression(validationExpression string, hasParams bool) CompilationResult { +// CompileCELExpression returns a compiled CEL expression. +func CompileCELExpression(expressionAccessor ExpressionAccessor, hasParams bool) CompilationResult { var env *cel.Env envs, err := getEnvs() if err != nil { @@ -176,6 +177,7 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo Type: apiservercel.ErrorTypeInternal, Detail: "compiler initialization failed: " + err.Error(), }, + ExpressionAccessor: expressionAccessor, } } if hasParams { @@ -184,13 +186,14 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo env = envs.noParams } - ast, issues := env.Compile(validationExpression) + ast, issues := env.Compile(expressionAccessor.GetExpression()) if issues != nil { return CompilationResult{ Error: &apiservercel.Error{ Type: apiservercel.ErrorTypeInvalid, Detail: "compilation failed: " + issues.String(), }, + ExpressionAccessor: expressionAccessor, } } if ast.OutputType() != cel.BoolType { @@ -199,6 +202,7 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo Type: apiservercel.ErrorTypeInvalid, Detail: "cel expression must evaluate to a bool", }, + ExpressionAccessor: expressionAccessor, } } @@ -210,6 +214,7 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo Type: apiservercel.ErrorTypeInternal, Detail: "unexpected compilation error: " + err.Error(), }, + ExpressionAccessor: expressionAccessor, } } prog, err := env.Program(ast, @@ -223,9 +228,11 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo Type: apiservercel.ErrorTypeInvalid, Detail: "program instantiation failed: " + err.Error(), }, + ExpressionAccessor: expressionAccessor, } } return CompilationResult{ - Program: prog, + Program: prog, + ExpressionAccessor: expressionAccessor, } } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/compiler_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go similarity index 91% rename from staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/compiler_test.go rename to staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go index f60efb775df..ea427607c72 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/compiler_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/compile_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package validatingadmissionpolicy +package cel import ( "strings" @@ -104,22 +104,34 @@ func TestCompileValidatingPolicyExpression(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { for _, expr := range tc.expressions { - result := CompileValidatingPolicyExpression(expr, tc.hasParams) + result := CompileCELExpression(&fakeExpressionAccessor{ + expr, + }, tc.hasParams) if result.Error != nil { t.Errorf("Unexpected error: %v", result.Error) } } for expr, expectErr := range tc.errorExpressions { - result := CompileValidatingPolicyExpression(expr, tc.hasParams) + result := CompileCELExpression(&fakeExpressionAccessor{ + expr, + }, tc.hasParams) if result.Error == nil { t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr) continue } if !strings.Contains(result.Error.Error(), expectErr) { - t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error) + t.Errorf("Expected compilation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error) } continue } }) } } + +type fakeExpressionAccessor struct { + expression string +} + +func (f *fakeExpressionAccessor) GetExpression() string { + return f.expression +} 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 new file mode 100644 index 00000000000..4e152a0bb6d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go @@ -0,0 +1,244 @@ +/* +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 ( + "errors" + "fmt" + "reflect" + "time" + + "github.com/google/cel-go/interpreter" + + admissionv1 "k8s.io/api/admission/v1" + authenticationv1 "k8s.io/api/authentication/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/admission/plugin/webhook/generic" +) + +// filterCompiler implement the interface FilterCompiler. +type filterCompiler struct { +} + +func NewFilterCompiler() FilterCompiler { + return &filterCompiler{} +} + +type evaluationActivation struct { + object, oldObject, params, request 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 + case RequestVarName: + return a.request, 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, hasParam bool) Filter { + if len(expressionAccessors) == 0 { + return nil + } + compilationResults := make([]CompilationResult, len(expressionAccessors)) + for i, expressionAccessor := range expressionAccessors { + compilationResults[i] = CompileCELExpression(expressionAccessor, hasParam) + } + 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 +} + +// Evaluate 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, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]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 + + oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject) + if err != nil { + return nil, err + } + objectVal, err := objectToResolveVal(versionedAttr.VersionedObject) + if err != nil { + return nil, err + } + paramsVal, err := objectToResolveVal(versionedParams) + if err != nil { + return nil, err + } + + requestVal, err := convertObjectToUnstructured(request) + if err != nil { + return nil, err + } + va := &evaluationActivation{ + object: objectVal, + oldObject: oldObjectVal, + params: paramsVal, + request: requestVal.Object, + } + + for i, compilationResult := range f.compilationResults { + var evaluation = &evaluations[i] + evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor + if compilationResult.Error != nil { + evaluation.Error = errors.New(fmt.Sprintf("compilation error: %v", compilationResult.Error)) + continue + } + if compilationResult.Program == nil { + evaluation.Error = errors.New("unexpected internal error compiling expression") + continue + } + t1 := time.Now() + evalResult, _, err := compilationResult.Program.Eval(va) + elapsed := time.Since(t1) + evaluation.Elapsed = elapsed + if err != nil { + evaluation.Error = errors.New(fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err)) + } else { + evaluation.EvalResult = evalResult + } + } + + return evaluations, 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) *admissionv1.AdmissionRequest { + // FIXME: how to get resource GVK, GVR and subresource? + gvk := attr.GetKind() + gvr := attr.GetResource() + 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(), + }, + } +} + +// 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/filter_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go new file mode 100644 index 00000000000..c2a4cc69675 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter_test.go @@ -0,0 +1,584 @@ +/* +Copyright 2019 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 ( + "errors" + "strings" + "testing" + + celtypes "github.com/google/cel-go/common/types" + "github.com/stretchr/testify/require" + apiservercel "k8s.io/apiserver/pkg/cel" + + corev1 "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/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" +) + +type condition struct { + Expression string +} + +func (c *condition) GetExpression() string { + return c.Expression +} + +func TestCompile(t *testing.T) { + cases := []struct { + name string + validation []ExpressionAccessor + errorExpressions map[string]string + }{ + { + name: "invalid syntax", + validation: []ExpressionAccessor{ + &condition{ + Expression: "1 < 'asdf'", + }, + &condition{ + Expression: "1 < 2", + }, + }, + errorExpressions: map[string]string{ + "1 < 'asdf'": "found no matching overload for '_<_' applied to '(int, string)", + }, + }, + { + name: "valid syntax", + validation: []ExpressionAccessor{ + &condition{ + Expression: "1 < 2", + }, + &condition{ + Expression: "object.spec.string.matches('[0-9]+')", + }, + &condition{ + Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'", + }, + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var c filterCompiler + e := c.Compile(tc.validation, false) + if e == nil { + t.Fatalf("unexpected nil validator") + } + validations := tc.validation + CompilationResults := e.(*filter).compilationResults + require.Equal(t, len(validations), len(CompilationResults)) + + meets := make([]bool, len(validations)) + for expr, expectErr := range tc.errorExpressions { + for i, result := range CompilationResults { + if validations[i].GetExpression() == expr { + if result.Error == nil { + t.Errorf("Expect expression '%s' to contain error '%v' but got no error", expr, expectErr) + } else if !strings.Contains(result.Error.Error(), expectErr) { + t.Errorf("Expected validations '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error) + } + meets[i] = true + } + } + } + for i, meet := range meets { + if !meet && CompilationResults[i].Error != nil { + t.Errorf("Unexpected err '%v' for expression '%s'", CompilationResults[i].Error, validations[i].GetExpression()) + } + } + }) + } +} + +func TestFilter(t *testing.T) { + configMapParams := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{ + "fakeString": "fake", + }, + } + crdParams := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "testSize": 10, + }, + }, + } + podObject := corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Spec: corev1.PodSpec{ + NodeName: "testnode", + }, + } + + var nilUnstructured *unstructured.Unstructured + cases := []struct { + name string + attributes admission.Attributes + params runtime.Object + validations []ExpressionAccessor + results []EvaluationResult + hasParamKind bool + }{ + { + name: "valid syntax for object", + validations: []ExpressionAccessor{ + &condition{ + Expression: "has(object.subsets) && object.subsets.size() < 2", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: false, + }, + { + name: "valid syntax for metadata", + validations: []ExpressionAccessor{ + &condition{ + Expression: "object.metadata.name == 'endpoints1'", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: false, + }, + { + name: "valid syntax for oldObject", + validations: []ExpressionAccessor{ + &condition{ + Expression: "oldObject == null", + }, + &condition{ + Expression: "object != null", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: false, + }, + { + name: "valid syntax for request", + validations: []ExpressionAccessor{ + &condition{ + Expression: "request.operation == 'CREATE'", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: false, + }, + { + name: "valid syntax for configMap", + validations: []ExpressionAccessor{ + &condition{ + Expression: "request.namespace != params.data.fakeString", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: true, + params: configMapParams, + }, + { + name: "test failure", + validations: []ExpressionAccessor{ + &condition{ + Expression: "object.subsets.size() > 2", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.False, + }, + }, + hasParamKind: true, + params: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string]string{ + "fakeString": "fake", + }, + }, + }, + { + name: "test failure with multiple validations", + validations: []ExpressionAccessor{ + &condition{ + Expression: "has(object.subsets)", + }, + &condition{ + Expression: "object.subsets.size() > 2", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + { + EvalResult: celtypes.False, + }, + }, + hasParamKind: true, + params: configMapParams, + }, + { + name: "test failure policy with multiple failed validations", + validations: []ExpressionAccessor{ + &condition{ + Expression: "oldObject != null", + }, + &condition{ + Expression: "object.subsets.size() > 2", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.False, + }, + { + EvalResult: celtypes.False, + }, + }, + hasParamKind: true, + params: configMapParams, + }, + { + name: "test Object null in delete", + validations: []ExpressionAccessor{ + &condition{ + Expression: "oldObject != null", + }, + &condition{ + Expression: "object == null", + }, + }, + attributes: newValidAttribute(nil, true), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: true, + params: configMapParams, + }, + { + name: "test runtime error", + validations: []ExpressionAccessor{ + &condition{ + Expression: "oldObject.x == 100", + }, + }, + attributes: newValidAttribute(nil, true), + results: []EvaluationResult{ + { + Error: errors.New("expression 'oldObject.x == 100' resulted in error"), + }, + }, + hasParamKind: true, + params: configMapParams, + }, + { + name: "test against crd param", + validations: []ExpressionAccessor{ + &condition{ + Expression: "object.subsets.size() < params.spec.testSize", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: true, + params: crdParams, + }, + { + name: "test compile failure", + validations: []ExpressionAccessor{ + &condition{ + Expression: "fail to compile test", + }, + &condition{ + Expression: "object.subsets.size() > params.spec.testSize", + }, + }, + attributes: newValidAttribute(nil, false), + results: []EvaluationResult{ + { + Error: errors.New("compilation error"), + }, + { + EvalResult: celtypes.False, + }, + }, + hasParamKind: true, + params: crdParams, + }, + { + name: "test pod", + validations: []ExpressionAccessor{ + &condition{ + Expression: "object.spec.nodeName == 'testnode'", + }, + }, + attributes: newValidAttribute(&podObject, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: true, + params: crdParams, + }, + { + name: "test deny paramKind without paramRef", + validations: []ExpressionAccessor{ + &condition{ + Expression: "params != null", + }, + }, + attributes: newValidAttribute(&podObject, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.False, + }, + }, + hasParamKind: true, + }, + { + name: "test allow paramKind without paramRef", + validations: []ExpressionAccessor{ + &condition{ + Expression: "params == null", + }, + }, + attributes: newValidAttribute(&podObject, false), + results: []EvaluationResult{ + { + EvalResult: celtypes.True, + }, + }, + hasParamKind: true, + params: runtime.Object(nilUnstructured), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + c := filterCompiler{} + f := c.Compile(tc.validations, tc.hasParamKind) + 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) + } + + evalResults, err := f.ForInput(versionedAttr, tc.params, CreateAdmissionRequest(versionedAttr.Attributes)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + require.Equal(t, len(evalResults), len(tc.results)) + for i, result := range tc.results { + if result.EvalResult != evalResults[i].EvalResult { + t.Errorf("Expected result '%v' but got '%v'", result.EvalResult, evalResults[i].EvalResult) + } + if result.Error != nil && !strings.Contains(evalResults[i].Error.Error(), result.Error.Error()) { + t.Errorf("Expected result '%v' but got '%v'", result.Error, evalResults[i].Error) + } + } + }) + } +} + +// newObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file. +func newObjectInterfacesForTest() admission.ObjectInterfaces { + scheme := runtime.NewScheme() + corev1.AddToScheme(scheme) + return admission.NewObjectInterfacesFromScheme(scheme) +} + +func newValidAttribute(object runtime.Object, isDelete bool) admission.Attributes { + var oldObject runtime.Object + if !isDelete { + if object == nil { + object = &corev1.Endpoints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "endpoints1", + }, + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}}, + }, + }, + } + } + } else { + object = nil + oldObject = &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}}, + }, + }, + } + } + return admission.NewAttributesRecord(object, oldObject, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil) + +} + +func TestCompilationErrors(t *testing.T) { + cases := []struct { + name string + results []CompilationResult + expected []error + }{ + { + name: "no errors, empty list", + results: []CompilationResult{}, + expected: []error{}, + }, + { + name: "no errors, several results", + results: []CompilationResult{ + {}, {}, {}, + }, + expected: []error{}, + }, + { + name: "all errors", + results: []CompilationResult{ + { + Error: &apiservercel.Error{ + Detail: "error1", + }, + }, + { + Error: &apiservercel.Error{ + Detail: "error2", + }, + }, + { + Error: &apiservercel.Error{ + Detail: "error3", + }, + }, + }, + expected: []error{ + errors.New("error1"), + errors.New("error2"), + errors.New("error3"), + }, + }, + { + name: "mixed errors and non errors", + results: []CompilationResult{ + {}, + { + Error: &apiservercel.Error{ + Detail: "error1", + }, + }, + {}, + { + Error: &apiservercel.Error{ + Detail: "error2", + }, + }, + {}, + {}, + { + Error: &apiservercel.Error{ + Detail: "error3", + }, + }, + {}, + }, + expected: []error{ + errors.New("error1"), + errors.New("error2"), + errors.New("error3"), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + e := filter{ + compilationResults: tc.results, + } + compilationErrors := e.CompilationErrors() + if compilationErrors == nil { + t.Fatalf("unexpected nil value returned") + } + require.Equal(t, len(compilationErrors), len(tc.expected)) + + for i, expectedError := range tc.expected { + if expectedError.Error() != compilationErrors[i].Error() { + t.Errorf("Expected error '%v' but got '%v'", expectedError.Error(), compilationErrors[i].Error()) + } + } + }) + } +} 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 new file mode 100644 index 00000000000..d5c4f1acd7a --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/interface.go @@ -0,0 +1,68 @@ +/* +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 ( + "time" + + "github.com/google/cel-go/common/types/ref" + + v1 "k8s.io/api/admission/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" +) + +var _ ExpressionAccessor = &MatchCondition{} + +type ExpressionAccessor interface { + GetExpression() string +} + +// EvaluationResult contains the minimal required fields and metadata of a cel evaluation +type EvaluationResult struct { + EvalResult ref.Val + ExpressionAccessor ExpressionAccessor + Elapsed time.Duration + Error error +} + +// MatchCondition contains the inputs needed to compile, evaluate and match a cel expression +type MatchCondition struct { + Expression string +} + +func (v *MatchCondition) GetExpression() string { + return v.Expression +} + +// 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, hasParam bool) Filter +} + +// Filter contains a function to evaluate compiled CEL-typed values +// 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 { + // ForInput converts compiled CEL-typed values into evaluated CEL-typed values + ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *v1.AdmissionRequest) ([]EvaluationResult, error) + + // CompilationErrors returns a list of errors from the compilation of the evaluator + CompilationErrors() []error +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go index d658b588523..09d8e5df9ad 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/admission_test.go @@ -25,6 +25,9 @@ import ( "time" "github.com/stretchr/testify/require" + + admissionv1 "k8s.io/api/admission/v1" + admissionRegistrationv1 "k8s.io/api/admissionregistration/v1" "k8s.io/api/admissionregistration/v1alpha1" v1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" @@ -36,6 +39,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission/initializer" + "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/features" @@ -107,6 +111,11 @@ var ( Kind: paramsGVK.Kind, }, FailurePolicy: ptrTo(v1alpha1.Fail), + Validations: []v1alpha1.Validation{ + { + Expression: "messageId for deny policy", + }, + }, }, } @@ -146,28 +155,132 @@ var ( } ) -// Interface which has fake compile and match functionality for use in tests +// Interface which has fake compile functionality for use in tests // So that we can test the controller without pulling in any CEL functionality type fakeCompiler struct { - DefaultMatch bool - CompileFuncs map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicy) Validator - DefinitionMatchFuncs map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicy, admission.Attributes) bool - BindingMatchFuncs map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool + CompileFuncs map[string]func([]cel.ExpressionAccessor, bool) cel.Filter } -var _ ValidatorCompiler = &fakeCompiler{} +var _ cel.FilterCompiler = &fakeCompiler{} func (f *fakeCompiler) HasSynced() bool { return true } -func (f *fakeCompiler) ValidateInitialization() error { +func (f *fakeCompiler) Compile( + expressions []cel.ExpressionAccessor, + hasParam bool, +) cel.Filter { + key := expressions[0].GetExpression() + if fun, ok := f.CompileFuncs[key]; ok { + return fun(expressions, hasParam) + } + return nil } +func (f *fakeCompiler) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, compileFunc func([]cel.ExpressionAccessor, bool) cel.Filter) { + //Key must be something that we can decipher from the inputs to Validate so using expression which will be passed to validate on the filter + key := definition.Spec.Validations[0].Expression + if compileFunc != nil { + if f.CompileFuncs == nil { + f.CompileFuncs = make(map[string]func([]cel.ExpressionAccessor, bool) cel.Filter) + } + f.CompileFuncs[key] = compileFunc + } +} + +var _ cel.ExpressionAccessor = &fakeEvalRequest{} + +type fakeEvalRequest struct { + Key string +} + +func (f *fakeEvalRequest) GetExpression() string { + return "" +} + +var _ cel.Filter = &fakeFilter{} + +type fakeFilter struct { + keyId string +} + +func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]cel.EvaluationResult, error) { + return []cel.EvaluationResult{}, nil +} + +func (f *fakeFilter) CompilationErrors() []error { + return []error{} +} + +var _ Validator = &fakeValidator{} + +type fakeValidator struct { + *fakeFilter + ValidateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision +} + +func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []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 { + validatorMap = make(map[string]*fakeValidator) + } + + f.ValidateFunc = validateFunc + validatorMap[validateKey] = f +} + +func (f *fakeValidator) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + return f.ValidateFunc(versionedAttr, versionedParams) +} + +var _ Matcher = &fakeMatcher{} + +func (f *fakeMatcher) ValidateInitialization() error { + return nil +} + +type fakeMatcher struct { + DefaultMatch bool + DefinitionMatchFuncs map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicy, admission.Attributes) bool + BindingMatchFuncs map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool +} + +func (f *fakeMatcher) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, matchFunc func(*v1alpha1.ValidatingAdmissionPolicy, admission.Attributes) bool) { + namespace, name := definition.Namespace, definition.Name + key := namespacedName{ + name: name, + namespace: namespace, + } + + if matchFunc != nil { + if f.DefinitionMatchFuncs == nil { + f.DefinitionMatchFuncs = make(map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicy, admission.Attributes) bool) + } + f.DefinitionMatchFuncs[key] = matchFunc + } +} + +func (f *fakeMatcher) RegisterBinding(binding *v1alpha1.ValidatingAdmissionPolicyBinding, matchFunc func(*v1alpha1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool) { + namespace, name := binding.Namespace, binding.Name + key := namespacedName{ + name: name, + namespace: namespace, + } + + if matchFunc != nil { + if f.BindingMatchFuncs == nil { + f.BindingMatchFuncs = make(map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool) + } + f.BindingMatchFuncs[key] = matchFunc + } +} + // Matches says whether this policy definition matches the provided admission // resource request -func (f *fakeCompiler) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) { +func (f *fakeMatcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) { namespace, name := definition.Namespace, definition.Name key := namespacedName{ name: name, @@ -183,7 +296,7 @@ func (f *fakeCompiler) DefinitionMatches(a admission.Attributes, o admission.Obj // Matches says whether this policy definition matches the provided admission // resource request -func (f *fakeCompiler) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) { +func (f *fakeMatcher) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) { namespace, name := binding.Namespace, binding.Name key := namespacedName{ name: name, @@ -197,60 +310,14 @@ func (f *fakeCompiler) BindingMatches(a admission.Attributes, o admission.Object return f.DefaultMatch, nil } -func (f *fakeCompiler) Compile( - definition *v1alpha1.ValidatingAdmissionPolicy, -) Validator { - namespace, name := definition.Namespace, definition.Name - key := namespacedName{ - name: name, - namespace: namespace, - } - if fun, ok := f.CompileFuncs[key]; ok { - return fun(definition) - } +var validatorMap map[string]*fakeValidator - return nil +func reset() { + validatorMap = make(map[string]*fakeValidator) } -func (f *fakeCompiler) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, compileFunc func(*v1alpha1.ValidatingAdmissionPolicy) Validator, matchFunc func(*v1alpha1.ValidatingAdmissionPolicy, admission.Attributes) bool) { - namespace, name := definition.Namespace, definition.Name - key := namespacedName{ - name: name, - namespace: namespace, - } - if compileFunc != nil { - - if f.CompileFuncs == nil { - f.CompileFuncs = make(map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicy) Validator) - } - f.CompileFuncs[key] = compileFunc - } - - if matchFunc != nil { - if f.DefinitionMatchFuncs == nil { - f.DefinitionMatchFuncs = make(map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicy, admission.Attributes) bool) - } - f.DefinitionMatchFuncs[key] = matchFunc - } -} - -func (f *fakeCompiler) RegisterBinding(binding *v1alpha1.ValidatingAdmissionPolicyBinding, matchFunc func(*v1alpha1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool) { - namespace, name := binding.Namespace, binding.Name - key := namespacedName{ - name: name, - namespace: namespace, - } - - if matchFunc != nil { - if f.BindingMatchFuncs == nil { - f.BindingMatchFuncs = make(map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicyBinding, admission.Attributes) bool) - } - f.BindingMatchFuncs[key] = matchFunc - } -} - -func setupFakeTest(t *testing.T, comp *fakeCompiler) (plugin admission.ValidationInterface, paramTracker, policyTracker clienttesting.ObjectTracker, controller *celAdmissionController) { - return setupTestCommon(t, comp, true) +func setupFakeTest(t *testing.T, comp *fakeCompiler, match *fakeMatcher) (plugin admission.ValidationInterface, paramTracker, policyTracker clienttesting.ObjectTracker, controller *celAdmissionController) { + return setupTestCommon(t, comp, match, true) } // Starts CEL admission controller and sets up a plugin configured with it as well @@ -262,7 +329,7 @@ func setupFakeTest(t *testing.T, comp *fakeCompiler) (plugin admission.Validatio // PolicyTracker expects FakePolicyDefinition and FakePolicyBinding types // !TODO: refactor this test/framework to remove startInformers argument and // clean up the return args, and in general make it more accessible. -func setupTestCommon(t *testing.T, compiler ValidatorCompiler, shouldStartInformers bool) (plugin admission.ValidationInterface, paramTracker, policyTracker clienttesting.ObjectTracker, controller *celAdmissionController) { +func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher, shouldStartInformers bool) (plugin admission.ValidationInterface, paramTracker, policyTracker clienttesting.ObjectTracker, controller *celAdmissionController) { testContext, testContextCancel := context.WithCancel(context.Background()) t.Cleanup(testContextCancel) @@ -297,7 +364,14 @@ func setupTestCommon(t *testing.T, compiler ValidatorCompiler, shouldStartInform // Override compiler used by controller for tests controller = handler.evaluator.(*celAdmissionController) - controller.policyController.ValidatorCompiler = compiler + controller.policyController.filterCompiler = compiler + controller.policyController.newValidator = func(filter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType) Validator { + f := filter.(*fakeFilter) + v := validatorMap[f.keyId] + v.fakeFilter = f + return v + } + controller.policyController.matcher = matcher t.Cleanup(func() { testContextCancel() @@ -582,14 +656,15 @@ func must3[T any, I any](val T, _ I, err error) T { //////////////////////////////////////////////////////////////////////////////// func TestPluginNotReady(t *testing.T) { - compiler := &fakeCompiler{ - // Match everything by default + reset() + compiler := &fakeCompiler{} + matcher := &fakeMatcher{ DefaultMatch: true, } // Show that an unstarted informer (or one that has failed its listwatch) // will show proper error from plugin - handler, _, _, _ := setupTestCommon(t, compiler, false) + handler, _, _, _ := setupTestCommon(t, compiler, matcher, false) err := handler.Validate( context.Background(), // Object is irrelevant/unchecked for this test. Just test that @@ -601,7 +676,7 @@ func TestPluginNotReady(t *testing.T) { require.ErrorContains(t, err, "not yet ready to handle request") // Show that by now starting the informer, the error is dissipated - handler, _, _, _ = setupTestCommon(t, compiler, true) + handler, _, _, _ = setupTestCommon(t, compiler, matcher, true) err = handler.Validate( context.Background(), // Object is irrelevant/unchecked for this test. Just test that @@ -614,25 +689,39 @@ func TestPluginNotReady(t *testing.T) { } func TestBasicPolicyDefinitionFailure(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() datalock := sync.Mutex{} numCompiles := 0 - compiler := &fakeCompiler{ - // Match everything by default + compiler := &fakeCompiler{} + validator := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - compiler.RegisterDefinition(denyPolicy, func(policy *v1alpha1.ValidatingAdmissionPolicy) Validator { + + compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter { datalock.Lock() numCompiles += 1 datalock.Unlock() - return testValidator{} - }, nil) + return &fakeFilter{ + keyId: denyPolicy.Spec.Validations[0].Expression, + } + }) - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler) + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Denied", + }, + } + }) + + handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) require.NoError(t, paramTracker.Add(fakeParams)) require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) @@ -655,33 +744,17 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) { require.ErrorContains(t, err, `Denied`) } -type validatorFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) - -func (f validatorFunc) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) { - return f(versionedAttr, versionedParams) -} - -type testValidator struct { -} - -func (v testValidator) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) { - // Policy always denies - return []PolicyDecision{ - { - Action: ActionDeny, - Message: "Denied", - }, - }, nil -} - // Shows that if a definition does not match the input, it will not be used. // But with a different input it will be used. func TestDefinitionDoesntMatch(t *testing.T) { - compiler := &fakeCompiler{ - // Match everything by default + reset() + compiler := &fakeCompiler{} + validator := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler) + + handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() @@ -689,26 +762,37 @@ func TestDefinitionDoesntMatch(t *testing.T) { passedParams := []*unstructured.Unstructured{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, - func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { - datalock.Lock() - numCompiles += 1 - datalock.Unlock() + compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter { + datalock.Lock() + numCompiles += 1 + datalock.Unlock() - return testValidator{} + return &fakeFilter{ + keyId: denyPolicy.Spec.Validations[0].Expression, + } + }) - }, func(vap *v1alpha1.ValidatingAdmissionPolicy, a admission.Attributes) bool { - // Match names with even-numbered length - obj := a.GetObject() + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Denied", + }, + } + }) - accessor, err := meta.Accessor(obj) - if err != nil { - t.Fatal(err) - return false - } + matcher.RegisterDefinition(denyPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy, a admission.Attributes) bool { + // Match names with even-numbered length + obj := a.GetObject() - return len(accessor.GetName())%2 == 0 - }) + accessor, err := meta.Accessor(obj) + if err != nil { + t.Fatal(err) + return false + } + + return len(accessor.GetName())%2 == 0 + }) require.NoError(t, paramTracker.Add(fakeParams)) require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) @@ -762,15 +846,17 @@ func TestDefinitionDoesntMatch(t *testing.T) { } func TestReconfigureBinding(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() - compiler := &fakeCompiler{ - // Match everything by default + compiler := &fakeCompiler{} + validator := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler) + handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 @@ -787,15 +873,24 @@ func TestReconfigureBinding(t *testing.T) { }, } - compiler.RegisterDefinition(denyPolicy, - func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { - datalock.Lock() - numCompiles += 1 - datalock.Unlock() + compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter { + datalock.Lock() + numCompiles += 1 + datalock.Unlock() - return testValidator{} + return &fakeFilter{ + keyId: denyPolicy.Spec.Validations[0].Expression, + } + }) - }, nil) + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Denied", + }, + } + }) denyBinding2 := &v1alpha1.ValidatingAdmissionPolicyBinding{ ObjectMeta: metav1.ObjectMeta{ @@ -870,25 +965,39 @@ func TestReconfigureBinding(t *testing.T) { // Shows that a policy which is in effect will stop being in effect when removed func TestRemoveDefinition(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() - compiler := &fakeCompiler{ - // Match everything by default + compiler := &fakeCompiler{} + validator := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler) + + handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { + compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter { datalock.Lock() numCompiles += 1 datalock.Unlock() - return testValidator{} - }, nil) + return &fakeFilter{ + keyId: denyPolicy.Spec.Validations[0].Expression, + } + }) + + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Denied", + }, + } + }) require.NoError(t, paramTracker.Add(fakeParams)) require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) @@ -923,24 +1032,39 @@ func TestRemoveDefinition(t *testing.T) { // Shows that a binding which is in effect will stop being in effect when removed func TestRemoveBinding(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() - compiler := &fakeCompiler{ - // Match everything by default + + compiler := &fakeCompiler{} + validator := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler) + + handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { + compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter { datalock.Lock() numCompiles += 1 datalock.Unlock() - return testValidator{} - }, nil) + return &fakeFilter{ + keyId: denyPolicy.Spec.Validations[0].Expression, + } + }) + + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Denied", + }, + } + }) require.NoError(t, paramTracker.Add(fakeParams)) require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) @@ -970,13 +1094,16 @@ func TestRemoveBinding(t *testing.T) { // Shows that an error is surfaced if a paramSource specified in a binding does // not actually exist func TestInvalidParamSourceGVK(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() - compiler := &fakeCompiler{ - // Match everything by default + + compiler := &fakeCompiler{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler) + + handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) passedParams := make(chan *unstructured.Unstructured) badPolicy := *denyPolicy @@ -1012,25 +1139,40 @@ func TestInvalidParamSourceGVK(t *testing.T) { // Shows that an error is surfaced if a param specified in a binding does not // actually exist func TestInvalidParamSourceInstanceName(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() - compiler := &fakeCompiler{ - // Match everything by default + + compiler := &fakeCompiler{} + validator := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler) + + handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} passedParams := []*unstructured.Unstructured{} numCompiles := 0 - compiler.RegisterDefinition(denyPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { + compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter { datalock.Lock() numCompiles += 1 datalock.Unlock() - return testValidator{} - }, nil) + return &fakeFilter{ + keyId: denyPolicy.Spec.Validations[0].Expression, + } + }) + + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Denied", + }, + } + }) require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace)) @@ -1060,13 +1202,17 @@ func TestInvalidParamSourceInstanceName(t *testing.T) { // Also shows that if binding has specified params in this instance then they // are silently ignored. func TestEmptyParamSource(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() - compiler := &fakeCompiler{ - // Match everything by default + + compiler := &fakeCompiler{} + validator := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler) + + handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) datalock := sync.Mutex{} numCompiles := 0 @@ -1075,13 +1221,24 @@ func TestEmptyParamSource(t *testing.T) { noParamSourcePolicy := *denyPolicy noParamSourcePolicy.Spec.ParamKind = nil - compiler.RegisterDefinition(&noParamSourcePolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { + compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter { datalock.Lock() numCompiles += 1 datalock.Unlock() - return testValidator{} - }, nil) + return &fakeFilter{ + keyId: denyPolicy.Spec.Validations[0].Expression, + } + }) + + validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Denied", + }, + } + }) require.NoError(t, tracker.Create(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace)) require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithNoParamRef, denyBindingWithNoParamRef.Namespace)) @@ -1108,21 +1265,49 @@ func TestEmptyParamSource(t *testing.T) { // one policy stops using the param. The expectation is the second policy // keeps behaving normally func TestMultiplePoliciesSharedParamType(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() - compiler := &fakeCompiler{ - // Match everything by default + compiler := &fakeCompiler{} + validator1 := &fakeValidator{} + validator2 := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, paramTracker, tracker, controller := setupFakeTest(t, compiler) + + handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher) // Use ConfigMap native-typed param policy1 := *denyPolicy policy1.Name = "denypolicy1.example.com" + policy1.Spec = v1alpha1.ValidatingAdmissionPolicySpec{ + ParamKind: &v1alpha1.ParamKind{ + APIVersion: paramsGVK.GroupVersion().String(), + Kind: paramsGVK.Kind, + }, + FailurePolicy: ptrTo(v1alpha1.Fail), + Validations: []v1alpha1.Validation{ + { + Expression: "policy1", + }, + }, + } policy2 := *denyPolicy policy2.Name = "denypolicy2.example.com" + policy2.Spec = v1alpha1.ValidatingAdmissionPolicySpec{ + ParamKind: &v1alpha1.ParamKind{ + APIVersion: paramsGVK.GroupVersion().String(), + Kind: paramsGVK.Kind, + }, + FailurePolicy: ptrTo(v1alpha1.Fail), + Validations: []v1alpha1.Validation{ + { + Expression: "policy2", + }, + }, + } binding1 := *denyBinding binding2 := *denyBinding @@ -1138,32 +1323,40 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { compiles2 := atomic.Int64{} evaluations2 := atomic.Int64{} - compiler.RegisterDefinition(&policy1, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { + compiler.RegisterDefinition(&policy1, func([]cel.ExpressionAccessor, bool) cel.Filter { compiles1.Add(1) - return validatorFunc(func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) { - evaluations1.Add(1) - return []PolicyDecision{ - { - Action: ActionAdmit, - }, - }, nil - }) - }, nil) + return &fakeFilter{ + keyId: policy1.Spec.Validations[0].Expression, + } + }) - compiler.RegisterDefinition(&policy2, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { + validator1.RegisterDefinition(&policy1, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + evaluations1.Add(1) + return []PolicyDecision{ + { + Action: ActionAdmit, + }, + } + }) + + compiler.RegisterDefinition(&policy2, func([]cel.ExpressionAccessor, bool) cel.Filter { compiles2.Add(1) - return validatorFunc(func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) { - evaluations2.Add(1) - return []PolicyDecision{ - { - Action: ActionDeny, - Message: "Policy2Denied", - }, - }, nil - }) - }, nil) + return &fakeFilter{ + keyId: policy2.Spec.Validations[0].Expression, + } + }) + + validator2.RegisterDefinition(&policy2, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + evaluations2.Add(1) + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Policy2Denied", + }, + } + }) require.NoError(t, tracker.Create(definitionsGVR, &policy1, policy1.Namespace)) require.NoError(t, tracker.Create(bindingsGVR, &binding1, binding1.Namespace)) @@ -1234,13 +1427,16 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) { // Shows that we can refer to native-typed params just fine // (as opposed to CRD params) func TestNativeTypeParam(t *testing.T) { + reset() testContext, testContextCancel := context.WithCancel(context.Background()) defer testContextCancel() - compiler := &fakeCompiler{ - // Match everything by default + + compiler := &fakeCompiler{} + validator := &fakeValidator{} + matcher := &fakeMatcher{ DefaultMatch: true, } - handler, _, tracker, controller := setupFakeTest(t, compiler) + handler, _, tracker, controller := setupFakeTest(t, compiler, matcher) compiles := atomic.Int64{} evaluations := atomic.Int64{} @@ -1252,29 +1448,31 @@ func TestNativeTypeParam(t *testing.T) { Kind: "ConfigMap", } - compiler.RegisterDefinition(&nativeTypeParamPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { + compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter { compiles.Add(1) - return validatorFunc(func(versionedAttr *whgeneric.VersionedAttributes, params runtime.Object) ([]PolicyDecision, error) { - evaluations.Add(1) + return &fakeFilter{ + keyId: nativeTypeParamPolicy.Spec.Validations[0].Expression, + } + }) - // show that the passed params was a ConfigMap native type - if _, ok := params.(*v1.ConfigMap); ok { - return []PolicyDecision{ - { - Action: ActionDeny, - Message: "correct type", - }, - }, nil - } + validator.RegisterDefinition(&nativeTypeParamPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision { + evaluations.Add(1) + if _, ok := versionedParams.(*v1.ConfigMap); ok { return []PolicyDecision{ { Action: ActionDeny, - Message: "Incorrect param type", + Message: "correct type", }, - }, nil - }) - }, nil) + } + } + return []PolicyDecision{ + { + Action: ActionDeny, + Message: "Incorrect param type", + }, + } + }) configMapParam := &v1.ConfigMap{ TypeMeta: metav1.TypeMeta{ diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go index dfb3dda21a5..cafb6e38917 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller.go @@ -24,19 +24,19 @@ import ( "sync/atomic" "time" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching" - "k8s.io/api/admissionregistration/v1alpha1" k8serrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/admission" celmetrics "k8s.io/apiserver/pkg/admission/cel" + "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" + "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching" whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/client-go/dynamic" "k8s.io/client-go/informers" @@ -69,6 +69,14 @@ type policyData struct { bindings []bindingInfo } +// contains the cel PolicyDecisions along with the ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding +// that determined the decision +type policyDecisionWithMetadata struct { + PolicyDecision + Definition *v1alpha1.ValidatingAdmissionPolicy + Binding *v1alpha1.ValidatingAdmissionPolicyBinding +} + // namespaceName is used as a key in definitionInfo and bindingInfos type namespacedName struct { namespace, name string @@ -118,9 +126,8 @@ func NewAdmissionController( restMapper, client, dynamicClient, - &CELValidatorCompiler{ - Matcher: matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client), - }, + cel.NewFilterCompiler(), + NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)), generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy]( informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()), generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding]( @@ -213,7 +220,7 @@ func (c *celAdmissionController) Validate( for _, definitionInfo := range policyDatas { definition := definitionInfo.lastReconciledValue - matches, matchKind, err := c.policyController.DefinitionMatches(a, o, definition) + matches, matchKind, err := c.policyController.matcher.DefinitionMatches(a, o, definition) if err != nil { // Configuration error. addConfigError(err, definition, nil) @@ -232,7 +239,7 @@ func (c *celAdmissionController) Validate( // If the key is inside dependentBindings, there is guaranteed to // be a bindingInfo for it binding := bindingInfo.lastReconciledValue - matches, err := c.policyController.BindingMatches(a, o, binding) + matches, err := c.policyController.matcher.BindingMatches(a, o, binding) if err != nil { // Configuration error. addConfigError(err, definition, binding) @@ -310,13 +317,7 @@ func (c *celAdmissionController) Validate( versionedAttr = va } - decisions, err := bindingInfo.validator.Validate(versionedAttr, param) - if err != nil { - // runtime error. Apply failure policy - wrappedError := fmt.Errorf("failed to evaluate CEL expression: %w", err) - addConfigError(wrappedError, definition, binding) - continue - } + decisions := bindingInfo.validator.Validate(versionedAttr, param) for _, decision := range decisions { switch decision.Action { @@ -354,7 +355,7 @@ func (c *celAdmissionController) Validate( reason = metav1.StatusReasonInvalid } err.ErrStatus.Reason = reason - err.ErrStatus.Code = ReasonToCode(reason) + err.ErrStatus.Code = reasonToCode(reason) err.ErrStatus.Details.Causes = append(err.ErrStatus.Details.Causes, metav1.StatusCause{Message: message}) return err } @@ -366,7 +367,7 @@ func (c *celAdmissionController) HasSynced() bool { } func (c *celAdmissionController) ValidateInitialization() error { - return c.policyController.ValidateInitialization() + return c.policyController.matcher.ValidateInitialization() } func (c *celAdmissionController) refreshPolicies() { diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go index 2952640eb3e..4622491e99d 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/controller_reconcile.go @@ -22,6 +22,7 @@ import ( "sync" "time" + v1 "k8s.io/api/admissionregistration/v1" "k8s.io/api/admissionregistration/v1alpha1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" @@ -29,6 +30,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/sets" celmetrics "k8s.io/apiserver/pkg/admission/cel" + "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/dynamicinformer" @@ -48,7 +50,11 @@ type policyController struct { // Provided to the policy's Compile function as an injected dependency to // assist with compiling its expressions to CEL - ValidatorCompiler + filterCompiler cel.FilterCompiler + + matcher Matcher + + newValidator // Lock which protects: // - cachedPolicies @@ -81,21 +87,26 @@ type policyController struct { client kubernetes.Interface } +type newValidator func(cel.Filter, *v1.FailurePolicyType) Validator + func newPolicyController( restMapper meta.RESTMapper, client kubernetes.Interface, dynamicClient dynamic.Interface, - validatorCompiler ValidatorCompiler, + filterCompiler cel.FilterCompiler, + matcher Matcher, policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy], bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding], ) *policyController { res := &policyController{} *res = policyController{ - ValidatorCompiler: validatorCompiler, + filterCompiler: filterCompiler, definitionInfo: make(map[namespacedName]*definitionInfo), bindingInfos: make(map[namespacedName]*bindingInfo), paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo), definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]), + matcher: matcher, + newValidator: NewValidator, policyDefinitionsController: generic.NewController( policiesInformer, res.reconcilePolicyDefinition, @@ -424,7 +435,14 @@ func (c *policyController) latestPolicyData() []policyData { for bindingNN := range c.definitionsToBindings[definitionNN] { bindingInfo := c.bindingInfos[bindingNN] if bindingInfo.validator == nil && definitionInfo.configurationError == nil { - bindingInfo.validator = c.ValidatorCompiler.Compile(definitionInfo.lastReconciledValue) + hasParam := false + if definitionInfo.lastReconciledValue.Spec.ParamKind != nil { + hasParam = true + } + bindingInfo.validator = c.newValidator( + c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), hasParam), + convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy), + ) } bindingInfos = append(bindingInfos, *bindingInfo) } @@ -447,6 +465,33 @@ func (c *policyController) latestPolicyData() []policyData { return res } +func convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(policyType *v1alpha1.FailurePolicyType) *v1.FailurePolicyType { + if policyType == nil { + return nil + } + + var v1FailPolicy v1.FailurePolicyType + if *policyType == v1alpha1.Fail { + v1FailPolicy = v1.Fail + } else if *policyType == v1alpha1.Ignore { + v1FailPolicy = v1.Ignore + } + return &v1FailPolicy +} + +func convertv1alpha1Validations(inputValidations []v1alpha1.Validation) []cel.ExpressionAccessor { + celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations)) + for i, validation := range inputValidations { + validation := ValidationCondition{ + Expression: validation.Expression, + Message: validation.Message, + Reason: validation.Reason, + } + celExpressionAccessor[i] = &validation + } + return celExpressionAccessor +} + func getNamespaceName(namespace, name string) namespacedName { return namespacedName{ namespace: namespace, diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go index 7cb33b3724e..9847e4856b7 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/interface.go @@ -18,34 +18,42 @@ package validatingadmissionpolicy import ( "k8s.io/api/admissionregistration/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" ) -// Validator defines the func used to validate an object against the validator's rules. -// 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). -type Validator interface { - Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) +var _ cel.ExpressionAccessor = &ValidationCondition{} + +// ValidationCondition contains the inputs needed to compile, evaluate and validate a cel expression +type ValidationCondition struct { + Expression string + Message string + Reason *metav1.StatusReason } -// ValidatorCompiler is Dependency Injected into the PolicyDefinition's `Compile` -// function to assist with converting types and values to/from CEL-typed values. -type ValidatorCompiler interface { +func (v *ValidationCondition) GetExpression() string { + return v.Expression +} + +// Matcher is used for matching ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding to attributes +type Matcher interface { admission.InitializationValidator - // Matches says whether this policy definition matches the provided admission + // DefinitionMatches says whether this policy definition matches the provided admission // resource request DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) - // Matches says whether this policy definition matches the provided admission + // BindingMatches says whether this policy definition matches the provided admission // resource request BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) - - // Compile is used for the cel expression compilation - Compile( - policy *v1alpha1.ValidatingAdmissionPolicy, - ) Validator +} + +// 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 } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matcher.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matcher.go new file mode 100644 index 00000000000..a659a99f14c --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matcher.go @@ -0,0 +1,78 @@ +/* +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 validatingadmissionpolicy + +import ( + "k8s.io/api/admissionregistration/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching" +) + +var _ matching.MatchCriteria = &matchCriteria{} + +type matchCriteria struct { + constraints *v1alpha1.MatchResources +} + +// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector +func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) { + return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector) +} + +// GetParsedObjectSelector returns the converted LabelSelector which implements labels.Selector +func (m *matchCriteria) GetParsedObjectSelector() (labels.Selector, error) { + return metav1.LabelSelectorAsSelector(m.constraints.ObjectSelector) +} + +// GetMatchResources returns the matchConstraints +func (m *matchCriteria) GetMatchResources() v1alpha1.MatchResources { + return *m.constraints +} + +type matcher struct { + Matcher *matching.Matcher +} + +func NewMatcher(m *matching.Matcher) Matcher { + return &matcher{ + Matcher: m, + } +} + +// ValidateInitialization checks if Matcher is initialized. +func (c *matcher) ValidateInitialization() error { + return c.Matcher.ValidateInitialization() +} + +// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request +func (c *matcher) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) { + criteria := matchCriteria{constraints: definition.Spec.MatchConstraints} + return c.Matcher.Matches(a, o, &criteria) +} + +// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request +func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) { + if binding.Spec.MatchResources == nil { + return true, nil + } + criteria := matchCriteria{constraints: binding.Spec.MatchResources} + isMatch, _, err := c.Matcher.Matches(a, o, &criteria) + return isMatch, err +} diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/policy_decision.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/policy_decision.go index b13a2ab74a1..47e50dd62c8 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/policy_decision.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/policy_decision.go @@ -20,7 +20,6 @@ import ( "net/http" "time" - "k8s.io/api/admissionregistration/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -39,6 +38,7 @@ const ( EvalDeny PolicyDecisionEvaluation = "deny" ) +// PolicyDecision contains the action determined from a cel evaluation along with metadata such as message, reason and duration type PolicyDecision struct { Action PolicyDecisionAction Evaluation PolicyDecisionEvaluation @@ -47,13 +47,7 @@ type PolicyDecision struct { Elapsed time.Duration } -type policyDecisionWithMetadata struct { - PolicyDecision - Definition *v1alpha1.ValidatingAdmissionPolicy - Binding *v1alpha1.ValidatingAdmissionPolicyBinding -} - -func ReasonToCode(r metav1.StatusReason) int32 { +func reasonToCode(r metav1.StatusReason) int32 { switch r { case metav1.StatusReasonForbidden: return http.StatusForbidden diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go index f4e84408d2b..6e81518a14b 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator.go @@ -18,298 +18,92 @@ package validatingadmissionpolicy import ( "fmt" - "reflect" + "k8s.io/klog/v2" "strings" - "time" celtypes "github.com/google/cel-go/common/types" - "github.com/google/cel-go/interpreter" - admissionv1 "k8s.io/api/admission/v1" - "k8s.io/api/admissionregistration/v1alpha1" - authenticationv1 "k8s.io/api/authentication/v1" + v1 "k8s.io/api/admissionregistration/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/admission" - "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching" + "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" ) -var _ ValidatorCompiler = &CELValidatorCompiler{} -var _ matching.MatchCriteria = &matchCriteria{} - -type matchCriteria struct { - constraints *v1alpha1.MatchResources +// validator implements the Validator interface +type validator struct { + filter cel.Filter + failPolicy *v1.FailurePolicyType } -// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector -func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) { - return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector) -} - -// GetParsedObjectSelector returns the converted LabelSelector which implements labels.Selector -func (m *matchCriteria) GetParsedObjectSelector() (labels.Selector, error) { - return metav1.LabelSelectorAsSelector(m.constraints.ObjectSelector) -} - -// GetMatchResources returns the matchConstraints -func (m *matchCriteria) GetMatchResources() v1alpha1.MatchResources { - return *m.constraints -} - -// CELValidatorCompiler implement the interface ValidatorCompiler. -type CELValidatorCompiler struct { - Matcher *matching.Matcher -} - -// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request -func (c *CELValidatorCompiler) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) { - criteria := matchCriteria{constraints: definition.Spec.MatchConstraints} - return c.Matcher.Matches(a, o, &criteria) -} - -// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request -func (c *CELValidatorCompiler) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) { - if binding.Spec.MatchResources == nil { - return true, nil - } - criteria := matchCriteria{constraints: binding.Spec.MatchResources} - isMatch, _, err := c.Matcher.Matches(a, o, &criteria) - return isMatch, err -} - -// ValidateInitialization checks if Matcher is initialized. -func (c *CELValidatorCompiler) ValidateInitialization() error { - return c.Matcher.ValidateInitialization() -} - -type validationActivation struct { - object, oldObject, params, request interface{} -} - -// ResolveName returns a value from the activation by qualified name, or false if the name -// could not be found. -func (a *validationActivation) 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 - case RequestVarName: - return a.request, true - default: - return nil, false +func NewValidator(filter cel.Filter, failPolicy *v1.FailurePolicyType) Validator { + return &validator{ + filter: filter, + failPolicy: failPolicy, } } -// Parent returns the parent of the current activation, may be nil. -// If non-nil, the parent will be searched during resolve calls. -func (a *validationActivation) Parent() interpreter.Activation { - return nil -} - -// Compile compiles the cel expression defined in ValidatingAdmissionPolicy -func (c *CELValidatorCompiler) Compile(p *v1alpha1.ValidatingAdmissionPolicy) Validator { - if len(p.Spec.Validations) == 0 { - return nil - } - hasParam := false - if p.Spec.ParamKind != nil { - hasParam = true - } - compilationResults := make([]CompilationResult, len(p.Spec.Validations)) - for i, validation := range p.Spec.Validations { - compilationResults[i] = CompileValidatingPolicyExpression(validation.Expression, hasParam) - } - return &CELValidator{policy: p, compilationResults: compilationResults} -} - -// CELValidator implements the Validator interface -type CELValidator struct { - policy *v1alpha1.ValidatingAdmissionPolicy - compilationResults []CompilationResult -} - -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 -} - -func policyDecisionActionForError(f v1alpha1.FailurePolicyType) PolicyDecisionAction { - if f == v1alpha1.Ignore { +func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction { + if f == v1.Ignore { return ActionAdmit } return ActionDeny } -// Validate validates all cel expressions in Validator and returns a PolicyDecision for each CEL expression or returns an error. -// An error will be returned if failed to convert the object/oldObject/params/request to unstructured. -// Each PolicyDecision will have a decision and a message. -// policyDecision.message will be empty if the decision is allowed and no error met. -func (v *CELValidator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) { - // TODO: replace unstructured with ref.Val for CEL variables when native type support is available - decisions := make([]PolicyDecision, len(v.compilationResults)) - var err error - - oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject) - if err != nil { - return nil, err - } - objectVal, err := objectToResolveVal(versionedAttr.VersionedObject) - if err != nil { - return nil, err - } - paramsVal, err := objectToResolveVal(versionedParams) - if err != nil { - return nil, err - } - - request := createAdmissionRequest(versionedAttr.Attributes) - requestVal, err := convertObjectToUnstructured(request) - if err != nil { - return nil, err - } - va := &validationActivation{ - object: objectVal, - oldObject: oldObjectVal, - params: paramsVal, - request: requestVal.Object, - } - - var f v1alpha1.FailurePolicyType - if v.policy.Spec.FailurePolicy == nil { - f = v1alpha1.Fail +// 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 { + var f v1.FailurePolicyType + if v.failPolicy == nil { + f = v1.Fail } else { - f = *v.policy.Spec.FailurePolicy + f = *v.failPolicy } - for i, compilationResult := range v.compilationResults { - validation := v.policy.Spec.Validations[i] + evalResults, err := v.filter.ForInput(versionedAttr, versionedParams, cel.CreateAdmissionRequest(versionedAttr.Attributes)) + if err != nil { + return []PolicyDecision{ + { + Action: policyDecisionActionForError(f), + Evaluation: EvalError, + Message: err.Error(), + }, + } + } + decisions := make([]PolicyDecision, len(evalResults)) - var policyDecision = &decisions[i] - - if compilationResult.Error != nil { - policyDecision.Action = policyDecisionActionForError(f) - policyDecision.Evaluation = EvalError - policyDecision.Message = fmt.Sprintf("compilation error: %v", compilationResult.Error) + for i, evalResult := range evalResults { + var decision = &decisions[i] + // TODO: move this to generics + validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition) + if !ok { + klog.Error("Invalid type conversion to ValidationCondition") + decision.Action = policyDecisionActionForError(f) + decision.Evaluation = EvalError + decision.Message = "Invalid type sent to validator, expected ValidationCondition" continue } - if compilationResult.Program == nil { - policyDecision.Action = policyDecisionActionForError(f) - policyDecision.Evaluation = EvalError - policyDecision.Message = "unexpected internal error compiling expression" - continue - } - t1 := time.Now() - evalResult, _, err := compilationResult.Program.Eval(va) - elapsed := time.Since(t1) - policyDecision.Elapsed = elapsed - if err != nil { - policyDecision.Action = policyDecisionActionForError(f) - policyDecision.Evaluation = EvalError - policyDecision.Message = fmt.Sprintf("expression '%v' resulted in error: %v", v.policy.Spec.Validations[i].Expression, err) - } else if evalResult != celtypes.True { - policyDecision.Action = ActionDeny + + if evalResult.Error != nil { + decision.Action = policyDecisionActionForError(f) + decision.Evaluation = EvalError + decision.Message = evalResult.Error.Error() + } else if evalResult.EvalResult != celtypes.True { + decision.Action = ActionDeny if validation.Reason == nil { - policyDecision.Reason = metav1.StatusReasonInvalid + decision.Reason = metav1.StatusReasonInvalid } else { - policyDecision.Reason = *validation.Reason + decision.Reason = *validation.Reason } if len(validation.Message) > 0 { - policyDecision.Message = strings.TrimSpace(validation.Message) + decision.Message = strings.TrimSpace(validation.Message) } else { - policyDecision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression)) + decision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression)) } } else { - policyDecision.Action = ActionAdmit - policyDecision.Evaluation = EvalAdmit + decision.Action = ActionAdmit + decision.Evaluation = EvalAdmit } } - - return decisions, nil -} - -func createAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionRequest { - // FIXME: how to get resource GVK, GVR and subresource? - gvk := attr.GetKind() - gvr := attr.GetResource() - 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(), - }, - } + return decisions } diff --git a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go index f71f3c46077..c10e33fd5cb 100644 --- a/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go +++ b/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/validator_test.go @@ -17,551 +17,459 @@ limitations under the License. package validatingadmissionpolicy import ( + "errors" "strings" "testing" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "github.com/stretchr/testify/require" + celtypes "github.com/google/cel-go/common/types" + + admissionv1 "k8s.io/api/admission/v1" v1 "k8s.io/api/admissionregistration/v1" - "k8s.io/api/admissionregistration/v1alpha1" - corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/admission/plugin/cel" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" ) -func TestCompile(t *testing.T) { - cases := []struct { - name string - policy *v1alpha1.ValidatingAdmissionPolicy - errorExpressions map[string]string - }{ - { - name: "invalid syntax", - policy: &v1alpha1.ValidatingAdmissionPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: v1alpha1.ValidatingAdmissionPolicySpec{ - FailurePolicy: func() *v1alpha1.FailurePolicyType { - r := v1alpha1.FailurePolicyType("Fail") - return &r - }(), - ParamKind: &v1alpha1.ParamKind{ - APIVersion: "rules.example.com/v1", - Kind: "ReplicaLimit", - }, - Validations: []v1alpha1.Validation{ - { - Expression: "1 < 'asdf'", - }, - { - Expression: "1 < 2", - }, - }, - MatchConstraints: &v1alpha1.MatchResources{ - MatchPolicy: func() *v1alpha1.MatchPolicyType { - r := v1alpha1.MatchPolicyType("Exact") - return &r - }(), - ResourceRules: []v1alpha1.NamedRuleWithOperations{ - { - RuleWithOperations: v1alpha1.RuleWithOperations{ - Operations: []v1.OperationType{"CREATE"}, - Rule: v1.Rule{ - APIGroups: []string{"a"}, - APIVersions: []string{"a"}, - Resources: []string{"a"}, - }, - }, - }, - }, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"a": "b"}, - }, - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"a": "b"}, - }, - }, - }, - }, - errorExpressions: map[string]string{ - "1 < 'asdf'": "found no matching overload for '_<_' applied to '(int, string)", - }, - }, - { - name: "valid syntax", - policy: &v1alpha1.ValidatingAdmissionPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: v1alpha1.ValidatingAdmissionPolicySpec{ - FailurePolicy: func() *v1alpha1.FailurePolicyType { - r := v1alpha1.FailurePolicyType("Fail") - return &r - }(), - Validations: []v1alpha1.Validation{ - { - Expression: "1 < 2", - }, - { - Expression: "object.spec.string.matches('[0-9]+')", - }, - { - Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'", - }, - }, - MatchConstraints: &v1alpha1.MatchResources{ - MatchPolicy: func() *v1alpha1.MatchPolicyType { - r := v1alpha1.MatchPolicyType("Exact") - return &r - }(), - ResourceRules: []v1alpha1.NamedRuleWithOperations{ - { - RuleWithOperations: v1alpha1.RuleWithOperations{ - Operations: []v1.OperationType{"CREATE"}, - Rule: v1.Rule{ - APIGroups: []string{"a"}, - APIVersions: []string{"a"}, - Resources: []string{"a"}, - }, - }, - }, - }, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"a": "b"}, - }, - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"a": "b"}, - }, - }, - }, - }, - }, - } +var _ cel.Filter = &fakeCelFilter{} - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - var c CELValidatorCompiler - validator := c.Compile(tc.policy) - if validator == nil { - t.Fatalf("unexpected nil validator") - } - validations := tc.policy.Spec.Validations - CompilationResults := validator.(*CELValidator).compilationResults - require.Equal(t, len(validations), len(CompilationResults)) - - meets := make([]bool, len(validations)) - for expr, expectErr := range tc.errorExpressions { - for i, result := range CompilationResults { - if validations[i].Expression == expr { - if result.Error == nil { - t.Errorf("Expect expression '%s' to contain error '%v' but got no error", expr, expectErr) - } else if !strings.Contains(result.Error.Error(), expectErr) { - t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error) - } - meets[i] = true - } - } - } - for i, meet := range meets { - if !meet && CompilationResults[i].Error != nil { - t.Errorf("Unexpected err '%v' for expression '%s'", CompilationResults[i].Error, validations[i].Expression) - } - } - }) - } +type fakeCelFilter struct { + evaluations []cel.EvaluationResult + throwError bool } -func getValidPolicy(validations []v1alpha1.Validation, params *v1alpha1.ParamKind, fp *v1alpha1.FailurePolicyType) *v1alpha1.ValidatingAdmissionPolicy { - if fp == nil { - fp = func() *v1alpha1.FailurePolicyType { - r := v1alpha1.FailurePolicyType("Fail") - return &r - }() - } - return &v1alpha1.ValidatingAdmissionPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: v1alpha1.ValidatingAdmissionPolicySpec{ - FailurePolicy: fp, - Validations: validations, - ParamKind: params, - MatchConstraints: &v1alpha1.MatchResources{ - MatchPolicy: func() *v1alpha1.MatchPolicyType { - r := v1alpha1.MatchPolicyType("Exact") - return &r - }(), - ResourceRules: []v1alpha1.NamedRuleWithOperations{ - { - RuleWithOperations: v1alpha1.RuleWithOperations{ - Operations: []v1.OperationType{"CREATE"}, - Rule: v1.Rule{ - APIGroups: []string{"a"}, - APIVersions: []string{"a"}, - Resources: []string{"a"}, - }, - }, - }, - }, - ObjectSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"a": "b"}, - }, - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"a": "b"}, - }, - }, - }, +func (f *fakeCelFilter) ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]cel.EvaluationResult, error) { + if f.throwError { + return nil, errors.New("test error") } + return f.evaluations, nil } -func generatedDecision(k PolicyDecisionAction, m string, r metav1.StatusReason) PolicyDecision { - return PolicyDecision{Action: k, Message: m, Reason: r} +func (f *fakeCelFilter) CompilationErrors() []error { + return []error{} } func TestValidate(t *testing.T) { - // we fake the paramKind in ValidatingAdmissionPolicy for testing since the params is directly passed from cel admission - // Inside validator.go, we only check if paramKind exists - hasParamKind := &v1alpha1.ParamKind{ - APIVersion: "v1", - Kind: "ConfigMap", - } - ignorePolicy := func() *v1alpha1.FailurePolicyType { - r := v1alpha1.FailurePolicyType("Ignore") - return &r - }() - forbiddenReason := func() *metav1.StatusReason { - r := metav1.StatusReasonForbidden - return &r - }() + ignore := v1.Ignore + fail := v1.Fail - configMapParams := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Data: map[string]string{ - "fakeString": "fake", - }, - } - crdParams := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "spec": map[string]interface{}{ - "testSize": 10, - }, - }, - } - podObject := corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - }, - Spec: corev1.PodSpec{ - NodeName: "testnode", - }, - } + forbiddenReason := metav1.StatusReasonForbidden + unauthorizedReason := metav1.StatusReasonUnauthorized - var nilUnstructured *unstructured.Unstructured + fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil) + fakeVersionedAttr, _ := generic.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil) cases := []struct { - name string - policy *v1alpha1.ValidatingAdmissionPolicy - attributes admission.Attributes - params runtime.Object - policyDecisions []PolicyDecision + name string + failPolicy *v1.FailurePolicyType + evaluations []cel.EvaluationResult + policyDecision []PolicyDecision + throwError bool }{ { - name: "valid syntax for object", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test pass", + evaluations: []cel.EvaluationResult{ { - Expression: "has(object.subsets) && object.subsets.size() < 2", + EvalResult: celtypes.True, + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionAdmit, }, - }, nil, nil), - attributes: newValidAttribute(nil, false), - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), }, }, { - name: "valid syntax for metadata", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test multiple pass", + evaluations: []cel.EvaluationResult{ { - Expression: "object.metadata.name == 'endpoints1'", + EvalResult: celtypes.True, + ExpressionAccessor: &ValidationCondition{}, + }, + { + EvalResult: celtypes.True, + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionAdmit, + }, + { + Action: ActionAdmit, }, - }, nil, nil), - attributes: newValidAttribute(nil, false), - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), }, }, { - name: "valid syntax for oldObject", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test error with failurepolicy ignore", + evaluations: []cel.EvaluationResult{ { - Expression: "oldObject == null", + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, }, + }, + policyDecision: []PolicyDecision{ { - Expression: "object != null", + Action: ActionAdmit, + }, + }, + failPolicy: &ignore, + }, + { + name: "test error with failurepolicy nil", + evaluations: []cel.EvaluationResult{ + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, }, - }, nil, nil), - attributes: newValidAttribute(nil, false), - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), - generatedDecision(ActionAdmit, "", ""), }, }, { - name: "valid syntax for request", - policy: getValidPolicy([]v1alpha1.Validation{ - {Expression: "request.operation == 'CREATE'"}, - }, nil, nil), - attributes: newValidAttribute(nil, false), - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), + name: "test fail with failurepolicy fail", + evaluations: []cel.EvaluationResult{ + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + }, + }, + failPolicy: &fail, + }, + { + name: "test fail with failurepolicy ignore with multiple validations", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &ValidationCondition{}, + }, + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionAdmit, + }, + { + Action: ActionAdmit, + }, + }, + failPolicy: &ignore, + }, + { + name: "test fail with failurepolicy nil with multiple validations", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &ValidationCondition{}, + }, + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionAdmit, + }, + { + Action: ActionDeny, + }, }, }, { - name: "valid syntax for configMap", - policy: getValidPolicy([]v1alpha1.Validation{ - {Expression: "request.namespace != params.data.fakeString"}, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, false), - params: configMapParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), + name: "test fail with failurepolicy fail with multiple validations", + evaluations: []cel.EvaluationResult{ + { + EvalResult: celtypes.True, + ExpressionAccessor: &ValidationCondition{}, + }, + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionAdmit, + }, + { + Action: ActionDeny, + }, + }, + failPolicy: &fail, + }, + { + name: "test fail with failurepolicy ignore with multiple failed validations", + evaluations: []cel.EvaluationResult{ + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionAdmit, + }, + { + Action: ActionAdmit, + }, + }, + failPolicy: &ignore, + }, + { + name: "test fail with failurepolicy nil with multiple failed validations", + evaluations: []cel.EvaluationResult{ + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, + }, + }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + }, + { + Action: ActionDeny, + }, }, }, { - name: "test failure policy with Ignore", - policy: getValidPolicy([]v1alpha1.Validation{ - {Expression: "object.subsets.size() > 2"}, - }, hasParamKind, ignorePolicy), - attributes: newValidAttribute(nil, false), - params: &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", + name: "test fail with failurepolicy fail with multiple failed validations", + evaluations: []cel.EvaluationResult{ + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, }, - Data: map[string]string{ - "fakeString": "fake", + { + Error: errors.New(""), + ExpressionAccessor: &ValidationCondition{}, }, }, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionDeny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid), + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + }, + { + Action: ActionDeny, + }, }, + failPolicy: &fail, }, { - name: "test failure policy with multiple validations", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test reason for fail no reason set", + evaluations: []cel.EvaluationResult{ { - Expression: "has(object.subsets)", + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Expression: "this.expression == unit.test", + }, }, - { - Expression: "object.subsets.size() > 2", - }, - }, hasParamKind, ignorePolicy), - attributes: newValidAttribute(nil, false), - params: configMapParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), - generatedDecision(ActionDeny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid), }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + Reason: metav1.StatusReasonInvalid, + Message: "failed expression: this.expression == unit.test", + }, + }, + failPolicy: &fail, }, { - name: "test failure policy with multiple failed validations", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test reason for fail reason set", + evaluations: []cel.EvaluationResult{ { - Expression: "oldObject != null", + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &forbiddenReason, + Expression: "this.expression == unit.test", + }, }, - { - Expression: "object.subsets.size() > 2", - }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, false), - params: configMapParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionDeny, "failed expression: oldObject != null", metav1.StatusReasonInvalid), - generatedDecision(ActionDeny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid), }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + Reason: metav1.StatusReasonForbidden, + Message: "failed expression: this.expression == unit.test", + }, + }, + failPolicy: &fail, }, { - name: "test Object nul in delete", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test reason for failed validations multiple validations", + evaluations: []cel.EvaluationResult{ { - Expression: "oldObject != null", + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &forbiddenReason, + Expression: "this.expression == unit.test", + }, }, { - Expression: "object == null", + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &unauthorizedReason, + Expression: "this.expression.2 == unit.test.2", + }, }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, true), - params: configMapParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), - generatedDecision(ActionAdmit, "", ""), }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + Reason: metav1.StatusReasonForbidden, + Message: "failed expression: this.expression == unit.test", + }, + { + Action: ActionDeny, + Reason: metav1.StatusReasonUnauthorized, + Message: "failed expression: this.expression.2 == unit.test.2", + }, + }, + failPolicy: &fail, }, { - name: "test reason for failed validation", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test message for failed validations", + evaluations: []cel.EvaluationResult{ { - Expression: "oldObject == null", - Reason: forbiddenReason, + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &forbiddenReason, + Expression: "this.expression == unit.test", + Message: "test", + }, }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, true), - params: configMapParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionDeny, "failed expression: oldObject == null", metav1.StatusReasonForbidden), }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + Reason: metav1.StatusReasonForbidden, + Message: "test", + }, + }, + failPolicy: &fail, }, { - name: "test message for failed validation", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test message for failed validations multiple validations", + evaluations: []cel.EvaluationResult{ { - Expression: "oldObject == null", - Reason: forbiddenReason, - Message: "old object should be present", + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &forbiddenReason, + Expression: "this.expression == unit.test", + Message: "test1", + }, + }, + { + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &forbiddenReason, + Expression: "this.expression == unit.test", + Message: "test2", + }, }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, true), - params: configMapParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionDeny, "old object should be present", metav1.StatusReasonForbidden), }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + Reason: metav1.StatusReasonForbidden, + Message: "test1", + }, + { + Action: ActionDeny, + Reason: metav1.StatusReasonForbidden, + Message: "test2", + }, + }, + failPolicy: &fail, }, { - name: "test runtime error", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test filter error", + evaluations: []cel.EvaluationResult{ { - Expression: "oldObject.x == 100", + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &forbiddenReason, + Expression: "this.expression == unit.test", + Message: "test1", + }, }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, true), - params: configMapParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionDeny, "resulted in error", ""), }, + policyDecision: []PolicyDecision{ + { + Action: ActionDeny, + Message: "test error", + }, + }, + failPolicy: &fail, + throwError: true, }, { - name: "test against crd param", - policy: getValidPolicy([]v1alpha1.Validation{ + name: "test filter error multiple evaluations", + evaluations: []cel.EvaluationResult{ { - Expression: "object.subsets.size() < params.spec.testSize", + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &forbiddenReason, + Expression: "this.expression == unit.test", + Message: "test1", + }, + }, + { + EvalResult: celtypes.False, + ExpressionAccessor: &ValidationCondition{ + Reason: &forbiddenReason, + Expression: "this.expression == unit.test", + Message: "test2", + }, }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, false), - params: crdParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), }, - }, - { - name: "test compile failure with FailurePolicy Fail", - policy: getValidPolicy([]v1alpha1.Validation{ + policyDecision: []PolicyDecision{ { - Expression: "fail to compile test", + Action: ActionDeny, + Message: "test error", }, - { - Expression: "object.subsets.size() > params.spec.testSize", - }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, false), - params: crdParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionDeny, "compilation error: compilation failed: ERROR: :1:6: Syntax error:", ""), - generatedDecision(ActionDeny, "failed expression: object.subsets.size() > params.spec.testSize", metav1.StatusReasonInvalid), - }, - }, - { - name: "test compile failure with FailurePolicy Ignore", - policy: getValidPolicy([]v1alpha1.Validation{ - { - Expression: "fail to compile test", - }, - { - Expression: "object.subsets.size() > params.spec.testSize", - }, - }, hasParamKind, ignorePolicy), - attributes: newValidAttribute(nil, false), - params: crdParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "compilation error: compilation failed: ERROR:", ""), - generatedDecision(ActionDeny, "failed expression: object.subsets.size() > params.spec.testSize", metav1.StatusReasonInvalid), - }, - }, - { - name: "test pod", - policy: getValidPolicy([]v1alpha1.Validation{ - { - Expression: "object.spec.nodeName == 'testnode'", - }, - }, nil, nil), - attributes: newValidAttribute(&podObject, false), - params: crdParams, - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), - }, - }, - { - name: "test deny paramKind without paramRef", - policy: getValidPolicy([]v1alpha1.Validation{ - { - Expression: "params != null", - Reason: forbiddenReason, - Message: "params as required", - }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, true), - // Simulate a interface holding a nil pointer, since this is how param is passed to Validate - // if paramRef is unset on a binding - params: runtime.Object(nilUnstructured), - policyDecisions: []PolicyDecision{ - generatedDecision(ActionDeny, "params as required", metav1.StatusReasonForbidden), - }, - }, - { - name: "test allow paramKind without paramRef", - policy: getValidPolicy([]v1alpha1.Validation{ - { - Expression: "params == null", - Reason: forbiddenReason, - }, - }, hasParamKind, nil), - attributes: newValidAttribute(nil, true), - // Simulate a interface holding a nil pointer, since this is how param is passed to Validate - // if paramRef is unset on a binding - params: runtime.Object(nilUnstructured), - policyDecisions: []PolicyDecision{ - generatedDecision(ActionAdmit, "", ""), }, + failPolicy: &fail, + throwError: true, }, } - for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - c := CELValidatorCompiler{} - validator := c.Compile(tc.policy) - if validator == nil { - t.Fatalf("unexpected nil validator") + v := validator{ + failPolicy: tc.failPolicy, + filter: &fakeCelFilter{ + evaluations: tc.evaluations, + throwError: tc.throwError, + }, } - validations := tc.policy.Spec.Validations - CompilationResults := validator.(*CELValidator).compilationResults - require.Equal(t, len(validations), len(CompilationResults)) + policyResults := v.Validate(fakeVersionedAttr, nil) - versionedAttr, err := generic.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) - if err != nil { - t.Fatalf("unexpected error on conversion: %v", err) - } + require.Equal(t, len(policyResults), len(tc.policyDecision)) - policyResults, err := validator.Validate(versionedAttr, tc.params) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - require.Equal(t, len(policyResults), len(tc.policyDecisions)) - for i, policyDecision := range tc.policyDecisions { + for i, policyDecision := range tc.policyDecision { if policyDecision.Action != policyResults[i].Action { t.Errorf("Expected policy decision kind '%v' but got '%v'", policyDecision.Action, policyResults[i].Action) } @@ -575,39 +483,3 @@ func TestValidate(t *testing.T) { }) } } - -// newObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file. -func newObjectInterfacesForTest() admission.ObjectInterfaces { - scheme := runtime.NewScheme() - corev1.AddToScheme(scheme) - return admission.NewObjectInterfacesFromScheme(scheme) -} - -func newValidAttribute(object runtime.Object, isDelete bool) admission.Attributes { - var oldObject runtime.Object - if !isDelete { - if object == nil { - object = &corev1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{ - Name: "endpoints1", - }, - Subsets: []corev1.EndpointSubset{ - { - Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}}, - }, - }, - } - } - } else { - object = nil - oldObject = &corev1.Endpoints{ - Subsets: []corev1.EndpointSubset{ - { - Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}}, - }, - }, - } - } - return admission.NewAttributesRecord(object, oldObject, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil) - -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 80e8e1e92a6..4ddcc3845de 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1450,6 +1450,7 @@ k8s.io/apiserver/pkg/admission/cel k8s.io/apiserver/pkg/admission/configuration k8s.io/apiserver/pkg/admission/initializer k8s.io/apiserver/pkg/admission/metrics +k8s.io/apiserver/pkg/admission/plugin/cel k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle k8s.io/apiserver/pkg/admission/plugin/resourcequota k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota