1
16
17 package conversion
18
19 import (
20 "context"
21 "encoding/json"
22 "fmt"
23 "net/http"
24 "reflect"
25 "strings"
26 "sync"
27 "testing"
28 "time"
29
30 "github.com/google/go-cmp/cmp"
31
32 "k8s.io/apimachinery/pkg/api/errors"
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
35 "k8s.io/apimachinery/pkg/runtime"
36 "k8s.io/apimachinery/pkg/runtime/schema"
37 "k8s.io/apimachinery/pkg/types"
38 "k8s.io/apimachinery/pkg/util/sets"
39 "k8s.io/apimachinery/pkg/util/uuid"
40 "k8s.io/apimachinery/pkg/util/wait"
41 etcd3watcher "k8s.io/apiserver/pkg/storage/etcd3"
42 "k8s.io/client-go/dynamic"
43 _ "k8s.io/component-base/logs/testinit"
44
45 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
46 apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
47 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
48 serveroptions "k8s.io/apiextensions-apiserver/pkg/cmd/server/options"
49 "k8s.io/apiextensions-apiserver/test/integration/fixtures"
50 "k8s.io/apiextensions-apiserver/test/integration/storage"
51 )
52
53 type Checker func(t *testing.T, ctc *conversionTestContext)
54
55 func checks(checkers ...Checker) []Checker {
56 return checkers
57 }
58
59 func TestWebhookConverterWithWatchCache(t *testing.T) {
60 testWebhookConverter(t, true)
61 }
62 func TestWebhookConverterWithoutWatchCache(t *testing.T) {
63 testWebhookConverter(t, false)
64 }
65
66 func testWebhookConverter(t *testing.T, watchCache bool) {
67 tests := []struct {
68 group string
69 handler http.Handler
70 reviewVersions []string
71 checks []Checker
72 }{
73 {
74 group: "noop-converter-v1",
75 handler: NewObjectConverterWebhookHandler(t, noopConverter),
76 reviewVersions: []string{"v1", "v1beta1"},
77 checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1")),
78 },
79 {
80 group: "noop-converter-v1beta1",
81 handler: NewObjectConverterWebhookHandler(t, noopConverter),
82 reviewVersions: []string{"v1beta1", "v1"},
83 checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1")),
84 },
85 {
86 group: "nontrivial-converter-v1",
87 handler: NewObjectConverterWebhookHandler(t, nontrivialConverter),
88 reviewVersions: []string{"v1", "v1beta1"},
89 checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1", "v1beta2"), validateNonTrivialConverted, validateNonTrivialConvertedList, validateStoragePruning, validateDefaulting),
90 },
91 {
92 group: "nontrivial-converter-v1beta1",
93 handler: NewObjectConverterWebhookHandler(t, nontrivialConverter),
94 reviewVersions: []string{"v1beta1", "v1"},
95 checks: checks(validateStorageVersion, validateServed, validateMixedStorageVersions("v1alpha1", "v1beta1", "v1beta2"), validateNonTrivialConverted, validateNonTrivialConvertedList, validateStoragePruning, validateDefaulting),
96 },
97 {
98 group: "metadata-mutating-v1",
99 handler: NewObjectConverterWebhookHandler(t, metadataMutatingConverter),
100 reviewVersions: []string{"v1", "v1beta1"},
101 checks: checks(validateObjectMetaMutation),
102 },
103 {
104 group: "metadata-mutating-v1beta1",
105 handler: NewObjectConverterWebhookHandler(t, metadataMutatingConverter),
106 reviewVersions: []string{"v1beta1", "v1"},
107 checks: checks(validateObjectMetaMutation),
108 },
109 {
110 group: "metadata-uid-mutating-v1",
111 handler: NewObjectConverterWebhookHandler(t, uidMutatingConverter),
112 reviewVersions: []string{"v1", "v1beta1"},
113 checks: checks(validateUIDMutation),
114 },
115 {
116 group: "metadata-uid-mutating-v1beta1",
117 handler: NewObjectConverterWebhookHandler(t, uidMutatingConverter),
118 reviewVersions: []string{"v1beta1", "v1"},
119 checks: checks(validateUIDMutation),
120 },
121 {
122 group: "empty-response-v1",
123 handler: NewReviewWebhookHandler(t, nil, emptyV1ResponseConverter),
124 reviewVersions: []string{"v1", "v1beta1"},
125 checks: checks(expectConversionFailureMessage("empty-response", "returned 0 objects, expected 1")),
126 },
127 {
128 group: "empty-response-v1beta1",
129 handler: NewReviewWebhookHandler(t, emptyV1Beta1ResponseConverter, nil),
130 reviewVersions: []string{"v1beta1", "v1"},
131 checks: checks(expectConversionFailureMessage("empty-response", "returned 0 objects, expected 1")),
132 },
133 {
134 group: "failure-message-v1",
135 handler: NewReviewWebhookHandler(t, nil, failureV1ResponseConverter("custom webhook conversion error")),
136 reviewVersions: []string{"v1", "v1beta1"},
137 checks: checks(expectConversionFailureMessage("failure-message", "custom webhook conversion error")),
138 },
139 {
140 group: "failure-message-v1beta1",
141 handler: NewReviewWebhookHandler(t, failureV1Beta1ResponseConverter("custom webhook conversion error"), nil),
142 reviewVersions: []string{"v1beta1", "v1"},
143 checks: checks(expectConversionFailureMessage("failure-message", "custom webhook conversion error")),
144 },
145 {
146 group: "unhandled-v1",
147 handler: NewReviewWebhookHandler(t, nil, nil),
148 reviewVersions: []string{"v1", "v1beta1"},
149 checks: checks(expectConversionFailureMessage("server-error", "the server rejected our request")),
150 },
151 {
152 group: "unhandled-v1beta1",
153 handler: NewReviewWebhookHandler(t, nil, nil),
154 reviewVersions: []string{"v1beta1", "v1"},
155 checks: checks(expectConversionFailureMessage("server-error", "the server rejected our request")),
156 },
157 }
158
159
160
161
162 etcd3watcher.TestOnlySetFatalOnDecodeError(false)
163 defer etcd3watcher.TestOnlySetFatalOnDecodeError(true)
164
165 tearDown, config, options, err := fixtures.StartDefaultServer(t, fmt.Sprintf("--watch-cache=%v", watchCache))
166 if err != nil {
167 t.Fatal(err)
168 }
169
170 apiExtensionsClient, err := clientset.NewForConfig(config)
171 if err != nil {
172 tearDown()
173 t.Fatal(err)
174 }
175
176 dynamicClient, err := dynamic.NewForConfig(config)
177 if err != nil {
178 tearDown()
179 t.Fatal(err)
180 }
181 defer tearDown()
182
183 crd := multiVersionFixture.DeepCopy()
184
185 RESTOptionsGetter := serveroptions.NewCRDRESTOptionsGetter(*options.RecommendedOptions.Etcd, nil, nil)
186 restOptions, err := RESTOptionsGetter.GetRESTOptions(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural})
187 if err != nil {
188 t.Fatal(err)
189 }
190 etcdClient, _, err := storage.GetEtcdClients(restOptions.StorageConfig.Transport)
191 if err != nil {
192 t.Fatal(err)
193 }
194 defer etcdClient.Close()
195
196 etcdObjectReader := storage.NewEtcdObjectReader(etcdClient, &restOptions, crd)
197 ctcTearDown, ctc := newConversionTestContext(t, apiExtensionsClient, dynamicClient, etcdObjectReader, crd)
198 defer ctcTearDown()
199
200
201 marker, err := ctc.versionedClient("marker", "v1beta1").Create(context.TODO(), newConversionMultiVersionFixture("marker", "marker", "v1beta1"), metav1.CreateOptions{})
202 if err != nil {
203 t.Fatal(err)
204 }
205
206 for _, test := range tests {
207 t.Run(test.group, func(t *testing.T) {
208 upCh, handler := closeOnCall(test.handler)
209 tearDown, webhookClientConfig, err := StartConversionWebhookServer(handler)
210 if err != nil {
211 t.Fatal(err)
212 }
213 defer tearDown()
214
215 ctc.setConversionWebhook(t, webhookClientConfig, test.reviewVersions)
216 defer ctc.removeConversionWebhook(t)
217
218
219 if err := wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) {
220 _, err := ctc.versionedClient(marker.GetNamespace(), "v1alpha1").Get(context.TODO(), marker.GetName(), metav1.GetOptions{})
221 select {
222 case <-upCh:
223 return true, nil
224 default:
225 t.Logf("Waiting for webhook to become effective, getting marker object: %v", err)
226 return false, nil
227 }
228 }); err != nil {
229 t.Fatal(err)
230 }
231
232 for i, checkFn := range test.checks {
233 name := fmt.Sprintf("check-%d", i)
234 t.Run(name, func(t *testing.T) {
235 defer ctc.setAndWaitStorageVersion(t, "v1beta1")
236 ctc.namespace = fmt.Sprintf("webhook-conversion-%s-%s", test.group, name)
237 checkFn(t, ctc)
238 })
239 }
240 })
241 }
242 }
243
244 func validateStorageVersion(t *testing.T, ctc *conversionTestContext) {
245 ns := ctc.namespace
246
247 for _, version := range ctc.crd.Spec.Versions {
248 t.Run(version.Name, func(t *testing.T) {
249 name := "storageversion-" + version.Name
250 client := ctc.versionedClient(ns, version.Name)
251 obj, err := client.Create(context.TODO(), newConversionMultiVersionFixture(ns, name, version.Name), metav1.CreateOptions{})
252 if err != nil {
253 t.Fatal(err)
254 }
255 ctc.setAndWaitStorageVersion(t, "v1beta2")
256
257 if _, err = client.Get(context.TODO(), obj.GetName(), metav1.GetOptions{}); err != nil {
258 t.Fatal(err)
259 }
260
261 ctc.setAndWaitStorageVersion(t, "v1beta1")
262 })
263 }
264 }
265
266
267
268 func validateMixedStorageVersions(versions ...string) func(t *testing.T, ctc *conversionTestContext) {
269 return func(t *testing.T, ctc *conversionTestContext) {
270 ns := ctc.namespace
271 clients := ctc.versionedClients(ns)
272
273
274 objNames := []string{}
275 for _, version := range versions {
276 ctc.setAndWaitStorageVersion(t, version)
277
278 name := "mixedstorage-stored-as-" + version
279 obj, err := clients[version].Create(context.TODO(), newConversionMultiVersionFixture(ns, name, version), metav1.CreateOptions{})
280 if err != nil {
281 t.Fatal(err)
282 }
283 objNames = append(objNames, obj.GetName())
284 }
285
286
287 for clientVersion, client := range clients {
288 t.Run(clientVersion, func(t *testing.T) {
289 o1, err := client.Get(context.TODO(), objNames[0], metav1.GetOptions{})
290 if err != nil {
291 t.Fatal(err)
292 }
293 for _, objName := range objNames[1:] {
294 o2, err := client.Get(context.TODO(), objName, metav1.GetOptions{})
295 if err != nil {
296 t.Fatal(err)
297 }
298
299
300 delete(o1.Object, "metadata")
301 delete(o2.Object, "metadata")
302 if !reflect.DeepEqual(o1.Object, o2.Object) {
303 t.Errorf("Expected custom resource to be same regardless of which storage version is used to create, but got: %s", cmp.Diff(o1, o2))
304 }
305 }
306 })
307 }
308 }
309 }
310
311 func validateServed(t *testing.T, ctc *conversionTestContext) {
312 ns := ctc.namespace
313
314 for _, version := range ctc.crd.Spec.Versions {
315 t.Run(version.Name, func(t *testing.T) {
316 name := "served-" + version.Name
317 client := ctc.versionedClient(ns, version.Name)
318 obj, err := client.Create(context.TODO(), newConversionMultiVersionFixture(ns, name, version.Name), metav1.CreateOptions{})
319 if err != nil {
320 t.Fatal(err)
321 }
322 ctc.setServed(t, version.Name, false)
323 ctc.waitForServed(t, version.Name, false, client, obj)
324 ctc.setServed(t, version.Name, true)
325 ctc.waitForServed(t, version.Name, true, client, obj)
326 })
327 }
328 }
329
330 func validateNonTrivialConverted(t *testing.T, ctc *conversionTestContext) {
331 ns := ctc.namespace
332
333 for _, createVersion := range ctc.crd.Spec.Versions {
334 t.Run(fmt.Sprintf("getting objects created as %s", createVersion.Name), func(t *testing.T) {
335 name := "converted-" + createVersion.Name
336 client := ctc.versionedClient(ns, createVersion.Name)
337
338 fixture := newConversionMultiVersionFixture(ns, name, createVersion.Name)
339 if err := unstructured.SetNestedField(fixture.Object, "foo", "garbage"); err != nil {
340 t.Fatal(err)
341 }
342 if _, err := client.Create(context.TODO(), fixture, metav1.CreateOptions{}); err != nil {
343 t.Fatal(err)
344 }
345
346
347 obj, err := ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
348 if err != nil {
349 t.Fatal(err)
350 }
351 verifyMultiVersionObject(t, "v1beta1", obj)
352
353 for _, getVersion := range ctc.crd.Spec.Versions {
354 client := ctc.versionedClient(ns, getVersion.Name)
355 obj, err := client.Get(context.TODO(), name, metav1.GetOptions{})
356 if err != nil {
357 t.Fatal(err)
358 }
359 verifyMultiVersionObject(t, getVersion.Name, obj)
360 }
361
362
363 if _, err := client.Patch(context.TODO(), name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"main":"true"}}}`), metav1.PatchOptions{}); err != nil {
364 t.Fatal(err)
365 }
366
367 obj, err = ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
368 if err != nil {
369 t.Fatal(err)
370 }
371 verifyMultiVersionObject(t, "v1beta1", obj)
372
373
374 if _, err := client.Patch(context.TODO(), name, types.MergePatchType, []byte(`{"metadata":{"annotations":{"status":"true"}}}`), metav1.PatchOptions{}, "status"); err != nil {
375 t.Fatal(err)
376 }
377
378 obj, err = ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
379 if err != nil {
380 t.Fatal(err)
381 }
382 verifyMultiVersionObject(t, "v1beta1", obj)
383 })
384 }
385 }
386
387 func validateNonTrivialConvertedList(t *testing.T, ctc *conversionTestContext) {
388 ns := ctc.namespace + "-list"
389
390 names := sets.String{}
391 for _, createVersion := range ctc.crd.Spec.Versions {
392 name := "converted-" + createVersion.Name
393 client := ctc.versionedClient(ns, createVersion.Name)
394 fixture := newConversionMultiVersionFixture(ns, name, createVersion.Name)
395 if err := unstructured.SetNestedField(fixture.Object, "foo", "garbage"); err != nil {
396 t.Fatal(err)
397 }
398 _, err := client.Create(context.TODO(), fixture, metav1.CreateOptions{})
399 if err != nil {
400 t.Fatal(err)
401 }
402 names.Insert(name)
403 }
404
405 for _, listVersion := range ctc.crd.Spec.Versions {
406 t.Run(fmt.Sprintf("listing objects as %s", listVersion.Name), func(t *testing.T) {
407 client := ctc.versionedClient(ns, listVersion.Name)
408 obj, err := client.List(context.TODO(), metav1.ListOptions{})
409 if err != nil {
410 t.Fatal(err)
411 }
412 if len(obj.Items) != len(ctc.crd.Spec.Versions) {
413 t.Fatal("unexpected number of items")
414 }
415 foundNames := sets.String{}
416 for _, u := range obj.Items {
417 foundNames.Insert(u.GetName())
418 verifyMultiVersionObject(t, listVersion.Name, &u)
419 }
420 if !foundNames.Equal(names) {
421 t.Errorf("unexpected set of returned items: %s", foundNames.Difference(names))
422 }
423 })
424 }
425 }
426
427 func validateStoragePruning(t *testing.T, ctc *conversionTestContext) {
428 ns := ctc.namespace
429
430 for _, createVersion := range ctc.crd.Spec.Versions {
431 t.Run(fmt.Sprintf("getting objects created as %s", createVersion.Name), func(t *testing.T) {
432 name := "storagepruning-" + createVersion.Name
433 client := ctc.versionedClient(ns, createVersion.Name)
434
435 fixture := newConversionMultiVersionFixture(ns, name, createVersion.Name)
436 if err := unstructured.SetNestedField(fixture.Object, "foo", "garbage"); err != nil {
437 t.Fatal(err)
438 }
439 _, err := client.Create(context.TODO(), fixture, metav1.CreateOptions{})
440 if err != nil {
441 t.Fatal(err)
442 }
443
444
445 obj, err := ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
446 if err != nil {
447 t.Fatal(err)
448 }
449 verifyMultiVersionObject(t, "v1beta1", obj)
450
451
452 if err := unstructured.SetNestedField(obj.Object, "foo", "garbage"); err != nil {
453 t.Fatal(err)
454 }
455 labels := obj.GetLabels()
456 if labels == nil {
457 labels = map[string]string{}
458 }
459 labels["mutated"] = "true"
460 obj.SetLabels(labels)
461 if err := ctc.etcdObjectReader.SetStoredCustomResource(ns, name, obj); err != nil {
462 t.Fatal(err)
463 }
464
465 for _, getVersion := range ctc.crd.Spec.Versions {
466 client := ctc.versionedClient(ns, getVersion.Name)
467 obj, err := client.Get(context.TODO(), name, metav1.GetOptions{})
468 if err != nil {
469 t.Fatal(err)
470 }
471
472
473 labels := obj.GetLabels()
474 if labels["mutated"] != "true" {
475 t.Errorf("expected object %s in version %s to have label 'mutated=true'", name, getVersion.Name)
476 }
477
478 verifyMultiVersionObject(t, getVersion.Name, obj)
479 }
480 })
481 }
482 }
483
484 func validateObjectMetaMutation(t *testing.T, ctc *conversionTestContext) {
485 ns := ctc.namespace
486
487 t.Logf("Creating object in storage version v1beta1")
488 storageVersion := "v1beta1"
489 ctc.setAndWaitStorageVersion(t, storageVersion)
490 name := "objectmeta-mutation-" + storageVersion
491 client := ctc.versionedClient(ns, storageVersion)
492 obj, err := client.Create(context.TODO(), newConversionMultiVersionFixture(ns, name, storageVersion), metav1.CreateOptions{})
493 if err != nil {
494 t.Fatal(err)
495 }
496 validateObjectMetaMutationObject(t, false, false, obj)
497
498 t.Logf("Getting object in other version v1beta2")
499 client = ctc.versionedClient(ns, "v1beta2")
500 obj, err = client.Get(context.TODO(), name, metav1.GetOptions{})
501 if err != nil {
502 t.Fatal(err)
503 }
504 validateObjectMetaMutationObject(t, true, true, obj)
505
506 t.Logf("Creating object in non-storage version")
507 name = "objectmeta-mutation-v1beta2"
508 client = ctc.versionedClient(ns, "v1beta2")
509 obj, err = client.Create(context.TODO(), newConversionMultiVersionFixture(ns, name, "v1beta2"), metav1.CreateOptions{})
510 if err != nil {
511 t.Fatal(err)
512 }
513 validateObjectMetaMutationObject(t, true, true, obj)
514
515 t.Logf("Listing objects in non-storage version")
516 client = ctc.versionedClient(ns, "v1beta2")
517 list, err := client.List(context.TODO(), metav1.ListOptions{})
518 if err != nil {
519 t.Fatal(err)
520 }
521 for _, obj := range list.Items {
522 validateObjectMetaMutationObject(t, true, true, &obj)
523 }
524 }
525
526 func validateObjectMetaMutationObject(t *testing.T, expectAnnotations, expectLabels bool, obj *unstructured.Unstructured) {
527 if expectAnnotations {
528 if _, found := obj.GetAnnotations()["from"]; !found {
529 t.Errorf("expected 'from=stable.example.com/v1beta1' annotation")
530 }
531 if _, found := obj.GetAnnotations()["to"]; !found {
532 t.Errorf("expected 'to=stable.example.com/v1beta2' annotation")
533 }
534 } else {
535 if v, found := obj.GetAnnotations()["from"]; found {
536 t.Errorf("unexpected 'from' annotation: %s", v)
537 }
538 if v, found := obj.GetAnnotations()["to"]; found {
539 t.Errorf("unexpected 'to' annotation: %s", v)
540 }
541 }
542 if expectLabels {
543 if _, found := obj.GetLabels()["from"]; !found {
544 t.Errorf("expected 'from=stable.example.com.v1beta1' label")
545 }
546 if _, found := obj.GetLabels()["to"]; !found {
547 t.Errorf("expected 'to=stable.example.com.v1beta2' label")
548 }
549 } else {
550 if v, found := obj.GetLabels()["from"]; found {
551 t.Errorf("unexpected 'from' label: %s", v)
552 }
553 if v, found := obj.GetLabels()["to"]; found {
554 t.Errorf("unexpected 'to' label: %s", v)
555 }
556 }
557 if sets.NewString(obj.GetFinalizers()...).Has("foo") {
558 t.Errorf("unexpected 'foo' finalizer")
559 }
560 if obj.GetGeneration() == 42 {
561 t.Errorf("unexpected generation 42")
562 }
563 if v, found, err := unstructured.NestedString(obj.Object, "metadata", "garbage"); err != nil {
564 t.Errorf("unexpected error accessing 'metadata.garbage': %v", err)
565 } else if found {
566 t.Errorf("unexpected 'metadata.garbage': %s", v)
567 }
568 }
569
570 func validateUIDMutation(t *testing.T, ctc *conversionTestContext) {
571 ns := ctc.namespace
572
573 t.Logf("Creating object in non-storage version v1beta1")
574 storageVersion := "v1beta1"
575 ctc.setAndWaitStorageVersion(t, storageVersion)
576 name := "uid-mutation-" + storageVersion
577 client := ctc.versionedClient(ns, "v1beta2")
578 obj, err := client.Create(context.TODO(), newConversionMultiVersionFixture(ns, name, "v1beta2"), metav1.CreateOptions{})
579 if err == nil {
580 t.Fatalf("expected creation error, but got: %v", obj)
581 } else if !strings.Contains(err.Error(), "must have the same UID") {
582 t.Errorf("expected 'must have the same UID' error message, but got: %v", err)
583 }
584 }
585
586 func validateDefaulting(t *testing.T, ctc *conversionTestContext) {
587 if _, defaulting := ctc.crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["defaults"]; !defaulting {
588 return
589 }
590
591 ns := ctc.namespace
592 storageVersion := "v1beta1"
593
594 for _, createVersion := range ctc.crd.Spec.Versions {
595 t.Run(fmt.Sprintf("getting objects created as %s", createVersion.Name), func(t *testing.T) {
596 name := "defaulting-" + createVersion.Name
597 client := ctc.versionedClient(ns, createVersion.Name)
598
599 fixture := newConversionMultiVersionFixture(ns, name, createVersion.Name)
600 if err := unstructured.SetNestedField(fixture.Object, map[string]interface{}{}, "defaults"); err != nil {
601 t.Fatal(err)
602 }
603 created, err := client.Create(context.TODO(), fixture, metav1.CreateOptions{})
604 if err != nil {
605 t.Fatal(err)
606 }
607
608
609
610
611
612 defaults, found, err := unstructured.NestedMap(created.Object, "defaults")
613 if err != nil {
614 t.Fatal(err)
615 } else if !found {
616 t.Fatalf("expected .defaults to exist")
617 }
618 expectedLen := 1
619 if !createVersion.Storage {
620 expectedLen++
621 }
622 if len(defaults) != expectedLen {
623 t.Fatalf("after %s create expected .defaults to have %d values, but got: %v", createVersion.Name, expectedLen, defaults)
624 }
625 if _, found := defaults[createVersion.Name].(bool); !found {
626 t.Errorf("after %s create expected .defaults[%s] to be true, but .defaults is: %v", createVersion.Name, createVersion.Name, defaults)
627 }
628 if _, found := defaults[storageVersion].(bool); !found {
629 t.Errorf("after %s create expected .defaults[%s] to be true because it is the storage version, but .defaults is: %v", createVersion.Name, storageVersion, defaults)
630 }
631
632
633 persisted, err := ctc.etcdObjectReader.GetStoredCustomResource(ns, name)
634 if err != nil {
635 t.Fatal(err)
636 }
637 if _, found, err := unstructured.NestedBool(persisted.Object, "defaults", storageVersion); err != nil {
638 t.Fatal(err)
639 } else if createVersion.Name != storageVersion && found {
640 t.Errorf("after %s create .defaults[storage version %s] not to be persisted, but got in etcd: %v", createVersion.Name, storageVersion, defaults)
641 }
642
643
644 for _, v := range ctc.crd.Spec.Versions {
645 if v.Name == createVersion.Name {
646
647 continue
648 }
649
650 got, err := ctc.versionedClient(ns, v.Name).Get(context.TODO(), created.GetName(), metav1.GetOptions{})
651 if err != nil {
652 t.Fatal(err)
653 }
654
655 if _, found, err := unstructured.NestedBool(got.Object, "defaults", v.Name); err != nil {
656 t.Fatal(err)
657 } else if v.Name != storageVersion && found {
658 t.Errorf("after %s GET expected .defaults[%s] not to be true because only storage version %s is defaulted on read, but .defaults is: %v", v.Name, v.Name, storageVersion, defaults)
659 }
660
661 if _, found, err := unstructured.NestedBool(got.Object, "defaults", storageVersion); err != nil {
662 t.Fatal(err)
663 } else if !found {
664 t.Errorf("after non-create, non-storage %s GET expected .defaults[storage version %s] to be true, but .defaults is: %v", v.Name, storageVersion, defaults)
665 }
666 }
667 })
668 }
669 }
670
671 func expectConversionFailureMessage(id, message string) func(t *testing.T, ctc *conversionTestContext) {
672 return func(t *testing.T, ctc *conversionTestContext) {
673 ns := ctc.namespace
674 clients := ctc.versionedClients(ns)
675 var err error
676
677 obj, err := clients["v1beta1"].Create(context.TODO(), newConversionMultiVersionFixture(ns, id, "v1beta1"), metav1.CreateOptions{})
678 if err != nil {
679 t.Fatal(err)
680 }
681
682
683 objv1beta2 := newConversionMultiVersionFixture(ns, id, "v1beta2")
684 meta, _, _ := unstructured.NestedFieldCopy(obj.Object, "metadata")
685 unstructured.SetNestedField(objv1beta2.Object, meta, "metadata")
686
687 for _, verb := range []string{"get", "list", "create", "update", "patch", "delete", "deletecollection"} {
688 t.Run(verb, func(t *testing.T) {
689 switch verb {
690 case "get":
691 _, err = clients["v1beta2"].Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
692 case "list":
693 _, err = clients["v1beta2"].List(context.TODO(), metav1.ListOptions{})
694 case "create":
695 _, err = clients["v1beta2"].Create(context.TODO(), newConversionMultiVersionFixture(ns, id, "v1beta2"), metav1.CreateOptions{})
696 case "update":
697 _, err = clients["v1beta2"].Update(context.TODO(), objv1beta2, metav1.UpdateOptions{})
698 case "patch":
699 _, err = clients["v1beta2"].Patch(context.TODO(), obj.GetName(), types.MergePatchType, []byte(`{"metadata":{"annotations":{"patch":"true"}}}`), metav1.PatchOptions{})
700 case "delete":
701 err = clients["v1beta2"].Delete(context.TODO(), obj.GetName(), metav1.DeleteOptions{})
702 case "deletecollection":
703 err = clients["v1beta2"].DeleteCollection(context.TODO(), metav1.DeleteOptions{}, metav1.ListOptions{})
704 default:
705 t.Errorf("unknown verb %q", verb)
706 }
707
708 if err == nil {
709 t.Errorf("expected error with message %s, but got no error", message)
710 } else if !strings.Contains(err.Error(), message) {
711 t.Errorf("expected error with message %s, but got %v", message, err)
712 }
713 })
714 }
715 for _, subresource := range []string{"status", "scale"} {
716 for _, verb := range []string{"get", "update", "patch"} {
717 t.Run(fmt.Sprintf("%s-%s", subresource, verb), func(t *testing.T) {
718 switch verb {
719 case "get":
720 _, err = clients["v1beta2"].Get(context.TODO(), obj.GetName(), metav1.GetOptions{}, subresource)
721 case "update":
722 o := objv1beta2
723 if subresource == "scale" {
724 o = &unstructured.Unstructured{
725 Object: map[string]interface{}{
726 "apiVersion": "autoscaling/v1",
727 "kind": "Scale",
728 "metadata": map[string]interface{}{
729 "name": obj.GetName(),
730 },
731 "spec": map[string]interface{}{
732 "replicas": 42,
733 },
734 },
735 }
736 }
737 _, err = clients["v1beta2"].Update(context.TODO(), o, metav1.UpdateOptions{}, subresource)
738 case "patch":
739 _, err = clients["v1beta2"].Patch(context.TODO(), obj.GetName(), types.MergePatchType, []byte(`{"metadata":{"annotations":{"patch":"true"}}}`), metav1.PatchOptions{}, subresource)
740 default:
741 t.Errorf("unknown subresource verb %q", verb)
742 }
743
744 if err == nil {
745 t.Errorf("expected error with message %s, but got no error", message)
746 } else if !strings.Contains(err.Error(), message) {
747 t.Errorf("expected error with message %s, but got %v", message, err)
748 }
749 })
750 }
751 }
752 }
753 }
754
755 func noopConverter(desiredAPIVersion string, obj runtime.RawExtension) (runtime.RawExtension, error) {
756 u := &unstructured.Unstructured{Object: map[string]interface{}{}}
757 if err := json.Unmarshal(obj.Raw, u); err != nil {
758 return runtime.RawExtension{}, fmt.Errorf("failed to deserialize object: %s with error: %v", string(obj.Raw), err)
759 }
760 u.Object["apiVersion"] = desiredAPIVersion
761 raw, err := json.Marshal(u)
762 if err != nil {
763 return runtime.RawExtension{}, fmt.Errorf("failed to serialize object: %v with error: %v", u, err)
764 }
765 return runtime.RawExtension{Raw: raw}, nil
766 }
767
768 func emptyV1ResponseConverter(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error) {
769 review.Response = &apiextensionsv1.ConversionResponse{
770 UID: review.Request.UID,
771 ConvertedObjects: []runtime.RawExtension{},
772 Result: metav1.Status{Status: "Success"},
773 }
774 return review, nil
775 }
776 func emptyV1Beta1ResponseConverter(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error) {
777 review.Response = &apiextensionsv1beta1.ConversionResponse{
778 UID: review.Request.UID,
779 ConvertedObjects: []runtime.RawExtension{},
780 Result: metav1.Status{Status: "Success"},
781 }
782 return review, nil
783 }
784
785 func failureV1ResponseConverter(message string) func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error) {
786 return func(review *apiextensionsv1.ConversionReview) (*apiextensionsv1.ConversionReview, error) {
787 review.Response = &apiextensionsv1.ConversionResponse{
788 UID: review.Request.UID,
789 ConvertedObjects: []runtime.RawExtension{},
790 Result: metav1.Status{Message: message, Status: "Failure"},
791 }
792 return review, nil
793 }
794 }
795
796 func failureV1Beta1ResponseConverter(message string) func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error) {
797 return func(review *apiextensionsv1beta1.ConversionReview) (*apiextensionsv1beta1.ConversionReview, error) {
798 review.Response = &apiextensionsv1beta1.ConversionResponse{
799 UID: review.Request.UID,
800 ConvertedObjects: []runtime.RawExtension{},
801 Result: metav1.Status{Message: message, Status: "Failure"},
802 }
803 return review, nil
804 }
805 }
806
807 func nontrivialConverter(desiredAPIVersion string, obj runtime.RawExtension) (runtime.RawExtension, error) {
808 u := &unstructured.Unstructured{Object: map[string]interface{}{}}
809 if err := json.Unmarshal(obj.Raw, u); err != nil {
810 return runtime.RawExtension{}, fmt.Errorf("failed to deserialize object: %s with error: %v", string(obj.Raw), err)
811 }
812
813 currentAPIVersion := u.GetAPIVersion()
814
815 if currentAPIVersion == "stable.example.com/v1beta2" && (desiredAPIVersion == "stable.example.com/v1alpha1" || desiredAPIVersion == "stable.example.com/v1beta1") {
816 u.Object["num"] = u.Object["numv2"]
817 u.Object["content"] = u.Object["contentv2"]
818 delete(u.Object, "numv2")
819 delete(u.Object, "contentv2")
820 } else if (currentAPIVersion == "stable.example.com/v1alpha1" || currentAPIVersion == "stable.example.com/v1beta1") && desiredAPIVersion == "stable.example.com/v1beta2" {
821 u.Object["numv2"] = u.Object["num"]
822 u.Object["contentv2"] = u.Object["content"]
823 delete(u.Object, "num")
824 delete(u.Object, "content")
825 } else if currentAPIVersion == "stable.example.com/v1alpha1" && desiredAPIVersion == "stable.example.com/v1beta1" {
826
827 } else if currentAPIVersion == "stable.example.com/v1beta1" && desiredAPIVersion == "stable.example.com/v1alpha1" {
828
829 } else if currentAPIVersion != desiredAPIVersion {
830 return runtime.RawExtension{}, fmt.Errorf("cannot convert from %s to %s", currentAPIVersion, desiredAPIVersion)
831 }
832 u.Object["apiVersion"] = desiredAPIVersion
833 raw, err := json.Marshal(u)
834 if err != nil {
835 return runtime.RawExtension{}, fmt.Errorf("failed to serialize object: %v with error: %v", u, err)
836 }
837 return runtime.RawExtension{Raw: raw}, nil
838 }
839
840 func metadataMutatingConverter(desiredAPIVersion string, obj runtime.RawExtension) (runtime.RawExtension, error) {
841 obj, err := nontrivialConverter(desiredAPIVersion, obj)
842 if err != nil {
843 return runtime.RawExtension{}, err
844 }
845
846 u := &unstructured.Unstructured{Object: map[string]interface{}{}}
847 if err := json.Unmarshal(obj.Raw, u); err != nil {
848 return runtime.RawExtension{}, fmt.Errorf("failed to deserialize object: %s with error: %v", string(obj.Raw), err)
849 }
850
851
852 if !strings.Contains(u.GetName(), "mutation") {
853 return obj, nil
854 }
855
856 currentAPIVersion := u.GetAPIVersion()
857
858
859 annotations := u.GetAnnotations()
860 if annotations == nil {
861 annotations = map[string]string{}
862 }
863 annotations["from"] = currentAPIVersion
864 annotations["to"] = desiredAPIVersion
865 u.SetAnnotations(annotations)
866
867
868 labels := u.GetLabels()
869 if labels == nil {
870 labels = map[string]string{}
871 }
872 labels["from"] = strings.Replace(currentAPIVersion, "/", ".", 1)
873 labels["to"] = strings.Replace(desiredAPIVersion, "/", ".", 1)
874 u.SetLabels(labels)
875
876
877 u.SetGeneration(42)
878 u.SetOwnerReferences([]metav1.OwnerReference{{
879 APIVersion: "v1",
880 Kind: "Namespace",
881 Name: "default",
882 UID: "1234",
883 Controller: nil,
884 BlockOwnerDeletion: nil,
885 }})
886 u.SetResourceVersion("42")
887 u.SetFinalizers([]string{"foo"})
888 if err := unstructured.SetNestedField(u.Object, "foo", "metadata", "garbage"); err != nil {
889 return runtime.RawExtension{}, err
890 }
891
892 raw, err := json.Marshal(u)
893 if err != nil {
894 return runtime.RawExtension{}, fmt.Errorf("failed to serialize object: %v with error: %v", u, err)
895 }
896 return runtime.RawExtension{Raw: raw}, nil
897 }
898
899 func uidMutatingConverter(desiredAPIVersion string, obj runtime.RawExtension) (runtime.RawExtension, error) {
900 u := &unstructured.Unstructured{Object: map[string]interface{}{}}
901 if err := json.Unmarshal(obj.Raw, u); err != nil {
902 return runtime.RawExtension{}, fmt.Errorf("failed to deserialize object: %s with error: %v", string(obj.Raw), err)
903 }
904
905
906 if strings.Contains(u.GetName(), "mutation") {
907
908 if err := unstructured.SetNestedField(u.Object, "42", "metadata", "uid"); err != nil {
909 return runtime.RawExtension{}, err
910 }
911 }
912
913 u.Object["apiVersion"] = desiredAPIVersion
914 raw, err := json.Marshal(u)
915 if err != nil {
916 return runtime.RawExtension{}, fmt.Errorf("failed to serialize object: %v with error: %v", u, err)
917 }
918 return runtime.RawExtension{Raw: raw}, nil
919 }
920
921 func newConversionTestContext(t *testing.T, apiExtensionsClient clientset.Interface, dynamicClient dynamic.Interface, etcdObjectReader *storage.EtcdObjectReader, v1CRD *apiextensionsv1.CustomResourceDefinition) (func(), *conversionTestContext) {
922 v1CRD, err := fixtures.CreateNewV1CustomResourceDefinition(v1CRD, apiExtensionsClient, dynamicClient)
923 if err != nil {
924 t.Fatal(err)
925 }
926 crd, err := apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), v1CRD.Name, metav1.GetOptions{})
927 if err != nil {
928 t.Fatal(err)
929 }
930
931 tearDown := func() {
932 if err := fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionsClient); err != nil {
933 t.Fatal(err)
934 }
935 }
936
937 return tearDown, &conversionTestContext{apiExtensionsClient: apiExtensionsClient, dynamicClient: dynamicClient, crd: crd, etcdObjectReader: etcdObjectReader}
938 }
939
940 type conversionTestContext struct {
941 namespace string
942 apiExtensionsClient clientset.Interface
943 dynamicClient dynamic.Interface
944 crd *apiextensionsv1.CustomResourceDefinition
945 etcdObjectReader *storage.EtcdObjectReader
946 }
947
948 func (c *conversionTestContext) versionedClient(ns string, version string) dynamic.ResourceInterface {
949 gvr := schema.GroupVersionResource{Group: c.crd.Spec.Group, Version: version, Resource: c.crd.Spec.Names.Plural}
950 if c.crd.Spec.Scope != apiextensionsv1.ClusterScoped {
951 return c.dynamicClient.Resource(gvr).Namespace(ns)
952 }
953 return c.dynamicClient.Resource(gvr)
954 }
955
956 func (c *conversionTestContext) versionedClients(ns string) map[string]dynamic.ResourceInterface {
957 ret := map[string]dynamic.ResourceInterface{}
958 for _, v := range c.crd.Spec.Versions {
959 ret[v.Name] = c.versionedClient(ns, v.Name)
960 }
961 return ret
962 }
963
964 func (c *conversionTestContext) setConversionWebhook(t *testing.T, webhookClientConfig *apiextensionsv1.WebhookClientConfig, reviewVersions []string) {
965 crd, err := c.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), c.crd.Name, metav1.GetOptions{})
966 if err != nil {
967 t.Fatal(err)
968 }
969 crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
970 Strategy: apiextensionsv1.WebhookConverter,
971 Webhook: &apiextensionsv1.WebhookConversion{
972 ClientConfig: webhookClientConfig,
973 ConversionReviewVersions: reviewVersions,
974 },
975 }
976 crd, err = c.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
977 if err != nil {
978 t.Fatal(err)
979 }
980 c.crd = crd
981
982 }
983
984 func (c *conversionTestContext) removeConversionWebhook(t *testing.T) {
985 crd, err := c.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), c.crd.Name, metav1.GetOptions{})
986 if err != nil {
987 t.Fatal(err)
988 }
989 crd.Spec.Conversion = &apiextensionsv1.CustomResourceConversion{
990 Strategy: apiextensionsv1.NoneConverter,
991 }
992
993 crd, err = c.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
994 if err != nil {
995 t.Fatal(err)
996 }
997 c.crd = crd
998 }
999
1000 func (c *conversionTestContext) setAndWaitStorageVersion(t *testing.T, version string) {
1001 c.setStorageVersion(t, version)
1002
1003
1004 client := c.versionedClient("probe", "v1beta1")
1005 name := fmt.Sprintf("probe-%v", uuid.NewUUID())
1006 storageProbe, err := client.Create(context.TODO(), newConversionMultiVersionFixture("probe", name, "v1beta1"), metav1.CreateOptions{})
1007 if err != nil {
1008 t.Fatal(err)
1009 }
1010
1011
1012 c.waitForStorageVersion(t, version, c.versionedClient(storageProbe.GetNamespace(), "v1beta1"), storageProbe)
1013
1014 err = client.Delete(context.TODO(), name, metav1.DeleteOptions{})
1015 if err != nil {
1016 t.Fatal(err)
1017 }
1018 }
1019
1020 func (c *conversionTestContext) setStorageVersion(t *testing.T, version string) {
1021 crd, err := c.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), c.crd.Name, metav1.GetOptions{})
1022 if err != nil {
1023 t.Fatal(err)
1024 }
1025 for i, v := range crd.Spec.Versions {
1026 crd.Spec.Versions[i].Storage = v.Name == version
1027 }
1028 crd, err = c.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
1029 if err != nil {
1030 t.Fatal(err)
1031 }
1032 c.crd = crd
1033 }
1034
1035 func (c *conversionTestContext) waitForStorageVersion(t *testing.T, version string, versionedClient dynamic.ResourceInterface, obj *unstructured.Unstructured) *unstructured.Unstructured {
1036 if err := c.etcdObjectReader.WaitForStorageVersion(version, obj.GetNamespace(), obj.GetName(), 30*time.Second, func() {
1037 if _, err := versionedClient.Patch(context.TODO(), obj.GetName(), types.MergePatchType, []byte(`{}`), metav1.PatchOptions{}); err != nil {
1038 t.Fatalf("failed to update object: %v", err)
1039 }
1040 }); err != nil {
1041 t.Fatalf("failed waiting for storage version %s: %v", version, err)
1042 }
1043
1044 t.Logf("Effective storage version: %s", version)
1045
1046 return obj
1047 }
1048
1049 func (c *conversionTestContext) setServed(t *testing.T, version string, served bool) {
1050 crd, err := c.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Get(context.TODO(), c.crd.Name, metav1.GetOptions{})
1051 if err != nil {
1052 t.Fatal(err)
1053 }
1054 for i, v := range crd.Spec.Versions {
1055 if v.Name == version {
1056 crd.Spec.Versions[i].Served = served
1057 }
1058 }
1059 crd, err = c.apiExtensionsClient.ApiextensionsV1().CustomResourceDefinitions().Update(context.TODO(), crd, metav1.UpdateOptions{})
1060 if err != nil {
1061 t.Fatal(err)
1062 }
1063 c.crd = crd
1064 }
1065
1066 func (c *conversionTestContext) waitForServed(t *testing.T, version string, served bool, versionedClient dynamic.ResourceInterface, obj *unstructured.Unstructured) {
1067 timeout := 30 * time.Second
1068 waitCh := time.After(timeout)
1069 for {
1070 obj, err := versionedClient.Get(context.TODO(), obj.GetName(), metav1.GetOptions{})
1071 if (err == nil && served) || (errors.IsNotFound(err) && served == false) {
1072 return
1073 }
1074 select {
1075 case <-waitCh:
1076 t.Fatalf("Timed out after %v waiting for CRD served=%t for version %s for %v. Last error: %v", timeout, served, version, obj, err)
1077 case <-time.After(10 * time.Millisecond):
1078 }
1079 }
1080 }
1081
1082 var multiVersionFixture = &apiextensionsv1.CustomResourceDefinition{
1083 ObjectMeta: metav1.ObjectMeta{Name: "multiversion.stable.example.com"},
1084 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
1085 Group: "stable.example.com",
1086 Names: apiextensionsv1.CustomResourceDefinitionNames{
1087 Plural: "multiversion",
1088 Singular: "multiversion",
1089 Kind: "MultiVersion",
1090 ShortNames: []string{"mv"},
1091 ListKind: "MultiVersionList",
1092 Categories: []string{"all"},
1093 },
1094 Scope: apiextensionsv1.NamespaceScoped,
1095 PreserveUnknownFields: false,
1096 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
1097 {
1098
1099 Name: "v1beta1",
1100 Served: true,
1101 Storage: true,
1102 Subresources: &apiextensionsv1.CustomResourceSubresources{
1103 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
1104 Scale: &apiextensionsv1.CustomResourceSubresourceScale{
1105 SpecReplicasPath: ".spec.num.num1",
1106 StatusReplicasPath: ".status.num.num2",
1107 },
1108 },
1109 Schema: &apiextensionsv1.CustomResourceValidation{
1110 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
1111 Type: "object",
1112 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1113 "content": {
1114 Type: "object",
1115 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1116 "key": {Type: "string"},
1117 },
1118 },
1119 "num": {
1120 Type: "object",
1121 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1122 "num1": {Type: "integer"},
1123 "num2": {Type: "integer"},
1124 },
1125 },
1126 "defaults": {
1127 Type: "object",
1128 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1129 "v1alpha1": {Type: "boolean"},
1130 "v1beta1": {Type: "boolean", Default: jsonPtr(true)},
1131 "v1beta2": {Type: "boolean"},
1132 },
1133 },
1134 },
1135 },
1136 },
1137 },
1138 {
1139
1140 Name: "v1alpha1",
1141 Served: true,
1142 Storage: false,
1143 Subresources: &apiextensionsv1.CustomResourceSubresources{
1144 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
1145 Scale: &apiextensionsv1.CustomResourceSubresourceScale{
1146 SpecReplicasPath: ".spec.num.num1",
1147 StatusReplicasPath: ".status.num.num2",
1148 },
1149 },
1150 Schema: &apiextensionsv1.CustomResourceValidation{
1151 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
1152 Type: "object",
1153 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1154 "content": {
1155 Type: "object",
1156 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1157 "key": {Type: "string"},
1158 },
1159 },
1160 "num": {
1161 Type: "object",
1162 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1163 "num1": {Type: "integer"},
1164 "num2": {Type: "integer"},
1165 },
1166 },
1167 "defaults": {
1168 Type: "object",
1169 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1170 "v1alpha1": {Type: "boolean", Default: jsonPtr(true)},
1171 "v1beta1": {Type: "boolean"},
1172 "v1beta2": {Type: "boolean"},
1173 },
1174 },
1175 },
1176 },
1177 },
1178 },
1179 {
1180
1181 Name: "v1beta2",
1182 Served: true,
1183 Storage: false,
1184 Subresources: &apiextensionsv1.CustomResourceSubresources{
1185 Status: &apiextensionsv1.CustomResourceSubresourceStatus{},
1186 Scale: &apiextensionsv1.CustomResourceSubresourceScale{
1187 SpecReplicasPath: ".spec.num.num1",
1188 StatusReplicasPath: ".status.num.num2",
1189 },
1190 },
1191 Schema: &apiextensionsv1.CustomResourceValidation{
1192 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
1193 Type: "object",
1194 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1195 "contentv2": {
1196 Type: "object",
1197 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1198 "key": {Type: "string"},
1199 },
1200 },
1201 "numv2": {
1202 Type: "object",
1203 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1204 "num1": {Type: "integer"},
1205 "num2": {Type: "integer"},
1206 },
1207 },
1208 "defaults": {
1209 Type: "object",
1210 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1211 "v1alpha1": {Type: "boolean"},
1212 "v1beta1": {Type: "boolean"},
1213 "v1beta2": {Type: "boolean", Default: jsonPtr(true)},
1214 },
1215 },
1216 },
1217 },
1218 },
1219 },
1220 },
1221 },
1222 }
1223
1224 func newConversionMultiVersionFixture(namespace, name, version string) *unstructured.Unstructured {
1225 u := &unstructured.Unstructured{
1226 Object: map[string]interface{}{
1227 "apiVersion": "stable.example.com/" + version,
1228 "kind": "MultiVersion",
1229 "metadata": map[string]interface{}{
1230 "namespace": namespace,
1231 "name": name,
1232 },
1233 },
1234 }
1235
1236 switch version {
1237 case "v1alpha1":
1238 u.Object["content"] = map[string]interface{}{
1239 "key": "value",
1240 }
1241 u.Object["num"] = map[string]interface{}{
1242 "num1": int64(1),
1243 "num2": int64(1000000),
1244 }
1245 case "v1beta1":
1246 u.Object["content"] = map[string]interface{}{
1247 "key": "value",
1248 }
1249 u.Object["num"] = map[string]interface{}{
1250 "num1": int64(1),
1251 "num2": int64(1000000),
1252 }
1253 case "v1beta2":
1254 u.Object["contentv2"] = map[string]interface{}{
1255 "key": "value",
1256 }
1257 u.Object["numv2"] = map[string]interface{}{
1258 "num1": int64(1),
1259 "num2": int64(1000000),
1260 }
1261 default:
1262 panic(fmt.Sprintf("unknown version %s", version))
1263 }
1264
1265 return u
1266 }
1267
1268 func verifyMultiVersionObject(t *testing.T, v string, obj *unstructured.Unstructured) {
1269 j := runtime.DeepCopyJSON(obj.Object)
1270
1271 if expected := "stable.example.com/" + v; obj.GetAPIVersion() != expected {
1272 t.Errorf("unexpected apiVersion %q, expected %q", obj.GetAPIVersion(), expected)
1273 return
1274 }
1275
1276 delete(j, "metadata")
1277
1278 var expected = map[string]map[string]interface{}{
1279 "v1alpha1": {
1280 "apiVersion": "stable.example.com/v1alpha1",
1281 "kind": "MultiVersion",
1282 "content": map[string]interface{}{
1283 "key": "value",
1284 },
1285 "num": map[string]interface{}{
1286 "num1": int64(1),
1287 "num2": int64(1000000),
1288 },
1289 },
1290 "v1beta1": {
1291 "apiVersion": "stable.example.com/v1beta1",
1292 "kind": "MultiVersion",
1293 "content": map[string]interface{}{
1294 "key": "value",
1295 },
1296 "num": map[string]interface{}{
1297 "num1": int64(1),
1298 "num2": int64(1000000),
1299 },
1300 },
1301 "v1beta2": {
1302 "apiVersion": "stable.example.com/v1beta2",
1303 "kind": "MultiVersion",
1304 "contentv2": map[string]interface{}{
1305 "key": "value",
1306 },
1307 "numv2": map[string]interface{}{
1308 "num1": int64(1),
1309 "num2": int64(1000000),
1310 },
1311 },
1312 }
1313 if !reflect.DeepEqual(expected[v], j) {
1314 t.Errorf("unexpected %s object: %s", v, cmp.Diff(expected[v], j))
1315 }
1316 }
1317
1318 func closeOnCall(h http.Handler) (chan struct{}, http.Handler) {
1319 ch := make(chan struct{})
1320 once := sync.Once{}
1321 return ch, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1322 once.Do(func() {
1323 close(ch)
1324 })
1325 h.ServeHTTP(w, r)
1326 })
1327 }
1328
1329 func jsonPtr(x interface{}) *apiextensionsv1.JSON {
1330 bs, err := json.Marshal(x)
1331 if err != nil {
1332 panic(err)
1333 }
1334 ret := apiextensionsv1.JSON{Raw: bs}
1335 return &ret
1336 }
1337
View as plain text