1
16
17 package conversion
18
19 import (
20 "reflect"
21 "strings"
22 "testing"
23
24 "github.com/google/go-cmp/cmp"
25
26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28 "k8s.io/apimachinery/pkg/runtime"
29 "k8s.io/apimachinery/pkg/util/validation"
30
31 v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
32 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
33 )
34
35 func TestRestoreObjectMeta(t *testing.T) {
36 tests := []struct {
37 name string
38 original map[string]interface{}
39 converted map[string]interface{}
40 expected map[string]interface{}
41 expectedError bool
42 }{
43 {"no converted metadata",
44 map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
45 map[string]interface{}{"spec": map[string]interface{}{}},
46 map[string]interface{}{"spec": map[string]interface{}{}},
47 true,
48 },
49 {"invalid converted metadata",
50 map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
51 map[string]interface{}{"metadata": []interface{}{"foo"}},
52 map[string]interface{}{"metadata": []interface{}{"foo"}},
53 true,
54 },
55 {"no original metadata",
56 map[string]interface{}{"spec": map[string]interface{}{}},
57 map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
58 map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
59 false,
60 },
61 {"invalid original metadata",
62 map[string]interface{}{"metadata": []interface{}{"foo"}},
63 map[string]interface{}{"metadata": map[string]interface{}{}, "spec": map[string]interface{}{}},
64 map[string]interface{}{"metadata": []interface{}{"foo"}, "spec": map[string]interface{}{}},
65 true,
66 },
67 {"changed label, annotations and non-label",
68 map[string]interface{}{"metadata": map[string]interface{}{
69 "foo": "bar",
70 "labels": map[string]interface{}{"a": "A", "b": "B"},
71 "annotations": map[string]interface{}{"a": "1", "b": "2"},
72 }, "spec": map[string]interface{}{}},
73 map[string]interface{}{"metadata": map[string]interface{}{
74 "foo": "abc",
75 "labels": map[string]interface{}{"a": "AA", "b": "B"},
76 "annotations": map[string]interface{}{"a": "1", "b": "22"},
77 }, "spec": map[string]interface{}{}},
78 map[string]interface{}{"metadata": map[string]interface{}{
79 "foo": "bar",
80 "labels": map[string]interface{}{"a": "AA", "b": "B"},
81 "annotations": map[string]interface{}{"a": "1", "b": "22"},
82 }, "spec": map[string]interface{}{}},
83 false,
84 },
85 {"added labels and annotations",
86 map[string]interface{}{"metadata": map[string]interface{}{
87 "foo": "bar",
88 }, "spec": map[string]interface{}{}},
89 map[string]interface{}{"metadata": map[string]interface{}{
90 "foo": "abc",
91 "labels": map[string]interface{}{"a": "AA", "b": "B"},
92 "annotations": map[string]interface{}{"a": "1", "b": "22"},
93 }, "spec": map[string]interface{}{}},
94 map[string]interface{}{"metadata": map[string]interface{}{
95 "foo": "bar",
96 "labels": map[string]interface{}{"a": "AA", "b": "B"},
97 "annotations": map[string]interface{}{"a": "1", "b": "22"},
98 }, "spec": map[string]interface{}{}},
99 false,
100 },
101 {"added labels and annotations, with nil before",
102 map[string]interface{}{"metadata": map[string]interface{}{
103 "foo": "bar",
104 "labels": nil,
105 "annotations": nil,
106 }, "spec": map[string]interface{}{}},
107 map[string]interface{}{"metadata": map[string]interface{}{
108 "foo": "abc",
109 "labels": map[string]interface{}{"a": "AA", "b": "B"},
110 "annotations": map[string]interface{}{"a": "1", "b": "22"},
111 }, "spec": map[string]interface{}{}},
112 map[string]interface{}{"metadata": map[string]interface{}{
113 "foo": "bar",
114 "labels": map[string]interface{}{"a": "AA", "b": "B"},
115 "annotations": map[string]interface{}{"a": "1", "b": "22"},
116 }, "spec": map[string]interface{}{}},
117 false,
118 },
119 {"removed labels and annotations",
120 map[string]interface{}{"metadata": map[string]interface{}{
121 "foo": "bar",
122 "labels": map[string]interface{}{"a": "AA", "b": "B"},
123 "annotations": map[string]interface{}{"a": "1", "b": "22"},
124 }, "spec": map[string]interface{}{}},
125 map[string]interface{}{"metadata": map[string]interface{}{
126 "foo": "abc",
127 }, "spec": map[string]interface{}{}},
128 map[string]interface{}{"metadata": map[string]interface{}{
129 "foo": "bar",
130 }, "spec": map[string]interface{}{}},
131 false,
132 },
133 {"nil'ed labels and annotations",
134 map[string]interface{}{"metadata": map[string]interface{}{
135 "foo": "bar",
136 "labels": map[string]interface{}{"a": "AA", "b": "B"},
137 "annotations": map[string]interface{}{"a": "1", "b": "22"},
138 }, "spec": map[string]interface{}{}},
139 map[string]interface{}{"metadata": map[string]interface{}{
140 "foo": "abc",
141 "labels": nil,
142 "annotations": nil,
143 }, "spec": map[string]interface{}{}},
144 map[string]interface{}{"metadata": map[string]interface{}{
145 "foo": "bar",
146 }, "spec": map[string]interface{}{}},
147 false,
148 },
149 {"added labels and annotations",
150 map[string]interface{}{"metadata": map[string]interface{}{
151 "foo": "bar",
152 }, "spec": map[string]interface{}{}},
153 map[string]interface{}{"metadata": map[string]interface{}{
154 "foo": "abc",
155 "labels": map[string]interface{}{"a": nil, "b": "B"},
156 "annotations": map[string]interface{}{"a": nil, "b": "22"},
157 }, "spec": map[string]interface{}{}},
158 map[string]interface{}{"metadata": map[string]interface{}{
159 "foo": "bar",
160 }, "spec": map[string]interface{}{}},
161 true,
162 },
163 {"invalid label key",
164 map[string]interface{}{"metadata": map[string]interface{}{}},
165 map[string]interface{}{"metadata": map[string]interface{}{"labels": map[string]interface{}{"some/non-qualified/label": "x"}}},
166 map[string]interface{}{"metadata": map[string]interface{}{}},
167 true,
168 },
169 {"invalid annotation key",
170 map[string]interface{}{"metadata": map[string]interface{}{}},
171 map[string]interface{}{"metadata": map[string]interface{}{"labels": map[string]interface{}{"some/non-qualified/label": "x"}}},
172 map[string]interface{}{"metadata": map[string]interface{}{}},
173 true,
174 },
175 {"invalid label value",
176 map[string]interface{}{"metadata": map[string]interface{}{}},
177 map[string]interface{}{"metadata": map[string]interface{}{"labels": map[string]interface{}{"foo": "üäö"}}},
178 map[string]interface{}{"metadata": map[string]interface{}{}},
179 true,
180 },
181 {"too big label value",
182 map[string]interface{}{"metadata": map[string]interface{}{}},
183 map[string]interface{}{"metadata": map[string]interface{}{"labels": map[string]interface{}{"foo": strings.Repeat("x", validation.LabelValueMaxLength+1)}}},
184 map[string]interface{}{"metadata": map[string]interface{}{}},
185 true,
186 },
187 {"too big annotation value",
188 map[string]interface{}{"metadata": map[string]interface{}{}},
189 map[string]interface{}{"metadata": map[string]interface{}{"annotations": map[string]interface{}{"foo": strings.Repeat("x", 256*(1<<10)+1)}}},
190 map[string]interface{}{"metadata": map[string]interface{}{}},
191 true,
192 },
193 }
194 for _, tt := range tests {
195 t.Run(tt.name, func(t *testing.T) {
196 if err := restoreObjectMeta(&unstructured.Unstructured{Object: tt.original}, &unstructured.Unstructured{Object: tt.converted}); err == nil && tt.expectedError {
197 t.Fatalf("expected error, but didn't get one")
198 } else if err != nil && !tt.expectedError {
199 t.Fatalf("unexpected error: %v", err)
200 }
201
202 if !reflect.DeepEqual(tt.converted, tt.expected) {
203 t.Errorf("unexpected result: %s", cmp.Diff(tt.expected, tt.converted))
204 }
205 })
206 }
207 }
208
209 func TestGetObjectsToConvert(t *testing.T) {
210 v1Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v1", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv1"}}}
211 v2Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v2", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv2"}}}
212 v3Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v3", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv3"}}}
213
214 testcases := []struct {
215 Name string
216 Object runtime.Object
217 APIVersion string
218
219 ExpectObjects []runtime.RawExtension
220 }{
221 {
222 Name: "empty list",
223 Object: &unstructured.UnstructuredList{},
224 APIVersion: "foo/v1",
225 ExpectObjects: nil,
226 },
227 {
228 Name: "one-item list, in desired version",
229 Object: &unstructured.UnstructuredList{
230 Items: []unstructured.Unstructured{*v1Object},
231 },
232 APIVersion: "foo/v1",
233 ExpectObjects: nil,
234 },
235 {
236 Name: "one-item list, not in desired version",
237 Object: &unstructured.UnstructuredList{
238 Items: []unstructured.Unstructured{*v2Object},
239 },
240 APIVersion: "foo/v1",
241 ExpectObjects: []runtime.RawExtension{{Object: v2Object}},
242 },
243 {
244 Name: "multi-item list, in desired version",
245 Object: &unstructured.UnstructuredList{
246 Items: []unstructured.Unstructured{*v1Object, *v1Object, *v1Object},
247 },
248 APIVersion: "foo/v1",
249 ExpectObjects: nil,
250 },
251 {
252 Name: "multi-item list, mixed versions",
253 Object: &unstructured.UnstructuredList{
254 Items: []unstructured.Unstructured{*v1Object, *v2Object, *v3Object},
255 },
256 APIVersion: "foo/v1",
257 ExpectObjects: []runtime.RawExtension{{Object: v2Object}, {Object: v3Object}},
258 },
259 {
260 Name: "single item, in desired version",
261 Object: v1Object,
262 APIVersion: "foo/v1",
263 ExpectObjects: nil,
264 },
265 {
266 Name: "single item, not in desired version",
267 Object: v2Object,
268 APIVersion: "foo/v1",
269 ExpectObjects: []runtime.RawExtension{{Object: v2Object}},
270 },
271 }
272 for _, tc := range testcases {
273 t.Run(tc.Name, func(t *testing.T) {
274 if objects := getObjectsToConvert(tc.Object, tc.APIVersion); !reflect.DeepEqual(objects, tc.ExpectObjects) {
275 t.Errorf("unexpected diff: %s", cmp.Diff(tc.ExpectObjects, objects))
276 }
277 })
278 }
279 }
280
281 func TestCreateConversionReviewObjects(t *testing.T) {
282 objects := []runtime.RawExtension{
283 {Object: &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v2", "Kind": "Widget"}}},
284 }
285
286 testcases := []struct {
287 Name string
288 Versions []string
289
290 ExpectRequest runtime.Object
291 ExpectResponse runtime.Object
292 ExpectErr string
293 }{
294 {
295 Name: "no supported versions",
296 Versions: []string{"vx"},
297 ExpectErr: "no supported conversion review versions",
298 },
299 {
300 Name: "v1",
301 Versions: []string{"v1", "v1beta1", "v2"},
302 ExpectRequest: &v1.ConversionReview{
303 Request: &v1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: objects},
304 Response: &v1.ConversionResponse{},
305 },
306 ExpectResponse: &v1.ConversionReview{},
307 },
308 {
309 Name: "v1beta1",
310 Versions: []string{"v1beta1", "v1", "v2"},
311 ExpectRequest: &v1beta1.ConversionReview{
312 Request: &v1beta1.ConversionRequest{UID: "uid", DesiredAPIVersion: "foo/v1", Objects: objects},
313 Response: &v1beta1.ConversionResponse{},
314 },
315 ExpectResponse: &v1beta1.ConversionReview{},
316 },
317 }
318
319 for _, tc := range testcases {
320 t.Run(tc.Name, func(t *testing.T) {
321 request, response, err := createConversionReviewObjects(tc.Versions, objects, "foo/v1", "uid")
322
323 if err == nil && len(tc.ExpectErr) > 0 {
324 t.Errorf("expected error, got none")
325 } else if err != nil && len(tc.ExpectErr) == 0 {
326 t.Errorf("unexpected error %v", err)
327 } else if err != nil && !strings.Contains(err.Error(), tc.ExpectErr) {
328 t.Errorf("expected error containing %q, got %v", tc.ExpectErr, err)
329 }
330
331 if e, a := tc.ExpectRequest, request; !reflect.DeepEqual(e, a) {
332 t.Errorf("unexpected diff: %s", cmp.Diff(e, a))
333 }
334 if e, a := tc.ExpectResponse, response; !reflect.DeepEqual(e, a) {
335 t.Errorf("unexpected diff: %s", cmp.Diff(e, a))
336 }
337 })
338 }
339 }
340
341 func TestGetConvertedObjectsFromResponse(t *testing.T) {
342 v1Object := &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "foo/v1", "kind": "Widget", "metadata": map[string]interface{}{"name": "myv1"}}}
343
344 testcases := []struct {
345 Name string
346 Response runtime.Object
347
348 ExpectObjects []runtime.RawExtension
349 ExpectErr string
350 }{
351 {
352 Name: "nil response",
353 Response: nil,
354 ExpectErr: "unrecognized response type",
355 },
356 {
357 Name: "unknown type",
358 Response: &unstructured.Unstructured{},
359 ExpectErr: "unrecognized response type",
360 },
361
362 {
363 Name: "minimal valid v1beta1",
364 Response: &v1beta1.ConversionReview{
365
366 Response: &v1beta1.ConversionResponse{
367
368 Result: metav1.Status{Status: metav1.StatusSuccess},
369 },
370 },
371 ExpectObjects: nil,
372 },
373 {
374 Name: "valid v1beta1 with objects",
375 Response: &v1beta1.ConversionReview{
376
377 Response: &v1beta1.ConversionResponse{
378
379 Result: metav1.Status{Status: metav1.StatusSuccess},
380 ConvertedObjects: []runtime.RawExtension{{Object: v1Object}},
381 },
382 },
383 ExpectObjects: []runtime.RawExtension{{Object: v1Object}},
384 },
385 {
386 Name: "error v1beta1, empty status",
387 Response: &v1beta1.ConversionReview{
388 Response: &v1beta1.ConversionResponse{
389 Result: metav1.Status{Status: ""},
390 },
391 },
392 ExpectErr: `response.result.status was '', not 'Success'`,
393 },
394 {
395 Name: "error v1beta1, failure status",
396 Response: &v1beta1.ConversionReview{
397 Response: &v1beta1.ConversionResponse{
398 Result: metav1.Status{Status: metav1.StatusFailure},
399 },
400 },
401 ExpectErr: `response.result.status was 'Failure', not 'Success'`,
402 },
403 {
404 Name: "error v1beta1, custom status",
405 Response: &v1beta1.ConversionReview{
406 Response: &v1beta1.ConversionResponse{
407 Result: metav1.Status{Status: metav1.StatusFailure, Message: "some failure message"},
408 },
409 },
410 ExpectErr: `some failure message`,
411 },
412 {
413 Name: "invalid v1beta1, no response",
414 Response: &v1beta1.ConversionReview{},
415 ExpectErr: "no response provided",
416 },
417
418 {
419 Name: "minimal valid v1",
420 Response: &v1.ConversionReview{
421 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
422 Response: &v1.ConversionResponse{
423 UID: "uid",
424 Result: metav1.Status{Status: metav1.StatusSuccess},
425 },
426 },
427 ExpectObjects: nil,
428 },
429 {
430 Name: "valid v1 with objects",
431 Response: &v1.ConversionReview{
432 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
433 Response: &v1.ConversionResponse{
434 UID: "uid",
435 Result: metav1.Status{Status: metav1.StatusSuccess},
436 ConvertedObjects: []runtime.RawExtension{{Object: v1Object}},
437 },
438 },
439 ExpectObjects: []runtime.RawExtension{{Object: v1Object}},
440 },
441 {
442 Name: "invalid v1, no uid",
443 Response: &v1.ConversionReview{
444 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
445 Response: &v1.ConversionResponse{
446 Result: metav1.Status{Status: metav1.StatusSuccess},
447 },
448 },
449 ExpectErr: `expected response.uid="uid"`,
450 },
451 {
452 Name: "invalid v1, no apiVersion",
453 Response: &v1.ConversionReview{
454 TypeMeta: metav1.TypeMeta{Kind: "ConversionReview"},
455 Response: &v1.ConversionResponse{
456 UID: "uid",
457 Result: metav1.Status{Status: metav1.StatusSuccess},
458 },
459 },
460 ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
461 },
462 {
463 Name: "invalid v1, no kind",
464 Response: &v1.ConversionReview{
465 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1"},
466 Response: &v1.ConversionResponse{
467 UID: "uid",
468 Result: metav1.Status{Status: metav1.StatusSuccess},
469 },
470 },
471 ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
472 },
473 {
474 Name: "invalid v1, mismatched apiVersion",
475 Response: &v1.ConversionReview{
476 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v2", Kind: "ConversionReview"},
477 Response: &v1.ConversionResponse{
478 UID: "uid",
479 Result: metav1.Status{Status: metav1.StatusSuccess},
480 },
481 },
482 ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
483 },
484 {
485 Name: "invalid v1, mismatched kind",
486 Response: &v1.ConversionReview{
487 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview2"},
488 Response: &v1.ConversionResponse{
489 UID: "uid",
490 Result: metav1.Status{Status: metav1.StatusSuccess},
491 },
492 },
493 ExpectErr: `expected webhook response of apiextensions.k8s.io/v1, Kind=ConversionReview`,
494 },
495 {
496 Name: "error v1, empty status",
497 Response: &v1.ConversionReview{
498 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
499 Response: &v1.ConversionResponse{
500 UID: "uid",
501 Result: metav1.Status{Status: ""},
502 },
503 },
504 ExpectErr: `response.result.status was '', not 'Success'`,
505 },
506 {
507 Name: "error v1, failure status",
508 Response: &v1.ConversionReview{
509 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
510 Response: &v1.ConversionResponse{
511 UID: "uid",
512 Result: metav1.Status{Status: metav1.StatusFailure},
513 },
514 },
515 ExpectErr: `response.result.status was 'Failure', not 'Success'`,
516 },
517 {
518 Name: "error v1, custom status",
519 Response: &v1.ConversionReview{
520 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
521 Response: &v1.ConversionResponse{
522 UID: "uid",
523 Result: metav1.Status{Status: metav1.StatusFailure, Message: "some failure message"},
524 },
525 },
526 ExpectErr: `some failure message`,
527 },
528 {
529 Name: "invalid v1, no response",
530 Response: &v1.ConversionReview{
531 TypeMeta: metav1.TypeMeta{APIVersion: "apiextensions.k8s.io/v1", Kind: "ConversionReview"},
532 },
533 ExpectErr: "no response provided",
534 },
535 }
536
537 for _, tc := range testcases {
538 t.Run(tc.Name, func(t *testing.T) {
539
540 objects, err := getConvertedObjectsFromResponse("uid", tc.Response)
541
542 if err == nil && len(tc.ExpectErr) > 0 {
543 t.Errorf("expected error, got none")
544 } else if err != nil && len(tc.ExpectErr) == 0 {
545 t.Errorf("unexpected error %v", err)
546 } else if err != nil && !strings.Contains(err.Error(), tc.ExpectErr) {
547 t.Errorf("expected error containing %q, got %v", tc.ExpectErr, err)
548 }
549
550 if !reflect.DeepEqual(objects, tc.ExpectObjects) {
551 t.Errorf("unexpected diff: %s", cmp.Diff(tc.ExpectObjects, objects))
552 }
553
554 })
555 }
556 }
557
View as plain text