@@ -17,22 +17,43 @@ limitations under the License.
17
17
package apiserver
18
18
19
19
import (
20
+ "context"
20
21
"encoding/json"
21
22
"io"
22
23
"io/ioutil"
24
+ "net"
23
25
"net/http"
24
26
"net/http/httptest"
25
- "sigs.k8s.io/yaml"
27
+ "net/url"
28
+ "strconv"
26
29
"testing"
30
+ "time"
31
+
32
+ "sigs.k8s.io/yaml"
27
33
28
34
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
29
35
"k8s.io/apiextensions-apiserver/pkg/apiserver/conversion"
36
+ "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake"
37
+ informers "k8s.io/apiextensions-apiserver/pkg/client/informers/externalversions"
30
38
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/v1"
39
+ "k8s.io/apiextensions-apiserver/pkg/controller/establish"
40
+ metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
31
41
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
42
+ "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
43
+ "k8s.io/apimachinery/pkg/runtime"
32
44
"k8s.io/apimachinery/pkg/runtime/schema"
33
45
"k8s.io/apimachinery/pkg/runtime/serializer/protobuf"
46
+ "k8s.io/apimachinery/pkg/types"
47
+ "k8s.io/apiserver/pkg/admission"
48
+ "k8s.io/apiserver/pkg/authorization/authorizer"
34
49
"k8s.io/apiserver/pkg/endpoints/discovery"
35
50
apirequest "k8s.io/apiserver/pkg/endpoints/request"
51
+ "k8s.io/apiserver/pkg/registry/generic"
52
+ genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
53
+ "k8s.io/apiserver/pkg/registry/rest"
54
+ "k8s.io/apiserver/pkg/server/options"
55
+ etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
56
+ "k8s.io/apiserver/pkg/util/webhook"
36
57
"k8s.io/client-go/tools/cache"
37
58
)
38
59
@@ -418,3 +439,246 @@ func TestRouting(t *testing.T) {
418
439
})
419
440
}
420
441
}
442
+
443
+ func TestHandlerConversionWithWatchCache (t * testing.T ) {
444
+ testHandlerConversion (t , true )
445
+ }
446
+
447
+ func TestHandlerConversionWithoutWatchCache (t * testing.T ) {
448
+ testHandlerConversion (t , false )
449
+ }
450
+
451
+ func testHandlerConversion (t * testing.T , enableWatchCache bool ) {
452
+ cl := fake .NewSimpleClientset ()
453
+ informers := informers .NewSharedInformerFactory (fake .NewSimpleClientset (), 0 )
454
+ crdInformer := informers .Apiextensions ().V1 ().CustomResourceDefinitions ()
455
+
456
+ server , storageConfig := etcd3testing .NewUnsecuredEtcd3TestClientServer (t )
457
+ defer server .Terminate (t )
458
+
459
+ crd := multiVersionFixture .DeepCopy ()
460
+ if _ , err := cl .ApiextensionsV1 ().CustomResourceDefinitions ().Create (context .TODO (), crd , metav1.CreateOptions {}); err != nil {
461
+ t .Fatal (err )
462
+ }
463
+ if err := crdInformer .Informer ().GetStore ().Add (crd ); err != nil {
464
+ t .Fatal (err )
465
+ }
466
+
467
+ etcdOptions := options .NewEtcdOptions (storageConfig )
468
+ etcdOptions .StorageConfig .Codec = unstructured .UnstructuredJSONScheme
469
+ restOptionsGetter := generic.RESTOptions {
470
+ StorageConfig : & etcdOptions .StorageConfig ,
471
+ Decorator : generic .UndecoratedStorage ,
472
+ EnableGarbageCollection : true ,
473
+ DeleteCollectionWorkers : 1 ,
474
+ ResourcePrefix : crd .Spec .Group + "/" + crd .Spec .Names .Plural ,
475
+ CountMetricPollPeriod : time .Minute ,
476
+ }
477
+ if enableWatchCache {
478
+ restOptionsGetter .Decorator = genericregistry .StorageWithCacher (100 )
479
+ }
480
+
481
+ handler , err := NewCustomResourceDefinitionHandler (
482
+ & versionDiscoveryHandler {}, & groupDiscoveryHandler {},
483
+ crdInformer ,
484
+ http .HandlerFunc (func (http.ResponseWriter , * http.Request ) {}),
485
+ restOptionsGetter ,
486
+ dummyAdmissionImpl {},
487
+ & establish.EstablishingController {},
488
+ dummyServiceResolverImpl {},
489
+ func (r webhook.AuthenticationInfoResolver ) webhook.AuthenticationInfoResolver { return r },
490
+ 1 ,
491
+ dummyAuthorizerImpl {},
492
+ time .Minute , time .Minute , nil , 3 * 1024 * 1024 )
493
+ if err != nil {
494
+ t .Fatal (err )
495
+ }
496
+
497
+ crdInfo , err := handler .getOrCreateServingInfoFor (crd .UID , crd .Name )
498
+ if err != nil {
499
+ t .Fatal (err )
500
+ }
501
+
502
+ updateValidateFunc := func (ctx context.Context , obj , old runtime.Object ) error { return nil }
503
+ validateFunc := func (ctx context.Context , obj runtime.Object ) error { return nil }
504
+ startResourceVersion := ""
505
+
506
+ if enableWatchCache {
507
+ // Let watch cache establish initial list
508
+ time .Sleep (time .Second )
509
+ }
510
+
511
+ // Create and delete a marker object to get a starting resource version
512
+ {
513
+ u := & unstructured.Unstructured {Object : map [string ]interface {}{}}
514
+ u .SetGroupVersionKind (schema.GroupVersionKind {Group : "stable.example.com" , Version : "v1beta1" , Kind : "MultiVersion" })
515
+ u .SetName ("marker" )
516
+ if item , err := crdInfo .storages ["v1beta1" ].CustomResource .Create (context .TODO (), u , validateFunc , & metav1.CreateOptions {}); err != nil {
517
+ t .Fatal (err )
518
+ } else {
519
+ startResourceVersion = item .(* unstructured.Unstructured ).GetResourceVersion ()
520
+ }
521
+ if _ , _ , err := crdInfo .storages ["v1beta1" ].CustomResource .Delete (context .TODO (), u .GetName (), validateFunc , & metav1.DeleteOptions {}); err != nil {
522
+ t .Fatal (err )
523
+ }
524
+ }
525
+
526
+ // Create and get every version, expect returned result to match creation GVK
527
+ for _ , version := range crd .Spec .Versions {
528
+ expectGVK := schema.GroupVersionKind {Group : "stable.example.com" , Version : version .Name , Kind : "MultiVersion" }
529
+ u := & unstructured.Unstructured {Object : map [string ]interface {}{}}
530
+ u .SetGroupVersionKind (expectGVK )
531
+ u .SetName ("my-" + version .Name )
532
+ unstructured .SetNestedField (u .Object , int64 (1 ), "spec" , "num" )
533
+
534
+ // Create
535
+ if item , err := crdInfo .storages [version .Name ].CustomResource .Create (context .TODO (), u , validateFunc , & metav1.CreateOptions {}); err != nil {
536
+ t .Fatal (err )
537
+ } else if item .GetObjectKind ().GroupVersionKind () != expectGVK {
538
+ t .Errorf ("expected create result to be %#v, got %#v" , expectGVK , item .GetObjectKind ().GroupVersionKind ())
539
+ } else {
540
+ u = item .(* unstructured.Unstructured )
541
+ }
542
+
543
+ // Update
544
+ u .SetAnnotations (map [string ]string {"updated" : "true" })
545
+ if item , _ , err := crdInfo .storages [version .Name ].CustomResource .Update (context .TODO (), u .GetName (), rest .DefaultUpdatedObjectInfo (u ), validateFunc , updateValidateFunc , false , & metav1.UpdateOptions {}); err != nil {
546
+ t .Fatal (err )
547
+ } else if item .GetObjectKind ().GroupVersionKind () != expectGVK {
548
+ t .Errorf ("expected update result to be %#v, got %#v" , expectGVK , item .GetObjectKind ().GroupVersionKind ())
549
+ }
550
+
551
+ // Get
552
+ if item , err := crdInfo .storages [version .Name ].CustomResource .Get (context .TODO (), u .GetName (), & metav1.GetOptions {}); err != nil {
553
+ t .Fatal (err )
554
+ } else if item .GetObjectKind ().GroupVersionKind () != expectGVK {
555
+ t .Errorf ("expected get result to be %#v, got %#v" , expectGVK , item .GetObjectKind ().GroupVersionKind ())
556
+ }
557
+
558
+ if enableWatchCache {
559
+ // Allow time to propagate the create into the cache
560
+ time .Sleep (time .Second )
561
+ // Get cached
562
+ if item , err := crdInfo .storages [version .Name ].CustomResource .Get (context .TODO (), u .GetName (), & metav1.GetOptions {ResourceVersion : "0" }); err != nil {
563
+ t .Fatal (err )
564
+ } else if item .GetObjectKind ().GroupVersionKind () != expectGVK {
565
+ t .Errorf ("expected cached get result to be %#v, got %#v" , expectGVK , item .GetObjectKind ().GroupVersionKind ())
566
+ }
567
+ }
568
+ }
569
+
570
+ // List every version, expect all returned items to match request GVK
571
+ for _ , version := range crd .Spec .Versions {
572
+ expectGVK := schema.GroupVersionKind {Group : "stable.example.com" , Version : version .Name , Kind : "MultiVersion" }
573
+
574
+ if list , err := crdInfo .storages [version .Name ].CustomResource .List (context .TODO (), & metainternalversion.ListOptions {}); err != nil {
575
+ t .Fatal (err )
576
+ } else {
577
+ for _ , item := range list .(* unstructured.UnstructuredList ).Items {
578
+ if item .GroupVersionKind () != expectGVK {
579
+ t .Errorf ("expected list item to be %#v, got %#v" , expectGVK , item .GroupVersionKind ())
580
+ }
581
+ }
582
+ }
583
+
584
+ if enableWatchCache {
585
+ // List from watch cache
586
+ if list , err := crdInfo .storages [version .Name ].CustomResource .List (context .TODO (), & metainternalversion.ListOptions {ResourceVersion : "0" }); err != nil {
587
+ t .Fatal (err )
588
+ } else {
589
+ for _ , item := range list .(* unstructured.UnstructuredList ).Items {
590
+ if item .GroupVersionKind () != expectGVK {
591
+ t .Errorf ("expected cached list item to be %#v, got %#v" , expectGVK , item .GroupVersionKind ())
592
+ }
593
+ }
594
+ }
595
+ }
596
+
597
+ watch , err := crdInfo .storages [version .Name ].CustomResource .Watch (context .TODO (), & metainternalversion.ListOptions {ResourceVersion : startResourceVersion })
598
+ if err != nil {
599
+ t .Fatal (err )
600
+ }
601
+ // 5 events: delete marker, create v1alpha1, create v1beta1, update v1alpha1, update v1beta1
602
+ for i := 0 ; i < 5 ; i ++ {
603
+ select {
604
+ case event , ok := <- watch .ResultChan ():
605
+ if ! ok {
606
+ t .Fatalf ("watch closed" )
607
+ }
608
+ item , isUnstructured := event .Object .(* unstructured.Unstructured )
609
+ if ! isUnstructured {
610
+ t .Fatalf ("unexpected object type %T: %#v" , item , event )
611
+ }
612
+ if item .GroupVersionKind () != expectGVK {
613
+ t .Errorf ("expected watch object to be %#v, got %#v" , expectGVK , item .GroupVersionKind ())
614
+ }
615
+ case <- time .After (time .Second ):
616
+ t .Errorf ("timed out waiting for watch event" )
617
+ }
618
+ }
619
+ // Expect no more watch events
620
+ select {
621
+ case event := <- watch .ResultChan ():
622
+ t .Errorf ("unexpected event: %#v" , event )
623
+ case <- time .After (time .Second ):
624
+ }
625
+ }
626
+ }
627
+
628
+ type dummyAdmissionImpl struct {}
629
+
630
+ func (dummyAdmissionImpl ) Handles (operation admission.Operation ) bool { return false }
631
+
632
+ type dummyAuthorizerImpl struct {}
633
+
634
+ func (dummyAuthorizerImpl ) Authorize (ctx context.Context , a authorizer.Attributes ) (authorized authorizer.Decision , reason string , err error ) {
635
+ return authorizer .DecisionAllow , "" , nil
636
+ }
637
+
638
+ type dummyServiceResolverImpl struct {}
639
+
640
+ func (dummyServiceResolverImpl ) ResolveEndpoint (namespace , name string , port int32 ) (* url.URL , error ) {
641
+ return & url.URL {Scheme : "https" , Host : net .JoinHostPort (name + "." + namespace + ".svc" , strconv .Itoa (int (port )))}, nil
642
+ }
643
+
644
+ var multiVersionFixture = & apiextensionsv1.CustomResourceDefinition {
645
+ ObjectMeta : metav1.ObjectMeta {Name : "multiversion.stable.example.com" , UID : types .UID ("12345" )},
646
+ Spec : apiextensionsv1.CustomResourceDefinitionSpec {
647
+ Group : "stable.example.com" ,
648
+ Names : apiextensionsv1.CustomResourceDefinitionNames {
649
+ Plural : "multiversion" , Singular : "multiversion" , Kind : "MultiVersion" , ShortNames : []string {"mv" }, ListKind : "MultiVersionList" , Categories : []string {"all" },
650
+ },
651
+ Conversion : & apiextensionsv1.CustomResourceConversion {Strategy : apiextensionsv1 .NoneConverter },
652
+ Scope : apiextensionsv1 .ClusterScoped ,
653
+ PreserveUnknownFields : false ,
654
+ Versions : []apiextensionsv1.CustomResourceDefinitionVersion {
655
+ {
656
+ // storage version, same schema as v1alpha1
657
+ Name : "v1beta1" , Served : true , Storage : true ,
658
+ Subresources : & apiextensionsv1.CustomResourceSubresources {Status : & apiextensionsv1.CustomResourceSubresourceStatus {}},
659
+ Schema : & apiextensionsv1.CustomResourceValidation {
660
+ OpenAPIV3Schema : & apiextensionsv1.JSONSchemaProps {
661
+ Type : "object" ,
662
+ Properties : map [string ]apiextensionsv1.JSONSchemaProps {"num" : {Type : "integer" , Description : "v1beta1 num field" }},
663
+ },
664
+ },
665
+ },
666
+ {
667
+ // same schema as v1beta1
668
+ Name : "v1alpha1" , Served : true , Storage : false ,
669
+ Subresources : & apiextensionsv1.CustomResourceSubresources {Status : & apiextensionsv1.CustomResourceSubresourceStatus {}},
670
+ Schema : & apiextensionsv1.CustomResourceValidation {
671
+ OpenAPIV3Schema : & apiextensionsv1.JSONSchemaProps {
672
+ Type : "object" ,
673
+ Properties : map [string ]apiextensionsv1.JSONSchemaProps {"num" : {Type : "integer" , Description : "v1alpha1 num field" }},
674
+ },
675
+ },
676
+ },
677
+ },
678
+ },
679
+ Status : apiextensionsv1.CustomResourceDefinitionStatus {
680
+ AcceptedNames : apiextensionsv1.CustomResourceDefinitionNames {
681
+ Plural : "multiversion" , Singular : "multiversion" , Kind : "MultiVersion" , ShortNames : []string {"mv" }, ListKind : "MultiVersionList" , Categories : []string {"all" },
682
+ },
683
+ },
684
+ }
0 commit comments