mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Merge pull request #92519 from deads2k/condition-validation
add validation functions for metav1.Conditions
This commit is contained in:
commit
9a343edd1f
@ -18,6 +18,7 @@ package validation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -184,3 +185,78 @@ func ValidateManagedFields(fieldsList []metav1.ManagedFieldsEntry, fldPath *fiel
|
|||||||
}
|
}
|
||||||
return allErrs
|
return allErrs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ValidateConditions(conditions []metav1.Condition, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
|
conditionTypeToFirstIndex := map[string]int{}
|
||||||
|
for i, condition := range conditions {
|
||||||
|
if _, ok := conditionTypeToFirstIndex[condition.Type]; ok {
|
||||||
|
allErrs = append(allErrs, field.Duplicate(fldPath.Index(i).Child("type"), condition.Type))
|
||||||
|
} else {
|
||||||
|
conditionTypeToFirstIndex[condition.Type] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
allErrs = append(allErrs, ValidateCondition(condition, fldPath.Index(i))...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// validConditionStatuses is used internally to check validity and provide a good message
|
||||||
|
var validConditionStatuses = sets.NewString(string(metav1.ConditionTrue), string(metav1.ConditionFalse), string(metav1.ConditionUnknown))
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxReasonLen = 1 * 1024
|
||||||
|
maxMessageLen = 32 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateCondition(condition metav1.Condition, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrs field.ErrorList
|
||||||
|
|
||||||
|
// type is set and is a valid format
|
||||||
|
allErrs = append(allErrs, ValidateLabelName(condition.Type, fldPath.Child("type"))...)
|
||||||
|
|
||||||
|
// status is set and is an accepted value
|
||||||
|
if !validConditionStatuses.Has(string(condition.Status)) {
|
||||||
|
allErrs = append(allErrs, field.NotSupported(fldPath.Child("status"), condition.Status, validConditionStatuses.List()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if condition.ObservedGeneration < 0 {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("observedGeneration"), condition.ObservedGeneration, "must be greater than or equal to zero"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if condition.LastTransitionTime.IsZero() {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("lastTransitionTime"), "must be set"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(condition.Reason) == 0 {
|
||||||
|
allErrs = append(allErrs, field.Required(fldPath.Child("reason"), "must be set"))
|
||||||
|
} else {
|
||||||
|
for _, currErr := range isValidConditionReason(condition.Reason) {
|
||||||
|
allErrs = append(allErrs, field.Invalid(fldPath.Child("reason"), condition.Reason, currErr))
|
||||||
|
}
|
||||||
|
if len(condition.Reason) > maxReasonLen {
|
||||||
|
allErrs = append(allErrs, field.TooLong(fldPath.Child("reason"), condition.Reason, maxReasonLen))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(condition.Message) > maxMessageLen {
|
||||||
|
allErrs = append(allErrs, field.TooLong(fldPath.Child("message"), condition.Message, maxMessageLen))
|
||||||
|
}
|
||||||
|
|
||||||
|
return allErrs
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditionReasonFmt string = "[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?"
|
||||||
|
const conditionReasonErrMsg string = "a condition reason must start with alphabetic character, optionally followed by a string of alphanumeric characters or '_,:', and must end with an alphanumeric character or '_'"
|
||||||
|
|
||||||
|
var conditionReasonRegexp = regexp.MustCompile("^" + conditionReasonFmt + "$")
|
||||||
|
|
||||||
|
// isValidConditionReason tests for a string that conforms to rules for condition reasons. This checks the format, but not the length.
|
||||||
|
func isValidConditionReason(value string) []string {
|
||||||
|
if !conditionReasonRegexp.MatchString(value) {
|
||||||
|
return []string{validation.RegexError(conditionReasonErrMsg, conditionReasonFmt, "my_name", "MY_NAME", "MyName", "ReasonA,ReasonB", "ReasonA:ReasonB")}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -293,3 +293,136 @@ func TestValidateMangedFieldsValid(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateConditions(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
conditions []metav1.Condition
|
||||||
|
validateErrs func(t *testing.T, errs field.ErrorList)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bunch-of-invalid-fields",
|
||||||
|
conditions: []metav1.Condition{{
|
||||||
|
Type: ":invalid",
|
||||||
|
Status: "unknown",
|
||||||
|
ObservedGeneration: -1,
|
||||||
|
LastTransitionTime: metav1.Time{},
|
||||||
|
Reason: "invalid;val",
|
||||||
|
Message: "",
|
||||||
|
}},
|
||||||
|
validateErrs: func(t *testing.T, errs field.ErrorList) {
|
||||||
|
needle := `status.conditions[0].type: Invalid value: ":invalid": name part must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or '123-abc', regex used for validation is '([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]')`
|
||||||
|
if !hasError(errs, needle) {
|
||||||
|
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
needle = `status.conditions[0].status: Unsupported value: "unknown": supported values: "False", "True", "Unknown"`
|
||||||
|
if !hasError(errs, needle) {
|
||||||
|
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
needle = `status.conditions[0].observedGeneration: Invalid value: -1: must be greater than or equal to zero`
|
||||||
|
if !hasError(errs, needle) {
|
||||||
|
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
needle = `status.conditions[0].lastTransitionTime: Required value: must be set`
|
||||||
|
if !hasError(errs, needle) {
|
||||||
|
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
needle = `status.conditions[0].reason: Invalid value: "invalid;val": a condition reason must start with alphabetic character, optionally followed by a string of alphanumeric characters or '_,:', and must end with an alphanumeric character or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', or 'ReasonA,ReasonB', or 'ReasonA:ReasonB', regex used for validation is '[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?')`
|
||||||
|
if !hasError(errs, needle) {
|
||||||
|
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicates",
|
||||||
|
conditions: []metav1.Condition{{
|
||||||
|
Type: "First",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "Second",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "First",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validateErrs: func(t *testing.T, errs field.ErrorList) {
|
||||||
|
needle := `status.conditions[2].type: Duplicate value: "First"`
|
||||||
|
if !hasError(errs, needle) {
|
||||||
|
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "colon-allowed-in-reason",
|
||||||
|
conditions: []metav1.Condition{{
|
||||||
|
Type: "First",
|
||||||
|
Reason: "valid:val",
|
||||||
|
}},
|
||||||
|
validateErrs: func(t *testing.T, errs field.ErrorList) {
|
||||||
|
needle := `status.conditions[0].reason`
|
||||||
|
if hasPrefixError(errs, needle) {
|
||||||
|
t.Errorf("has %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "comma-allowed-in-reason",
|
||||||
|
conditions: []metav1.Condition{{
|
||||||
|
Type: "First",
|
||||||
|
Reason: "valid,val",
|
||||||
|
}},
|
||||||
|
validateErrs: func(t *testing.T, errs field.ErrorList) {
|
||||||
|
needle := `status.conditions[0].reason`
|
||||||
|
if hasPrefixError(errs, needle) {
|
||||||
|
t.Errorf("has %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "reason-does-not-end-in-delimiter",
|
||||||
|
conditions: []metav1.Condition{{
|
||||||
|
Type: "First",
|
||||||
|
Reason: "valid,val:",
|
||||||
|
}},
|
||||||
|
validateErrs: func(t *testing.T, errs field.ErrorList) {
|
||||||
|
needle := `status.conditions[0].reason: Invalid value: "valid,val:": a condition reason must start with alphabetic character, optionally followed by a string of alphanumeric characters or '_,:', and must end with an alphanumeric character or '_' (e.g. 'my_name', or 'MY_NAME', or 'MyName', or 'ReasonA,ReasonB', or 'ReasonA:ReasonB', regex used for validation is '[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?')`
|
||||||
|
if !hasError(errs, needle) {
|
||||||
|
t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
errs := ValidateConditions(test.conditions, field.NewPath("status").Child("conditions"))
|
||||||
|
test.validateErrs(t, errs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasError(errs field.ErrorList, needle string) bool {
|
||||||
|
for _, curr := range errs {
|
||||||
|
if curr.Error() == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasPrefixError(errs field.ErrorList, prefix string) bool {
|
||||||
|
for _, curr := range errs {
|
||||||
|
if strings.HasPrefix(curr.Error(), prefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func errorsAsString(errs field.ErrorList) string {
|
||||||
|
messages := []string{}
|
||||||
|
for _, curr := range errs {
|
||||||
|
messages = append(messages, curr.Error())
|
||||||
|
}
|
||||||
|
return strings.Join(messages, "\n")
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user