ValidatingAdmissionPolicy: Variable Composition (#118642)

* [API REVIEW] Variable Composition

* lazy map.

* variable composition implementation.

* check variables during VAP validation.

* generated: ./hack/update-vendor.sh

* generated: UPDATE_COMPATIBILITY_FIXTURE_DATA

(cd staging/src/k8s.io/api/ && env UPDATE_COMPATIBILITY_FIXTURE_DATA=true go test)

* cost calucation.

* tests for cost calculations.

* e2e test for variables.

* fix doc for Validation.Expression.

* generated: ./hack/update-codegen.sh

* fix missing utilruntime import.

* generated: ./hack/update-openapi-spec.sh
This commit is contained in:
Jiahui Feng
2023-07-13 17:13:28 -07:00
committed by GitHub
parent 1e21da87b8
commit b635f2a401
36 changed files with 2105 additions and 131 deletions

View File

@@ -26,6 +26,7 @@ import (
"k8s.io/apimachinery/pkg/api/validation/path"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
@@ -723,6 +724,14 @@ func validateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmiss
func validateValidatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissionregistration.ValidatingAdmissionPolicySpec, opts validationOptions, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
var compiler plugincel.Compiler // composition compiler is stateful, create one lazily per policy
getCompiler := func() plugincel.Compiler {
if compiler == nil {
needsComposition := len(spec.Variables) > 0
compiler = createCompiler(needsComposition)
}
return compiler
}
if spec.FailurePolicy == nil {
allErrors = append(allErrors, field.Required(fldPath.Child("failurePolicy"), ""))
} else if !supportedFailurePolicies.Has(string(*spec.FailurePolicy)) {
@@ -744,12 +753,17 @@ func validateValidatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissi
if !opts.ignoreMatchConditions {
allErrors = append(allErrors, validateMatchConditions(spec.MatchConditions, opts, fldPath.Child("matchConditions"))...)
}
if len(spec.Variables) > 0 {
for i, variable := range spec.Variables {
allErrors = append(allErrors, validateVariable(getCompiler(), &variable, spec.ParamKind, opts, fldPath.Child("variables").Index(i))...)
}
}
if len(spec.Validations) == 0 && len(spec.AuditAnnotations) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("validations"), "validations or auditAnnotations must contain at least one item"))
allErrors = append(allErrors, field.Required(fldPath.Child("auditAnnotations"), "validations or auditAnnotations must contain at least one item"))
} else {
for i, validation := range spec.Validations {
allErrors = append(allErrors, validateValidation(&validation, spec.ParamKind, opts, fldPath.Child("validations").Index(i))...)
allErrors = append(allErrors, validateValidation(getCompiler(), &validation, spec.ParamKind, opts, fldPath.Child("validations").Index(i))...)
}
if spec.AuditAnnotations != nil {
keys := sets.NewString()
@@ -757,7 +771,7 @@ func validateValidatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissi
allErrors = append(allErrors, field.Invalid(fldPath.Child("auditAnnotations"), spec.AuditAnnotations, fmt.Sprintf("must not have more than %d auditAnnotations", maxAuditAnnotations)))
}
for i, auditAnnotation := range spec.AuditAnnotations {
allErrors = append(allErrors, validateAuditAnnotation(meta, &auditAnnotation, spec.ParamKind, opts, fldPath.Child("auditAnnotations").Index(i))...)
allErrors = append(allErrors, validateAuditAnnotation(getCompiler(), meta, &auditAnnotation, spec.ParamKind, opts, fldPath.Child("auditAnnotations").Index(i))...)
if keys.Has(auditAnnotation.Key) {
allErrors = append(allErrors, field.Duplicate(fldPath.Child("auditAnnotations").Index(i).Child("key"), auditAnnotation.Key))
}
@@ -935,7 +949,42 @@ func validateMatchCondition(v *admissionregistration.MatchCondition, opts valida
return allErrors
}
func validateValidation(v *admissionregistration.Validation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList {
func validateVariable(compiler plugincel.Compiler, v *admissionregistration.Variable, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
if len(v.Name) == 0 || strings.TrimSpace(v.Name) == "" {
allErrors = append(allErrors, field.Required(fldPath.Child("name"), "name is not specified"))
} else {
if !isCELIdentifier(v.Name) {
allErrors = append(allErrors, field.Invalid(fldPath.Child("name"), v.Name, "name is not a valid CEL identifier"))
}
}
if len(v.Expression) == 0 || strings.TrimSpace(v.Expression) == "" {
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified"))
} else {
if compiler, ok := compiler.(*plugincel.CompositedCompiler); ok {
envType := environment.NewExpressions
if opts.preexistingExpressions.validationExpressions.Has(v.Expression) {
envType = environment.StoredExpressions
}
variable := &validatingadmissionpolicy.Variable{
Name: v.Name,
Expression: v.Expression,
}
result := compiler.CompileAndStoreVariable(variable, plugincel.OptionalVariableDeclarations{
HasParams: paramKind != nil,
HasAuthorizer: true,
}, envType)
if result.Error != nil {
allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), variable, result.Error))
}
} else {
allErrors = append(allErrors, field.InternalError(fldPath, fmt.Errorf("variable composition is not allowed")))
}
}
return allErrors
}
func validateValidation(compiler plugincel.Compiler, v *admissionregistration.Validation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
trimmedExpression := strings.TrimSpace(v.Expression)
trimmedMsg := strings.TrimSpace(v.Message)
@@ -943,14 +992,14 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
if len(trimmedExpression) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified"))
} else {
allErrors = append(allErrors, validateValidationExpression(v.Expression, paramKind != nil, opts, fldPath.Child("expression"))...)
allErrors = append(allErrors, validateValidationExpression(compiler, v.Expression, paramKind != nil, opts, fldPath.Child("expression"))...)
}
if len(v.MessageExpression) > 0 && len(trimmedMessageExpression) == 0 {
allErrors = append(allErrors, field.Invalid(fldPath.Child("messageExpression"), v.MessageExpression, "must be non-empty if specified"))
} else if len(trimmedMessageExpression) != 0 {
// use v.MessageExpression instead of trimmedMessageExpression so that
// the compiler output shows the correct column.
allErrors = append(allErrors, validateMessageExpression(v.MessageExpression, opts, fldPath.Child("messageExpression"))...)
allErrors = append(allErrors, validateMessageExpression(compiler, v.MessageExpression, opts, fldPath.Child("messageExpression"))...)
}
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"))
@@ -965,31 +1014,35 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
return allErrors
}
func validateCELCondition(expression plugincel.ExpressionAccessor, variables plugincel.OptionalVariableDeclarations, envType environment.Type, fldPath *field.Path) field.ErrorList {
func validateCELCondition(compiler plugincel.Compiler, expression plugincel.ExpressionAccessor, variables plugincel.OptionalVariableDeclarations, envType environment.Type, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
result := compiler.CompileCELExpression(expression, variables, envType)
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.GetExpression(), 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)))
}
allErrors = append(allErrors, convertCELErrorToValidationError(fldPath, expression, result.Error))
}
return allErrors
}
func validateValidationExpression(expression string, hasParams bool, opts validationOptions, fldPath *field.Path) field.ErrorList {
func convertCELErrorToValidationError(fldPath *field.Path, expression plugincel.ExpressionAccessor, err error) *field.Error {
if celErr, ok := err.(*cel.Error); ok {
switch celErr.Type {
case cel.ErrorTypeRequired:
return field.Required(fldPath, celErr.Detail)
case cel.ErrorTypeInvalid:
return field.Invalid(fldPath, expression.GetExpression(), celErr.Detail)
case cel.ErrorTypeInternal:
return field.InternalError(fldPath, celErr)
}
}
return field.InternalError(fldPath, fmt.Errorf("unsupported error type: %w", err))
}
func validateValidationExpression(compiler plugincel.Compiler, expression string, hasParams bool, opts validationOptions, fldPath *field.Path) field.ErrorList {
envType := environment.NewExpressions
if opts.preexistingExpressions.validationExpressions.Has(expression) {
envType = environment.StoredExpressions
}
return validateCELCondition(&validatingadmissionpolicy.ValidationCondition{
return validateCELCondition(compiler, &validatingadmissionpolicy.ValidationCondition{
Expression: expression,
}, plugincel.OptionalVariableDeclarations{
HasParams: hasParams,
@@ -1002,7 +1055,7 @@ func validateMatchConditionsExpression(expression string, opts validationOptions
if opts.preexistingExpressions.matchConditionExpressions.Has(expression) {
envType = environment.StoredExpressions
}
return validateCELCondition(&matchconditions.MatchCondition{
return validateCELCondition(statelessCELCompiler, &matchconditions.MatchCondition{
Expression: expression,
}, plugincel.OptionalVariableDeclarations{
HasParams: opts.allowParamsInMatchConditions,
@@ -1010,12 +1063,12 @@ func validateMatchConditionsExpression(expression string, opts validationOptions
}, envType, fldPath)
}
func validateMessageExpression(expression string, opts validationOptions, fldPath *field.Path) field.ErrorList {
func validateMessageExpression(compiler plugincel.Compiler, expression string, opts validationOptions, fldPath *field.Path) field.ErrorList {
envType := environment.NewExpressions
if opts.preexistingExpressions.validationMessageExpressions.Has(expression) {
envType = environment.StoredExpressions
}
return validateCELCondition(&validatingadmissionpolicy.MessageExpressionCondition{
return validateCELCondition(compiler, &validatingadmissionpolicy.MessageExpressionCondition{
MessageExpression: expression,
}, plugincel.OptionalVariableDeclarations{
HasParams: opts.allowParamsInMatchConditions,
@@ -1023,7 +1076,7 @@ func validateMessageExpression(expression string, opts validationOptions, fldPat
}, envType, fldPath)
}
func validateAuditAnnotation(meta metav1.ObjectMeta, v *admissionregistration.AuditAnnotation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList {
func validateAuditAnnotation(compiler plugincel.Compiler, meta metav1.ObjectMeta, v *admissionregistration.AuditAnnotation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
if len(meta.GetName()) != 0 {
name := meta.GetName()
@@ -1166,4 +1219,38 @@ func validateFieldRef(fieldRef string, fldPath *field.Path) field.ErrorList {
return nil
}
var compiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
// statelessCELCompiler does not support variable composition (and thus is stateless). It should be used when
// variable composition is not allowed, for example, when validating MatchConditions.
var statelessCELCompiler = plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
func createCompiler(allowComposition bool) plugincel.Compiler {
if !allowComposition {
return statelessCELCompiler
}
compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
if err != nil {
// should never happen, but cannot panic either.
utilruntime.HandleError(err)
return plugincel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
}
return compiler
}
var celIdentRegex = regexp.MustCompile("^[_a-zA-Z][_a-zA-Z0-9]*$")
var celReserved = sets.NewString("true", "false", "null", "in",
"as", "break", "const", "continue", "else",
"for", "function", "if", "import", "let",
"loop", "package", "namespace", "return",
"var", "void", "while")
func isCELIdentifier(name string) bool {
// IDENT ::= [_a-zA-Z][_a-zA-Z0-9]* - RESERVED
// BOOL_LIT ::= "true" | "false"
// NULL_LIT ::= "null"
// RESERVED ::= BOOL_LIT | NULL_LIT | "in"
// | "as" | "break" | "const" | "continue" | "else"
// | "for" | "function" | "if" | "import" | "let"
// | "loop" | "package" | "namespace" | "return"
// | "var" | "void" | "while"
return celIdentRegex.MatchString(name) && !celReserved.Has(name)
}