mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-22 19:31:44 +00:00
add validation for metav1 conditions
This commit is contained in:
parent
6cedc0853f
commit
e5fdc77a3d
@ -18,6 +18,7 @@ package validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"unicode"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@ -184,3 +185,78 @@ func ValidateManagedFields(fieldsList []metav1.ManagedFieldsEntry, fldPath *fiel
|
||||
}
|
||||
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