refactor admission cel validator and compiler to be reusable

This commit is contained in:
Igor Velichkovich 2023-02-15 16:08:59 -06:00
parent 44bedc2a46
commit e96ef31187
16 changed files with 1909 additions and 988 deletions

View File

@ -18,6 +18,7 @@ package validation
import ( import (
"fmt" "fmt"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
"regexp" "regexp"
"strings" "strings"
@ -28,7 +29,7 @@ import (
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
utilvalidation "k8s.io/apimachinery/pkg/util/validation" utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field" "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/cel"
"k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/apis/admissionregistration"
@ -733,7 +734,11 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
if len(trimmedExpression) == 0 { if len(trimmedExpression) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified")) allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified"))
} else { } 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 { if result.Error != nil {
switch result.Error.Type { switch result.Error.Type {
case cel.ErrorTypeRequired: case cel.ErrorTypeRequired:

View File

@ -0,0 +1,10 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- jpbetz
- cici37
- alexzielenski
reviewers:
- jpbetz
- cici37
- alexzielenski

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package validatingadmissionpolicy package cel
import ( import (
"sync" "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 { type CompilationResult struct {
Program cel.Program Program cel.Program
Error *apiservercel.Error Error *apiservercel.Error
ExpressionAccessor ExpressionAccessor
} }
// CompileValidatingPolicyExpression returns a compiled vaalidating policy CEL expression. // CompileCELExpression returns a compiled CEL expression.
func CompileValidatingPolicyExpression(validationExpression string, hasParams bool) CompilationResult { func CompileCELExpression(expressionAccessor ExpressionAccessor, hasParams bool) CompilationResult {
var env *cel.Env var env *cel.Env
envs, err := getEnvs() envs, err := getEnvs()
if err != nil { if err != nil {
@ -176,6 +177,7 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
Type: apiservercel.ErrorTypeInternal, Type: apiservercel.ErrorTypeInternal,
Detail: "compiler initialization failed: " + err.Error(), Detail: "compiler initialization failed: " + err.Error(),
}, },
ExpressionAccessor: expressionAccessor,
} }
} }
if hasParams { if hasParams {
@ -184,13 +186,14 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
env = envs.noParams env = envs.noParams
} }
ast, issues := env.Compile(validationExpression) ast, issues := env.Compile(expressionAccessor.GetExpression())
if issues != nil { if issues != nil {
return CompilationResult{ return CompilationResult{
Error: &apiservercel.Error{ Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid, Type: apiservercel.ErrorTypeInvalid,
Detail: "compilation failed: " + issues.String(), Detail: "compilation failed: " + issues.String(),
}, },
ExpressionAccessor: expressionAccessor,
} }
} }
if ast.OutputType() != cel.BoolType { if ast.OutputType() != cel.BoolType {
@ -199,6 +202,7 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
Type: apiservercel.ErrorTypeInvalid, Type: apiservercel.ErrorTypeInvalid,
Detail: "cel expression must evaluate to a bool", Detail: "cel expression must evaluate to a bool",
}, },
ExpressionAccessor: expressionAccessor,
} }
} }
@ -210,6 +214,7 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
Type: apiservercel.ErrorTypeInternal, Type: apiservercel.ErrorTypeInternal,
Detail: "unexpected compilation error: " + err.Error(), Detail: "unexpected compilation error: " + err.Error(),
}, },
ExpressionAccessor: expressionAccessor,
} }
} }
prog, err := env.Program(ast, prog, err := env.Program(ast,
@ -223,9 +228,11 @@ func CompileValidatingPolicyExpression(validationExpression string, hasParams bo
Type: apiservercel.ErrorTypeInvalid, Type: apiservercel.ErrorTypeInvalid,
Detail: "program instantiation failed: " + err.Error(), Detail: "program instantiation failed: " + err.Error(),
}, },
ExpressionAccessor: expressionAccessor,
} }
} }
return CompilationResult{ return CompilationResult{
Program: prog, Program: prog,
ExpressionAccessor: expressionAccessor,
} }
} }

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package validatingadmissionpolicy package cel
import ( import (
"strings" "strings"
@ -104,22 +104,34 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
for _, tc := range cases { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
for _, expr := range tc.expressions { for _, expr := range tc.expressions {
result := CompileValidatingPolicyExpression(expr, tc.hasParams) result := CompileCELExpression(&fakeExpressionAccessor{
expr,
}, tc.hasParams)
if result.Error != nil { if result.Error != nil {
t.Errorf("Unexpected error: %v", result.Error) t.Errorf("Unexpected error: %v", result.Error)
} }
} }
for expr, expectErr := range tc.errorExpressions { for expr, expectErr := range tc.errorExpressions {
result := CompileValidatingPolicyExpression(expr, tc.hasParams) result := CompileCELExpression(&fakeExpressionAccessor{
expr,
}, tc.hasParams)
if result.Error == nil { if result.Error == nil {
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr) t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
continue continue
} }
if !strings.Contains(result.Error.Error(), expectErr) { 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 continue
} }
}) })
} }
} }
type fakeExpressionAccessor struct {
expression string
}
func (f *fakeExpressionAccessor) GetExpression() string {
return f.expression
}

