-
Notifications
You must be signed in to change notification settings - Fork 874
/
validation.go
303 lines (255 loc) · 12.8 KB
/
validation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
/*
Copyright 2021 The Karmada Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package validation
import (
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
apivalidation "k8s.io/apimachinery/pkg/api/validation"
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/pointer"
policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
"github.com/karmada-io/karmada/pkg/util"
)
// LabelValueMaxLength is a label's max length
const LabelValueMaxLength int = 63
// ValidatePropagationSpec validates a PropagationSpec before creation or update.
func ValidatePropagationSpec(spec policyv1alpha1.PropagationSpec) field.ErrorList {
var allErrs field.ErrorList
allErrs = append(allErrs, ValidatePlacement(spec.Placement, field.NewPath("spec").Child("placement"))...)
if spec.Failover != nil && spec.Failover.Application != nil && !spec.PropagateDeps {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec").Child("propagateDeps"), spec.PropagateDeps, "application failover is set, propagateDeps must be true"))
}
allErrs = append(allErrs, ValidateFailover(spec.Failover, field.NewPath("spec").Child("failover"))...)
allErrs = append(allErrs, validateResourceSelectorsIfPreemptionEnabled(spec, field.NewPath("spec").Child("resourceSelectors"))...)
return allErrs
}
// validateResourceSelectorsIfPreemptionEnabled validates ResourceSelectors if Preemption is Always.
func validateResourceSelectorsIfPreemptionEnabled(spec policyv1alpha1.PropagationSpec, fldPath *field.Path) field.ErrorList {
if spec.Preemption != policyv1alpha1.PreemptAlways {
return nil
}
var allErrs field.ErrorList
for index, resourceSelector := range spec.ResourceSelectors {
if len(resourceSelector.Name) == 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Index(index).Child("name"), resourceSelector.Name, "name can not be empty if preemption is Always, the empty name may cause unexpected resources preemption"))
}
}
return allErrs
}
// ValidatePlacement validates a placement before creation or update.
func ValidatePlacement(placement policyv1alpha1.Placement, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if placement.ClusterAffinity != nil && placement.ClusterAffinities != nil {
allErrs = append(allErrs, field.Invalid(fldPath, placement, "clusterAffinities can not co-exist with clusterAffinity"))
}
allErrs = append(allErrs, ValidateClusterAffinity(placement.ClusterAffinity, fldPath.Child("clusterAffinity"))...)
allErrs = append(allErrs, ValidateClusterAffinities(placement.ClusterAffinities, fldPath.Child("clusterAffinities"))...)
allErrs = append(allErrs, ValidateSpreadConstraint(placement.SpreadConstraints, fldPath.Child("spreadConstraints"))...)
return allErrs
}
// ValidateClusterAffinity validates a clusterAffinity before creation or update.
func ValidateClusterAffinity(affinity *policyv1alpha1.ClusterAffinity, fldPath *field.Path) field.ErrorList {
if affinity == nil {
return nil
}
var allErrs field.ErrorList
err := ValidatePolicyFieldSelector(affinity.FieldSelector)
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("fieldSelector"), affinity.FieldSelector, err.Error()))
}
return allErrs
}
// ValidateClusterAffinities validates clusterAffinities before creation or update.
func ValidateClusterAffinities(affinities []policyv1alpha1.ClusterAffinityTerm, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
affinityNames := make(map[string]bool)
for index, term := range affinities {
for _, err := range validation.IsQualifiedName(term.AffinityName) {
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), term.AffinityName, err))
}
if _, exist := affinityNames[term.AffinityName]; exist {
allErrs = append(allErrs, field.Invalid(fldPath, affinities, "each affinity term in a policy must have a unique name"))
} else {
affinityNames[term.AffinityName] = true
}
allErrs = append(allErrs, ValidateClusterAffinity(&term.ClusterAffinity, fldPath.Index(index))...)
}
return allErrs
}
// ValidatePolicyFieldSelector tests if the fieldSelector of propagation policy or override policy is valid.
func ValidatePolicyFieldSelector(fieldSelector *policyv1alpha1.FieldSelector) error {
if fieldSelector == nil {
return nil
}
for _, matchExpression := range fieldSelector.MatchExpressions {
switch matchExpression.Key {
case util.ProviderField, util.RegionField, util.ZoneField:
default:
return fmt.Errorf("unsupported key %q, must be provider, region, or zone", matchExpression.Key)
}
switch matchExpression.Operator {
case corev1.NodeSelectorOpIn, corev1.NodeSelectorOpNotIn:
default:
return fmt.Errorf("unsupported operator %q, must be In or NotIn", matchExpression.Operator)
}
}
return nil
}
// ValidateSpreadConstraint tests if the constraints is valid.
func ValidateSpreadConstraint(spreadConstraints []policyv1alpha1.SpreadConstraint, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
spreadByFieldsWithErrorMark := make(map[policyv1alpha1.SpreadFieldValue]*bool)
for index, constraint := range spreadConstraints {
// SpreadByField and SpreadByLabel should not co-exist
if len(constraint.SpreadByField) > 0 && len(constraint.SpreadByLabel) > 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), constraint, "spreadByLabel should not co-exist with spreadByField"))
}
// If MinGroups provided, it should not be lower than 0.
if constraint.MinGroups < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), constraint, "minGroups lower than 0 is not allowed"))
}
// If MaxGroups provided, it should not be lower than 0.
if constraint.MaxGroups < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), constraint, "maxGroups lower than 0 is not allowed"))
}
// If MaxGroups provided, it should greater or equal than MinGroups.
if constraint.MaxGroups > 0 && constraint.MaxGroups < constraint.MinGroups {
allErrs = append(allErrs, field.Invalid(fldPath.Index(index), constraint, "maxGroups lower than minGroups is not allowed"))
}
if len(constraint.SpreadByField) > 0 {
marked := spreadByFieldsWithErrorMark[constraint.SpreadByField]
if !pointer.BoolDeref(marked, true) {
allErrs = append(allErrs, field.Invalid(fldPath, spreadConstraints, fmt.Sprintf("multiple %s spread constraints are not allowed", constraint.SpreadByField)))
*marked = true
}
if marked == nil {
spreadByFieldsWithErrorMark[constraint.SpreadByField] = pointer.Bool(false)
}
}
}
if len(spreadByFieldsWithErrorMark) > 0 {
// If one of spread constraints are using 'SpreadByField', the 'SpreadByFieldCluster' must be included.
// For example, when using 'SpreadByFieldRegion' to specify region groups, at the meantime, you must use
// 'SpreadByFieldCluster' to specify how many clusters should be selected.
if _, ok := spreadByFieldsWithErrorMark[policyv1alpha1.SpreadByFieldCluster]; !ok {
allErrs = append(allErrs, field.Invalid(fldPath, spreadConstraints, "the cluster spread constraint must be enabled in one of the constraints in case of SpreadByField is enabled"))
}
}
return allErrs
}
// ValidateFailover validates that the failoverBehavior is correctly defined.
func ValidateFailover(failoverBehavior *policyv1alpha1.FailoverBehavior, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if failoverBehavior == nil {
return nil
}
allErrs = append(allErrs, ValidateApplicationFailover(failoverBehavior.Application, fldPath.Child("application"))...)
return allErrs
}
// ValidateApplicationFailover validates that the application failover is correctly defined.
func ValidateApplicationFailover(applicationFailoverBehavior *policyv1alpha1.ApplicationFailoverBehavior, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
if applicationFailoverBehavior == nil {
return nil
}
if *applicationFailoverBehavior.DecisionConditions.TolerationSeconds < 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("decisionConditions").Child("tolerationSeconds"), *applicationFailoverBehavior.DecisionConditions.TolerationSeconds, "must be greater than or equal to 0"))
}
if applicationFailoverBehavior.PurgeMode != policyv1alpha1.Graciously && applicationFailoverBehavior.GracePeriodSeconds != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("gracePeriodSeconds"), *applicationFailoverBehavior.GracePeriodSeconds, "only takes effect when purgeMode is graciously"))
}
if applicationFailoverBehavior.PurgeMode == policyv1alpha1.Graciously && applicationFailoverBehavior.GracePeriodSeconds == nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("gracePeriodSeconds"), applicationFailoverBehavior.GracePeriodSeconds, "should not be empty when purgeMode is graciously"))
}
if applicationFailoverBehavior.GracePeriodSeconds != nil && *applicationFailoverBehavior.GracePeriodSeconds <= 0 {
allErrs = append(allErrs, field.Invalid(fldPath.Child("gracePeriodSeconds"), *applicationFailoverBehavior.GracePeriodSeconds, "must be greater than 0"))
}
return allErrs
}
// ValidateOverrideSpec validates that the overrider specification is correctly defined.
func ValidateOverrideSpec(overrideSpec *policyv1alpha1.OverrideSpec) field.ErrorList {
var allErrs field.ErrorList
if overrideSpec == nil {
return nil
}
specPath := field.NewPath("spec")
//nolint:staticcheck
// disable `deprecation` check for backward compatibility.
if overrideSpec.TargetCluster != nil {
allErrs = append(allErrs, ValidateClusterAffinity(overrideSpec.TargetCluster, specPath.Child("targetCluster"))...)
}
//nolint:staticcheck
// disable `deprecation` check for backward compatibility.
if overrideSpec.TargetCluster != nil && overrideSpec.OverrideRules != nil {
allErrs = append(allErrs, field.Invalid(specPath.Child("targetCluster"), overrideSpec.TargetCluster, "overrideRules and targetCluster can't co-exist"))
}
//nolint:staticcheck
// disable `deprecation` check for backward compatibility.
if !emptyOverrides(overrideSpec.Overriders) && overrideSpec.OverrideRules != nil {
allErrs = append(allErrs, field.Invalid(specPath.Child("overriders"), overrideSpec.Overriders, "overrideRules and overriders can't co-exist"))
}
allErrs = append(allErrs, ValidateOverrideRules(overrideSpec.OverrideRules, specPath)...)
return allErrs
}
// emptyOverrides check if the overriders of override policy is empty.
func emptyOverrides(overriders policyv1alpha1.Overriders) bool {
if len(overriders.Plaintext) != 0 {
return false
}
if len(overriders.ImageOverrider) != 0 {
return false
}
if len(overriders.CommandOverrider) != 0 {
return false
}
if len(overriders.ArgsOverrider) != 0 {
return false
}
if len(overriders.LabelsOverrider) != 0 {
return false
}
if len(overriders.AnnotationsOverrider) != 0 {
return false
}
return true
}
// ValidateOverrideRules validates the overrideRules of override policy.
func ValidateOverrideRules(overrideRules []policyv1alpha1.RuleWithCluster, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
for overrideRuleIndex, rule := range overrideRules {
rulePath := fldPath.Child("overrideRules").Index(overrideRuleIndex)
// validates provided annotations.
for annotationIndex, annotation := range rule.Overriders.AnnotationsOverrider {
annotationPath := rulePath.Child("overriders").Child("annotationsOverrider").Index(annotationIndex)
allErrs = append(allErrs, apivalidation.ValidateAnnotations(annotation.Value, annotationPath.Child("value"))...)
}
// validates provided labels.
for labelIndex, label := range rule.Overriders.LabelsOverrider {
labelPath := rulePath.Child("overriders").Child("labelsOverrider").Index(labelIndex)
allErrs = append(allErrs, metav1validation.ValidateLabels(label.Value, labelPath.Child("value"))...)
}
// validates predicate path.
for imageIndex, image := range rule.Overriders.ImageOverrider {
imagePath := rulePath.Child("overriders").Child("imageOverrider").Index(imageIndex)
if image.Predicate != nil && !strings.HasPrefix(image.Predicate.Path, "/") {
allErrs = append(allErrs, field.Invalid(imagePath.Child("predicate").Child("path"), image.Predicate.Path, "path should be start with / character"))
}
}
// validates the targetCluster.
allErrs = append(allErrs, ValidateClusterAffinity(rule.TargetCluster, rulePath.Child("targetCluster"))...)
}
return allErrs
}