mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 10:51:29 +00:00
refactor admission cel validator and compiler to be reusable
This commit is contained in:
parent
44bedc2a46
commit
e96ef31187
@ -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:
|
||||
|
10
staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/OWNERS
Normal file
10
staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/OWNERS
Normal file
@ -0,0 +1,10 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- jpbetz
|
||||
- cici37
|
||||
- alexzielenski
|
||||
reviewers:
|
||||
- jpbetz
|
||||
- cici37
|
||||
- alexzielenski
|
@ -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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
244
staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go
Normal file
244
staging/src/k8s.io/apiserver/pkg/admission/plugin/cel/filter.go
Normal 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
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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{
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
1
vendor/modules.txt
vendored
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user