View File

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

View File

@ -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())
}
}
})
}
}

View File

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

View File

@ -25,6 +25,9 @@ import (
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
admissionv1 "k8s.io/api/admission/v1"
admissionRegistrationv1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1" "k8s.io/api/admissionregistration/v1alpha1"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors"
@ -36,6 +39,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer" "k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic" "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/features"
@ -107,6 +111,11 @@ var (
Kind: paramsGVK.Kind, Kind: paramsGVK.Kind,
}, },
FailurePolicy: ptrTo(v1alpha1.Fail), 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 // So that we can test the controller without pulling in any CEL functionality
type fakeCompiler struct { type fakeCompiler struct {
DefaultMatch bool CompileFuncs map[string]func([]cel.ExpressionAccessor, bool) cel.Filter
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
} }
var _ ValidatorCompiler = &fakeCompiler{} var _ cel.FilterCompiler = &fakeCompiler{}
func (f *fakeCompiler) HasSynced() bool { func (f *fakeCompiler) HasSynced() bool {
return true 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 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 // Matches says whether this policy definition matches the provided admission
// resource request // 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 namespace, name := definition.Namespace, definition.Name
key := namespacedName{ key := namespacedName{
name: name, 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 // Matches says whether this policy definition matches the provided admission
// resource request // 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 namespace, name := binding.Namespace, binding.Name
key := namespacedName{ key := namespacedName{
name: name, name: name,
@ -197,60 +310,14 @@ func (f *fakeCompiler) BindingMatches(a admission.Attributes, o admission.Object
return f.DefaultMatch, nil return f.DefaultMatch, nil
} }
func (f *fakeCompiler) Compile( var validatorMap map[string]*fakeValidator
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)
}
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) { func setupFakeTest(t *testing.T, comp *fakeCompiler, match *fakeMatcher) (plugin admission.ValidationInterface, paramTracker, policyTracker clienttesting.ObjectTracker, controller *celAdmissionController) {
namespace, name := definition.Namespace, definition.Name return setupTestCommon(t, comp, match, true)
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)
} }
// Starts CEL admission controller and sets up a plugin configured with it as well // 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 // PolicyTracker expects FakePolicyDefinition and FakePolicyBinding types
// !TODO: refactor this test/framework to remove startInformers argument and // !TODO: refactor this test/framework to remove startInformers argument and
// clean up the return args, and in general make it more accessible. // 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()) testContext, testContextCancel := context.WithCancel(context.Background())
t.Cleanup(testContextCancel) t.Cleanup(testContextCancel)
@ -297,7 +364,14 @@ func setupTestCommon(t *testing.T, compiler ValidatorCompiler, shouldStartInform
// Override compiler used by controller for tests // Override compiler used by controller for tests
controller = handler.evaluator.(*celAdmissionController) 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() { t.Cleanup(func() {
testContextCancel() testContextCancel()
@ -582,14 +656,15 @@ func must3[T any, I any](val T, _ I, err error) T {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
func TestPluginNotReady(t *testing.T) { func TestPluginNotReady(t *testing.T) {
compiler := &fakeCompiler{ reset()
// Match everything by default compiler := &fakeCompiler{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
// Show that an unstarted informer (or one that has failed its listwatch) // Show that an unstarted informer (or one that has failed its listwatch)
// will show proper error from plugin // will show proper error from plugin
handler, _, _, _ := setupTestCommon(t, compiler, false) handler, _, _, _ := setupTestCommon(t, compiler, matcher, false)
err := handler.Validate( err := handler.Validate(
context.Background(), context.Background(),
// Object is irrelevant/unchecked for this test. Just test that // 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") require.ErrorContains(t, err, "not yet ready to handle request")
// Show that by now starting the informer, the error is dissipated // 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( err = handler.Validate(
context.Background(), context.Background(),
// Object is irrelevant/unchecked for this test. Just test that // Object is irrelevant/unchecked for this test. Just test that
@ -614,25 +689,39 @@ func TestPluginNotReady(t *testing.T) {
} }
func TestBasicPolicyDefinitionFailure(t *testing.T) { func TestBasicPolicyDefinitionFailure(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
datalock := sync.Mutex{} datalock := sync.Mutex{}
numCompiles := 0 numCompiles := 0
compiler := &fakeCompiler{ compiler := &fakeCompiler{}
// Match everything by default validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
compiler.RegisterDefinition(denyPolicy, func(policy *v1alpha1.ValidatingAdmissionPolicy) Validator {
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
datalock.Lock() datalock.Lock()
numCompiles += 1 numCompiles += 1
datalock.Unlock() datalock.Unlock()
return testValidator{} return &fakeFilter{
}, nil) 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, paramTracker.Add(fakeParams))
require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace))
@ -655,33 +744,17 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
require.ErrorContains(t, err, `Denied`) 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. // Shows that if a definition does not match the input, it will not be used.
// But with a different input it will be used. // But with a different input it will be used.
func TestDefinitionDoesntMatch(t *testing.T) { func TestDefinitionDoesntMatch(t *testing.T) {
compiler := &fakeCompiler{ reset()
// Match everything by default compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler)
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher)
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
@ -689,26 +762,37 @@ func TestDefinitionDoesntMatch(t *testing.T) {
passedParams := []*unstructured.Unstructured{} passedParams := []*unstructured.Unstructured{}
numCompiles := 0 numCompiles := 0
compiler.RegisterDefinition(denyPolicy, compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { datalock.Lock()
datalock.Lock() numCompiles += 1
numCompiles += 1 datalock.Unlock()
datalock.Unlock()
return testValidator{} return &fakeFilter{
keyId: denyPolicy.Spec.Validations[0].Expression,
}
})
}, func(vap *v1alpha1.ValidatingAdmissionPolicy, a admission.Attributes) bool { validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
// Match names with even-numbered length return []PolicyDecision{
obj := a.GetObject() {
Action: ActionDeny,
Message: "Denied",
},
}
})
accessor, err := meta.Accessor(obj) matcher.RegisterDefinition(denyPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy, a admission.Attributes) bool {
if err != nil { // Match names with even-numbered length
t.Fatal(err) obj := a.GetObject()
return false
}
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, paramTracker.Add(fakeParams))
require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace))
@ -762,15 +846,17 @@ func TestDefinitionDoesntMatch(t *testing.T) {
} }
func TestReconfigureBinding(t *testing.T) { func TestReconfigureBinding(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
compiler := &fakeCompiler{ compiler := &fakeCompiler{}
// Match everything by default validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler) handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher)
datalock := sync.Mutex{} datalock := sync.Mutex{}
numCompiles := 0 numCompiles := 0
@ -787,15 +873,24 @@ func TestReconfigureBinding(t *testing.T) {
}, },
} }
compiler.RegisterDefinition(denyPolicy, compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { datalock.Lock()
datalock.Lock() numCompiles += 1
numCompiles += 1 datalock.Unlock()
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{ denyBinding2 := &v1alpha1.ValidatingAdmissionPolicyBinding{
ObjectMeta: metav1.ObjectMeta{ 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 // Shows that a policy which is in effect will stop being in effect when removed
func TestRemoveDefinition(t *testing.T) { func TestRemoveDefinition(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
compiler := &fakeCompiler{ compiler := &fakeCompiler{}
// Match everything by default validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler)
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher)
datalock := sync.Mutex{} datalock := sync.Mutex{}
numCompiles := 0 numCompiles := 0
compiler.RegisterDefinition(denyPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
datalock.Lock() datalock.Lock()
numCompiles += 1 numCompiles += 1
datalock.Unlock() datalock.Unlock()
return testValidator{} return &fakeFilter{
}, nil) 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, paramTracker.Add(fakeParams))
require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) 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 // Shows that a binding which is in effect will stop being in effect when removed
func TestRemoveBinding(t *testing.T) { func TestRemoveBinding(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
compiler := &fakeCompiler{
// Match everything by default compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler)
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher)
datalock := sync.Mutex{} datalock := sync.Mutex{}
numCompiles := 0 numCompiles := 0
compiler.RegisterDefinition(denyPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
datalock.Lock() datalock.Lock()
numCompiles += 1 numCompiles += 1
datalock.Unlock() datalock.Unlock()
return testValidator{} return &fakeFilter{
}, nil) 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, paramTracker.Add(fakeParams))
require.NoError(t, tracker.Create(definitionsGVR, denyPolicy, denyPolicy.Namespace)) 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 // Shows that an error is surfaced if a paramSource specified in a binding does
// not actually exist // not actually exist
func TestInvalidParamSourceGVK(t *testing.T) { func TestInvalidParamSourceGVK(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
compiler := &fakeCompiler{
// Match everything by default compiler := &fakeCompiler{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, _, tracker, controller := setupFakeTest(t, compiler)
handler, _, tracker, controller := setupFakeTest(t, compiler, matcher)
passedParams := make(chan *unstructured.Unstructured) passedParams := make(chan *unstructured.Unstructured)
badPolicy := *denyPolicy 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 // Shows that an error is surfaced if a param specified in a binding does not
// actually exist // actually exist
func TestInvalidParamSourceInstanceName(t *testing.T) { func TestInvalidParamSourceInstanceName(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
compiler := &fakeCompiler{
// Match everything by default compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, _, tracker, controller := setupFakeTest(t, compiler)
handler, _, tracker, controller := setupFakeTest(t, compiler, matcher)
datalock := sync.Mutex{} datalock := sync.Mutex{}
passedParams := []*unstructured.Unstructured{} passedParams := []*unstructured.Unstructured{}
numCompiles := 0 numCompiles := 0
compiler.RegisterDefinition(denyPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
datalock.Lock() datalock.Lock()
numCompiles += 1 numCompiles += 1
datalock.Unlock() datalock.Unlock()
return testValidator{} return &fakeFilter{
}, nil) 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(definitionsGVR, denyPolicy, denyPolicy.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.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 // Also shows that if binding has specified params in this instance then they
// are silently ignored. // are silently ignored.
func TestEmptyParamSource(t *testing.T) { func TestEmptyParamSource(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
compiler := &fakeCompiler{
// Match everything by default compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, _, tracker, controller := setupFakeTest(t, compiler)
handler, _, tracker, controller := setupFakeTest(t, compiler, matcher)
datalock := sync.Mutex{} datalock := sync.Mutex{}
numCompiles := 0 numCompiles := 0
@ -1075,13 +1221,24 @@ func TestEmptyParamSource(t *testing.T) {
noParamSourcePolicy := *denyPolicy noParamSourcePolicy := *denyPolicy
noParamSourcePolicy.Spec.ParamKind = nil noParamSourcePolicy.Spec.ParamKind = nil
compiler.RegisterDefinition(&noParamSourcePolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
datalock.Lock() datalock.Lock()
numCompiles += 1 numCompiles += 1
datalock.Unlock() datalock.Unlock()
return testValidator{} return &fakeFilter{
}, nil) 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(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithNoParamRef, denyBindingWithNoParamRef.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 // one policy stops using the param. The expectation is the second policy
// keeps behaving normally // keeps behaving normally
func TestMultiplePoliciesSharedParamType(t *testing.T) { func TestMultiplePoliciesSharedParamType(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
compiler := &fakeCompiler{ compiler := &fakeCompiler{}
// Match everything by default validator1 := &fakeValidator{}
validator2 := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler)
handler, paramTracker, tracker, controller := setupFakeTest(t, compiler, matcher)
// Use ConfigMap native-typed param // Use ConfigMap native-typed param
policy1 := *denyPolicy policy1 := *denyPolicy
policy1.Name = "denypolicy1.example.com" 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 := *denyPolicy
policy2.Name = "denypolicy2.example.com" 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 binding1 := *denyBinding
binding2 := *denyBinding binding2 := *denyBinding
@ -1138,32 +1323,40 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
compiles2 := atomic.Int64{} compiles2 := atomic.Int64{}
evaluations2 := 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) compiles1.Add(1)
return validatorFunc(func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) { return &fakeFilter{
evaluations1.Add(1) keyId: policy1.Spec.Validations[0].Expression,
return []PolicyDecision{ }
{ })
Action: ActionAdmit,
},
}, nil
})
}, nil)
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) compiles2.Add(1)
return validatorFunc(func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) { return &fakeFilter{
evaluations2.Add(1) keyId: policy2.Spec.Validations[0].Expression,
return []PolicyDecision{ }
{ })
Action: ActionDeny,
Message: "Policy2Denied", validator2.RegisterDefinition(&policy2, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
}, evaluations2.Add(1)
}, nil return []PolicyDecision{
}) {
}, nil) Action: ActionDeny,
Message: "Policy2Denied",
},
}
})
require.NoError(t, tracker.Create(definitionsGVR, &policy1, policy1.Namespace)) require.NoError(t, tracker.Create(definitionsGVR, &policy1, policy1.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, &binding1, binding1.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 // Shows that we can refer to native-typed params just fine
// (as opposed to CRD params) // (as opposed to CRD params)
func TestNativeTypeParam(t *testing.T) { func TestNativeTypeParam(t *testing.T) {
reset()
testContext, testContextCancel := context.WithCancel(context.Background()) testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel() defer testContextCancel()
compiler := &fakeCompiler{
// Match everything by default compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true, DefaultMatch: true,
} }
handler, _, tracker, controller := setupFakeTest(t, compiler) handler, _, tracker, controller := setupFakeTest(t, compiler, matcher)
compiles := atomic.Int64{} compiles := atomic.Int64{}
evaluations := atomic.Int64{} evaluations := atomic.Int64{}
@ -1252,29 +1448,31 @@ func TestNativeTypeParam(t *testing.T) {
Kind: "ConfigMap", Kind: "ConfigMap",
} }
compiler.RegisterDefinition(&nativeTypeParamPolicy, func(vap *v1alpha1.ValidatingAdmissionPolicy) Validator { compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
compiles.Add(1) compiles.Add(1)
return validatorFunc(func(versionedAttr *whgeneric.VersionedAttributes, params runtime.Object) ([]PolicyDecision, error) { return &fakeFilter{
evaluations.Add(1) keyId: nativeTypeParamPolicy.Spec.Validations[0].Expression,
}
})
// show that the passed params was a ConfigMap native type validator.RegisterDefinition(&nativeTypeParamPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
if _, ok := params.(*v1.ConfigMap); ok { evaluations.Add(1)
return []PolicyDecision{ if _, ok := versionedParams.(*v1.ConfigMap); ok {
{
Action: ActionDeny,
Message: "correct type",
},
}, nil
}
return []PolicyDecision{ return []PolicyDecision{
{ {
Action: ActionDeny, Action: ActionDeny,
Message: "Incorrect param type", Message: "correct type",
}, },
}, nil }
}) }
}, nil) return []PolicyDecision{
{
Action: ActionDeny,
Message: "Incorrect param type",
},
}
})
configMapParam := &v1.ConfigMap{ configMapParam := &v1.ConfigMap{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{

View File

@ -24,19 +24,19 @@ import (
"sync/atomic" "sync/atomic"
"time" "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" "k8s.io/api/admissionregistration/v1alpha1"
k8serrors "k8s.io/apimachinery/pkg/api/errors" k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta" "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" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
celmetrics "k8s.io/apiserver/pkg/admission/cel" 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/internal/generic"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic" whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
@ -69,6 +69,14 @@ type policyData struct {
bindings []bindingInfo 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 // namespaceName is used as a key in definitionInfo and bindingInfos
type namespacedName struct { type namespacedName struct {
namespace, name string namespace, name string
@ -118,9 +126,8 @@ func NewAdmissionController(
restMapper, restMapper,
client, client,
dynamicClient, dynamicClient,
&CELValidatorCompiler{ cel.NewFilterCompiler(),
Matcher: matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client), NewMatcher(matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)),
},
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy]( generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicy](
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()), informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding]( generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
@ -213,7 +220,7 @@ func (c *celAdmissionController) Validate(
for _, definitionInfo := range policyDatas { for _, definitionInfo := range policyDatas {
definition := definitionInfo.lastReconciledValue 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 { if err != nil {
// Configuration error. // Configuration error.
addConfigError(err, definition, nil) addConfigError(err, definition, nil)
@ -232,7 +239,7 @@ func (c *celAdmissionController) Validate(
// If the key is inside dependentBindings, there is guaranteed to // If the key is inside dependentBindings, there is guaranteed to
// be a bindingInfo for it // be a bindingInfo for it
binding := bindingInfo.lastReconciledValue binding := bindingInfo.lastReconciledValue
matches, err := c.policyController.BindingMatches(a, o, binding) matches, err := c.policyController.matcher.BindingMatches(a, o, binding)
if err != nil { if err != nil {
// Configuration error. // Configuration error.
addConfigError(err, definition, binding) addConfigError(err, definition, binding)
@ -310,13 +317,7 @@ func (c *celAdmissionController) Validate(
versionedAttr = va versionedAttr = va
} }
decisions, err := bindingInfo.validator.Validate(versionedAttr, param) decisions := 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
}
for _, decision := range decisions { for _, decision := range decisions {
switch decision.Action { switch decision.Action {
@ -354,7 +355,7 @@ func (c *celAdmissionController) Validate(
reason = metav1.StatusReasonInvalid reason = metav1.StatusReasonInvalid
} }
err.ErrStatus.Reason = reason 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}) err.ErrStatus.Details.Causes = append(err.ErrStatus.Details.Causes, metav1.StatusCause{Message: message})
return err return err
} }
@ -366,7 +367,7 @@ func (c *celAdmissionController) HasSynced() bool {
} }
func (c *celAdmissionController) ValidateInitialization() error { func (c *celAdmissionController) ValidateInitialization() error {
return c.policyController.ValidateInitialization() return c.policyController.matcher.ValidateInitialization()
} }
func (c *celAdmissionController) refreshPolicies() { func (c *celAdmissionController) refreshPolicies() {

View File

@ -22,6 +22,7 @@ import (
"sync" "sync"
"time" "time"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1" "k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/meta"
@ -29,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
celmetrics "k8s.io/apiserver/pkg/admission/cel" 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/internal/generic"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer" "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 // Provided to the policy's Compile function as an injected dependency to
// assist with compiling its expressions to CEL // assist with compiling its expressions to CEL
ValidatorCompiler filterCompiler cel.FilterCompiler
matcher Matcher
newValidator
// Lock which protects: // Lock which protects:
// - cachedPolicies // - cachedPolicies
@ -81,21 +87,26 @@ type policyController struct {
client kubernetes.Interface client kubernetes.Interface
} }
type newValidator func(cel.Filter, *v1.FailurePolicyType) Validator
func newPolicyController( func newPolicyController(
restMapper meta.RESTMapper, restMapper meta.RESTMapper,
client kubernetes.Interface, client kubernetes.Interface,
dynamicClient dynamic.Interface, dynamicClient dynamic.Interface,
validatorCompiler ValidatorCompiler, filterCompiler cel.FilterCompiler,
matcher Matcher,
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy], policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding], bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding],
) *policyController { ) *policyController {
res := &policyController{} res := &policyController{}
*res = policyController{ *res = policyController{
ValidatorCompiler: validatorCompiler, filterCompiler: filterCompiler,
definitionInfo: make(map[namespacedName]*definitionInfo), definitionInfo: make(map[namespacedName]*definitionInfo),
bindingInfos: make(map[namespacedName]*bindingInfo), bindingInfos: make(map[namespacedName]*bindingInfo),
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo), paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]), definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
matcher: matcher,
newValidator: NewValidator,
policyDefinitionsController: generic.NewController( policyDefinitionsController: generic.NewController(
policiesInformer, policiesInformer,
res.reconcilePolicyDefinition, res.reconcilePolicyDefinition,
@ -424,7 +435,14 @@ func (c *policyController) latestPolicyData() []policyData {
for bindingNN := range c.definitionsToBindings[definitionNN] { for bindingNN := range c.definitionsToBindings[definitionNN] {
bindingInfo := c.bindingInfos[bindingNN] bindingInfo := c.bindingInfos[bindingNN]
if bindingInfo.validator == nil && definitionInfo.configurationError == nil { 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) bindingInfos = append(bindingInfos, *bindingInfo)
} }
@ -447,6 +465,33 @@ func (c *policyController) latestPolicyData() []policyData {
return res 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 { func getNamespaceName(namespace, name string) namespacedName {
return namespacedName{ return namespacedName{
namespace: namespace, namespace: namespace,

View File

@ -18,34 +18,42 @@ package validatingadmissionpolicy
import ( import (
"k8s.io/api/admissionregistration/v1alpha1" "k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
) )
// Validator defines the func used to validate an object against the validator's rules. var _ cel.ExpressionAccessor = &ValidationCondition{}
// 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). // ValidationCondition contains the inputs needed to compile, evaluate and validate a cel expression
type Validator interface { type ValidationCondition struct {
Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) Expression string
Message string
Reason *metav1.StatusReason
} }
// ValidatorCompiler is Dependency Injected into the PolicyDefinition's `Compile` func (v *ValidationCondition) GetExpression() string {
// function to assist with converting types and values to/from CEL-typed values. return v.Expression
type ValidatorCompiler interface { }
// Matcher is used for matching ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding to attributes
type Matcher interface {
admission.InitializationValidator admission.InitializationValidator
// Matches says whether this policy definition matches the provided admission // DefinitionMatches says whether this policy definition matches the provided admission
// resource request // resource request
DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) 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 // resource request
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error)
}
// Compile is used for the cel expression compilation
Compile( // Validator is contains logic for converting ValidationEvaluation to PolicyDecisions
policy *v1alpha1.ValidatingAdmissionPolicy, type Validator interface {
) Validator // Validate is used to take cel evaluations and convert into decisions
Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision
} }

View File

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

View File

@ -20,7 +20,6 @@ import (
"net/http" "net/http"
"time" "time"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
@ -39,6 +38,7 @@ const (
EvalDeny PolicyDecisionEvaluation = "deny" EvalDeny PolicyDecisionEvaluation = "deny"
) )
// PolicyDecision contains the action determined from a cel evaluation along with metadata such as message, reason and duration
type PolicyDecision struct { type PolicyDecision struct {
Action PolicyDecisionAction Action PolicyDecisionAction
Evaluation PolicyDecisionEvaluation Evaluation PolicyDecisionEvaluation
@ -47,13 +47,7 @@ type PolicyDecision struct {
Elapsed time.Duration Elapsed time.Duration
} }
type policyDecisionWithMetadata struct { func reasonToCode(r metav1.StatusReason) int32 {
PolicyDecision
Definition *v1alpha1.ValidatingAdmissionPolicy
Binding *v1alpha1.ValidatingAdmissionPolicyBinding
}
func ReasonToCode(r metav1.StatusReason) int32 {
switch r { switch r {
case metav1.StatusReasonForbidden: case metav1.StatusReasonForbidden:
return http.StatusForbidden return http.StatusForbidden

View File

@ -18,298 +18,92 @@ package validatingadmissionpolicy
import ( import (
"fmt" "fmt"
"reflect" "k8s.io/klog/v2"
"strings" "strings"
"time"
celtypes "github.com/google/cel-go/common/types" celtypes "github.com/google/cel-go/common/types"
"github.com/google/cel-go/interpreter"
admissionv1 "k8s.io/api/admission/v1" v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
authenticationv1 "k8s.io/api/authentication/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
) )
var _ ValidatorCompiler = &CELValidatorCompiler{} // validator implements the Validator interface
var _ matching.MatchCriteria = &matchCriteria{} type validator struct {
filter cel.Filter
type matchCriteria struct { failPolicy *v1.FailurePolicyType
constraints *v1alpha1.MatchResources
} }
// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector func NewValidator(filter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) { return &validator{
return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector) filter: filter,
} failPolicy: failPolicy,
// 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
} }
} }
// Parent returns the parent of the current activation, may be nil. func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction {
// If non-nil, the parent will be searched during resolve calls. if f == v1.Ignore {
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 {
return ActionAdmit return ActionAdmit
} }
return ActionDeny return ActionDeny
} }
// Validate validates all cel expressions in Validator and returns a PolicyDecision for each CEL expression or returns an error. // Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
// An error will be returned if failed to convert the object/oldObject/params/request to unstructured. func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
// Each PolicyDecision will have a decision and a message. var f v1.FailurePolicyType
// policyDecision.message will be empty if the decision is allowed and no error met. if v.failPolicy == nil {
func (v *CELValidator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) { f = v1.Fail
// 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
} else { } else {
f = *v.policy.Spec.FailurePolicy f = *v.failPolicy
} }
for i, compilationResult := range v.compilationResults { evalResults, err := v.filter.ForInput(versionedAttr, versionedParams, cel.CreateAdmissionRequest(versionedAttr.Attributes))
validation := v.policy.Spec.Validations[i] if err != nil {
return []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
}
}
decisions := make([]PolicyDecision, len(evalResults))
var policyDecision = &decisions[i] for i, evalResult := range evalResults {
var decision = &decisions[i]
if compilationResult.Error != nil { // TODO: move this to generics
policyDecision.Action = policyDecisionActionForError(f) validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition)
policyDecision.Evaluation = EvalError if !ok {
policyDecision.Message = fmt.Sprintf("compilation error: %v", compilationResult.Error) klog.Error("Invalid type conversion to ValidationCondition")
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = "Invalid type sent to validator, expected ValidationCondition"
continue continue
} }
if compilationResult.Program == nil {
policyDecision.Action = policyDecisionActionForError(f) if evalResult.Error != nil {
policyDecision.Evaluation = EvalError decision.Action = policyDecisionActionForError(f)
policyDecision.Message = "unexpected internal error compiling expression" decision.Evaluation = EvalError
continue decision.Message = evalResult.Error.Error()
} } else if evalResult.EvalResult != celtypes.True {
t1 := time.Now() decision.Action = ActionDeny
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 validation.Reason == nil { if validation.Reason == nil {
policyDecision.Reason = metav1.StatusReasonInvalid decision.Reason = metav1.StatusReasonInvalid
} else { } else {
policyDecision.Reason = *validation.Reason decision.Reason = *validation.Reason
} }
if len(validation.Message) > 0 { if len(validation.Message) > 0 {
policyDecision.Message = strings.TrimSpace(validation.Message) decision.Message = strings.TrimSpace(validation.Message)
} else { } else {
policyDecision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression)) decision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
} }
} else { } else {
policyDecision.Action = ActionAdmit decision.Action = ActionAdmit
policyDecision.Evaluation = EvalAdmit decision.Evaluation = EvalAdmit
} }
} }
return decisions
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(),
},
}
} }

View File

@ -17,551 +17,459 @@ limitations under the License.
package validatingadmissionpolicy package validatingadmissionpolicy
import ( import (
"errors"
"strings" "strings"
"testing" "testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/stretchr/testify/require" "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" 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" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic" "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
) )
func TestCompile(t *testing.T) { var _ cel.Filter = &fakeCelFilter{}
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"},
},
},
},
},
},
}
for _, tc := range cases { type fakeCelFilter struct {
t.Run(tc.name, func(t *testing.T) { evaluations []cel.EvaluationResult
var c CELValidatorCompiler throwError bool
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)
}
}
})
}
} }
func getValidPolicy(validations []v1alpha1.Validation, params *v1alpha1.ParamKind, fp *v1alpha1.FailurePolicyType) *v1alpha1.ValidatingAdmissionPolicy { func (f *fakeCelFilter) ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]cel.EvaluationResult, error) {
if fp == nil { if f.throwError {
fp = func() *v1alpha1.FailurePolicyType { return nil, errors.New("test error")
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"},
},
},
},
} }
return f.evaluations, nil
} }
func generatedDecision(k PolicyDecisionAction, m string, r metav1.StatusReason) PolicyDecision { func (f *fakeCelFilter) CompilationErrors() []error {
return PolicyDecision{Action: k, Message: m, Reason: r} return []error{}
} }
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
// we fake the paramKind in ValidatingAdmissionPolicy for testing since the params is directly passed from cel admission ignore := v1.Ignore
// Inside validator.go, we only check if paramKind exists fail := v1.Fail
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
}()
configMapParams := &corev1.ConfigMap{ forbiddenReason := metav1.StatusReasonForbidden
ObjectMeta: metav1.ObjectMeta{ unauthorizedReason := metav1.StatusReasonUnauthorized
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 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 { cases := []struct {
name string name string
policy *v1alpha1.ValidatingAdmissionPolicy failPolicy *v1.FailurePolicyType
attributes admission.Attributes evaluations []cel.EvaluationResult
params runtime.Object policyDecision []PolicyDecision
policyDecisions []PolicyDecision throwError bool
}{ }{
{ {
name: "valid syntax for object", name: "test pass",
policy: getValidPolicy([]v1alpha1.Validation{ 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", name: "test multiple pass",
policy: getValidPolicy([]v1alpha1.Validation{ 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", name: "test error with failurepolicy ignore",
policy: getValidPolicy([]v1alpha1.Validation{ 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", name: "test fail with failurepolicy fail",
policy: getValidPolicy([]v1alpha1.Validation{ evaluations: []cel.EvaluationResult{
{Expression: "request.operation == 'CREATE'"}, {
}, nil, nil), Error: errors.New(""),
attributes: newValidAttribute(nil, false), ExpressionAccessor: &ValidationCondition{},
policyDecisions: []PolicyDecision{ },
generatedDecision(ActionAdmit, "", ""), },
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", name: "test fail with failurepolicy fail with multiple validations",
policy: getValidPolicy([]v1alpha1.Validation{ evaluations: []cel.EvaluationResult{
{Expression: "request.namespace != params.data.fakeString"}, {
}, hasParamKind, nil), EvalResult: celtypes.True,
attributes: newValidAttribute(nil, false), ExpressionAccessor: &ValidationCondition{},
params: configMapParams, },
policyDecisions: []PolicyDecision{ {
generatedDecision(ActionAdmit, "", ""), 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", name: "test fail with failurepolicy fail with multiple failed validations",
policy: getValidPolicy([]v1alpha1.Validation{ evaluations: []cel.EvaluationResult{
{Expression: "object.subsets.size() > 2"}, {
}, hasParamKind, ignorePolicy), Error: errors.New(""),
attributes: newValidAttribute(nil, false), ExpressionAccessor: &ValidationCondition{},
params: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
}, },
Data: map[string]string{ {
"fakeString": "fake", Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
}, },
}, },
policyDecisions: []PolicyDecision{ policyDecision: []PolicyDecision{
generatedDecision(ActionDeny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid), {
Action: ActionDeny,
},
{
Action: ActionDeny,
},
}, },
failPolicy: &fail,
}, },
{ {
name: "test failure policy with multiple validations", name: "test reason for fail no reason set",
policy: getValidPolicy([]v1alpha1.Validation{ 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", name: "test reason for fail reason set",
policy: getValidPolicy([]v1alpha1.Validation{ 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", name: "test reason for failed validations multiple validations",
policy: getValidPolicy([]v1alpha1.Validation{ 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", name: "test message for failed validations",
policy: getValidPolicy([]v1alpha1.Validation{ evaluations: []cel.EvaluationResult{
{ {
Expression: "oldObject == null", EvalResult: celtypes.False,
Reason: forbiddenReason, 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", name: "test message for failed validations multiple validations",
policy: getValidPolicy([]v1alpha1.Validation{ evaluations: []cel.EvaluationResult{
{ {
Expression: "oldObject == null", EvalResult: celtypes.False,
Reason: forbiddenReason, ExpressionAccessor: &ValidationCondition{
Message: "old object should be present", 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", name: "test filter error",
policy: getValidPolicy([]v1alpha1.Validation{ 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", name: "test filter error multiple evaluations",
policy: getValidPolicy([]v1alpha1.Validation{ 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, "", ""),
}, },
}, policyDecision: []PolicyDecision{
{
name: "test compile failure with FailurePolicy Fail",
policy: getValidPolicy([]v1alpha1.Validation{
{ {
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: <input>: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 { for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
c := CELValidatorCompiler{} v := validator{
validator := c.Compile(tc.policy) failPolicy: tc.failPolicy,
if validator == nil { filter: &fakeCelFilter{
t.Fatalf("unexpected nil validator") evaluations: tc.evaluations,
throwError: tc.throwError,
},
} }
validations := tc.policy.Spec.Validations policyResults := v.Validate(fakeVersionedAttr, nil)
CompilationResults := validator.(*CELValidator).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
versionedAttr, err := generic.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest()) require.Equal(t, len(policyResults), len(tc.policyDecision))
if err != nil {
t.Fatalf("unexpected error on conversion: %v", err)
}
policyResults, err := validator.Validate(versionedAttr, tc.params) for i, policyDecision := range tc.policyDecision {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
require.Equal(t, len(policyResults), len(tc.policyDecisions))
for i, policyDecision := range tc.policyDecisions {
if policyDecision.Action != policyResults[i].Action { if policyDecision.Action != policyResults[i].Action {
t.Errorf("Expected policy decision kind '%v' but got '%v'", 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)
}

1
vendor/modules.txt vendored
View File

@ -1450,6 +1450,7 @@ k8s.io/apiserver/pkg/admission/cel
k8s.io/apiserver/pkg/admission/configuration k8s.io/apiserver/pkg/admission/configuration
k8s.io/apiserver/pkg/admission/initializer k8s.io/apiserver/pkg/admission/initializer
k8s.io/apiserver/pkg/admission/metrics 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/namespace/lifecycle
k8s.io/apiserver/pkg/admission/plugin/resourcequota k8s.io/apiserver/pkg/admission/plugin/resourcequota
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota