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