1
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
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
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
77 allErrs = append(allErrs, validateConditions(newSVMBundle.Status.Conditions, fldPath.Child("conditions"))...)
78
79
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
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
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
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
166 allErrs = append(allErrs, metav1validation.ValidateLabelName(string(condition.Type), fldPath.Child("type"))...)
167
168
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
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
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
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 }
228
View as plain text