Merge pull request #116397 from jiahuif-forks/feature/validating-admission-policy/message-expression

MessageExpression for ValidatingAdmissionPolicy
This commit is contained in:
Kubernetes Prow Robot
2023-03-13 19:31:08 -07:00
committed by GitHub
25 changed files with 735 additions and 146 deletions

View File

@@ -256,6 +256,18 @@ type Validation struct {
// If not set, StatusReasonInvalid is used in the response to the client.
// +optional
Reason *metav1.StatusReason
// messageExpression declares a CEL expression that evaluates to the validation failure message that is returned when this rule fails.
// Since messageExpression is used as a failure message, it must evaluate to a string.
// If both message and messageExpression are present on a validation, then messageExpression will be used if validation fails.
// If messageExpression results in a runtime error, the runtime error is logged, and the validation failure message is produced
// as if the messageExpression field were unset. If messageExpression evaluates to an empty string, a string with only spaces, or a string
// that contains line breaks, then the validation failure message will also be produced as if the messageExpression field were unset, and
// the fact that messageExpression produced an empty string/string with only spaces/string with line breaks will be logged.
// messageExpression has access to all the same variables as the `expression` except for 'authorizer' and 'authorizer.requestResource'.
// Example:
// "object.x must be less than max ("+string(params.max)+")"
// +optional
MessageExpression string
}
// AuditAnnotation describes how to produce an audit annotation for an API request.

View File

@@ -548,6 +548,7 @@ func autoConvert_v1alpha1_Validation_To_admissionregistration_Validation(in *v1a
out.Expression = in.Expression
out.Message = in.Message
out.Reason = (*v1.StatusReason)(unsafe.Pointer(in.Reason))
out.MessageExpression = in.MessageExpression
return nil
}
@@ -560,6 +561,7 @@ func autoConvert_admissionregistration_Validation_To_v1alpha1_Validation(in *adm
out.Expression = in.Expression
out.Message = in.Message
out.Reason = (*v1.StatusReason)(unsafe.Pointer(in.Reason))
out.MessageExpression = in.MessageExpression
return nil
}

View File

@@ -782,26 +782,24 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
var allErrors field.ErrorList
trimmedExpression := strings.TrimSpace(v.Expression)
trimmedMsg := strings.TrimSpace(v.Message)
trimmedMessageExpression := strings.TrimSpace(v.MessageExpression)
if len(trimmedExpression) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), "expression is not specified"))
} else {
result := plugincel.CompileCELExpression(&validatingadmissionpolicy.ValidationCondition{
Expression: trimmedExpression,
Message: v.Message,
Reason: v.Reason,
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}, celconfig.PerCallLimit)
if result.Error != nil {
switch result.Error.Type {
case cel.ErrorTypeRequired:
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), result.Error.Detail))
case cel.ErrorTypeInvalid:
allErrors = append(allErrors, field.Invalid(fldPath.Child("expression"), v.Expression, result.Error.Detail))
case cel.ErrorTypeInternal:
allErrors = append(allErrors, field.InternalError(fldPath.Child("expression"), result.Error))
default:
allErrors = append(allErrors, field.InternalError(fldPath.Child("expression"), fmt.Errorf("unsupported error type: %w", result.Error)))
}
}
allErrors = append(allErrors, validateCELExpression(v.Expression, plugincel.OptionalVariableDeclarations{
HasParams: paramKind != nil,
HasAuthorizer: true,
}, 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, validateCELExpression(v.MessageExpression, plugincel.OptionalVariableDeclarations{
HasParams: paramKind != nil,
HasAuthorizer: false,
}, 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"))
@@ -816,6 +814,26 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
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 {
var allErrors field.ErrorList
if len(meta.GetName()) != 0 {

View File

@@ -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",

View File

@@ -2498,6 +2498,13 @@ func schema_k8sio_api_admissionregistration_v1alpha1_Validation(ref common.Refer
Format: "",
},
},
"messageExpression": {
SchemaProps: spec.SchemaProps{
Description: "messageExpression declares a CEL expression that evaluates to the validation failure message that is returned when this rule fails. Since messageExpression is used as a failure message, it must evaluate to a string. If both message and messageExpression are present on a validation, then messageExpression will be used if validation fails. If messageExpression results in a runtime error, the runtime error is logged, and the validation failure message is produced as if the messageExpression field were unset. If messageExpression evaluates to an empty string, a string with only spaces, or a string that contains line breaks, then the validation failure message will also be produced as if the messageExpression field were unset, and the fact that messageExpression produced an empty string/string with only spaces/string with line breaks will be logged. messageExpression has access to all the same variables as the `expression` except for 'authorizer' and 'authorizer.requestResource'. Example: \"object.x must be less than max (\"+string(params.max)+\")\"",
Type: []string{"string"},
Format: "",
},
},
},
Required: []string{"expression"},
},