mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-20 18:31:15 +00:00
implement message expression.
This commit is contained in:
parent
f4ee476a3c
commit
d8be7aa9ca
@ -782,26 +782,24 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
|
|||||||
var allErrors field.ErrorList
|
var allErrors field.ErrorList
|
||||||
trimmedExpression := strings.TrimSpace(v.Expression)
|
trimmedExpression := strings.TrimSpace(v.Expression)
|
||||||
trimmedMsg := strings.TrimSpace(v.Message)
|
trimmedMsg := strings.TrimSpace(v.Message)
|
||||||
|
trimmedMessageExpression := strings.TrimSpace(v.MessageExpression)
|
||||||
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.CompileCELExpression(&validatingadmissionpolicy.ValidationCondition{
|
allErrors = append(allErrors, validateCELExpression(v.Expression, plugincel.OptionalVariableDeclarations{
|
||||||
Expression: trimmedExpression,
|
HasParams: paramKind != nil,
|
||||||
Message: v.Message,
|
HasAuthorizer: true,
|
||||||
Reason: v.Reason,
|
}, fldPath.Child("expression"))...)
|
||||||
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}, celconfig.PerCallLimit)
|
}
|
||||||
if result.Error != nil {
|
if len(v.MessageExpression) > 0 && len(trimmedMessageExpression) == 0 {
|
||||||
switch result.Error.Type {
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("messageExpression"), v.MessageExpression, "must be non-empty if specified"))
|
||||||
case cel.ErrorTypeRequired:
|
} else if len(trimmedMessageExpression) != 0 {
|
||||||
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), result.Error.Detail))
|
// use v.MessageExpression instead of trimmedMessageExpression so that
|
||||||
case cel.ErrorTypeInvalid:
|
// the compiler output shows the correct column.
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("expression"), v.Expression, result.Error.Detail))
|
allErrors = append(allErrors, validateCELExpression(v.MessageExpression, plugincel.OptionalVariableDeclarations{
|
||||||
case cel.ErrorTypeInternal:
|
HasParams: paramKind != nil,
|
||||||
allErrors = append(allErrors, field.InternalError(fldPath.Child("expression"), result.Error))
|
HasAuthorizer: false,
|
||||||
default:
|
}, fldPath.Child("messageExpression"))...)
|
||||||
allErrors = append(allErrors, field.InternalError(fldPath.Child("expression"), fmt.Errorf("unsupported error type: %w", result.Error)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(v.Message) > 0 && len(trimmedMsg) == 0 {
|
if len(v.Message) > 0 && len(trimmedMsg) == 0 {
|
||||||
allErrors = append(allErrors, field.Invalid(fldPath.Child("message"), v.Message, "message must be non-empty if specified"))
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("message"), v.Message, "message must be non-empty if specified"))
|
||||||
@ -816,6 +814,26 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
|
|||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateCELExpression(expression string, variables plugincel.OptionalVariableDeclarations, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrors field.ErrorList
|
||||||
|
result := plugincel.CompileCELExpression(&validatingadmissionpolicy.ValidationCondition{
|
||||||
|
Expression: expression,
|
||||||
|
}, variables, celconfig.PerCallLimit)
|
||||||
|
if result.Error != nil {
|
||||||
|
switch result.Error.Type {
|
||||||
|
case cel.ErrorTypeRequired:
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath, result.Error.Detail))
|
||||||
|
case cel.ErrorTypeInvalid:
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, expression, result.Error.Detail))
|
||||||
|
case cel.ErrorTypeInternal:
|
||||||
|
allErrors = append(allErrors, field.InternalError(fldPath, result.Error))
|
||||||
|
default:
|
||||||
|
allErrors = append(allErrors, field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", result.Error)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
|
||||||
func validateAuditAnnotation(meta metav1.ObjectMeta, v *admissionregistration.AuditAnnotation, paramKind *admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList {
|
func validateAuditAnnotation(meta metav1.ObjectMeta, v *admissionregistration.AuditAnnotation, paramKind *admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList {
|
||||||
var allErrors field.ErrorList
|
var allErrors field.ErrorList
|
||||||
if len(meta.GetName()) != 0 {
|
if len(meta.GetName()) != 0 {
|
||||||
|
@ -2564,7 +2564,38 @@ func TestValidateValidatingAdmissionPolicy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
expectedError: `spec.validations[0].expression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:19: Syntax error: missing ']' at '<EOF>`,
|
expectedError: `spec.validations[0].expression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:20: Syntax error: missing ']' at '<EOF>`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid messageExpression",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "config",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
Validations: []admissionregistration.Validation{
|
||||||
|
{
|
||||||
|
Expression: "true",
|
||||||
|
MessageExpression: "object.x in [1, 2, ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MatchConstraints: &admissionregistration.MatchResources{
|
||||||
|
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
||||||
|
{
|
||||||
|
RuleWithOperations: admissionregistration.RuleWithOperations{
|
||||||
|
Operations: []admissionregistration.OperationType{"CREATE"},
|
||||||
|
Rule: admissionregistration.Rule{
|
||||||
|
APIGroups: []string{"a"},
|
||||||
|
APIVersions: []string{"a"},
|
||||||
|
Resources: []string{"*/*"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `spec.validations[0].messageExpression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:20: Syntax error: missing ']' at '<EOF>`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "invalid auditAnnotations key due to key name",
|
name: "invalid auditAnnotations key due to key name",
|
||||||
|
@ -18,7 +18,6 @@ package cel
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -32,6 +31,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
|
"k8s.io/apiserver/pkg/cel"
|
||||||
"k8s.io/apiserver/pkg/cel/library"
|
"k8s.io/apiserver/pkg/cel/library"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -78,6 +78,9 @@ func (a *evaluationActivation) Parent() interpreter.Activation {
|
|||||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, perCallLimit uint64) Filter {
|
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, perCallLimit uint64) Filter {
|
||||||
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
||||||
for i, expressionAccessor := range expressionAccessors {
|
for i, expressionAccessor := range expressionAccessors {
|
||||||
|
if expressionAccessor == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)
|
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)
|
||||||
}
|
}
|
||||||
return NewFilter(compilationResults)
|
return NewFilter(compilationResults)
|
||||||
@ -119,24 +122,24 @@ func objectToResolveVal(r runtime.Object) (interface{}, error) {
|
|||||||
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
|
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
|
||||||
// errors per evaluation are returned on the Evaluation object
|
// errors per evaluation are returned on the Evaluation object
|
||||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||||
func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, error) {
|
func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
|
||||||
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
|
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
|
||||||
evaluations := make([]EvaluationResult, len(f.compilationResults))
|
evaluations := make([]EvaluationResult, len(f.compilationResults))
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
|
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, -1, err
|
||||||
}
|
}
|
||||||
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
|
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, -1, err
|
||||||
}
|
}
|
||||||
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
|
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
|
||||||
if inputs.VersionedParams != nil {
|
if inputs.VersionedParams != nil {
|
||||||
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
|
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, -1, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,7 +150,7 @@ func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.Versione
|
|||||||
|
|
||||||
requestVal, err := convertObjectToUnstructured(request)
|
requestVal, err := convertObjectToUnstructured(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, -1, err
|
||||||
}
|
}
|
||||||
va := &evaluationActivation{
|
va := &evaluationActivation{
|
||||||
object: objectVal,
|
object: objectVal,
|
||||||
@ -161,13 +164,22 @@ func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.Versione
|
|||||||
remainingBudget := runtimeCELCostBudget
|
remainingBudget := runtimeCELCostBudget
|
||||||
for i, compilationResult := range f.compilationResults {
|
for i, compilationResult := range f.compilationResults {
|
||||||
var evaluation = &evaluations[i]
|
var evaluation = &evaluations[i]
|
||||||
|
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
|
||||||
|
continue
|
||||||
|
}
|
||||||
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
|
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
|
||||||
if compilationResult.Error != nil {
|
if compilationResult.Error != nil {
|
||||||
evaluation.Error = errors.New(fmt.Sprintf("compilation error: %v", compilationResult.Error))
|
evaluation.Error = &cel.Error{
|
||||||
|
Type: cel.ErrorTypeInvalid,
|
||||||
|
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if compilationResult.Program == nil {
|
if compilationResult.Program == nil {
|
||||||
evaluation.Error = errors.New("unexpected internal error compiling expression")
|
evaluation.Error = &cel.Error{
|
||||||
|
Type: cel.ErrorTypeInternal,
|
||||||
|
Detail: fmt.Sprintf("unexpected internal error compiling expression"),
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
t1 := time.Now()
|
t1 := time.Now()
|
||||||
@ -175,26 +187,38 @@ func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.Versione
|
|||||||
elapsed := time.Since(t1)
|
elapsed := time.Since(t1)
|
||||||
evaluation.Elapsed = elapsed
|
evaluation.Elapsed = elapsed
|
||||||
if evalDetails == nil {
|
if evalDetails == nil {
|
||||||
return nil, errors.New(fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()))
|
return nil, -1, &cel.Error{
|
||||||
|
Type: cel.ErrorTypeInternal,
|
||||||
|
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
rtCost := evalDetails.ActualCost()
|
rtCost := evalDetails.ActualCost()
|
||||||
if rtCost == nil {
|
if rtCost == nil {
|
||||||
return nil, errors.New(fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()))
|
return nil, -1, &cel.Error{
|
||||||
|
Type: cel.ErrorTypeInvalid,
|
||||||
|
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
|
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
|
||||||
return nil, errors.New(fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"))
|
return nil, -1, &cel.Error{
|
||||||
|
Type: cel.ErrorTypeInvalid,
|
||||||
|
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
remainingBudget -= int64(*rtCost)
|
remainingBudget -= int64(*rtCost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
evaluation.Error = errors.New(fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err))
|
evaluation.Error = &cel.Error{
|
||||||
|
Type: cel.ErrorTypeInvalid,
|
||||||
|
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
evaluation.EvalResult = evalResult
|
evaluation.EvalResult = evalResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return evaluations, nil
|
return evaluations, remainingBudget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
|
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
|
||||||
|
@ -38,6 +38,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type condition struct {
|
type condition struct {
|
||||||
@ -661,7 +662,7 @@ func TestFilter(t *testing.T) {
|
|||||||
|
|
||||||
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
|
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
evalResults, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, celconfig.RuntimeCELCostBudget)
|
evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, celconfig.RuntimeCELCostBudget)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@ -697,6 +698,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||||||
authorizer authorizer.Authorizer
|
authorizer authorizer.Authorizer
|
||||||
testRuntimeCELCostBudget int64
|
testRuntimeCELCostBudget int64
|
||||||
exceedBudget bool
|
exceedBudget bool
|
||||||
|
expectRemainingBudget *int64
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "expression exceed RuntimeCELCostBudget at fist expression",
|
name: "expression exceed RuntimeCELCostBudget at fist expression",
|
||||||
@ -739,10 +741,49 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||||||
Expression: "object.subsets.size() > 2",
|
Expression: "object.subsets.size() > 2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
attributes: newValidAttribute(nil, false),
|
attributes: newValidAttribute(nil, false),
|
||||||
hasParamKind: true,
|
hasParamKind: true,
|
||||||
params: configMapParams,
|
params: configMapParams,
|
||||||
exceedBudget: false,
|
exceedBudget: false,
|
||||||
|
testRuntimeCELCostBudget: 10,
|
||||||
|
expectRemainingBudget: pointer.Int64(4), // 10 - 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test RuntimeCELCostBudge exactly covers",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "oldObject != null",
|
||||||
|
},
|
||||||
|
&condition{
|
||||||
|
Expression: "object.subsets.size() > 2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(nil, false),
|
||||||
|
hasParamKind: true,
|
||||||
|
params: configMapParams,
|
||||||
|
exceedBudget: false,
|
||||||
|
testRuntimeCELCostBudget: 6,
|
||||||
|
expectRemainingBudget: pointer.Int64(0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test RuntimeCELCostBudge exactly covers then constant",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "oldObject != null",
|
||||||
|
},
|
||||||
|
&condition{
|
||||||
|
Expression: "object.subsets.size() > 2",
|
||||||
|
},
|
||||||
|
&condition{
|
||||||
|
Expression: "true", // zero cost
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(nil, false),
|
||||||
|
hasParamKind: true,
|
||||||
|
params: configMapParams,
|
||||||
|
exceedBudget: false,
|
||||||
|
testRuntimeCELCostBudget: 6,
|
||||||
|
expectRemainingBudget: pointer.Int64(0),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -767,19 +808,25 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||||||
}
|
}
|
||||||
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
|
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
evalResults, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, tc.testRuntimeCELCostBudget)
|
evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, tc.testRuntimeCELCostBudget)
|
||||||
if tc.exceedBudget && err == nil {
|
if tc.exceedBudget && err == nil {
|
||||||
t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil")
|
t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil")
|
||||||
}
|
}
|
||||||
if tc.exceedBudget && !strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") {
|
if tc.exceedBudget && !strings.Contains(err.Error(), "validation failed due to running out of cost budget, no further validation rules will be run") {
|
||||||
t.Errorf("Expected RuntimeCELCostBudge exceeded error but got: %v", err)
|
t.Errorf("Expected RuntimeCELCostBudge exceeded error but got: %v", err)
|
||||||
}
|
}
|
||||||
|
if err != nil && remaining != -1 {
|
||||||
|
t.Errorf("expected -1 remaining when error, but got %d", remaining)
|
||||||
|
}
|
||||||
if err != nil && !tc.exceedBudget {
|
if err != nil && !tc.exceedBudget {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
if tc.exceedBudget && len(evalResults) != 0 {
|
if tc.exceedBudget && len(evalResults) != 0 {
|
||||||
t.Fatalf("unexpected result returned: %v", evalResults)
|
t.Fatalf("unexpected result returned: %v", evalResults)
|
||||||
}
|
}
|
||||||
|
if tc.expectRemainingBudget != nil && *tc.expectRemainingBudget != remaining {
|
||||||
|
t.Errorf("wrong remaining budget, expect %d, but got %d", *tc.expectRemainingBudget, remaining)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,9 +92,10 @@ type OptionalVariableBindings struct {
|
|||||||
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
|
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
|
||||||
// versionedParams may be nil.
|
// versionedParams may be nil.
|
||||||
type Filter interface {
|
type Filter interface {
|
||||||
// ForInput converts compiled CEL-typed values into evaluated CEL-typed values
|
// ForInput converts compiled CEL-typed values into evaluated CEL-typed value.
|
||||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||||
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, error)
|
// If cost budget is calculated, the filter should return the remaining budget.
|
||||||
|
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error)
|
||||||
|
|
||||||
// CompilationErrors returns a list of errors from the compilation of the evaluator
|
// CompilationErrors returns a list of errors from the compilation of the evaluator
|
||||||
CompilationErrors() []error
|
CompilationErrors() []error
|
||||||
|
@ -212,7 +212,7 @@ func (f *fakeCompiler) Compile(
|
|||||||
options cel.OptionalVariableDeclarations,
|
options cel.OptionalVariableDeclarations,
|
||||||
perCallLimit uint64,
|
perCallLimit uint64,
|
||||||
) cel.Filter {
|
) cel.Filter {
|
||||||
if len(expressions) > 0 {
|
if len(expressions) > 0 && expressions[0] != nil {
|
||||||
key := expressions[0].GetExpression()
|
key := expressions[0].GetExpression()
|
||||||
if fun, ok := f.CompileFuncs[key]; ok {
|
if fun, ok := f.CompileFuncs[key]; ok {
|
||||||
return fun(expressions, options)
|
return fun(expressions, options)
|
||||||
@ -252,8 +252,8 @@ type fakeFilter struct {
|
|||||||
keyId string
|
keyId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, runtimeCELCostBudget int64) ([]cel.EvaluationResult, error) {
|
func (f *fakeFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, runtimeCELCostBudget int64) ([]cel.EvaluationResult, int64, error) {
|
||||||
return []cel.EvaluationResult{}, nil
|
return []cel.EvaluationResult{}, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeFilter) CompilationErrors() []error {
|
func (f *fakeFilter) CompilationErrors() []error {
|
||||||
@ -263,8 +263,8 @@ func (f *fakeFilter) CompilationErrors() []error {
|
|||||||
var _ Validator = &fakeValidator{}
|
var _ Validator = &fakeValidator{}
|
||||||
|
|
||||||
type fakeValidator struct {
|
type fakeValidator struct {
|
||||||
validationFilter, auditAnnotationFilter *fakeFilter
|
validationFilter, auditAnnotationFilter, messageFilter *fakeFilter
|
||||||
ValidateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult
|
ValidateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult) {
|
func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult) {
|
||||||
@ -418,10 +418,11 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
|||||||
// Override compiler used by controller for tests
|
// Override compiler used by controller for tests
|
||||||
controller = handler.evaluator.(*celAdmissionController)
|
controller = handler.evaluator.(*celAdmissionController)
|
||||||
controller.policyController.filterCompiler = compiler
|
controller.policyController.filterCompiler = compiler
|
||||||
controller.policyController.newValidator = func(validationFilter, auditAnnotationFilter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
controller.policyController.newValidator = func(validationFilter, auditAnnotationFilter, messageFilter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
||||||
f := validationFilter.(*fakeFilter)
|
f := validationFilter.(*fakeFilter)
|
||||||
v := validatorMap[f.keyId]
|
v := validatorMap[f.keyId]
|
||||||
v.validationFilter = f
|
v.validationFilter = f
|
||||||
|
v.messageFilter = f
|
||||||
v.auditAnnotationFilter = auditAnnotationFilter.(*fakeFilter)
|
v.auditAnnotationFilter = auditAnnotationFilter.(*fakeFilter)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ type policyController struct {
|
|||||||
authz authorizer.Authorizer
|
authz authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
type newValidator func(validationFilter cel.Filter, auditAnnotationFilter cel.Filter, failurePolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator
|
type newValidator func(validationFilter cel.Filter, auditAnnotationFilter cel.Filter, messageFilter cel.Filter, failurePolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator
|
||||||
|
|
||||||
func newPolicyController(
|
func newPolicyController(
|
||||||
restMapper meta.RESTMapper,
|
restMapper meta.RESTMapper,
|
||||||
@ -459,9 +459,11 @@ func (c *policyController) latestPolicyData() []policyData {
|
|||||||
hasParam = true
|
hasParam = true
|
||||||
}
|
}
|
||||||
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
||||||
|
expressionOptionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: false}
|
||||||
bindingInfo.validator = c.newValidator(
|
bindingInfo.validator = c.newValidator(
|
||||||
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
|
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
|
||||||
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, celconfig.PerCallLimit),
|
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, celconfig.PerCallLimit),
|
||||||
|
c.filterCompiler.Compile(convertV1Alpha1MessageExpressions(definitionInfo.lastReconciledValue.Spec.Validations), expressionOptionalVars, celconfig.PerCallLimit),
|
||||||
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
|
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
|
||||||
c.authz,
|
c.authz,
|
||||||
)
|
)
|
||||||
@ -514,6 +516,19 @@ func convertv1alpha1Validations(inputValidations []v1alpha1.Validation) []cel.Ex
|
|||||||
return celExpressionAccessor
|
return celExpressionAccessor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func convertV1Alpha1MessageExpressions(inputValidations []v1alpha1.Validation) []cel.ExpressionAccessor {
|
||||||
|
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
||||||
|
for i, validation := range inputValidations {
|
||||||
|
if validation.MessageExpression != "" {
|
||||||
|
condition := MessageExpressionCondition{
|
||||||
|
MessageExpression: validation.MessageExpression,
|
||||||
|
}
|
||||||
|
celExpressionAccessor[i] = &condition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return celExpressionAccessor
|
||||||
|
}
|
||||||
|
|
||||||
func convertv1alpha1AuditAnnotations(inputValidations []v1alpha1.AuditAnnotation) []cel.ExpressionAccessor {
|
func convertv1alpha1AuditAnnotations(inputValidations []v1alpha1.AuditAnnotation) []cel.ExpressionAccessor {
|
||||||
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
|
||||||
for i, validation := range inputValidations {
|
for i, validation := range inputValidations {
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 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 (
|
||||||
|
celgo "github.com/google/cel-go/cel"
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cel.ExpressionAccessor = (*MessageExpressionCondition)(nil)
|
||||||
|
|
||||||
|
type MessageExpressionCondition struct {
|
||||||
|
MessageExpression string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessageExpressionCondition) GetExpression() string {
|
||||||
|
return m.MessageExpression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MessageExpressionCondition) ReturnTypes() []*celgo.Type {
|
||||||
|
return []*celgo.Type{celgo.StringType}
|
||||||
|
}
|
@ -30,21 +30,25 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
|
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validator implements the Validator interface
|
// validator implements the Validator interface
|
||||||
type validator struct {
|
type validator struct {
|
||||||
validationFilter cel.Filter
|
validationFilter cel.Filter
|
||||||
auditAnnotationFilter cel.Filter
|
auditAnnotationFilter cel.Filter
|
||||||
|
messageFilter cel.Filter
|
||||||
failPolicy *v1.FailurePolicyType
|
failPolicy *v1.FailurePolicyType
|
||||||
authorizer authorizer.Authorizer
|
authorizer authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValidator(validationFilter, auditAnnotationFilter cel.Filter, failPolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
func NewValidator(validationFilter, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
||||||
return &validator{
|
return &validator{
|
||||||
validationFilter: validationFilter,
|
validationFilter: validationFilter,
|
||||||
auditAnnotationFilter: auditAnnotationFilter,
|
auditAnnotationFilter: auditAnnotationFilter,
|
||||||
|
messageFilter: messageFilter,
|
||||||
failPolicy: failPolicy,
|
failPolicy: failPolicy,
|
||||||
authorizer: authorizer,
|
authorizer: authorizer,
|
||||||
}
|
}
|
||||||
@ -75,7 +79,9 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
|
|||||||
}
|
}
|
||||||
|
|
||||||
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
|
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
|
||||||
evalResults, err := v.validationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, runtimeCELCostBudget)
|
expressionOptionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams}
|
||||||
|
admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes)
|
||||||
|
evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, runtimeCELCostBudget)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ValidateResult{
|
return ValidateResult{
|
||||||
Decisions: []PolicyDecision{
|
Decisions: []PolicyDecision{
|
||||||
@ -88,7 +94,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
decisions := make([]PolicyDecision, len(evalResults))
|
decisions := make([]PolicyDecision, len(evalResults))
|
||||||
|
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, remainingBudget)
|
||||||
for i, evalResult := range evalResults {
|
for i, evalResult := range evalResults {
|
||||||
var decision = &decisions[i]
|
var decision = &decisions[i]
|
||||||
// TODO: move this to generics
|
// TODO: move this to generics
|
||||||
@ -101,10 +107,23 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var messageResult *cel.EvaluationResult
|
||||||
|
var messageError *apiservercel.Error
|
||||||
|
if len(messageResults) > i {
|
||||||
|
messageResult = &messageResults[i]
|
||||||
|
}
|
||||||
|
messageError, _ = err.(*apiservercel.Error)
|
||||||
if evalResult.Error != nil {
|
if evalResult.Error != nil {
|
||||||
decision.Action = policyDecisionActionForError(f)
|
decision.Action = policyDecisionActionForError(f)
|
||||||
decision.Evaluation = EvalError
|
decision.Evaluation = EvalError
|
||||||
decision.Message = evalResult.Error.Error()
|
decision.Message = evalResult.Error.Error()
|
||||||
|
} else if messageError != nil &&
|
||||||
|
(messageError.Type == apiservercel.ErrorTypeInternal ||
|
||||||
|
(messageError.Type == apiservercel.ErrorTypeInvalid &&
|
||||||
|
strings.HasPrefix(messageError.Detail, "validation failed due to running out of cost budget"))) {
|
||||||
|
decision.Action = policyDecisionActionForError(f)
|
||||||
|
decision.Evaluation = EvalError
|
||||||
|
decision.Message = fmt.Sprintf("failed messageExpression: %s", err)
|
||||||
} else if evalResult.EvalResult != celtypes.True {
|
} else if evalResult.EvalResult != celtypes.True {
|
||||||
decision.Action = ActionDeny
|
decision.Action = ActionDeny
|
||||||
if validation.Reason == nil {
|
if validation.Reason == nil {
|
||||||
@ -112,11 +131,39 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
|
|||||||
} else {
|
} else {
|
||||||
decision.Reason = *validation.Reason
|
decision.Reason = *validation.Reason
|
||||||
}
|
}
|
||||||
if len(validation.Message) > 0 {
|
// decide the failure message
|
||||||
decision.Message = strings.TrimSpace(validation.Message)
|
var message string
|
||||||
} else {
|
// attempt to set message with messageExpression result
|
||||||
decision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
|
if messageResult != nil && messageResult.Error == nil && messageResult.EvalResult != nil {
|
||||||
|
// also fallback if the eval result is non-string (including null) or
|
||||||
|
// whitespaces.
|
||||||
|
if message, ok = messageResult.EvalResult.Value().(string); ok {
|
||||||
|
message = strings.TrimSpace(message)
|
||||||
|
// deny excessively long message from EvalResult
|
||||||
|
if len(message) > celconfig.MaxEvaluatedMessageExpressionSizeBytes {
|
||||||
|
klog.V(2).InfoS("excessively long message denied", "message", message)
|
||||||
|
message = ""
|
||||||
|
}
|
||||||
|
// deny message that contains newlines
|
||||||
|
if strings.ContainsAny(message, "\n") {
|
||||||
|
klog.V(2).InfoS("multi-line message denied", "message", message)
|
||||||
|
message = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
if messageResult != nil && messageResult.Error != nil {
|
||||||
|
// log any error with messageExpression
|
||||||
|
klog.V(2).ErrorS(messageResult.Error, "error while evaluating messageExpression")
|
||||||
|
}
|
||||||
|
// fallback to set message to the custom message
|
||||||
|
if message == "" && len(validation.Message) > 0 {
|
||||||
|
message = strings.TrimSpace(validation.Message)
|
||||||
|
}
|
||||||
|
// fallback to use the expression to compose a message
|
||||||
|
if message == "" {
|
||||||
|
message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
|
||||||
|
}
|
||||||
|
decision.Message = message
|
||||||
} else {
|
} else {
|
||||||
decision.Action = ActionAdmit
|
decision.Action = ActionAdmit
|
||||||
decision.Evaluation = EvalAdmit
|
decision.Evaluation = EvalAdmit
|
||||||
@ -124,7 +171,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
|
|||||||
}
|
}
|
||||||
|
|
||||||
options := cel.OptionalVariableBindings{VersionedParams: versionedParams}
|
options := cel.OptionalVariableBindings{VersionedParams: versionedParams}
|
||||||
auditAnnotationEvalResults, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, runtimeCELCostBudget)
|
auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, runtimeCELCostBudget)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ValidateResult{
|
return ValidateResult{
|
||||||
Decisions: []PolicyDecision{
|
Decisions: []PolicyDecision{
|
||||||
@ -139,6 +186,9 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
|
|||||||
|
|
||||||
auditAnnotationResults := make([]PolicyAuditAnnotation, len(auditAnnotationEvalResults))
|
auditAnnotationResults := make([]PolicyAuditAnnotation, len(auditAnnotationEvalResults))
|
||||||
for i, evalResult := range auditAnnotationEvalResults {
|
for i, evalResult := range auditAnnotationEvalResults {
|
||||||
|
if evalResult.ExpressionAccessor == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
var auditAnnotationResult = &auditAnnotationResults[i]
|
var auditAnnotationResult = &auditAnnotationResults[i]
|
||||||
// TODO: move this to generics
|
// TODO: move this to generics
|
||||||
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
|
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
|
||||||
|
@ -34,6 +34,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||||
|
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ cel.Filter = &fakeCelFilter{}
|
var _ cel.Filter = &fakeCelFilter{}
|
||||||
@ -43,11 +44,17 @@ type fakeCelFilter struct {
|
|||||||
throwError bool
|
throwError bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeCelFilter) ForInput(context.Context, *admission.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, int64) ([]cel.EvaluationResult, error) {
|
func (f *fakeCelFilter) ForInput(_ context.Context, _ *admission.VersionedAttributes, _ *admissionv1.AdmissionRequest, _ cel.OptionalVariableBindings, costBudget int64) ([]cel.EvaluationResult, int64, error) {
|
||||||
if f.throwError {
|
if costBudget <= 0 { // this filter will cost 1, so cost = 0 means fail.
|
||||||
return nil, errors.New("test error")
|
return nil, -1, &apiservercel.Error{
|
||||||
|
Type: apiservercel.ErrorTypeInvalid,
|
||||||
|
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return f.evaluations, nil
|
if f.throwError {
|
||||||
|
return nil, -1, errors.New("test error")
|
||||||
|
}
|
||||||
|
return f.evaluations, costBudget - 1, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeCelFilter) CompilationErrors() []error {
|
func (f *fakeCelFilter) CompilationErrors() []error {
|
||||||
@ -65,13 +72,15 @@ func TestValidate(t *testing.T) {
|
|||||||
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
|
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
failPolicy *v1.FailurePolicyType
|
failPolicy *v1.FailurePolicyType
|
||||||
evaluations []cel.EvaluationResult
|
evaluations []cel.EvaluationResult
|
||||||
auditEvaluations []cel.EvaluationResult
|
messageEvaluations []cel.EvaluationResult
|
||||||
policyDecision []PolicyDecision
|
auditEvaluations []cel.EvaluationResult
|
||||||
auditAnnotations []PolicyAuditAnnotation
|
policyDecision []PolicyDecision
|
||||||
throwError bool
|
auditAnnotations []PolicyAuditAnnotation
|
||||||
|
throwError bool
|
||||||
|
costBudget int64 // leave zero to use default
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "test pass",
|
name: "test pass",
|
||||||
@ -572,6 +581,244 @@ func TestValidate(t *testing.T) {
|
|||||||
},
|
},
|
||||||
failPolicy: &ignore,
|
failPolicy: &ignore,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression successful, empty message",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.String("evaluated message"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "evaluated message",
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression for multiple validations",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "I am not overwritten",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "I am overwritten",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.String("evaluated message"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "I am not overwritten",
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "evaluated message",
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression successful, overwritten message",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "I am overwritten",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.String("evaluated message"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "evaluated message",
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression user failure",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
Error: &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "test1", // original message used
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression eval to empty",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.String(" "),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "test1",
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression eval to multi-line",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.String("hello\nthere"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "test1",
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression eval result too long",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.String(strings.Repeat("x", 5*1024+1)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "test1",
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression eval to null",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.NullValue,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "test1",
|
||||||
|
Reason: forbiddenReason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "messageExpression out of budget after successful eval of expression",
|
||||||
|
evaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
ExpressionAccessor: &ValidationCondition{
|
||||||
|
Reason: &forbiddenReason,
|
||||||
|
Expression: "this.expression == unit.test",
|
||||||
|
Message: "test1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
messageEvaluations: []cel.EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.StringType, // does not matter
|
||||||
|
},
|
||||||
|
},
|
||||||
|
policyDecision: []PolicyDecision{
|
||||||
|
{
|
||||||
|
Action: ActionDeny,
|
||||||
|
Message: "running out of cost budget",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
costBudget: 1, // shared between expression and messageExpression, needs 1 + 1 = 2 in total
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
@ -581,13 +828,21 @@ func TestValidate(t *testing.T) {
|
|||||||
evaluations: tc.evaluations,
|
evaluations: tc.evaluations,
|
||||||
throwError: tc.throwError,
|
throwError: tc.throwError,
|
||||||
},
|
},
|
||||||
|
messageFilter: &fakeCelFilter{
|
||||||
|
evaluations: tc.messageEvaluations,
|
||||||
|
throwError: tc.throwError,
|
||||||
|
},
|
||||||
auditAnnotationFilter: &fakeCelFilter{
|
auditAnnotationFilter: &fakeCelFilter{
|
||||||
evaluations: tc.auditEvaluations,
|
evaluations: tc.auditEvaluations,
|
||||||
throwError: tc.throwError,
|
throwError: tc.throwError,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
validateResult := v.Validate(ctx, fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget)
|
var budget int64 = celconfig.RuntimeCELCostBudget
|
||||||
|
if tc.costBudget != 0 {
|
||||||
|
budget = tc.costBudget
|
||||||
|
}
|
||||||
|
validateResult := v.Validate(ctx, fakeVersionedAttr, nil, budget)
|
||||||
|
|
||||||
require.Equal(t, len(validateResult.Decisions), len(tc.policyDecision))
|
require.Equal(t, len(validateResult.Decisions), len(tc.policyDecision))
|
||||||
|
|
||||||
@ -630,6 +885,7 @@ func TestContextCanceled(t *testing.T) {
|
|||||||
v := validator{
|
v := validator{
|
||||||
failPolicy: &fail,
|
failPolicy: &fail,
|
||||||
validationFilter: f,
|
validationFilter: f,
|
||||||
|
messageFilter: f,
|
||||||
auditAnnotationFilter: &fakeCelFilter{
|
auditAnnotationFilter: &fakeCelFilter{
|
||||||
evaluations: nil,
|
evaluations: nil,
|
||||||
throwError: false,
|
throwError: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user