k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/apis/storagemigration/validation/validation.go (about) 1 /* 2 Copyright 2024 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package validation 18 19 import ( 20 "fmt" 21 "regexp" 22 "strconv" 23 24 "k8s.io/apimachinery/pkg/util/sets" 25 "k8s.io/apimachinery/pkg/util/validation" 26 "k8s.io/apimachinery/pkg/util/validation/field" 27 "k8s.io/kubernetes/pkg/apis/storagemigration" 28 29 corev1 "k8s.io/api/core/v1" 30 apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" 33 apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" 34 ) 35 36 func ValidateStorageVersionMigration(svm *storagemigration.StorageVersionMigration) field.ErrorList { 37 allErrs := field.ErrorList{} 38 allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&svm.ObjectMeta, false, apimachineryvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))...) 39 40 allErrs = checkAndAppendError(allErrs, field.NewPath("spec", "resource", "resource"), svm.Spec.Resource.Resource, "resource is required") 41 allErrs = checkAndAppendError(allErrs, field.NewPath("spec", "resource", "version"), svm.Spec.Resource.Version, "version is required") 42 43 return allErrs 44 } 45 46 func ValidateStorageVersionMigrationUpdate(newSVMBundle, oldSVMBundle *storagemigration.StorageVersionMigration) field.ErrorList { 47 allErrs := ValidateStorageVersionMigration(newSVMBundle) 48 allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&newSVMBundle.ObjectMeta, &oldSVMBundle.ObjectMeta, field.NewPath("metadata"))...) 49 50 // prevent changes to the group, version and resource 51 if newSVMBundle.Spec.Resource.Group != oldSVMBundle.Spec.Resource.Group { 52 allErrs = append(allErrs, field.Invalid(field.NewPath("group"), newSVMBundle.Spec.Resource.Group, "field is immutable")) 53 } 54 if newSVMBundle.Spec.Resource.Version != oldSVMBundle.Spec.Resource.Version { 55 allErrs = append(allErrs, field.Invalid(field.NewPath("version"), newSVMBundle.Spec.Resource.Version, "field is immutable")) 56 } 57 if newSVMBundle.Spec.Resource.Resource != oldSVMBundle.Spec.Resource.Resource { 58 allErrs = append(allErrs, field.Invalid(field.NewPath("resource"), newSVMBundle.Spec.Resource.Resource, "field is immutable")) 59 } 60 61 return allErrs 62 } 63 64 func ValidateStorageVersionMigrationStatusUpdate(newSVMBundle, oldSVMBundle *storagemigration.StorageVersionMigration) field.ErrorList { 65 allErrs := apivalidation.ValidateObjectMetaUpdate(&newSVMBundle.ObjectMeta, &oldSVMBundle.ObjectMeta, field.NewPath("metadata")) 66 67 fldPath := field.NewPath("status") 68 69 // resource version should be a non-negative integer 70 rvInt, err := convertResourceVersionToInt(newSVMBundle.Status.ResourceVersion) 71 if err != nil { 72 allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceVersion"), newSVMBundle.Status.ResourceVersion, err.Error())) 73 } 74 allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(rvInt, fldPath.Child("resourceVersion"))...) 75 76 // TODO: after switching to metav1.Conditions in beta replace this validation with metav1.ValidateConditions 77 allErrs = append(allErrs, validateConditions(newSVMBundle.Status.Conditions, fldPath.Child("conditions"))...) 78 79 // resource version should not change once it has been set 80 if len(oldSVMBundle.Status.ResourceVersion) != 0 && oldSVMBundle.Status.ResourceVersion != newSVMBundle.Status.ResourceVersion { 81 allErrs = append(allErrs, field.Invalid(fldPath.Child("resourceVersion"), newSVMBundle.Status.ResourceVersion, "resourceVersion cannot be updated")) 82 } 83 84 // at most one of success or failed may be true 85 if isSuccessful(newSVMBundle) && isFailed(newSVMBundle) { 86 allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Both success and failed conditions cannot be true at the same time")) 87 } 88 89 // running must be false when success is true or failed is true 90 if isSuccessful(newSVMBundle) && isRunning(newSVMBundle) { 91 allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Running condition cannot be true when success condition is true")) 92 } 93 if isFailed(newSVMBundle) && isRunning(newSVMBundle) { 94 allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Running condition cannot be true when failed condition is true")) 95 } 96 97 // success cannot be set to false once it is true 98 isOldSuccessful := isSuccessful(oldSVMBundle) 99 if isOldSuccessful && !isSuccessful(newSVMBundle) { 100 allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Success condition cannot be set to false once it is true")) 101 } 102 isOldFailed := isFailed(oldSVMBundle) 103 if isOldFailed && !isFailed(newSVMBundle) { 104 allErrs = append(allErrs, field.Invalid(fldPath.Child("conditions"), newSVMBundle.Status.Conditions, "Failed condition cannot be set to false once it is true")) 105 } 106 107 return allErrs 108 } 109 110 func isSuccessful(svm *storagemigration.StorageVersionMigration) bool { 111 successCondition := getCondition(svm, storagemigration.MigrationSucceeded) 112 if successCondition != nil && successCondition.Status == corev1.ConditionTrue { 113 return true 114 } 115 return false 116 } 117 118 func isFailed(svm *storagemigration.StorageVersionMigration) bool { 119 failedCondition := getCondition(svm, storagemigration.MigrationFailed) 120 if failedCondition != nil && failedCondition.Status == corev1.ConditionTrue { 121 return true 122 } 123 return false 124 } 125 126 func isRunning(svm *storagemigration.StorageVersionMigration) bool { 127 runningCondition := getCondition(svm, storagemigration.MigrationRunning) 128 if runningCondition != nil && runningCondition.Status == corev1.ConditionTrue { 129 return true 130 } 131 return false 132 } 133 134 func getCondition(svm *storagemigration.StorageVersionMigration, conditionType storagemigration.MigrationConditionType) *storagemigration.MigrationCondition { 135 for _, c := range svm.Status.Conditions { 136 if c.Type == conditionType { 137 return &c 138 } 139 } 140 141 return nil 142 } 143 144 func validateConditions(conditions []storagemigration.MigrationCondition, fldPath *field.Path) field.ErrorList { 145 var allErrs field.ErrorList 146 147 conditionTypeToFirstIndex := map[string]int{} 148 for i, condition := range conditions { 149 if _, ok := conditionTypeToFirstIndex[string(condition.Type)]; ok { 150 allErrs = append(allErrs, field.Duplicate(fldPath.Index(i).Child("type"), condition.Type)) 151 } else { 152 conditionTypeToFirstIndex[string(condition.Type)] = i 153 } 154 155 allErrs = append(allErrs, validateCondition(condition, fldPath.Index(i))...) 156 } 157 158 return allErrs 159 } 160 161 func validateCondition(condition storagemigration.MigrationCondition, fldPath *field.Path) field.ErrorList { 162 var allErrs field.ErrorList 163 var validConditionStatuses = sets.NewString(string(metav1.ConditionTrue), string(metav1.ConditionFalse), string(metav1.ConditionUnknown)) 164 165 // type is set and is a valid format 166 allErrs = append(allErrs, metav1validation.ValidateLabelName(string(condition.Type), fldPath.Child("type"))...) 167 168 // status is set and is an accepted value 169 if !validConditionStatuses.Has(string(condition.Status)) { 170 allErrs = append(allErrs, field.NotSupported(fldPath.Child("status"), condition.Status, validConditionStatuses.List())) 171 } 172 173 if condition.LastUpdateTime.IsZero() { 174 allErrs = append(allErrs, field.Required(fldPath.Child("lastTransitionTime"), "must be set")) 175 } 176 177 if len(condition.Reason) == 0 { 178 allErrs = append(allErrs, field.Required(fldPath.Child("reason"), "must be set")) 179 } else { 180 for _, currErr := range isValidConditionReason(condition.Reason) { 181 allErrs = append(allErrs, field.Invalid(fldPath.Child("reason"), condition.Reason, currErr)) 182 } 183 184 const maxReasonLen int = 1 * 1024 // 1024 185 if len(condition.Reason) > maxReasonLen { 186 allErrs = append(allErrs, field.TooLong(fldPath.Child("reason"), condition.Reason, maxReasonLen)) 187 } 188 } 189 190 const maxMessageLen int = 32 * 1024 // 32768 191 if len(condition.Message) > maxMessageLen { 192 allErrs = append(allErrs, field.TooLong(fldPath.Child("message"), condition.Message, maxMessageLen)) 193 } 194 195 return allErrs 196 } 197 func isValidConditionReason(value string) []string { 198 const conditionReasonFmt string = "[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?" 199 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 '_'" 200 var conditionReasonRegexp = regexp.MustCompile("^" + conditionReasonFmt + "$") 201 202 if !conditionReasonRegexp.MatchString(value) { 203 return []string{validation.RegexError(conditionReasonErrMsg, conditionReasonFmt, "my_name", "MY_NAME", "MyName", "ReasonA,ReasonB", "ReasonA:ReasonB")} 204 } 205 return nil 206 } 207 208 func checkAndAppendError(allErrs field.ErrorList, fieldPath *field.Path, value string, message string) field.ErrorList { 209 if len(value) == 0 { 210 allErrs = append(allErrs, field.Required(fieldPath, message)) 211 } 212 return allErrs 213 } 214 215 func convertResourceVersionToInt(rv string) (int64, error) { 216 // initial value of RV is expected to be empty, which means the resource version is not set 217 if len(rv) == 0 { 218 return 0, nil 219 } 220 221 resourceVersion, err := strconv.ParseInt(rv, 10, 64) 222 if err != nil { 223 return 0, fmt.Errorf("failed to parse resource version %q: %w", rv, err) 224 } 225 226 return resourceVersion, nil 227 }