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

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

View File

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

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

View File

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

View File

@ -22,6 +22,7 @@ import (
"sync"
"time"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
@ -29,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
celmetrics "k8s.io/apiserver/pkg/admission/cel"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/dynamicinformer"
@ -48,7 +50,11 @@ type policyController struct {
// Provided to the policy's Compile function as an injected dependency to
// assist with compiling its expressions to CEL
ValidatorCompiler
filterCompiler cel.FilterCompiler
matcher Matcher
newValidator
// Lock which protects:
// - cachedPolicies
@ -81,21 +87,26 @@ type policyController struct {
client kubernetes.Interface
}
type newValidator func(cel.Filter, *v1.FailurePolicyType) Validator
func newPolicyController(
restMapper meta.RESTMapper,
client kubernetes.Interface,
dynamicClient dynamic.Interface,
validatorCompiler ValidatorCompiler,
filterCompiler cel.FilterCompiler,
matcher Matcher,
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding],
) *policyController {
res := &policyController{}
*res = policyController{
ValidatorCompiler: validatorCompiler,
filterCompiler: filterCompiler,
definitionInfo: make(map[namespacedName]*definitionInfo),
bindingInfos: make(map[namespacedName]*bindingInfo),
paramsCRDControllers: make(map[v1alpha1.ParamKind]*paramInfo),
definitionsToBindings: make(map[namespacedName]sets.Set[namespacedName]),
matcher: matcher,
newValidator: NewValidator,
policyDefinitionsController: generic.NewController(
policiesInformer,
res.reconcilePolicyDefinition,
@ -424,7 +435,14 @@ func (c *policyController) latestPolicyData() []policyData {
for bindingNN := range c.definitionsToBindings[definitionNN] {
bindingInfo := c.bindingInfos[bindingNN]
if bindingInfo.validator == nil && definitionInfo.configurationError == nil {
bindingInfo.validator = c.ValidatorCompiler.Compile(definitionInfo.lastReconciledValue)
hasParam := false
if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
hasParam = true
}
bindingInfo.validator = c.newValidator(
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), hasParam),
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
)
}
bindingInfos = append(bindingInfos, *bindingInfo)
}
@ -447,6 +465,33 @@ func (c *policyController) latestPolicyData() []policyData {
return res
}
func convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(policyType *v1alpha1.FailurePolicyType) *v1.FailurePolicyType {
if policyType == nil {
return nil
}
var v1FailPolicy v1.FailurePolicyType
if *policyType == v1alpha1.Fail {
v1FailPolicy = v1.Fail
} else if *policyType == v1alpha1.Ignore {
v1FailPolicy = v1.Ignore
}
return &v1FailPolicy
}
func convertv1alpha1Validations(inputValidations []v1alpha1.Validation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
validation := ValidationCondition{
Expression: validation.Expression,
Message: validation.Message,
Reason: validation.Reason,
}
celExpressionAccessor[i] = &validation
}
return celExpressionAccessor
}
func getNamespaceName(namespace, name string) namespacedName {
return namespacedName{
namespace: namespace,

View File

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

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

View File

@ -18,298 +18,92 @@ package validatingadmissionpolicy
import (
"fmt"
"reflect"
"k8s.io/klog/v2"
"strings"
"time"
celtypes "github.com/google/cel-go/common/types"
"github.com/google/cel-go/interpreter"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/api/admissionregistration/v1alpha1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
var _ ValidatorCompiler = &CELValidatorCompiler{}
var _ matching.MatchCriteria = &matchCriteria{}
type matchCriteria struct {
constraints *v1alpha1.MatchResources
// validator implements the Validator interface
type validator struct {
filter cel.Filter
failPolicy *v1.FailurePolicyType
}
// GetParsedNamespaceSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedNamespaceSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.NamespaceSelector)
}
// GetParsedObjectSelector returns the converted LabelSelector which implements labels.Selector
func (m *matchCriteria) GetParsedObjectSelector() (labels.Selector, error) {
return metav1.LabelSelectorAsSelector(m.constraints.ObjectSelector)
}
// GetMatchResources returns the matchConstraints
func (m *matchCriteria) GetMatchResources() v1alpha1.MatchResources {
return *m.constraints
}
// CELValidatorCompiler implement the interface ValidatorCompiler.
type CELValidatorCompiler struct {
Matcher *matching.Matcher
}
// DefinitionMatches returns whether this ValidatingAdmissionPolicy matches the provided admission resource request
func (c *CELValidatorCompiler) DefinitionMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicy) (bool, schema.GroupVersionKind, error) {
criteria := matchCriteria{constraints: definition.Spec.MatchConstraints}
return c.Matcher.Matches(a, o, &criteria)
}
// BindingMatches returns whether this ValidatingAdmissionPolicyBinding matches the provided admission resource request
func (c *CELValidatorCompiler) BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, binding *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error) {
if binding.Spec.MatchResources == nil {
return true, nil
}
criteria := matchCriteria{constraints: binding.Spec.MatchResources}
isMatch, _, err := c.Matcher.Matches(a, o, &criteria)
return isMatch, err
}
// ValidateInitialization checks if Matcher is initialized.
func (c *CELValidatorCompiler) ValidateInitialization() error {
return c.Matcher.ValidateInitialization()
}
type validationActivation struct {
object, oldObject, params, request interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
// could not be found.
func (a *validationActivation) ResolveName(name string) (interface{}, bool) {
switch name {
case ObjectVarName:
return a.object, true
case OldObjectVarName:
return a.oldObject, true
case ParamsVarName:
return a.params, true
case RequestVarName:
return a.request, true
default:
return nil, false
func NewValidator(filter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
return &validator{
filter: filter,
failPolicy: failPolicy,
}
}
// Parent returns the parent of the current activation, may be nil.
// If non-nil, the parent will be searched during resolve calls.
func (a *validationActivation) Parent() interpreter.Activation {
return nil
}
// Compile compiles the cel expression defined in ValidatingAdmissionPolicy
func (c *CELValidatorCompiler) Compile(p *v1alpha1.ValidatingAdmissionPolicy) Validator {
if len(p.Spec.Validations) == 0 {
return nil
}
hasParam := false
if p.Spec.ParamKind != nil {
hasParam = true
}
compilationResults := make([]CompilationResult, len(p.Spec.Validations))
for i, validation := range p.Spec.Validations {
compilationResults[i] = CompileValidatingPolicyExpression(validation.Expression, hasParam)
}
return &CELValidator{policy: p, compilationResults: compilationResults}
}
// CELValidator implements the Validator interface
type CELValidator struct {
policy *v1alpha1.ValidatingAdmissionPolicy
compilationResults []CompilationResult
}
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
if obj == nil || reflect.ValueOf(obj).IsNil() {
return &unstructured.Unstructured{Object: nil}, nil
}
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: ret}, nil
}
func objectToResolveVal(r runtime.Object) (interface{}, error) {
if r == nil || reflect.ValueOf(r).IsNil() {
return nil, nil
}
v, err := convertObjectToUnstructured(r)
if err != nil {
return nil, err
}
return v.Object, nil
}
func policyDecisionActionForError(f v1alpha1.FailurePolicyType) PolicyDecisionAction {
if f == v1alpha1.Ignore {
func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction {
if f == v1.Ignore {
return ActionAdmit
}
return ActionDeny
}
// Validate validates all cel expressions in Validator and returns a PolicyDecision for each CEL expression or returns an error.
// An error will be returned if failed to convert the object/oldObject/params/request to unstructured.
// Each PolicyDecision will have a decision and a message.
// policyDecision.message will be empty if the decision is allowed and no error met.
func (v *CELValidator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) ([]PolicyDecision, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
decisions := make([]PolicyDecision, len(v.compilationResults))
var err error
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
if err != nil {
return nil, err
}
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
if err != nil {
return nil, err
}
paramsVal, err := objectToResolveVal(versionedParams)
if err != nil {
return nil, err
}
request := createAdmissionRequest(versionedAttr.Attributes)
requestVal, err := convertObjectToUnstructured(request)
if err != nil {
return nil, err
}
va := &validationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
}
var f v1alpha1.FailurePolicyType
if v.policy.Spec.FailurePolicy == nil {
f = v1alpha1.Fail
// Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object) []PolicyDecision {
var f v1.FailurePolicyType
if v.failPolicy == nil {
f = v1.Fail
} else {
f = *v.policy.Spec.FailurePolicy
f = *v.failPolicy
}
for i, compilationResult := range v.compilationResults {
validation := v.policy.Spec.Validations[i]
evalResults, err := v.filter.ForInput(versionedAttr, versionedParams, cel.CreateAdmissionRequest(versionedAttr.Attributes))
if err != nil {
return []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
}
}
decisions := make([]PolicyDecision, len(evalResults))
var policyDecision = &decisions[i]
if compilationResult.Error != nil {
policyDecision.Action = policyDecisionActionForError(f)
policyDecision.Evaluation = EvalError
policyDecision.Message = fmt.Sprintf("compilation error: %v", compilationResult.Error)
for i, evalResult := range evalResults {
var decision = &decisions[i]
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition)
if !ok {
klog.Error("Invalid type conversion to ValidationCondition")
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = "Invalid type sent to validator, expected ValidationCondition"
continue
}
if compilationResult.Program == nil {
policyDecision.Action = policyDecisionActionForError(f)
policyDecision.Evaluation = EvalError
policyDecision.Message = "unexpected internal error compiling expression"
continue
}
t1 := time.Now()
evalResult, _, err := compilationResult.Program.Eval(va)
elapsed := time.Since(t1)
policyDecision.Elapsed = elapsed
if err != nil {
policyDecision.Action = policyDecisionActionForError(f)
policyDecision.Evaluation = EvalError
policyDecision.Message = fmt.Sprintf("expression '%v' resulted in error: %v", v.policy.Spec.Validations[i].Expression, err)
} else if evalResult != celtypes.True {
policyDecision.Action = ActionDeny
if evalResult.Error != nil {
decision.Action = policyDecisionActionForError(f)
decision.Evaluation = EvalError
decision.Message = evalResult.Error.Error()
} else if evalResult.EvalResult != celtypes.True {
decision.Action = ActionDeny
if validation.Reason == nil {
policyDecision.Reason = metav1.StatusReasonInvalid
decision.Reason = metav1.StatusReasonInvalid
} else {
policyDecision.Reason = *validation.Reason
decision.Reason = *validation.Reason
}
if len(validation.Message) > 0 {
policyDecision.Message = strings.TrimSpace(validation.Message)
decision.Message = strings.TrimSpace(validation.Message)
} else {
policyDecision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
decision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
}
} else {
policyDecision.Action = ActionAdmit
policyDecision.Evaluation = EvalAdmit
decision.Action = ActionAdmit
decision.Evaluation = EvalAdmit
}
}
return decisions, nil
}
func createAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionRequest {
// FIXME: how to get resource GVK, GVR and subresource?
gvk := attr.GetKind()
gvr := attr.GetResource()
subresource := attr.GetSubresource()
requestGVK := attr.GetKind()
requestGVR := attr.GetResource()
requestSubResource := attr.GetSubresource()
aUserInfo := attr.GetUserInfo()
var userInfo authenticationv1.UserInfo
if aUserInfo != nil {
userInfo = authenticationv1.UserInfo{
Extra: make(map[string]authenticationv1.ExtraValue),
Groups: aUserInfo.GetGroups(),
UID: aUserInfo.GetUID(),
Username: aUserInfo.GetName(),
}
// Convert the extra information in the user object
for key, val := range aUserInfo.GetExtra() {
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
}
}
dryRun := attr.IsDryRun()
return &admissionv1.AdmissionRequest{
Kind: metav1.GroupVersionKind{
Group: gvk.Group,
Kind: gvk.Kind,
Version: gvk.Version,
},
Resource: metav1.GroupVersionResource{
Group: gvr.Group,
Resource: gvr.Resource,
Version: gvr.Version,
},
SubResource: subresource,
RequestKind: &metav1.GroupVersionKind{
Group: requestGVK.Group,
Kind: requestGVK.Kind,
Version: requestGVK.Version,
},
RequestResource: &metav1.GroupVersionResource{
Group: requestGVR.Group,
Resource: requestGVR.Resource,
Version: requestGVR.Version,
},
RequestSubResource: requestSubResource,
Name: attr.GetName(),
Namespace: attr.GetNamespace(),
Operation: admissionv1.Operation(attr.GetOperation()),
UserInfo: userInfo,
// Leave Object and OldObject unset since we don't provide access to them via request
DryRun: &dryRun,
Options: runtime.RawExtension{
Object: attr.GetOperationOptions(),
},
}
return decisions
}

View File

@ -17,551 +17,459 @@ limitations under the License.
package validatingadmissionpolicy
import (
"errors"
"strings"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/stretchr/testify/require"
celtypes "github.com/google/cel-go/common/types"
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
)
func TestCompile(t *testing.T) {
cases := []struct {
name string
policy *v1alpha1.ValidatingAdmissionPolicy
errorExpressions map[string]string
}{
{
name: "invalid syntax",
policy: &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}(),
ParamKind: &v1alpha1.ParamKind{
APIVersion: "rules.example.com/v1",
Kind: "ReplicaLimit",
},
Validations: []v1alpha1.Validation{
{
Expression: "1 < 'asdf'",
},
{
Expression: "1 < 2",
},
},
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
},
errorExpressions: map[string]string{
"1 < 'asdf'": "found no matching overload for '_<_' applied to '(int, string)",
},
},
{
name: "valid syntax",
policy: &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}(),
Validations: []v1alpha1.Validation{
{
Expression: "1 < 2",
},
{
Expression: "object.spec.string.matches('[0-9]+')",
},
{
Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'",
},
},
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
},
},
}
var _ cel.Filter = &fakeCelFilter{}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var c CELValidatorCompiler
validator := c.Compile(tc.policy)
if validator == nil {
t.Fatalf("unexpected nil validator")
}
validations := tc.policy.Spec.Validations
CompilationResults := validator.(*CELValidator).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
meets := make([]bool, len(validations))
for expr, expectErr := range tc.errorExpressions {
for i, result := range CompilationResults {
if validations[i].Expression == expr {
if result.Error == nil {
t.Errorf("Expect expression '%s' to contain error '%v' but got no error", expr, expectErr)
} else if !strings.Contains(result.Error.Error(), expectErr) {
t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
}
meets[i] = true
}
}
}
for i, meet := range meets {
if !meet && CompilationResults[i].Error != nil {
t.Errorf("Unexpected err '%v' for expression '%s'", CompilationResults[i].Error, validations[i].Expression)
}
}
})
}
type fakeCelFilter struct {
evaluations []cel.EvaluationResult
throwError bool
}
func getValidPolicy(validations []v1alpha1.Validation, params *v1alpha1.ParamKind, fp *v1alpha1.FailurePolicyType) *v1alpha1.ValidatingAdmissionPolicy {
if fp == nil {
fp = func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Fail")
return &r
}()
}
return &v1alpha1.ValidatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: v1alpha1.ValidatingAdmissionPolicySpec{
FailurePolicy: fp,
Validations: validations,
ParamKind: params,
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: func() *v1alpha1.MatchPolicyType {
r := v1alpha1.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []v1alpha1.NamedRuleWithOperations{
{
RuleWithOperations: v1alpha1.RuleWithOperations{
Operations: []v1.OperationType{"CREATE"},
Rule: v1.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
func (f *fakeCelFilter) ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]cel.EvaluationResult, error) {
if f.throwError {
return nil, errors.New("test error")
}
return f.evaluations, nil
}
func generatedDecision(k PolicyDecisionAction, m string, r metav1.StatusReason) PolicyDecision {
return PolicyDecision{Action: k, Message: m, Reason: r}
func (f *fakeCelFilter) CompilationErrors() []error {
return []error{}
}
func TestValidate(t *testing.T) {
// we fake the paramKind in ValidatingAdmissionPolicy for testing since the params is directly passed from cel admission
// Inside validator.go, we only check if paramKind exists
hasParamKind := &v1alpha1.ParamKind{
APIVersion: "v1",
Kind: "ConfigMap",
}
ignorePolicy := func() *v1alpha1.FailurePolicyType {
r := v1alpha1.FailurePolicyType("Ignore")
return &r
}()
forbiddenReason := func() *metav1.StatusReason {
r := metav1.StatusReasonForbidden
return &r
}()
ignore := v1.Ignore
fail := v1.Fail
configMapParams := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Data: map[string]string{
"fakeString": "fake",
},
}
crdParams := &unstructured.Unstructured{
Object: map[string]interface{}{
"spec": map[string]interface{}{
"testSize": 10,
},
},
}
podObject := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: corev1.PodSpec{
NodeName: "testnode",
},
}
forbiddenReason := metav1.StatusReasonForbidden
unauthorizedReason := metav1.StatusReasonUnauthorized
var nilUnstructured *unstructured.Unstructured
fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
fakeVersionedAttr, _ := generic.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
cases := []struct {
name string
policy *v1alpha1.ValidatingAdmissionPolicy
attributes admission.Attributes
params runtime.Object
policyDecisions []PolicyDecision
name string
failPolicy *v1.FailurePolicyType
evaluations []cel.EvaluationResult
policyDecision []PolicyDecision
throwError bool
}{
{
name: "valid syntax for object",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test pass",
evaluations: []cel.EvaluationResult{
{
Expression: "has(object.subsets) && object.subsets.size() < 2",
EvalResult: celtypes.True,
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionAdmit,
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []PolicyDecision{
generatedDecision(ActionAdmit, "", ""),
},
},
{
name: "valid syntax for metadata",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test multiple pass",
evaluations: []cel.EvaluationResult{
{
Expression: "object.metadata.name == 'endpoints1'",
EvalResult: celtypes.True,
ExpressionAccessor: &ValidationCondition{},
},
{
EvalResult: celtypes.True,
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionAdmit,
},
{
Action: ActionAdmit,
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []PolicyDecision{
generatedDecision(ActionAdmit, "", ""),
},
},
{
name: "valid syntax for oldObject",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test error with failurepolicy ignore",
evaluations: []cel.EvaluationResult{
{
Expression: "oldObject == null",
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Expression: "object != null",
Action: ActionAdmit,
},
},
failPolicy: &ignore,
},
{
name: "test error with failurepolicy nil",
evaluations: []cel.EvaluationResult{
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []PolicyDecision{
generatedDecision(ActionAdmit, "", ""),
generatedDecision(ActionAdmit, "", ""),
},
},
{
name: "valid syntax for request",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "request.operation == 'CREATE'"},
}, nil, nil),
attributes: newValidAttribute(nil, false),
policyDecisions: []PolicyDecision{
generatedDecision(ActionAdmit, "", ""),
name: "test fail with failurepolicy fail",
evaluations: []cel.EvaluationResult{
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
},
},
failPolicy: &fail,
},
{
name: "test fail with failurepolicy ignore with multiple validations",
evaluations: []cel.EvaluationResult{
{
EvalResult: celtypes.True,
ExpressionAccessor: &ValidationCondition{},
},
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionAdmit,
},
{
Action: ActionAdmit,
},
},
failPolicy: &ignore,
},
{
name: "test fail with failurepolicy nil with multiple validations",
evaluations: []cel.EvaluationResult{
{
EvalResult: celtypes.True,
ExpressionAccessor: &ValidationCondition{},
},
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionAdmit,
},
{
Action: ActionDeny,
},
},
},
{
name: "valid syntax for configMap",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "request.namespace != params.data.fakeString"},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionAdmit, "", ""),
name: "test fail with failurepolicy fail with multiple validations",
evaluations: []cel.EvaluationResult{
{
EvalResult: celtypes.True,
ExpressionAccessor: &ValidationCondition{},
},
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionAdmit,
},
{
Action: ActionDeny,
},
},
failPolicy: &fail,
},
{
name: "test fail with failurepolicy ignore with multiple failed validations",
evaluations: []cel.EvaluationResult{
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionAdmit,
},
{
Action: ActionAdmit,
},
},
failPolicy: &ignore,
},
{
name: "test fail with failurepolicy nil with multiple failed validations",
evaluations: []cel.EvaluationResult{
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
},
{
Action: ActionDeny,
},
},
},
{
name: "test failure policy with Ignore",
policy: getValidPolicy([]v1alpha1.Validation{
{Expression: "object.subsets.size() > 2"},
}, hasParamKind, ignorePolicy),
attributes: newValidAttribute(nil, false),
params: &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
name: "test fail with failurepolicy fail with multiple failed validations",
evaluations: []cel.EvaluationResult{
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
Data: map[string]string{
"fakeString": "fake",
{
Error: errors.New(""),
ExpressionAccessor: &ValidationCondition{},
},
},
policyDecisions: []PolicyDecision{
generatedDecision(ActionDeny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
},
{
Action: ActionDeny,
},
},
failPolicy: &fail,
},
{
name: "test failure policy with multiple validations",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test reason for fail no reason set",
evaluations: []cel.EvaluationResult{
{
Expression: "has(object.subsets)",
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Expression: "this.expression == unit.test",
},
},
{
Expression: "object.subsets.size() > 2",
},
}, hasParamKind, ignorePolicy),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionAdmit, "", ""),
generatedDecision(ActionDeny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
Reason: metav1.StatusReasonInvalid,
Message: "failed expression: this.expression == unit.test",
},
},
failPolicy: &fail,
},
{
name: "test failure policy with multiple failed validations",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test reason for fail reason set",
evaluations: []cel.EvaluationResult{
{
Expression: "oldObject != null",
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
},
},
{
Expression: "object.subsets.size() > 2",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: configMapParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionDeny, "failed expression: oldObject != null", metav1.StatusReasonInvalid),
generatedDecision(ActionDeny, "failed expression: object.subsets.size() > 2", metav1.StatusReasonInvalid),
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
Reason: metav1.StatusReasonForbidden,
Message: "failed expression: this.expression == unit.test",
},
},
failPolicy: &fail,
},
{
name: "test Object nul in delete",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test reason for failed validations multiple validations",
evaluations: []cel.EvaluationResult{
{
Expression: "oldObject != null",
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
},
},
{
Expression: "object == null",
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &unauthorizedReason,
Expression: "this.expression.2 == unit.test.2",
},
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionAdmit, "", ""),
generatedDecision(ActionAdmit, "", ""),
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
Reason: metav1.StatusReasonForbidden,
Message: "failed expression: this.expression == unit.test",
},
{
Action: ActionDeny,
Reason: metav1.StatusReasonUnauthorized,
Message: "failed expression: this.expression.2 == unit.test.2",
},
},
failPolicy: &fail,
},
{
name: "test reason for failed validation",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test message for failed validations",
evaluations: []cel.EvaluationResult{
{
Expression: "oldObject == null",
Reason: forbiddenReason,
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
Message: "test",
},
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionDeny, "failed expression: oldObject == null", metav1.StatusReasonForbidden),
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
Reason: metav1.StatusReasonForbidden,
Message: "test",
},
},
failPolicy: &fail,
},
{
name: "test message for failed validation",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test message for failed validations multiple validations",
evaluations: []cel.EvaluationResult{
{
Expression: "oldObject == null",
Reason: forbiddenReason,
Message: "old object should be present",
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
Message: "test1",
},
},
{
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
Message: "test2",
},
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionDeny, "old object should be present", metav1.StatusReasonForbidden),
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
Reason: metav1.StatusReasonForbidden,
Message: "test1",
},
{
Action: ActionDeny,
Reason: metav1.StatusReasonForbidden,
Message: "test2",
},
},
failPolicy: &fail,
},
{
name: "test runtime error",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test filter error",
evaluations: []cel.EvaluationResult{
{
Expression: "oldObject.x == 100",
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
Message: "test1",
},
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, true),
params: configMapParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionDeny, "resulted in error", ""),
},
policyDecision: []PolicyDecision{
{
Action: ActionDeny,
Message: "test error",
},
},
failPolicy: &fail,
throwError: true,
},
{
name: "test against crd param",
policy: getValidPolicy([]v1alpha1.Validation{
name: "test filter error multiple evaluations",
evaluations: []cel.EvaluationResult{
{
Expression: "object.subsets.size() < params.spec.testSize",
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
Message: "test1",
},
},
{
EvalResult: celtypes.False,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
Message: "test2",
},
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: crdParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionAdmit, "", ""),
},
},
{
name: "test compile failure with FailurePolicy Fail",
policy: getValidPolicy([]v1alpha1.Validation{
policyDecision: []PolicyDecision{
{
Expression: "fail to compile test",
Action: ActionDeny,
Message: "test error",
},
{
Expression: "object.subsets.size() > params.spec.testSize",
},
}, hasParamKind, nil),
attributes: newValidAttribute(nil, false),
params: crdParams,
policyDecisions: []PolicyDecision{
generatedDecision(ActionDeny, "compilation error: compilation failed: ERROR: <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 {
t.Run(tc.name, func(t *testing.T) {
c := CELValidatorCompiler{}
validator := c.Compile(tc.policy)
if validator == nil {
t.Fatalf("unexpected nil validator")
v := validator{
failPolicy: tc.failPolicy,
filter: &fakeCelFilter{
evaluations: tc.evaluations,
throwError: tc.throwError,
},
}
validations := tc.policy.Spec.Validations
CompilationResults := validator.(*CELValidator).compilationResults
require.Equal(t, len(validations), len(CompilationResults))
policyResults := v.Validate(fakeVersionedAttr, nil)
versionedAttr, err := generic.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
if err != nil {
t.Fatalf("unexpected error on conversion: %v", err)
}
require.Equal(t, len(policyResults), len(tc.policyDecision))
policyResults, err := validator.Validate(versionedAttr, tc.params)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
require.Equal(t, len(policyResults), len(tc.policyDecisions))
for i, policyDecision := range tc.policyDecisions {
for i, policyDecision := range tc.policyDecision {
if policyDecision.Action != policyResults[i].Action {
t.Errorf("Expected policy decision kind '%v' but got '%v'", policyDecision.Action, policyResults[i].Action)
}
@ -575,39 +483,3 @@ func TestValidate(t *testing.T) {
})
}
}
// newObjectInterfacesForTest returns an ObjectInterfaces appropriate for test cases in this file.
func newObjectInterfacesForTest() admission.ObjectInterfaces {
scheme := runtime.NewScheme()
corev1.AddToScheme(scheme)
return admission.NewObjectInterfacesFromScheme(scheme)
}
func newValidAttribute(object runtime.Object, isDelete bool) admission.Attributes {
var oldObject runtime.Object
if !isDelete {
if object == nil {
object = &corev1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: "endpoints1",
},
Subsets: []corev1.EndpointSubset{
{
Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}},
},
},
}
}
} else {
object = nil
oldObject = &corev1.Endpoints{
Subsets: []corev1.EndpointSubset{
{
Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}},
},
},
}
}
return admission.NewAttributesRecord(object, oldObject, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, &metav1.CreateOptions{}, false, nil)
}

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/initializer
k8s.io/apiserver/pkg/admission/metrics
k8s.io/apiserver/pkg/admission/plugin/cel
k8s.io/apiserver/pkg/admission/plugin/namespace/lifecycle
k8s.io/apiserver/pkg/admission/plugin/resourcequota
k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota