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