1
16
17 package util
18
19 import (
20 goerrors "errors"
21 "fmt"
22 "net/http"
23 "os"
24 "strings"
25 "syscall"
26 "testing"
27
28 "github.com/google/go-cmp/cmp"
29 "github.com/google/go-cmp/cmp/cmpopts"
30 "github.com/spf13/cobra"
31
32 corev1 "k8s.io/api/core/v1"
33 apiequality "k8s.io/apimachinery/pkg/api/equality"
34 "k8s.io/apimachinery/pkg/api/errors"
35 "k8s.io/apimachinery/pkg/api/meta"
36 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37 "k8s.io/apimachinery/pkg/runtime"
38 "k8s.io/apimachinery/pkg/runtime/schema"
39 "k8s.io/apimachinery/pkg/util/validation/field"
40 "k8s.io/kubectl/pkg/scheme"
41 "k8s.io/utils/exec"
42 )
43
44 func TestMerge(t *testing.T) {
45 tests := []struct {
46 obj runtime.Object
47 fragment string
48 expected runtime.Object
49 expectErr bool
50 }{
51 {
52 obj: &corev1.Pod{
53 ObjectMeta: metav1.ObjectMeta{
54 Name: "foo",
55 },
56 },
57 fragment: fmt.Sprintf(`{ "apiVersion": "%s" }`, "v1"),
58 expected: &corev1.Pod{
59 TypeMeta: metav1.TypeMeta{
60 Kind: "Pod",
61 APIVersion: "v1",
62 },
63 ObjectMeta: metav1.ObjectMeta{
64 Name: "foo",
65 },
66 Spec: corev1.PodSpec{},
67 },
68 },
69 {
70 obj: &corev1.Pod{
71 ObjectMeta: metav1.ObjectMeta{
72 Name: "foo",
73 },
74 },
75 fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "volumes": [ {"name": "v1"}, {"name": "v2"} ] } }`, "v1"),
76 expected: &corev1.Pod{
77 TypeMeta: metav1.TypeMeta{
78 Kind: "Pod",
79 APIVersion: "v1",
80 },
81 ObjectMeta: metav1.ObjectMeta{
82 Name: "foo",
83 },
84 Spec: corev1.PodSpec{
85 Volumes: []corev1.Volume{
86 {
87 Name: "v1",
88 },
89 {
90 Name: "v2",
91 },
92 },
93 },
94 },
95 },
96 {
97 obj: &corev1.Pod{},
98 fragment: "invalid json",
99 expected: &corev1.Pod{},
100 expectErr: true,
101 },
102 {
103 obj: &corev1.Service{},
104 fragment: `{ "apiVersion": "badVersion" }`,
105 expectErr: true,
106 },
107 {
108 obj: &corev1.Service{
109 Spec: corev1.ServiceSpec{},
110 },
111 fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "ports": [ { "port": 0 } ] } }`, "v1"),
112 expected: &corev1.Service{
113 TypeMeta: metav1.TypeMeta{
114 Kind: "Service",
115 APIVersion: "v1",
116 },
117 Spec: corev1.ServiceSpec{
118 Ports: []corev1.ServicePort{
119 {
120 Port: 0,
121 },
122 },
123 },
124 },
125 },
126 {
127 obj: &corev1.Service{
128 Spec: corev1.ServiceSpec{
129 Selector: map[string]string{
130 "version": "v1",
131 },
132 },
133 },
134 fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "selector": { "version": "v2" } } }`, "v1"),
135 expected: &corev1.Service{
136 TypeMeta: metav1.TypeMeta{
137 Kind: "Service",
138 APIVersion: "v1",
139 },
140 Spec: corev1.ServiceSpec{
141 Selector: map[string]string{
142 "version": "v2",
143 },
144 },
145 },
146 },
147 }
148
149 codec := runtime.NewCodec(scheme.DefaultJSONEncoder(),
150 scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...))
151 for i, test := range tests {
152 out, err := Merge(codec, test.obj, test.fragment)
153 if !test.expectErr {
154 if err != nil {
155 t.Errorf("testcase[%d], unexpected error: %v", i, err)
156 } else if !apiequality.Semantic.DeepEqual(test.expected, out) {
157 t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out))
158 }
159 }
160 if test.expectErr && err == nil {
161 t.Errorf("testcase[%d], unexpected non-error", i)
162 }
163 }
164 }
165
166 func TestStrategicMerge(t *testing.T) {
167 tests := []struct {
168 obj runtime.Object
169 dataStruct runtime.Object
170 fragment string
171 expected runtime.Object
172 expectErr bool
173 }{
174 {
175 obj: &corev1.Pod{
176 ObjectMeta: metav1.ObjectMeta{
177 Name: "foo",
178 },
179 Spec: corev1.PodSpec{
180 Containers: []corev1.Container{
181 {
182 Name: "c1",
183 Image: "red-image",
184 },
185 {
186 Name: "c2",
187 Image: "blue-image",
188 },
189 },
190 },
191 },
192 dataStruct: &corev1.Pod{},
193 fragment: fmt.Sprintf(`{ "apiVersion": "%s", "spec": { "containers": [ { "name": "c1", "image": "green-image" } ] } }`,
194 schema.GroupVersion{Group: "", Version: "v1"}.String()),
195 expected: &corev1.Pod{
196 TypeMeta: metav1.TypeMeta{
197 Kind: "Pod",
198 APIVersion: "v1",
199 },
200 ObjectMeta: metav1.ObjectMeta{
201 Name: "foo",
202 },
203 Spec: corev1.PodSpec{
204 Containers: []corev1.Container{
205 {
206 Name: "c1",
207 Image: "green-image",
208 },
209 {
210 Name: "c2",
211 Image: "blue-image",
212 },
213 },
214 },
215 },
216 },
217 {
218 obj: &corev1.Pod{},
219 dataStruct: &corev1.Pod{},
220 fragment: "invalid json",
221 expected: &corev1.Pod{},
222 expectErr: true,
223 },
224 {
225 obj: &corev1.Service{},
226 dataStruct: &corev1.Pod{},
227 fragment: `{ "apiVersion": "badVersion" }`,
228 expectErr: true,
229 },
230 }
231
232 codec := runtime.NewCodec(scheme.DefaultJSONEncoder(),
233 scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...))
234 for i, test := range tests {
235 out, err := StrategicMerge(codec, test.obj, test.fragment, test.dataStruct)
236 if !test.expectErr {
237 if err != nil {
238 t.Errorf("testcase[%d], unexpected error: %v", i, err)
239 } else if !apiequality.Semantic.DeepEqual(test.expected, out) {
240 t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out))
241 }
242 }
243 if test.expectErr && err == nil {
244 t.Errorf("testcase[%d], unexpected non-error", i)
245 }
246 }
247 }
248
249 func TestJSONPatch(t *testing.T) {
250 tests := []struct {
251 obj runtime.Object
252 fragment string
253 expected runtime.Object
254 expectErr bool
255 }{
256 {
257 obj: &corev1.Pod{
258 ObjectMeta: metav1.ObjectMeta{
259 Name: "foo",
260 Labels: map[string]string{
261 "run": "test",
262 },
263 },
264 },
265 fragment: `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"} ]`,
266 expected: &corev1.Pod{
267 TypeMeta: metav1.TypeMeta{
268 Kind: "Pod",
269 APIVersion: "v1",
270 },
271 ObjectMeta: metav1.ObjectMeta{
272 Name: "foo",
273 Labels: map[string]string{
274 "run": "test",
275 "foo": "bar",
276 },
277 },
278 Spec: corev1.PodSpec{},
279 },
280 },
281 {
282 obj: &corev1.Pod{},
283 fragment: "invalid json",
284 expected: &corev1.Pod{},
285 expectErr: true,
286 },
287 {
288 obj: &corev1.Pod{},
289 fragment: `[ {"op": "add", "path": "/metadata/labels/foo", "value": "bar"} ]`,
290 expectErr: true,
291 },
292 {
293 obj: &corev1.Pod{
294 ObjectMeta: metav1.ObjectMeta{
295 Name: "foo",
296 Finalizers: []string{"foo", "bar", "test"},
297 },
298 },
299 fragment: `[ {"op": "replace", "path": "/metadata/finalizers/-1", "value": "baz"} ]`,
300 expected: &corev1.Pod{
301 TypeMeta: metav1.TypeMeta{
302 Kind: "Pod",
303 APIVersion: "v1",
304 },
305 ObjectMeta: metav1.ObjectMeta{
306 Name: "foo",
307 Finalizers: []string{"foo", "bar", "baz"},
308 },
309 Spec: corev1.PodSpec{},
310 },
311 },
312 }
313
314 codec := runtime.NewCodec(scheme.DefaultJSONEncoder(),
315 scheme.Codecs.UniversalDecoder(scheme.Scheme.PrioritizedVersionsAllGroups()...))
316 for i, test := range tests {
317 out, err := JSONPatch(codec, test.obj, test.fragment)
318 if !test.expectErr {
319 if err != nil {
320 t.Errorf("testcase[%d], unexpected error: %v", i, err)
321 } else if !apiequality.Semantic.DeepEqual(test.expected, out) {
322 t.Errorf("\n\ntestcase[%d]\nexpected:\n%s", i, cmp.Diff(test.expected, out))
323 }
324 }
325 if test.expectErr && err == nil {
326 t.Errorf("testcase[%d], unexpected non-error", i)
327 }
328 }
329 }
330
331 type checkErrTestCase struct {
332 err error
333 expectedErr string
334 expectedCode int
335 }
336
337 func TestCheckInvalidErr(t *testing.T) {
338 testCheckError(t, []checkErrTestCase{
339 {
340 errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid1").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field"), "single", "details")}),
341 "The Invalid1 \"invalidation\" is invalid: field: Invalid value: \"single\": details\n",
342 DefaultErrorExitCode,
343 },
344 {
345 errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid2").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field1"), "multi1", "details"), field.Invalid(field.NewPath("field2"), "multi2", "details")}),
346 "The Invalid2 \"invalidation\" is invalid: \n* field1: Invalid value: \"multi1\": details\n* field2: Invalid value: \"multi2\": details\n",
347 DefaultErrorExitCode,
348 },
349 {
350 errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid3").GroupKind(), "invalidation", field.ErrorList{}),
351 "The Invalid3 \"invalidation\" is invalid",
352 DefaultErrorExitCode,
353 },
354 {
355 errors.NewInvalid(corev1.SchemeGroupVersion.WithKind("Invalid4").GroupKind(), "invalidation", field.ErrorList{field.Invalid(field.NewPath("field4"), "multi4", "details"), field.Invalid(field.NewPath("field4"), "multi4", "details")}),
356 "The Invalid4 \"invalidation\" is invalid: field4: Invalid value: \"multi4\": details\n",
357 DefaultErrorExitCode,
358 },
359 {
360 &errors.StatusError{ErrStatus: metav1.Status{
361 Status: metav1.StatusFailure,
362 Code: http.StatusUnprocessableEntity,
363 Reason: metav1.StatusReasonInvalid,
364
365 }},
366 "The request is invalid",
367 DefaultErrorExitCode,
368 },
369
370 {
371 &errors.StatusError{ErrStatus: metav1.Status{
372 Status: metav1.StatusFailure,
373 Code: http.StatusUnprocessableEntity,
374 Reason: metav1.StatusReasonInvalid,
375
376 Message: "Some message",
377 }},
378 "The request is invalid: Some message",
379 DefaultErrorExitCode,
380 },
381
382 {
383 &errors.StatusError{ErrStatus: metav1.Status{
384 Status: "Failure",
385 Message: `admission webhook "my.webhook" denied the request without explanation`,
386 Code: 422,
387 }},
388 `Error from server: admission webhook "my.webhook" denied the request without explanation`,
389 DefaultErrorExitCode,
390 },
391
392 {
393 &errors.StatusError{ErrStatus: metav1.Status{
394 Status: "Failure",
395 Message: `admission webhook "my.webhook" denied the request without explanation`,
396 Code: 422,
397 Details: &metav1.StatusDetails{},
398 }},
399 `Error from server: admission webhook "my.webhook" denied the request without explanation`,
400 DefaultErrorExitCode,
401 },
402
403 {
404 AddSourceToErr("creating", "configmap.yaml", &errors.StatusError{ErrStatus: metav1.Status{
405 Status: "Failure",
406 Message: `admission webhook "my.webhook" denied the request without explanation`,
407 Code: 422,
408 }}),
409 `Error from server: error when creating "configmap.yaml": admission webhook "my.webhook" denied the request without explanation`,
410 DefaultErrorExitCode,
411 },
412
413 {
414 &errors.StatusError{ErrStatus: metav1.Status{
415 Status: "Failure",
416 Reason: "Invalid",
417 Message: `admission webhook "my.webhook" denied the request without explanation`,
418 Code: 422,
419 }},
420 `The request is invalid: admission webhook "my.webhook" denied the request without explanation`,
421 DefaultErrorExitCode,
422 },
423 })
424 }
425
426 func TestCheckNoResourceMatchError(t *testing.T) {
427 testCheckError(t, []checkErrTestCase{
428 {
429 &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Resource: "foo"}},
430 `the server doesn't have a resource type "foo"`,
431 DefaultErrorExitCode,
432 },
433 {
434 &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Version: "theversion", Resource: "foo"}},
435 `the server doesn't have a resource type "foo" in version "theversion"`,
436 DefaultErrorExitCode,
437 },
438 {
439 &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "thegroup", Version: "theversion", Resource: "foo"}},
440 `the server doesn't have a resource type "foo" in group "thegroup" and version "theversion"`,
441 DefaultErrorExitCode,
442 },
443 {
444 &meta.NoResourceMatchError{PartialResource: schema.GroupVersionResource{Group: "thegroup", Resource: "foo"}},
445 `the server doesn't have a resource type "foo" in group "thegroup"`,
446 DefaultErrorExitCode,
447 },
448 })
449 }
450
451 func TestCheckExitError(t *testing.T) {
452 testCheckError(t, []checkErrTestCase{
453 {
454 exec.CodeExitError{Err: fmt.Errorf("pod foo/bar terminated"), Code: 42},
455 "pod foo/bar terminated",
456 42,
457 },
458 })
459 }
460
461 func testCheckError(t *testing.T, tests []checkErrTestCase) {
462 var errReturned string
463 var codeReturned int
464 errHandle := func(err string, code int) {
465 errReturned = err
466 codeReturned = code
467 }
468
469 for _, test := range tests {
470 checkErr(test.err, errHandle)
471
472 if errReturned != test.expectedErr {
473 t.Fatalf("Got: %s, expected: %s", errReturned, test.expectedErr)
474 }
475 if codeReturned != test.expectedCode {
476 t.Fatalf("Got: %d, expected: %d", codeReturned, test.expectedCode)
477 }
478 }
479 }
480
481 func TestDumpReaderToFile(t *testing.T) {
482 testString := "TEST STRING"
483 tempFile, err := os.CreateTemp(os.TempDir(), "hlpers_test_dump_")
484 if err != nil {
485 t.Errorf("unexpected error setting up a temporary file %v", err)
486 }
487 defer syscall.Unlink(tempFile.Name())
488 defer tempFile.Close()
489 defer func() {
490 if !t.Failed() {
491 os.Remove(tempFile.Name())
492 }
493 }()
494 err = DumpReaderToFile(strings.NewReader(testString), tempFile.Name())
495 if err != nil {
496 t.Errorf("error in DumpReaderToFile: %v", err)
497 }
498 data, err := os.ReadFile(tempFile.Name())
499 if err != nil {
500 t.Errorf("error when reading %s: %v", tempFile.Name(), err)
501 }
502 stringData := string(data)
503 if stringData != testString {
504 t.Fatalf("Wrong file content %s != %s", testString, stringData)
505 }
506 }
507
508 func TestDifferenceFunc(t *testing.T) {
509 tests := []struct {
510 name string
511 fullArray []string
512 subArray []string
513 expected []string
514 }{
515 {
516 name: "remove some",
517 fullArray: []string{"a", "b", "c", "d"},
518 subArray: []string{"c", "b"},
519 expected: []string{"a", "d"},
520 },
521 {
522 name: "remove all",
523 fullArray: []string{"a", "b", "c", "d"},
524 subArray: []string{"b", "d", "a", "c"},
525 expected: nil,
526 },
527 {
528 name: "remove none",
529 fullArray: []string{"a", "b", "c", "d"},
530 subArray: nil,
531 expected: []string{"a", "b", "c", "d"},
532 },
533 }
534
535 for _, tc := range tests {
536 result := Difference(tc.fullArray, tc.subArray)
537 if !cmp.Equal(tc.expected, result, cmpopts.SortSlices(func(x, y string) bool {
538 return x < y
539 })) {
540 t.Errorf("%s -> Expected: %v, but got: %v", tc.name, tc.expected, result)
541 }
542 }
543 }
544
545 func TestGetValidationDirective(t *testing.T) {
546 tests := []struct {
547 validateFlag string
548 expectedDirective string
549 expectedErr error
550 }{
551 {
552 expectedDirective: metav1.FieldValidationStrict,
553 },
554 {
555 validateFlag: "true",
556 expectedDirective: metav1.FieldValidationStrict,
557 },
558 {
559 validateFlag: "True",
560 expectedDirective: metav1.FieldValidationStrict,
561 },
562 {
563 validateFlag: "strict",
564 expectedDirective: metav1.FieldValidationStrict,
565 },
566 {
567 validateFlag: "warn",
568 expectedDirective: metav1.FieldValidationWarn,
569 },
570 {
571 validateFlag: "ignore",
572 expectedDirective: metav1.FieldValidationIgnore,
573 },
574 {
575 validateFlag: "false",
576 expectedDirective: metav1.FieldValidationIgnore,
577 },
578 {
579 validateFlag: "False",
580 expectedDirective: metav1.FieldValidationIgnore,
581 },
582 {
583 validateFlag: "foo",
584 expectedDirective: metav1.FieldValidationStrict,
585 expectedErr: goerrors.New(`invalid - validate option "foo"; must be one of: strict (or true), warn, ignore (or false)`),
586 },
587 }
588
589 for _, tc := range tests {
590 cmd := &cobra.Command{}
591 AddValidateFlags(cmd)
592 if tc.validateFlag != "" {
593 cmd.Flags().Set("validate", tc.validateFlag)
594 }
595 directive, err := GetValidationDirective(cmd)
596 if directive != tc.expectedDirective {
597 t.Errorf("validation directive, expected: %v, but got: %v", tc.expectedDirective, directive)
598 }
599 if tc.expectedErr != nil {
600 if err.Error() != tc.expectedErr.Error() {
601 t.Errorf("GetValidationDirective error, expected: %v, but got: %v", tc.expectedErr, err)
602 }
603 } else {
604 if err != nil {
605 t.Errorf("expecte no error, but got: %v", err)
606 }
607 }
608
609 }
610 }
611
View as plain text