mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-12-07 09:43:15 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user