1
16
17 package apiserver
18
19 import (
20 "bytes"
21 "context"
22 "encoding/json"
23 "errors"
24 "io"
25 "net"
26 "net/http"
27 "net/http/httptest"
28 "net/url"
29 "os"
30 "path/filepath"
31 "strconv"
32 "testing"
33 "time"
34
35 "sigs.k8s.io/yaml"
36
37 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
38 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
39 "k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
40 structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
41 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
42 informers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
43 listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1"
44 "k8s.io/apiextensions-apiserver/pkg/controller/establish"
45 metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
46 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
47 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
48 "k8s.io/apimachinery/pkg/runtime"
49 "k8s.io/apimachinery/pkg/runtime/schema"
50 serializerjson "k8s.io/apimachinery/pkg/runtime/serializer/json"
51 "k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
52 "k8s.io/apimachinery/pkg/types"
53 "k8s.io/apiserver/pkg/admission"
54 "k8s.io/apiserver/pkg/authorization/authorizer"
55 "k8s.io/apiserver/pkg/endpoints/discovery"
56 apirequest "k8s.io/apiserver/pkg/endpoints/request"
57 "k8s.io/apiserver/pkg/registry/generic"
58 genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
59 "k8s.io/apiserver/pkg/registry/rest"
60 "k8s.io/apiserver/pkg/server/options"
61 etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
62 "k8s.io/apiserver/pkg/util/webhook"
63 "k8s.io/client-go/tools/cache"
64 "k8s.io/kube-openapi/pkg/validation/spec"
65 )
66
67 func TestConvertFieldLabel(t *testing.T) {
68 tests := []struct {
69 name string
70 clusterScoped bool
71 label string
72 expectError bool
73 }{
74 {
75 name: "cluster scoped - name is ok",
76 clusterScoped: true,
77 label: "metadata.name",
78 },
79 {
80 name: "cluster scoped - namespace is not ok",
81 clusterScoped: true,
82 label: "metadata.namespace",
83 expectError: true,
84 },
85 {
86 name: "cluster scoped - other field is not ok",
87 clusterScoped: true,
88 label: "some.other.field",
89 expectError: true,
90 },
91 {
92 name: "namespace scoped - name is ok",
93 label: "metadata.name",
94 },
95 {
96 name: "namespace scoped - namespace is ok",
97 label: "metadata.namespace",
98 },
99 {
100 name: "namespace scoped - other field is not ok",
101 label: "some.other.field",
102 expectError: true,
103 },
104 }
105
106 for _, test := range tests {
107 t.Run(test.name, func(t *testing.T) {
108
109 crd := apiextensionsv1.CustomResourceDefinition{
110 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
111 Conversion: &apiextensionsv1.CustomResourceConversion{
112 Strategy: "None",
113 },
114 },
115 }
116
117 if test.clusterScoped {
118 crd.Spec.Scope = apiextensionsv1.ClusterScoped
119 } else {
120 crd.Spec.Scope = apiextensionsv1.NamespaceScoped
121 }
122 f, err := conversion.NewCRConverterFactory(nil, nil)
123 if err != nil {
124 t.Fatal(err)
125 }
126 _, c, err := f.NewConverter(&crd)
127 if err != nil {
128 t.Fatalf("Failed to create CR converter. error: %v", err)
129 }
130
131 label, value, err := c.ConvertFieldLabel(schema.GroupVersionKind{}, test.label, "value")
132 if e, a := test.expectError, err != nil; e != a {
133 t.Fatalf("err: expected %t, got %t", e, a)
134 }
135 if test.expectError {
136 if e, a := "field label not supported: "+test.label, err.Error(); e != a {
137 t.Errorf("err: expected %s, got %s", e, a)
138 }
139 return
140 }
141
142 if e, a := test.label, label; e != a {
143 t.Errorf("label: expected %s, got %s", e, a)
144 }
145 if e, a := "value", value; e != a {
146 t.Errorf("value: expected %s, got %s", e, a)
147 }
148 })
149 }
150 }
151
152 func TestRouting(t *testing.T) {
153 hasSynced := false
154
155 crdIndexer := cache.NewIndexer(cache.MetaNamespaceKeyFunc, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
156 crdLister := listers.NewCustomResourceDefinitionLister(crdIndexer)
157
158
159
160
161
162
163
164
165 delegateCalled := false
166 delegate := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
167 delegateCalled = true
168 if !hasSynced {
169 http.Error(w, "", 503)
170 return
171 }
172 http.Error(w, "", 418)
173 })
174 customV1 := schema.GroupVersion{Group: "custom", Version: "v1"}
175 handler := &crdHandler{
176 crdLister: crdLister,
177 delegate: delegate,
178 versionDiscoveryHandler: &versionDiscoveryHandler{
179 discovery: map[schema.GroupVersion]*discovery.APIVersionHandler{
180 customV1: discovery.NewAPIVersionHandler(Codecs, customV1, discovery.APIResourceListerFunc(func() []metav1.APIResource {
181 return nil
182 })),
183 },
184 delegate: delegate,
185 },
186 groupDiscoveryHandler: &groupDiscoveryHandler{
187 discovery: map[string]*discovery.APIGroupHandler{
188 "custom": discovery.NewAPIGroupHandler(Codecs, metav1.APIGroup{
189 Name: customV1.Group,
190 Versions: []metav1.GroupVersionForDiscovery{{GroupVersion: customV1.String(), Version: customV1.Version}},
191 PreferredVersion: metav1.GroupVersionForDiscovery{GroupVersion: customV1.String(), Version: customV1.Version},
192 }),
193 },
194 delegate: delegate,
195 },
196 }
197
198 testcases := []struct {
199 Name string
200 Method string
201 Path string
202 Headers map[string]string
203 Body io.Reader
204
205 APIGroup string
206 APIVersion string
207 Verb string
208 Resource string
209 IsResourceRequest bool
210
211 HasSynced bool
212
213 ExpectStatus int
214 ExpectResponse func(*testing.T, *http.Response, []byte)
215 ExpectDelegateCalled bool
216 }{
217 {
218 Name: "existing group discovery, presync",
219 Method: "GET",
220 Path: "/apis/custom",
221 APIGroup: "custom",
222 APIVersion: "",
223 HasSynced: false,
224 IsResourceRequest: false,
225 ExpectDelegateCalled: false,
226 ExpectStatus: 200,
227 },
228 {
229 Name: "existing group discovery",
230 Method: "GET",
231 Path: "/apis/custom",
232 APIGroup: "custom",
233 APIVersion: "",
234 HasSynced: true,
235 IsResourceRequest: false,
236 ExpectDelegateCalled: false,
237 ExpectStatus: 200,
238 },
239
240 {
241 Name: "nonexisting group discovery, presync",
242 Method: "GET",
243 Path: "/apis/other",
244 APIGroup: "other",
245 APIVersion: "",
246 HasSynced: false,
247 IsResourceRequest: false,
248 ExpectDelegateCalled: true,
249 ExpectStatus: 503,
250 },
251 {
252 Name: "nonexisting group discovery",
253 Method: "GET",
254 Path: "/apis/other",
255 APIGroup: "other",
256 APIVersion: "",
257 HasSynced: true,
258 IsResourceRequest: false,
259 ExpectDelegateCalled: true,
260 ExpectStatus: 418,
261 },
262
263 {
264 Name: "existing group version discovery, presync",
265 Method: "GET",
266 Path: "/apis/custom/v1",
267 APIGroup: "custom",
268 APIVersion: "v1",
269 HasSynced: false,
270 IsResourceRequest: false,
271 ExpectDelegateCalled: false,
272 ExpectStatus: 200,
273 },
274 {
275 Name: "existing group version discovery",
276 Method: "GET",
277 Path: "/apis/custom/v1",
278 APIGroup: "custom",
279 APIVersion: "v1",
280 HasSynced: true,
281 IsResourceRequest: false,
282 ExpectDelegateCalled: false,
283 ExpectStatus: 200,
284 },
285
286 {
287 Name: "nonexisting group version discovery, presync",
288 Method: "GET",
289 Path: "/apis/other/v1",
290 APIGroup: "other",
291 APIVersion: "v1",
292 HasSynced: false,
293 IsResourceRequest: false,
294 ExpectDelegateCalled: true,
295 ExpectStatus: 503,
296 },
297 {
298 Name: "nonexisting group version discovery",
299 Method: "GET",
300 Path: "/apis/other/v1",
301 APIGroup: "other",
302 APIVersion: "v1",
303 HasSynced: true,
304 IsResourceRequest: false,
305 ExpectDelegateCalled: true,
306 ExpectStatus: 418,
307 },
308
309 {
310 Name: "existing group, nonexisting version discovery, presync",
311 Method: "GET",
312 Path: "/apis/custom/v2",
313 APIGroup: "custom",
314 APIVersion: "v2",
315 HasSynced: false,
316 IsResourceRequest: false,
317 ExpectDelegateCalled: true,
318 ExpectStatus: 503,
319 },
320 {
321 Name: "existing group, nonexisting version discovery",
322 Method: "GET",
323 Path: "/apis/custom/v2",
324 APIGroup: "custom",
325 APIVersion: "v2",
326 HasSynced: true,
327 IsResourceRequest: false,
328 ExpectDelegateCalled: true,
329 ExpectStatus: 418,
330 },
331
332 {
333 Name: "nonexisting group, resource request, presync",
334 Method: "GET",
335 Path: "/apis/custom/v2/foos",
336 APIGroup: "custom",
337 APIVersion: "v2",
338 Verb: "list",
339 Resource: "foos",
340 HasSynced: false,
341 IsResourceRequest: true,
342 ExpectDelegateCalled: true,
343 ExpectStatus: 503,
344 },
345 {
346 Name: "nonexisting group, resource request",
347 Method: "GET",
348 Path: "/apis/custom/v2/foos",
349 APIGroup: "custom",
350 APIVersion: "v2",
351 Verb: "list",
352 Resource: "foos",
353 HasSynced: true,
354 IsResourceRequest: true,
355 ExpectDelegateCalled: true,
356 ExpectStatus: 418,
357 },
358 }
359
360 for _, tc := range testcases {
361 t.Run(tc.Name, func(t *testing.T) {
362 for _, contentType := range []string{"json", "yaml", "proto", "unknown"} {
363 t.Run(contentType, func(t *testing.T) {
364 delegateCalled = false
365 hasSynced = tc.HasSynced
366
367 recorder := httptest.NewRecorder()
368
369 req := httptest.NewRequest(tc.Method, tc.Path, tc.Body)
370 for k, v := range tc.Headers {
371 req.Header.Set(k, v)
372 }
373
374 expectStatus := tc.ExpectStatus
375 switch contentType {
376 case "json":
377 req.Header.Set("Accept", "application/json")
378 case "yaml":
379 req.Header.Set("Accept", "application/yaml")
380 case "proto":
381 req.Header.Set("Accept", "application/vnd.kubernetes.protobuf, application/json")
382 case "unknown":
383 req.Header.Set("Accept", "application/vnd.kubernetes.unknown")
384
385 if expectStatus == 200 {
386 expectStatus = 406
387 }
388 default:
389 t.Fatalf("unknown content type %v", contentType)
390 }
391
392 req = req.WithContext(apirequest.WithRequestInfo(req.Context(), &apirequest.RequestInfo{
393 Verb: tc.Verb,
394 Resource: tc.Resource,
395 APIGroup: tc.APIGroup,
396 APIVersion: tc.APIVersion,
397 IsResourceRequest: tc.IsResourceRequest,
398 Path: tc.Path,
399 }))
400
401 handler.ServeHTTP(recorder, req)
402
403 if tc.ExpectDelegateCalled != delegateCalled {
404 t.Errorf("expected delegated called %v, got %v", tc.ExpectDelegateCalled, delegateCalled)
405 }
406 result := recorder.Result()
407 content, _ := io.ReadAll(result.Body)
408 if e, a := expectStatus, result.StatusCode; e != a {
409 t.Log(string(content))
410 t.Errorf("expected %v, got %v", e, a)
411 }
412 if tc.ExpectResponse != nil {
413 tc.ExpectResponse(t, result, content)
414 }
415
416
417 if !delegateCalled && expectStatus >= 300 {
418 status := &metav1.Status{}
419
420 switch contentType {
421
422 case "json", "unknown":
423 if e, a := "application/json", result.Header.Get("Content-Type"); e != a {
424 t.Errorf("expected Content-Type %v, got %v", e, a)
425 }
426 if err := json.Unmarshal(content, status); err != nil {
427 t.Fatal(err)
428 }
429 case "yaml":
430 if e, a := "application/yaml", result.Header.Get("Content-Type"); e != a {
431 t.Errorf("expected Content-Type %v, got %v", e, a)
432 }
433 if err := yaml.Unmarshal(content, status); err != nil {
434 t.Fatal(err)
435 }
436 case "proto":
437 if e, a := "application/vnd.kubernetes.protobuf", result.Header.Get("Content-Type"); e != a {
438 t.Errorf("expected Content-Type %v, got %v", e, a)
439 }
440 if _, _, err := protobuf.NewSerializer(Scheme, Scheme).Decode(content, nil, status); err != nil {
441 t.Fatal(err)
442 }
443 default:
444 t.Fatalf("unknown content type %v", contentType)
445 }
446
447 if e, a := metav1.Unversioned.WithKind("Status"), status.GroupVersionKind(); e != a {
448 t.Errorf("expected %#v, got %#v", e, a)
449 }
450 if int(status.Code) != int(expectStatus) {
451 t.Errorf("expected %v, got %v", expectStatus, status.Code)
452 }
453 }
454 })
455 }
456 })
457 }
458 }
459
460 func TestHandlerConversionWithWatchCache(t *testing.T) {
461 testHandlerConversion(t, true)
462 }
463
464 func TestHandlerConversionWithoutWatchCache(t *testing.T) {
465 testHandlerConversion(t, false)
466 }
467
468 func testHandlerConversion(t *testing.T, enableWatchCache bool) {
469 cl := fake.NewSimpleClientset()
470 informers := informers.NewSharedInformerFactory(fake.NewSimpleClientset(), 0)
471 crdInformer := informers.Apiextensions().V1().CustomResourceDefinitions()
472
473 server, storageConfig := etcd3testing.NewUnsecuredEtcd3TestClientServer(t)
474 defer server.Terminate(t)
475
476 crd := multiVersionFixture.DeepCopy()
477
478
479 ctx := apirequest.WithNamespace(apirequest.NewContext(), metav1.NamespaceNone)
480 if _, err := cl.ApiextensionsV1().CustomResourceDefinitions().Create(ctx, crd, metav1.CreateOptions{}); err != nil {
481 t.Fatal(err)
482 }
483 if err := crdInformer.Informer().GetStore().Add(crd); err != nil {
484 t.Fatal(err)
485 }
486
487 etcdOptions := options.NewEtcdOptions(storageConfig)
488 etcdOptions.StorageConfig.Codec = unstructured.UnstructuredJSONScheme
489 restOptionsGetter := generic.RESTOptions{
490 StorageConfig: etcdOptions.StorageConfig.ForResource(schema.GroupResource{Group: crd.Spec.Group, Resource: crd.Spec.Names.Plural}),
491 Decorator: generic.UndecoratedStorage,
492 EnableGarbageCollection: true,
493 DeleteCollectionWorkers: 1,
494 ResourcePrefix: crd.Spec.Group + "/" + crd.Spec.Names.Plural,
495 CountMetricPollPeriod: time.Minute,
496 }
497 if enableWatchCache {
498 restOptionsGetter.Decorator = genericregistry.StorageWithCacher()
499 }
500
501 handler, err := NewCustomResourceDefinitionHandler(
502 &versionDiscoveryHandler{}, &groupDiscoveryHandler{},
503 crdInformer,
504 http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}),
505 restOptionsGetter,
506 dummyAdmissionImpl{},
507 &establish.EstablishingController{},
508 dummyServiceResolverImpl{},
509 func(r webhook.AuthenticationInfoResolver) webhook.AuthenticationInfoResolver { return r },
510 1,
511 dummyAuthorizerImpl{},
512 time.Minute, time.Minute, nil, 3*1024*1024)
513 if err != nil {
514 t.Fatal(err)
515 }
516
517 crdInfo, err := handler.getOrCreateServingInfoFor(crd.UID, crd.Name)
518 if err != nil {
519 t.Fatal(err)
520 }
521
522 updateValidateFunc := func(ctx context.Context, obj, old runtime.Object) error { return nil }
523 validateFunc := func(ctx context.Context, obj runtime.Object) error { return nil }
524 startResourceVersion := ""
525
526 if enableWatchCache {
527
528 time.Sleep(time.Second)
529 }
530
531
532 {
533 u := &unstructured.Unstructured{Object: map[string]interface{}{}}
534 u.SetGroupVersionKind(schema.GroupVersionKind{Group: "stable.example.com", Version: "v1beta1", Kind: "MultiVersion"})
535 u.SetName("marker")
536 if item, err := crdInfo.storages["v1beta1"].CustomResource.Create(ctx, u, validateFunc, &metav1.CreateOptions{}); err != nil {
537 t.Fatal(err)
538 } else {
539 startResourceVersion = item.(*unstructured.Unstructured).GetResourceVersion()
540 }
541 if _, _, err := crdInfo.storages["v1beta1"].CustomResource.Delete(ctx, u.GetName(), validateFunc, &metav1.DeleteOptions{}); err != nil {
542 t.Fatal(err)
543 }
544 }
545
546
547 for _, version := range crd.Spec.Versions {
548 expectGVK := schema.GroupVersionKind{Group: "stable.example.com", Version: version.Name, Kind: "MultiVersion"}
549 u := &unstructured.Unstructured{Object: map[string]interface{}{}}
550 u.SetGroupVersionKind(expectGVK)
551 u.SetName("my-" + version.Name)
552 unstructured.SetNestedField(u.Object, int64(1), "spec", "num")
553
554
555 if item, err := crdInfo.storages[version.Name].CustomResource.Create(ctx, u, validateFunc, &metav1.CreateOptions{}); err != nil {
556 t.Fatal(err)
557 } else if item.GetObjectKind().GroupVersionKind() != expectGVK {
558 t.Errorf("expected create result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
559 } else {
560 u = item.(*unstructured.Unstructured)
561 }
562
563
564 u.SetAnnotations(map[string]string{"updated": "true"})
565 if item, _, err := crdInfo.storages[version.Name].CustomResource.Update(ctx, u.GetName(), rest.DefaultUpdatedObjectInfo(u), validateFunc, updateValidateFunc, false, &metav1.UpdateOptions{}); err != nil {
566 t.Fatal(err)
567 } else if item.GetObjectKind().GroupVersionKind() != expectGVK {
568 t.Errorf("expected update result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
569 }
570
571
572 if item, err := crdInfo.storages[version.Name].CustomResource.Get(ctx, u.GetName(), &metav1.GetOptions{}); err != nil {
573 t.Fatal(err)
574 } else if item.GetObjectKind().GroupVersionKind() != expectGVK {
575 t.Errorf("expected get result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
576 }
577
578 if enableWatchCache {
579
580 time.Sleep(time.Second)
581
582 if item, err := crdInfo.storages[version.Name].CustomResource.Get(ctx, u.GetName(), &metav1.GetOptions{ResourceVersion: "0"}); err != nil {
583 t.Fatal(err)
584 } else if item.GetObjectKind().GroupVersionKind() != expectGVK {
585 t.Errorf("expected cached get result to be %#v, got %#v", expectGVK, item.GetObjectKind().GroupVersionKind())
586 }
587 }
588 }
589
590
591 for _, version := range crd.Spec.Versions {
592 expectGVK := schema.GroupVersionKind{Group: "stable.example.com", Version: version.Name, Kind: "MultiVersion"}
593
594 if list, err := crdInfo.storages[version.Name].CustomResource.List(ctx, &metainternalversion.ListOptions{}); err != nil {
595 t.Fatal(err)
596 } else {
597 for _, item := range list.(*unstructured.UnstructuredList).Items {
598 if item.GroupVersionKind() != expectGVK {
599 t.Errorf("expected list item to be %#v, got %#v", expectGVK, item.GroupVersionKind())
600 }
601 }
602 }
603
604 if enableWatchCache {
605
606 if list, err := crdInfo.storages[version.Name].CustomResource.List(ctx, &metainternalversion.ListOptions{ResourceVersion: "0"}); err != nil {
607 t.Fatal(err)
608 } else {
609 for _, item := range list.(*unstructured.UnstructuredList).Items {
610 if item.GroupVersionKind() != expectGVK {
611 t.Errorf("expected cached list item to be %#v, got %#v", expectGVK, item.GroupVersionKind())
612 }
613 }
614 }
615 }
616
617 watch, err := crdInfo.storages[version.Name].CustomResource.Watch(ctx, &metainternalversion.ListOptions{ResourceVersion: startResourceVersion})
618 if err != nil {
619 t.Fatal(err)
620 }
621
622 for i := 0; i < 5; i++ {
623 select {
624 case event, ok := <-watch.ResultChan():
625 if !ok {
626 t.Fatalf("watch closed")
627 }
628 item, isUnstructured := event.Object.(*unstructured.Unstructured)
629 if !isUnstructured {
630 t.Fatalf("unexpected object type %T: %#v", item, event)
631 }
632 if item.GroupVersionKind() != expectGVK {
633 t.Errorf("expected watch object to be %#v, got %#v", expectGVK, item.GroupVersionKind())
634 }
635 case <-time.After(time.Second):
636 t.Errorf("timed out waiting for watch event")
637 }
638 }
639
640 select {
641 case event := <-watch.ResultChan():
642 t.Errorf("unexpected event: %#v", event)
643 case <-time.After(time.Second):
644 }
645 }
646 }
647
648 func TestDecoder(t *testing.T) {
649 multiVersionJSON := `
650 {
651 "apiVersion": "stable.example.com/v1beta1",
652 "kind": "MultiVersion",
653 "metadata": {
654 "name": "my-mv"
655 },
656 "num": 1,
657 "num": 2,
658 "unknown": "foo"
659 }
660 `
661 multiVersionYAML := `
662 apiVersion: stable.example.com/v1beta1
663 kind: MultiVersion
664 metadata:
665 name: my-mv
666 num: 1
667 num: 2
668 unknown: foo`
669
670 expectedObjUnknownNotPreserved := &unstructured.Unstructured{}
671 err := expectedObjUnknownNotPreserved.UnmarshalJSON([]byte(`
672 {
673 "apiVersion": "stable.example.com/v1beta1",
674 "kind": "MultiVersion",
675 "metadata": {
676 "creationTimestamp": null,
677 "generation": 1,
678 "name": "my-mv"
679 },
680 "num": 2
681 }
682 `))
683 if err != nil {
684 t.Fatal(err)
685 }
686
687 expectedObjUnknownPreserved := &unstructured.Unstructured{}
688 err = expectedObjUnknownPreserved.UnmarshalJSON([]byte(`
689 {
690 "apiVersion": "stable.example.com/v1beta1",
691 "kind": "MultiVersion",
692 "metadata": {
693 "creationTimestamp": null,
694 "generation": 1,
695 "name": "my-mv"
696 },
697 "num": 2,
698 "unknown": "foo"
699 }
700 `))
701 if err != nil {
702 t.Fatal(err)
703 }
704
705 testcases := []struct {
706 name string
707 body string
708 yaml bool
709 strictDecoding bool
710 preserveUnknownFields bool
711 expectedObj *unstructured.Unstructured
712 expectedErr error
713 }{
714 {
715 name: "strict-decoding",
716 body: multiVersionJSON,
717 strictDecoding: true,
718 expectedObj: expectedObjUnknownNotPreserved,
719 expectedErr: errors.New(`strict decoding error: duplicate field "num", unknown field "unknown"`),
720 },
721 {
722 name: "non-strict-decoding",
723 body: multiVersionJSON,
724 strictDecoding: false,
725 expectedObj: expectedObjUnknownNotPreserved,
726 expectedErr: nil,
727 },
728 {
729 name: "strict-decoding-preserve-unknown",
730 body: multiVersionJSON,
731 strictDecoding: true,
732 preserveUnknownFields: true,
733 expectedObj: expectedObjUnknownPreserved,
734 expectedErr: errors.New(`strict decoding error: duplicate field "num"`),
735 },
736 {
737 name: "non-strict-decoding-preserve-unknown",
738 body: multiVersionJSON,
739 strictDecoding: false,
740 preserveUnknownFields: true,
741 expectedObj: expectedObjUnknownPreserved,
742 expectedErr: nil,
743 },
744 {
745 name: "strict-decoding-yaml",
746 body: multiVersionYAML,
747 yaml: true,
748 strictDecoding: true,
749 expectedObj: expectedObjUnknownNotPreserved,
750 expectedErr: errors.New(`strict decoding error: yaml: unmarshal errors:
751 line 7: key "num" already set in map, unknown field "unknown"`),
752 },
753 {
754 name: "non-strict-decoding-yaml",
755 body: multiVersionYAML,
756 yaml: true,
757 strictDecoding: false,
758 expectedObj: expectedObjUnknownNotPreserved,
759 expectedErr: nil,
760 },
761 {
762 name: "strict-decoding-preserve-unknown-yaml",
763 body: multiVersionYAML,
764 yaml: true,
765 strictDecoding: true,
766 preserveUnknownFields: true,
767 expectedObj: expectedObjUnknownPreserved,
768 expectedErr: errors.New(`strict decoding error: yaml: unmarshal errors:
769 line 7: key "num" already set in map`),
770 },
771 {
772 name: "non-strict-decoding-preserve-unknown-yaml",
773 body: multiVersionYAML,
774 yaml: true,
775 strictDecoding: false,
776 preserveUnknownFields: true,
777 expectedObj: expectedObjUnknownPreserved,
778 expectedErr: nil,
779 },
780 }
781 for _, tc := range testcases {
782 t.Run(tc.name, func(t *testing.T) {
783 v := "v1beta1"
784 structuralSchemas := map[string]*structuralschema.Structural{}
785 structuralSchema, err := structuralschema.NewStructural(&apiextensions.JSONSchemaProps{
786 Type: "object",
787 Properties: map[string]apiextensions.JSONSchemaProps{"num": {Type: "integer", Description: "v1beta1 num field"}},
788 })
789 if err != nil {
790 t.Fatal(err)
791 }
792 structuralSchemas[v] = structuralSchema
793 delegate := serializerjson.NewSerializerWithOptions(serializerjson.DefaultMetaFactory, unstructuredCreator{}, nil, serializerjson.SerializerOptions{Yaml: tc.yaml, Strict: tc.strictDecoding})
794 decoder := &schemaCoercingDecoder{
795 delegate: delegate,
796 validator: unstructuredSchemaCoercer{
797 dropInvalidMetadata: true,
798 repairGeneration: true,
799 structuralSchemas: structuralSchemas,
800 structuralSchemaGK: schema.GroupKind{
801 Group: "stable.example.com",
802 Kind: "MultiVersion",
803 },
804 returnUnknownFieldPaths: tc.strictDecoding,
805 preserveUnknownFields: tc.preserveUnknownFields,
806 },
807 }
808
809 obj, _, err := decoder.Decode([]byte(tc.body), nil, nil)
810 if obj != nil {
811 unstructured, ok := obj.(*unstructured.Unstructured)
812 if !ok {
813 t.Fatalf("obj is not an unstructured: %v", obj)
814 }
815 objBytes, err := unstructured.MarshalJSON()
816 if err != nil {
817 t.Fatalf("err marshaling json: %v", err)
818 }
819 expectedBytes, err := tc.expectedObj.MarshalJSON()
820 if err != nil {
821 t.Fatalf("err marshaling json: %v", err)
822 }
823 if bytes.Compare(objBytes, expectedBytes) != 0 {
824 t.Fatalf("expected obj: \n%v\n got obj: \n%v\n", tc.expectedObj, obj)
825 }
826 }
827 if err == nil || tc.expectedErr == nil {
828 if err != nil || tc.expectedErr != nil {
829 t.Fatalf("expected err: %v, got err: %v", tc.expectedErr, err)
830 }
831 } else if err.Error() != tc.expectedErr.Error() {
832 t.Fatalf("expected err: \n%v\n got err: \n%v\n", tc.expectedErr, err)
833 }
834 })
835 }
836
837 }
838
839 type dummyAdmissionImpl struct{}
840
841 func (dummyAdmissionImpl) Handles(operation admission.Operation) bool { return false }
842
843 type dummyAuthorizerImpl struct{}
844
845 func (dummyAuthorizerImpl) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
846 return authorizer.DecisionAllow, "", nil
847 }
848
849 type dummyServiceResolverImpl struct{}
850
851 func (dummyServiceResolverImpl) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) {
852 return &url.URL{Scheme: "https", Host: net.JoinHostPort(name+"."+namespace+".svc", strconv.Itoa(int(port)))}, nil
853 }
854
855 var multiVersionFixture = &apiextensionsv1.CustomResourceDefinition{
856 ObjectMeta: metav1.ObjectMeta{Name: "multiversion.stable.example.com", UID: types.UID("12345")},
857 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
858 Group: "stable.example.com",
859 Names: apiextensionsv1.CustomResourceDefinitionNames{
860 Plural: "multiversion", Singular: "multiversion", Kind: "MultiVersion", ShortNames: []string{"mv"}, ListKind: "MultiVersionList", Categories: []string{"all"},
861 },
862 Conversion: &apiextensionsv1.CustomResourceConversion{Strategy: apiextensionsv1.NoneConverter},
863 Scope: apiextensionsv1.ClusterScoped,
864 PreserveUnknownFields: false,
865 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
866 {
867
868 Name: "v1beta1", Served: true, Storage: true,
869 Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
870 Schema: &apiextensionsv1.CustomResourceValidation{
871 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
872 Type: "object",
873 Properties: map[string]apiextensionsv1.JSONSchemaProps{"num": {Type: "integer", Description: "v1beta1 num field"}},
874 },
875 },
876 },
877 {
878
879 Name: "v1alpha1", Served: true, Storage: false,
880 Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
881 Schema: &apiextensionsv1.CustomResourceValidation{
882 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
883 Type: "object",
884 Properties: map[string]apiextensionsv1.JSONSchemaProps{"num": {Type: "integer", Description: "v1alpha1 num field"}},
885 },
886 },
887 },
888 },
889 },
890 Status: apiextensionsv1.CustomResourceDefinitionStatus{
891 AcceptedNames: apiextensionsv1.CustomResourceDefinitionNames{
892 Plural: "multiversion", Singular: "multiversion", Kind: "MultiVersion", ShortNames: []string{"mv"}, ListKind: "MultiVersionList", Categories: []string{"all"},
893 },
894 },
895 }
896
897 func Test_defaultDeprecationWarning(t *testing.T) {
898 tests := []struct {
899 name string
900 deprecatedVersion string
901 crd apiextensionsv1.CustomResourceDefinitionSpec
902 want string
903 }{
904 {
905 name: "no replacement",
906 deprecatedVersion: "v1",
907 crd: apiextensionsv1.CustomResourceDefinitionSpec{
908 Group: "example.com",
909 Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Widget"},
910 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
911 {Name: "v1", Served: true, Deprecated: true},
912 {Name: "v2", Served: true, Deprecated: true},
913 {Name: "v3", Served: false},
914 },
915 },
916 want: "example.com/v1 Widget is deprecated",
917 },
918 {
919 name: "replacement sorting",
920 deprecatedVersion: "v1",
921 crd: apiextensionsv1.CustomResourceDefinitionSpec{
922 Group: "example.com",
923 Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Widget"},
924 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
925 {Name: "v1", Served: true},
926 {Name: "v1alpha1", Served: true},
927 {Name: "v1alpha2", Served: true},
928 {Name: "v1beta1", Served: true},
929 {Name: "v1beta2", Served: true},
930 {Name: "v2", Served: true},
931 {Name: "v2alpha1", Served: true},
932 {Name: "v2alpha2", Served: true},
933 {Name: "v2beta1", Served: true},
934 {Name: "v2beta2", Served: true},
935 {Name: "v3", Served: false},
936 {Name: "v3alpha1", Served: false},
937 {Name: "v3alpha2", Served: false},
938 {Name: "v3beta1", Served: false},
939 {Name: "v3beta2", Served: false},
940 },
941 },
942 want: "example.com/v1 Widget is deprecated; use example.com/v2 Widget",
943 },
944 {
945 name: "no newer replacement of equal stability",
946 deprecatedVersion: "v2",
947 crd: apiextensionsv1.CustomResourceDefinitionSpec{
948 Group: "example.com",
949 Names: apiextensionsv1.CustomResourceDefinitionNames{Kind: "Widget"},
950 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
951 {Name: "v1", Served: true},
952 {Name: "v3", Served: false},
953 {Name: "v3alpha1", Served: true},
954 {Name: "v3beta1", Served: true},
955 {Name: "v4", Served: true, Deprecated: true},
956 },
957 },
958 want: "example.com/v2 Widget is deprecated",
959 },
960 }
961 for _, tt := range tests {
962 t.Run(tt.name, func(t *testing.T) {
963 if got := defaultDeprecationWarning(tt.deprecatedVersion, tt.crd); got != tt.want {
964 t.Errorf("defaultDeprecationWarning() = %v, want %v", got, tt.want)
965 }
966 })
967 }
968 }
969
970 func TestBuildOpenAPIModelsForApply(t *testing.T) {
971
972 tests := []apiextensionsv1.CustomResourceValidation{
973 {
974 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
975 Type: "object",
976 Properties: map[string]apiextensionsv1.JSONSchemaProps{"num": {Type: "integer", Description: "v1beta1 num field"}},
977 },
978 },
979 {
980 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
981 Type: "",
982 XIntOrString: true,
983 },
984 },
985 {
986 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
987 Type: "object",
988 Properties: map[string]apiextensionsv1.JSONSchemaProps{
989 "oneOf": {
990 OneOf: []apiextensionsv1.JSONSchemaProps{
991 {Type: "boolean"},
992 {Type: "string"},
993 },
994 },
995 },
996 },
997 },
998 {
999 OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
1000 Type: "object",
1001 Properties: map[string]apiextensionsv1.JSONSchemaProps{
1002 "nullable": {
1003 Type: "integer",
1004 Nullable: true,
1005 },
1006 },
1007 },
1008 },
1009 }
1010
1011 staticSpec, err := getOpenAPISpecFromFile()
1012 if err != nil {
1013 t.Fatalf("Failed to load openapi spec: %v", err)
1014 }
1015
1016 crd := apiextensionsv1.CustomResourceDefinition{
1017 ObjectMeta: metav1.ObjectMeta{Name: "example.stable.example.com", UID: types.UID("12345")},
1018 Spec: apiextensionsv1.CustomResourceDefinitionSpec{
1019 Group: "stable.example.com",
1020 Names: apiextensionsv1.CustomResourceDefinitionNames{
1021 Plural: "examples", Singular: "example", Kind: "Example", ShortNames: []string{"ex"}, ListKind: "ExampleList", Categories: []string{"all"},
1022 },
1023 Conversion: &apiextensionsv1.CustomResourceConversion{Strategy: apiextensionsv1.NoneConverter},
1024 Scope: apiextensionsv1.ClusterScoped,
1025 PreserveUnknownFields: false,
1026 Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
1027 {
1028 Name: "v1beta1", Served: true, Storage: true,
1029 Subresources: &apiextensionsv1.CustomResourceSubresources{Status: &apiextensionsv1.CustomResourceSubresourceStatus{}},
1030 },
1031 },
1032 },
1033 }
1034
1035 convertedDefs := map[string]*spec.Schema{}
1036 for k, v := range staticSpec.Definitions {
1037 vCopy := v
1038 convertedDefs[k] = &vCopy
1039 }
1040
1041 for i, test := range tests {
1042 crd.Spec.Versions[0].Schema = &test
1043 models, err := buildOpenAPIModelsForApply(convertedDefs, &crd)
1044 if err != nil {
1045 t.Fatalf("failed to convert to apply model: %v", err)
1046 }
1047 if models == nil {
1048 t.Fatalf("%d: failed to convert to apply model: nil", i)
1049 }
1050 }
1051 }
1052
1053 func getOpenAPISpecFromFile() (*spec.Swagger, error) {
1054 path := filepath.Join("testdata", "swagger.json")
1055 _, err := os.Stat(path)
1056 if err != nil {
1057 return nil, err
1058 }
1059 byteSpec, err := os.ReadFile(path)
1060 if err != nil {
1061 return nil, err
1062 }
1063 staticSpec := &spec.Swagger{}
1064
1065 err = yaml.Unmarshal(byteSpec, staticSpec)
1066 if err != nil {
1067 return nil, err
1068 }
1069
1070 return staticSpec, nil
1071 }
1072
View as plain text