1
16
17 package validation
18
19 import (
20 "fmt"
21 "strings"
22 "testing"
23
24 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25 "k8s.io/apimachinery/pkg/types"
26 "k8s.io/apimachinery/pkg/util/validation/field"
27 )
28
29 func TestValidateLabels(t *testing.T) {
30 successCases := []map[string]string{
31 {"simple": "bar"},
32 {"now-with-dashes": "bar"},
33 {"1-starts-with-num": "bar"},
34 {"1234": "bar"},
35 {"simple/simple": "bar"},
36 {"now-with-dashes/simple": "bar"},
37 {"now-with-dashes/now-with-dashes": "bar"},
38 {"now.with.dots/simple": "bar"},
39 {"now-with.dashes-and.dots/simple": "bar"},
40 {"1-num.2-num/3-num": "bar"},
41 {"1234/5678": "bar"},
42 {"1.2.3.4/5678": "bar"},
43 {"UpperCaseAreOK123": "bar"},
44 {"goodvalue": "123_-.BaR"},
45 }
46 for i := range successCases {
47 errs := ValidateLabels(successCases[i], field.NewPath("field"))
48 if len(errs) != 0 {
49 t.Errorf("case[%d] expected success, got %#v", i, errs)
50 }
51 }
52
53 namePartErrMsg := "name part must consist of"
54 nameErrMsg := "a qualified name must consist of"
55 labelErrMsg := "a valid label must be an empty string or consist of"
56 maxLengthErrMsg := "must be no more than"
57
58 labelNameErrorCases := []struct {
59 labels map[string]string
60 expect string
61 }{
62 {map[string]string{"nospecialchars^=@": "bar"}, namePartErrMsg},
63 {map[string]string{"cantendwithadash-": "bar"}, namePartErrMsg},
64 {map[string]string{"only/one/slash": "bar"}, nameErrMsg},
65 {map[string]string{strings.Repeat("a", 254): "bar"}, maxLengthErrMsg},
66 }
67 for i := range labelNameErrorCases {
68 errs := ValidateLabels(labelNameErrorCases[i].labels, field.NewPath("field"))
69 if len(errs) != 1 {
70 t.Errorf("case[%d]: expected failure", i)
71 } else {
72 if !strings.Contains(errs[0].Detail, labelNameErrorCases[i].expect) {
73 t.Errorf("case[%d]: error details do not include %q: %q", i, labelNameErrorCases[i].expect, errs[0].Detail)
74 }
75 }
76 }
77
78 labelValueErrorCases := []struct {
79 labels map[string]string
80 expect string
81 }{
82 {map[string]string{"toolongvalue": strings.Repeat("a", 64)}, maxLengthErrMsg},
83 {map[string]string{"backslashesinvalue": "some\\bad\\value"}, labelErrMsg},
84 {map[string]string{"nocommasallowed": "bad,value"}, labelErrMsg},
85 {map[string]string{"strangecharsinvalue": "?#$notsogood"}, labelErrMsg},
86 }
87 for i := range labelValueErrorCases {
88 errs := ValidateLabels(labelValueErrorCases[i].labels, field.NewPath("field"))
89 if len(errs) != 1 {
90 t.Errorf("case[%d]: expected failure", i)
91 } else {
92 if !strings.Contains(errs[0].Detail, labelValueErrorCases[i].expect) {
93 t.Errorf("case[%d]: error details do not include %q: %q", i, labelValueErrorCases[i].expect, errs[0].Detail)
94 }
95 }
96 }
97 }
98
99 func TestValidDryRun(t *testing.T) {
100 tests := [][]string{
101 {},
102 {"All"},
103 {"All", "All"},
104 }
105
106 for _, test := range tests {
107 t.Run(fmt.Sprintf("%v", test), func(t *testing.T) {
108 if errs := ValidateDryRun(field.NewPath("dryRun"), test); len(errs) != 0 {
109 t.Errorf("%v should be a valid dry-run value: %v", test, errs)
110 }
111 })
112 }
113 }
114
115 func TestInvalidDryRun(t *testing.T) {
116 tests := [][]string{
117 {"False"},
118 {"All", "False"},
119 }
120
121 for _, test := range tests {
122 t.Run(fmt.Sprintf("%v", test), func(t *testing.T) {
123 if len(ValidateDryRun(field.NewPath("dryRun"), test)) == 0 {
124 t.Errorf("%v shouldn't be a valid dry-run value", test)
125 }
126 })
127 }
128
129 }
130
131 func boolPtr(b bool) *bool {
132 return &b
133 }
134
135 func TestValidPatchOptions(t *testing.T) {
136 tests := []struct {
137 opts metav1.PatchOptions
138 patchType types.PatchType
139 }{{
140 opts: metav1.PatchOptions{
141 Force: boolPtr(true),
142 FieldManager: "kubectl",
143 },
144 patchType: types.ApplyPatchType,
145 }, {
146 opts: metav1.PatchOptions{
147 FieldManager: "kubectl",
148 },
149 patchType: types.ApplyPatchType,
150 }, {
151 opts: metav1.PatchOptions{},
152 patchType: types.MergePatchType,
153 }, {
154 opts: metav1.PatchOptions{
155 FieldManager: "patcher",
156 },
157 patchType: types.MergePatchType,
158 }}
159
160 for _, test := range tests {
161 t.Run(fmt.Sprintf("%v", test.opts), func(t *testing.T) {
162 errs := ValidatePatchOptions(&test.opts, test.patchType)
163 if len(errs) != 0 {
164 t.Fatalf("Expected no failures, got: %v", errs)
165 }
166 })
167 }
168 }
169
170 func TestInvalidPatchOptions(t *testing.T) {
171 tests := []struct {
172 opts metav1.PatchOptions
173 patchType types.PatchType
174 }{
175
176 {
177 opts: metav1.PatchOptions{},
178 patchType: types.ApplyPatchType,
179 },
180
181 {
182 opts: metav1.PatchOptions{
183 Force: boolPtr(true),
184 },
185 patchType: types.MergePatchType,
186 },
187
188 {
189 opts: metav1.PatchOptions{
190 FieldManager: "kubectl",
191 Force: boolPtr(false),
192 },
193 patchType: types.MergePatchType,
194 },
195 }
196
197 for _, test := range tests {
198 t.Run(fmt.Sprintf("%v", test.opts), func(t *testing.T) {
199 errs := ValidatePatchOptions(&test.opts, test.patchType)
200 if len(errs) == 0 {
201 t.Fatal("Expected failures, got none.")
202 }
203 })
204 }
205 }
206
207 func TestValidateFieldManagerValid(t *testing.T) {
208 tests := []string{
209 "filedManager",
210 "你好",
211 "🍔",
212 }
213
214 for _, test := range tests {
215 t.Run(test, func(t *testing.T) {
216 errs := ValidateFieldManager(test, field.NewPath("fieldManager"))
217 if len(errs) != 0 {
218 t.Errorf("Validation failed: %v", errs)
219 }
220 })
221 }
222 }
223
224 func TestValidateFieldManagerInvalid(t *testing.T) {
225 tests := []string{
226 "field\nmanager",
227 "fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
228 }
229
230 for _, test := range tests {
231 t.Run(test, func(t *testing.T) {
232 errs := ValidateFieldManager(test, field.NewPath("fieldManager"))
233 if len(errs) == 0 {
234 t.Errorf("Validation should have failed")
235 }
236 })
237 }
238 }
239
240 func TestValidateManagedFieldsInvalid(t *testing.T) {
241 tests := []metav1.ManagedFieldsEntry{{
242 Operation: metav1.ManagedFieldsOperationUpdate,
243 FieldsType: "RandomVersion",
244 APIVersion: "v1",
245 }, {
246 Operation: "RandomOperation",
247 FieldsType: "FieldsV1",
248 APIVersion: "v1",
249 }, {
250
251 FieldsType: "FieldsV1",
252 APIVersion: "v1",
253 }, {
254 Operation: metav1.ManagedFieldsOperationUpdate,
255 FieldsType: "FieldsV1",
256
257 Manager: "field\nmanager",
258 APIVersion: "v1",
259 }, {
260 Operation: metav1.ManagedFieldsOperationApply,
261 FieldsType: "FieldsV1",
262 APIVersion: "v1",
263 Subresource: "TooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLongTooLong",
264 }}
265
266 for _, test := range tests {
267 t.Run(fmt.Sprintf("%#v", test), func(t *testing.T) {
268 errs := ValidateManagedFields([]metav1.ManagedFieldsEntry{test}, field.NewPath("managedFields"))
269 if len(errs) == 0 {
270 t.Errorf("Validation should have failed")
271 }
272 })
273 }
274 }
275
276 func TestValidateMangedFieldsValid(t *testing.T) {
277 tests := []metav1.ManagedFieldsEntry{{
278 Operation: metav1.ManagedFieldsOperationUpdate,
279 APIVersion: "v1",
280
281 }, {
282 Operation: metav1.ManagedFieldsOperationUpdate,
283 FieldsType: "FieldsV1",
284 APIVersion: "v1",
285 }, {
286 Operation: metav1.ManagedFieldsOperationApply,
287 FieldsType: "FieldsV1",
288 APIVersion: "v1",
289 Subresource: "scale",
290 }, {
291 Operation: metav1.ManagedFieldsOperationApply,
292 FieldsType: "FieldsV1",
293 APIVersion: "v1",
294 Manager: "🍔",
295 }}
296
297 for _, test := range tests {
298 t.Run(fmt.Sprintf("%#v", test), func(t *testing.T) {
299 err := ValidateManagedFields([]metav1.ManagedFieldsEntry{test}, field.NewPath("managedFields"))
300 if err != nil {
301 t.Errorf("Validation failed: %v", err)
302 }
303 })
304 }
305 }
306
307 func TestValidateConditions(t *testing.T) {
308 tests := []struct {
309 name string
310 conditions []metav1.Condition
311 validateErrs func(t *testing.T, errs field.ErrorList)
312 }{{
313 name: "bunch-of-invalid-fields",
314 conditions: []metav1.Condition{{
315 Type: ":invalid",
316 Status: "unknown",
317 ObservedGeneration: -1,
318 LastTransitionTime: metav1.Time{},
319 Reason: "invalid;val",
320 Message: "",
321 }},
322 validateErrs: func(t *testing.T, errs field.ErrorList) {
323 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]')`
324 if !hasError(errs, needle) {
325 t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
326 }
327 needle = `status.conditions[0].status: Unsupported value: "unknown": supported values: "False", "True", "Unknown"`
328 if !hasError(errs, needle) {
329 t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
330 }
331 needle = `status.conditions[0].observedGeneration: Invalid value: -1: must be greater than or equal to zero`
332 if !hasError(errs, needle) {
333 t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
334 }
335 needle = `status.conditions[0].lastTransitionTime: Required value: must be set`
336 if !hasError(errs, needle) {
337 t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
338 }
339 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_])?')`
340 if !hasError(errs, needle) {
341 t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
342 }
343 },
344 }, {
345 name: "duplicates",
346 conditions: []metav1.Condition{{
347 Type: "First",
348 }, {
349 Type: "Second",
350 }, {
351 Type: "First",
352 }},
353 validateErrs: func(t *testing.T, errs field.ErrorList) {
354 needle := `status.conditions[2].type: Duplicate value: "First"`
355 if !hasError(errs, needle) {
356 t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
357 }
358 },
359 }, {
360 name: "colon-allowed-in-reason",
361 conditions: []metav1.Condition{{
362 Type: "First",
363 Reason: "valid:val",
364 }},
365 validateErrs: func(t *testing.T, errs field.ErrorList) {
366 needle := `status.conditions[0].reason`
367 if hasPrefixError(errs, needle) {
368 t.Errorf("has %q in\n%v", needle, errorsAsString(errs))
369 }
370 },
371 }, {
372 name: "comma-allowed-in-reason",
373 conditions: []metav1.Condition{{
374 Type: "First",
375 Reason: "valid,val",
376 }},
377 validateErrs: func(t *testing.T, errs field.ErrorList) {
378 needle := `status.conditions[0].reason`
379 if hasPrefixError(errs, needle) {
380 t.Errorf("has %q in\n%v", needle, errorsAsString(errs))
381 }
382 },
383 }, {
384 name: "reason-does-not-end-in-delimiter",
385 conditions: []metav1.Condition{{
386 Type: "First",
387 Reason: "valid,val:",
388 }},
389 validateErrs: func(t *testing.T, errs field.ErrorList) {
390 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_])?')`
391 if !hasError(errs, needle) {
392 t.Errorf("missing %q in\n%v", needle, errorsAsString(errs))
393 }
394 },
395 }}
396
397 for _, test := range tests {
398 t.Run(test.name, func(t *testing.T) {
399 errs := ValidateConditions(test.conditions, field.NewPath("status").Child("conditions"))
400 test.validateErrs(t, errs)
401 })
402 }
403 }
404
405 func TestLabelSelectorMatchExpression(t *testing.T) {
406 testCases := []struct {
407 name string
408 labelSelector *metav1.LabelSelector
409 wantErrorNumber int
410 validateErrs func(t *testing.T, errs field.ErrorList)
411 }{{
412 name: "Valid LabelSelector",
413 labelSelector: &metav1.LabelSelector{
414 MatchExpressions: []metav1.LabelSelectorRequirement{{
415 Key: "key",
416 Operator: metav1.LabelSelectorOpIn,
417 Values: []string{"value"},
418 }},
419 },
420 wantErrorNumber: 0,
421 validateErrs: nil,
422 }, {
423 name: "MatchExpression's key name isn't valid",
424 labelSelector: &metav1.LabelSelector{
425 MatchExpressions: []metav1.LabelSelectorRequirement{{
426 Key: "-key",
427 Operator: metav1.LabelSelectorOpIn,
428 Values: []string{"value"},
429 }},
430 },
431 wantErrorNumber: 1,
432 validateErrs: func(t *testing.T, errs field.ErrorList) {
433 errMessage := "name part must consist of alphanumeric characters"
434 if !partStringInErrorMessage(errs, errMessage) {
435 t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs))
436 }
437 },
438 }, {
439 name: "MatchExpression's operator isn't valid",
440 labelSelector: &metav1.LabelSelector{
441 MatchExpressions: []metav1.LabelSelectorRequirement{{
442 Key: "key",
443 Operator: "abc",
444 Values: []string{"value"},
445 }},
446 },
447 wantErrorNumber: 1,
448 validateErrs: func(t *testing.T, errs field.ErrorList) {
449 errMessage := "not a valid selector operator"
450 if !partStringInErrorMessage(errs, errMessage) {
451 t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs))
452 }
453 },
454 }, {
455 name: "MatchExpression's value name isn't valid",
456 labelSelector: &metav1.LabelSelector{
457 MatchExpressions: []metav1.LabelSelectorRequirement{{
458 Key: "key",
459 Operator: metav1.LabelSelectorOpIn,
460 Values: []string{"-value"},
461 }},
462 },
463 wantErrorNumber: 1,
464 validateErrs: func(t *testing.T, errs field.ErrorList) {
465 errMessage := "a valid label must be an empty string or consist of"
466 if !partStringInErrorMessage(errs, errMessage) {
467 t.Errorf("missing %q in\n%v", errMessage, errorsAsString(errs))
468 }
469 },
470 }}
471 for index, testCase := range testCases {
472 t.Run(testCase.name, func(t *testing.T) {
473 allErrs := ValidateLabelSelector(testCase.labelSelector, LabelSelectorValidationOptions{false}, field.NewPath("labelSelector"))
474 if len(allErrs) != testCase.wantErrorNumber {
475 t.Errorf("case[%d]: expected failure", index)
476 }
477 if len(allErrs) >= 1 && testCase.validateErrs != nil {
478 testCase.validateErrs(t, allErrs)
479 }
480 })
481 }
482 }
483
484 func hasError(errs field.ErrorList, needle string) bool {
485 for _, curr := range errs {
486 if curr.Error() == needle {
487 return true
488 }
489 }
490 return false
491 }
492
493 func hasPrefixError(errs field.ErrorList, prefix string) bool {
494 for _, curr := range errs {
495 if strings.HasPrefix(curr.Error(), prefix) {
496 return true
497 }
498 }
499 return false
500 }
501
502 func partStringInErrorMessage(errs field.ErrorList, prefix string) bool {
503 for _, curr := range errs {
504 if strings.Contains(curr.Error(), prefix) {
505 return true
506 }
507 }
508 return false
509 }
510
511 func errorsAsString(errs field.ErrorList) string {
512 messages := []string{}
513 for _, curr := range errs {
514 messages = append(messages, curr.Error())
515 }
516 return strings.Join(messages, "\n")
517 }
518
View as plain text