@@ -28,9 +28,11 @@ import (
28
28
gomegatypes "github.com/onsi/gomega/types"
29
29
30
30
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
31
+ apierrors "k8s.io/apimachinery/pkg/api/errors"
31
32
"k8s.io/apimachinery/pkg/api/meta"
32
33
"k8s.io/apimachinery/pkg/runtime/schema"
33
34
"k8s.io/apimachinery/pkg/types"
35
+ "k8s.io/client-go/discovery"
34
36
"k8s.io/client-go/kubernetes/scheme"
35
37
"k8s.io/client-go/rest"
36
38
@@ -529,23 +531,7 @@ func TestLazyRestMapperProvider(t *testing.T) {
529
531
g .Expect (err ).NotTo (gmg .HaveOccurred ())
530
532
531
533
// Register another CRD in runtime - "riders.crew.example.com".
532
-
533
- crd := & apiextensionsv1.CustomResourceDefinition {}
534
- err = c .Get (context .TODO (), types.NamespacedName {Name : "drivers.crew.example.com" }, crd )
535
- g .Expect (err ).NotTo (gmg .HaveOccurred ())
536
- g .Expect (crd .Spec .Names .Kind ).To (gmg .Equal ("Driver" ))
537
-
538
- newCRD := & apiextensionsv1.CustomResourceDefinition {}
539
- crd .DeepCopyInto (newCRD )
540
- newCRD .Name = "riders.crew.example.com"
541
- newCRD .Spec .Names = apiextensionsv1.CustomResourceDefinitionNames {
542
- Kind : "Rider" ,
543
- Plural : "riders" ,
544
- }
545
- newCRD .ResourceVersion = ""
546
-
547
- // Create the new CRD.
548
- g .Expect (c .Create (context .TODO (), newCRD )).To (gmg .Succeed ())
534
+ createNewCRD (context .TODO (), g , c , "crew.example.com" , "Rider" , "riders" )
549
535
550
536
// Wait a bit until the CRD is registered.
551
537
g .Eventually (func () error {
@@ -564,6 +550,153 @@ func TestLazyRestMapperProvider(t *testing.T) {
564
550
g .Expect (err ).NotTo (gmg .HaveOccurred ())
565
551
g .Expect (mapping .GroupVersionKind .Kind ).To (gmg .Equal ("rider" ))
566
552
})
553
+
554
+ t .Run ("LazyRESTMapper should invalidate the group cache if a version is not found" , func (t * testing.T ) {
555
+ g := gmg .NewWithT (t )
556
+ ctx := context .Background ()
557
+
558
+ httpClient , err := rest .HTTPClientFor (restCfg )
559
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
560
+
561
+ crt := newCountingRoundTripper (httpClient .Transport )
562
+ httpClient .Transport = crt
563
+
564
+ lazyRestMapper , err := apiutil .NewDynamicRESTMapper (restCfg , httpClient )
565
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
566
+
567
+ s := scheme .Scheme
568
+ err = apiextensionsv1 .AddToScheme (s )
569
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
570
+
571
+ c , err := client .New (restCfg , client.Options {Scheme : s })
572
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
573
+
574
+ // Register a new CRD ina new group to avoid collisions when deleting versions - "taxis.inventory.example.com".
575
+ group := "inventory.example.com"
576
+ kind := "Taxi"
577
+ plural := "taxis"
578
+ crdName := plural + "." + group
579
+ // Create a CRD with two versions: v1alpha1 and v1 where both are served and
580
+ // v1 is the storage version so we can easily remove v1alpha1 later.
581
+ crd := newCRD (ctx , g , c , group , kind , plural )
582
+ v1alpha1 := crd .Spec .Versions [0 ]
583
+ v1alpha1 .Name = "v1alpha1"
584
+ v1alpha1 .Storage = false
585
+ v1alpha1 .Served = true
586
+ v1 := crd .Spec .Versions [0 ]
587
+ v1 .Name = "v1"
588
+ v1 .Storage = true
589
+ v1 .Served = true
590
+ crd .Spec .Versions = []apiextensionsv1.CustomResourceDefinitionVersion {v1alpha1 , v1 }
591
+ g .Expect (c .Create (ctx , crd )).To (gmg .Succeed ())
592
+ t .Cleanup (func () {
593
+ g .Expect (c .Delete (ctx , crd )).To (gmg .Succeed ())
594
+ })
595
+
596
+ // Wait until the CRD is registered.
597
+ discHTTP , err := rest .HTTPClientFor (restCfg )
598
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
599
+ discClient , err := discovery .NewDiscoveryClientForConfigAndClient (restCfg , discHTTP )
600
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
601
+ g .Eventually (func (g gmg.Gomega ) {
602
+ _ , err = discClient .ServerResourcesForGroupVersion (group + "/v1" )
603
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
604
+ }).Should (gmg .Succeed (), "v1 should be available" )
605
+
606
+ // There are no requests before any call
607
+ g .Expect (crt .GetRequestCount ()).To (gmg .Equal (0 ))
608
+
609
+ // Since we don't specify what version we expect, restmapper will fetch them all and search there.
610
+ // To fetch a list of available versions
611
+ // #1: GET https://host/api
612
+ // #2: GET https://host/apis
613
+ // Then, for all available versions:
614
+ // #3: GET https://host/apis/inventory.example.com/v1alpha1
615
+ // #4: GET https://host/apis/inventory.example.com/v1
616
+ // This should fill the cache for apiGroups and versions.
617
+ mapping , err := lazyRestMapper .RESTMapping (schema.GroupKind {Group : group , Kind : kind })
618
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
619
+ g .Expect (mapping .GroupVersionKind .Kind ).To (gmg .Equal (kind ))
620
+ g .Expect (crt .GetRequestCount ()).To (gmg .Equal (4 ))
621
+ crt .Reset () // We reset the counter to check how many additional requests are made later.
622
+
623
+ // At this point v1alpha1 should be cached
624
+ _ , err = lazyRestMapper .RESTMapping (schema.GroupKind {Group : group , Kind : kind }, "v1alpha1" )
625
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
626
+ g .Expect (crt .GetRequestCount ()).To (gmg .Equal (0 ))
627
+
628
+ // We update the CRD to only have v1 version.
629
+ g .Expect (c .Get (ctx , types.NamespacedName {Name : crdName }, crd )).To (gmg .Succeed ())
630
+ for _ , version := range crd .Spec .Versions {
631
+ if version .Name == "v1" {
632
+ v1 = version
633
+ break
634
+ }
635
+ }
636
+ crd .Spec .Versions = []apiextensionsv1.CustomResourceDefinitionVersion {v1 }
637
+ g .Expect (c .Update (ctx , crd )).To (gmg .Succeed ())
638
+
639
+ // We wait until v1alpha1 is not available anymore.
640
+ g .Eventually (func (g gmg.Gomega ) {
641
+ _ , err = discClient .ServerResourcesForGroupVersion (group + "/v1alpha1" )
642
+ g .Expect (apierrors .IsNotFound (err )).To (gmg .BeTrue (), "v1alpha1 should not be available anymore" )
643
+ }).Should (gmg .Succeed ())
644
+
645
+ // Although v1alpha1 is not available anymore, the cache is not invalidated yet so it should return a mapping.
646
+ _ , err = lazyRestMapper .RESTMapping (schema.GroupKind {Group : group , Kind : kind }, "v1alpha1" )
647
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
648
+ g .Expect (crt .GetRequestCount ()).To (gmg .Equal (0 ))
649
+
650
+ // We request Limo, which is not in the mapper because it doesn't exist.
651
+ // This will trigger a reload of the lazy mapper cache.
652
+ // Reloading the cache will read v2 again and since it's not available anymore, it should invalidate the cache.
653
+ // #1: GET https://host/apis/inventory.example.com/v1alpha1
654
+ // #2: GET https://host/apis/inventory.example.com/v1
655
+ _ , err = lazyRestMapper .RESTMapping (schema.GroupKind {Group : group , Kind : "Limo" })
656
+ g .Expect (err ).To (beNoMatchError ())
657
+ g .Expect (crt .GetRequestCount ()).To (gmg .Equal (2 ))
658
+ crt .Reset ()
659
+
660
+ // Now we request v1alpha1 again and it should return an error since the cache was invalidated.
661
+ // #1: GET https://host/apis/inventory.example.com/v1alpha1
662
+ _ , err = lazyRestMapper .RESTMapping (schema.GroupKind {Group : group , Kind : kind }, "v1alpha1" )
663
+ g .Expect (err ).To (beNoMatchError ())
664
+ g .Expect (crt .GetRequestCount ()).To (gmg .Equal (1 ))
665
+
666
+ // Verify that when requesting the mapping without a version, it doesn't error
667
+ // and it returns v1.
668
+ mapping , err = lazyRestMapper .RESTMapping (schema.GroupKind {Group : group , Kind : kind })
669
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
670
+ g .Expect (mapping .Resource .Version ).To (gmg .Equal ("v1" ))
671
+ })
672
+ }
673
+
674
+ // createNewCRD creates a new CRD with the given group, kind, and plural and returns it.
675
+ func createNewCRD (ctx context.Context , g gmg.Gomega , c client.Client , group , kind , plural string ) * apiextensionsv1.CustomResourceDefinition {
676
+ newCRD := newCRD (ctx , g , c , group , kind , plural )
677
+ g .Expect (c .Create (ctx , newCRD )).To (gmg .Succeed ())
678
+
679
+ return newCRD
680
+ }
681
+
682
+ // newCRD returns a new CRD with the given group, kind, and plural.
683
+ func newCRD (ctx context.Context , g gmg.Gomega , c client.Client , group , kind , plural string ) * apiextensionsv1.CustomResourceDefinition {
684
+ crd := & apiextensionsv1.CustomResourceDefinition {}
685
+ err := c .Get (ctx , types.NamespacedName {Name : "drivers.crew.example.com" }, crd )
686
+ g .Expect (err ).NotTo (gmg .HaveOccurred ())
687
+ g .Expect (crd .Spec .Names .Kind ).To (gmg .Equal ("Driver" ))
688
+
689
+ newCRD := & apiextensionsv1.CustomResourceDefinition {}
690
+ crd .DeepCopyInto (newCRD )
691
+ newCRD .Spec .Group = group
692
+ newCRD .Name = plural + "." + group
693
+ newCRD .Spec .Names = apiextensionsv1.CustomResourceDefinitionNames {
694
+ Kind : kind ,
695
+ Plural : plural ,
696
+ }
697
+ newCRD .ResourceVersion = ""
698
+
699
+ return newCRD
567
700
}
568
701
569
702
func beNoMatchError () gomegatypes.GomegaMatcher {
@@ -594,6 +727,7 @@ func (e *errorMatcher) Match(actual interface{}) (success bool, err error) {
594
727
func (e * errorMatcher ) FailureMessage (actual interface {}) (message string ) {
595
728
return format .Message (actual , fmt .Sprintf ("to be %s error" , e .message ))
596
729
}
730
+
597
731
func (e * errorMatcher ) NegatedFailureMessage (actual interface {}) (message string ) {
598
732
return format .Message (actual , fmt .Sprintf ("not to be %s error" , e .message ))
599
733
}
0 commit comments