@@ -18,17 +18,20 @@ package apiutil_test
1818
1919import (
2020 "context"
21+ "fmt"
2122 "net/http"
2223 "testing"
2324
24- "k8s.io/apimachinery/pkg/api/meta"
25-
2625 _ "github.com/onsi/ginkgo/v2"
2726 gmg "github.com/onsi/gomega"
28-
27+ "github.com/onsi/gomega/format"
28+ gomegatypes "github.com/onsi/gomega/types"
2929 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
30+ apierrors "k8s.io/apimachinery/pkg/api/errors"
31+ "k8s.io/apimachinery/pkg/api/meta"
3032 "k8s.io/apimachinery/pkg/runtime/schema"
3133 "k8s.io/apimachinery/pkg/types"
34+ "k8s.io/client-go/discovery"
3235 "k8s.io/client-go/kubernetes/scheme"
3336 "k8s.io/client-go/rest"
3437 "sigs.k8s.io/controller-runtime/pkg/client"
@@ -493,23 +496,7 @@ func TestLazyRestMapperProvider(t *testing.T) {
493496 g .Expect (err ).NotTo (gmg .HaveOccurred ())
494497
495498 // 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" )
513500
514501 // Wait a bit until the CRD is registered.
515502 g .Eventually (func () error {
@@ -528,4 +515,190 @@ func TestLazyRestMapperProvider(t *testing.T) {
528515 g .Expect (err ).NotTo (gmg .HaveOccurred ())
529516 g .Expect (mapping .GroupVersionKind .Kind ).To (gmg .Equal ("rider" ))
530517 })
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 ))
531704}
0 commit comments