/* Copyright 2023 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package apiutil_test import ( "context" "fmt" "net/http" "testing" _ "github.com/onsi/ginkgo/v2" gmg "github.com/onsi/gomega" "github.com/onsi/gomega/format" gomegatypes "github.com/onsi/gomega/types" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/envtest" ) // countingRoundTripper is used to count HTTP requests. type countingRoundTripper struct { roundTripper http.RoundTripper requestCount int } func newCountingRoundTripper(rt http.RoundTripper) *countingRoundTripper { return &countingRoundTripper{roundTripper: rt} } // RoundTrip implements http.RoundTripper.RoundTrip that additionally counts requests. func (crt *countingRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { crt.requestCount++ return crt.roundTripper.RoundTrip(r) } // GetRequestCount returns how many requests have been made. func (crt *countingRoundTripper) GetRequestCount() int { return crt.requestCount } // Reset sets the counter to 0. func (crt *countingRoundTripper) Reset() { crt.requestCount = 0 } func setupEnvtest(t *testing.T) (*rest.Config, func(t *testing.T)) { t.Log("Setup envtest") g := gmg.NewWithT(t) testEnv := &envtest.Environment{ CRDDirectoryPaths: []string{"testdata"}, } cfg, err := testEnv.Start() g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(cfg).NotTo(gmg.BeNil()) teardownFunc := func(t *testing.T) { t.Log("Stop envtest") g.Expect(testEnv.Stop()).To(gmg.Succeed()) } return cfg, teardownFunc } func TestLazyRestMapperProvider(t *testing.T) { restCfg, tearDownFn := setupEnvtest(t) defer tearDownFn(t) t.Run("LazyRESTMapper should fetch data based on the request", func(t *testing.T) { g := gmg.NewWithT(t) // For each new group it performs just one request to the API server: // GET https://host/apis// httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) // There are no requests before any call g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}, "v1") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("deployment")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) mappings, err := lazyRestMapper.RESTMappings(schema.GroupKind{Group: "", Kind: "pod"}, "v1") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mappings).To(gmg.HaveLen(1)) g.Expect(mappings[0].GroupVersionKind.Kind).To(gmg.Equal("pod")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) kind, err := lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(kind.Kind).To(gmg.Equal("Ingress")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) kinds, err := lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Version: "v1", Resource: "tokenreviews"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(kinds).To(gmg.HaveLen(1)) g.Expect(kinds[0].Kind).To(gmg.Equal("TokenReview")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) resource, err := lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Version: "v1", Resource: "priorityclasses"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(resource.Resource).To(gmg.Equal("priorityclasses")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) resources, err := lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Version: "v1", Resource: "poddisruptionbudgets"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(resources).To(gmg.HaveLen(1)) g.Expect(resources[0].Resource).To(gmg.Equal("poddisruptionbudgets")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) }) t.Run("LazyRESTMapper should cache fetched data and doesn't perform any additional requests", func(t *testing.T) { g := gmg.NewWithT(t) httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("deployment")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) // Data taken from cache - there are no more additional requests. mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("deployment")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) kind, err := lazyRestMapper.KindFor((schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"})) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(kind.Kind).To(gmg.Equal("Deployment")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) resource, err := lazyRestMapper.ResourceFor((schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"})) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(resource.Resource).To(gmg.Equal("deployments")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) }) t.Run("LazyRESTMapper should work correctly with empty versions list", func(t *testing.T) { g := gmg.NewWithT(t) httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) // crew.example.com has 2 versions: v1 and v2 // If no versions were provided by user, we fetch all of them. // Here we expect 4 calls. // To initialize: // #1: GET https://host/api // #2: GET https://host/apis // Then, for each version it performs one request to the API server: // #3: GET https://host/apis/crew.example.com/v1 // #4: GET https://host/apis/crew.example.com/v2 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) // All subsequent calls won't send requests to the server. mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) }) t.Run("LazyRESTMapper should work correctly with multiple API group versions", func(t *testing.T) { g := gmg.NewWithT(t) httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) // We explicitly ask for 2 versions: v1 and v2. // For each version it performs one request to the API server: // #1: GET https://host/apis/crew.example.com/v1 // #2: GET https://host/apis/crew.example.com/v2 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1", "v2") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) // All subsequent calls won't send requests to the server as everything is stored in the cache. mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) }) t.Run("LazyRESTMapper should work correctly with different API group versions", func(t *testing.T) { g := gmg.NewWithT(t) httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) // Now we want resources for crew.example.com/v1 version only. // Here we expect 1 call: // #1: GET https://host/apis/crew.example.com/v1 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) // Get additional resources from v2. // It sends another request: // #2: GET https://host/apis/crew.example.com/v2 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v2") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) // No subsequent calls require additional API requests. mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}, "v1", "v2") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) }) t.Run("LazyRESTMapper should return an error if the group doesn't exist", func(t *testing.T) { g := gmg.NewWithT(t) // After initialization for each invalid group the mapper performs just 1 request to the API server. httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) // A version is specified but the group doesn't exist. // For each group, we expect 1 call to the version-specific discovery endpoint: // #1: GET https://host/apis// _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "INVALID1"}, "v1") g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "INVALID2"}, "v1") g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "INVALID3", Version: "v1"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "INVALID4", Version: "v1"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "INVALID5", Version: "v1"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "INVALID6", Version: "v1"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) // No version is specified but the group doesn't exist. // For each group, we expect 2 calls to discover all group versions: // #1: GET https://host/api // #2: GET https://host/apis _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "INVALID7"}) g.Expect(err).To(beNoMatchError()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(8)) _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "INVALID8"}) g.Expect(err).To(beNoMatchError()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(10)) _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "INVALID9"}) g.Expect(err).To(beNoMatchError()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(12)) _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "INVALID10"}) g.Expect(err).To(beNoMatchError()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(14)) _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "INVALID11"}) g.Expect(err).To(beNoMatchError()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(16)) _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "INVALID12"}) g.Expect(err).To(beNoMatchError()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(18)) }) t.Run("LazyRESTMapper should return an error if a resource doesn't exist", func(t *testing.T) { g := gmg.NewWithT(t) // For each invalid resource the mapper performs just 1 request to the API server. httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "INVALID"}, "v1") g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "", Kind: "INVALID"}, "v1") g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "INVALID"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Version: "v1", Resource: "INVALID"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Version: "v1", Resource: "INVALID"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Version: "v1", Resource: "INVALID"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) }) t.Run("LazyRESTMapper should return an error if the version doesn't exist", func(t *testing.T) { g := gmg.NewWithT(t) // After initialization, for each invalid resource mapper performs 1 requests to the API server. httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}, "INVALID") g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "", Kind: "pod"}, "INVALID") g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Version: "INVALID", Resource: "ingresses"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Version: "INVALID", Resource: "tokenreviews"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Version: "INVALID", Resource: "priorityclasses"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Version: "INVALID", Resource: "poddisruptionbudgets"}) g.Expect(err).To(gmg.HaveOccurred()) g.Expect(meta.IsNoMatchError(err)).To(gmg.BeTrue()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) }) t.Run("LazyRESTMapper should work correctly if the version isn't specified", func(t *testing.T) { g := gmg.NewWithT(t) httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) kind, err := lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Resource: "ingress"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(kind.Version).ToNot(gmg.BeEmpty()) kinds, err := lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Resource: "tokenreviews"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(kinds).ToNot(gmg.BeEmpty()) g.Expect(kinds[0].Version).ToNot(gmg.BeEmpty()) resorce, err := lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Resource: "priorityclasses"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(resorce.Version).ToNot(gmg.BeEmpty()) resorces, err := lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Resource: "poddisruptionbudgets"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(kinds).ToNot(gmg.BeEmpty()) g.Expect(resorces[0].Version).ToNot(gmg.BeEmpty()) }) t.Run("LazyRESTMapper can fetch CRDs if they were created at runtime", func(t *testing.T) { g := gmg.NewWithT(t) // To fetch all versions mapper does 2 requests: // GET https://host/api // GET https://host/apis // Then, for each version it performs just one request to the API server as usual: // GET https://host/apis// httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) // There are no requests before any call g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) // Since we don't specify what version we expect, restmapper will fetch them all and search there. // To fetch a list of available versions // #1: GET https://host/api // #2: GET https://host/apis // Then, for each currently registered version: // #3: GET https://host/apis/crew.example.com/v1 // #4: GET https://host/apis/crew.example.com/v2 mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "driver"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("driver")) g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) s := scheme.Scheme err = apiextensionsv1.AddToScheme(s) g.Expect(err).NotTo(gmg.HaveOccurred()) c, err := client.New(restCfg, client.Options{Scheme: s}) g.Expect(err).NotTo(gmg.HaveOccurred()) // Register another CRD in runtime - "riders.crew.example.com". createNewCRD(context.TODO(), g, c, "crew.example.com", "Rider", "riders") // Wait a bit until the CRD is registered. g.Eventually(func() error { _, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "rider"}) return err }).Should(gmg.Succeed()) // Since we don't specify what version we expect, restmapper will fetch them all and search there. // To fetch a list of available versions // #1: GET https://host/api // #2: GET https://host/apis // Then, for each currently registered version: // #3: GET https://host/apis/crew.example.com/v1 // #4: GET https://host/apis/crew.example.com/v2 mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "crew.example.com", Kind: "rider"}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("rider")) }) t.Run("LazyRESTMapper should invalidate the group cache if a version is not found", func(t *testing.T) { g := gmg.NewWithT(t) ctx := context.Background() httpClient, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient) g.Expect(err).NotTo(gmg.HaveOccurred()) s := scheme.Scheme err = apiextensionsv1.AddToScheme(s) g.Expect(err).NotTo(gmg.HaveOccurred()) c, err := client.New(restCfg, client.Options{Scheme: s}) g.Expect(err).NotTo(gmg.HaveOccurred()) // Register a new CRD ina new group to avoid collisions when deleting versions - "taxis.inventory.example.com". group := "inventory.example.com" kind := "Taxi" plural := "taxis" crdName := plural + "." + group // Create a CRD with two versions: v1alpha1 and v1 where both are served and // v1 is the storage version so we can easily remove v1alpha1 later. crd := newCRD(ctx, g, c, group, kind, plural) v1alpha1 := crd.Spec.Versions[0] v1alpha1.Name = "v1alpha1" v1alpha1.Storage = false v1alpha1.Served = true v1 := crd.Spec.Versions[0] v1.Name = "v1" v1.Storage = true v1.Served = true crd.Spec.Versions = []apiextensionsv1.CustomResourceDefinitionVersion{v1alpha1, v1} g.Expect(c.Create(ctx, crd)).To(gmg.Succeed()) t.Cleanup(func() { g.Expect(c.Delete(ctx, crd)).To(gmg.Succeed()) }) // Wait until the CRD is registered. discHTTP, err := rest.HTTPClientFor(restCfg) g.Expect(err).NotTo(gmg.HaveOccurred()) discClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, discHTTP) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Eventually(func(g gmg.Gomega) { _, err = discClient.ServerResourcesForGroupVersion(group + "/v1") g.Expect(err).NotTo(gmg.HaveOccurred()) }).Should(gmg.Succeed(), "v1 should be available") // There are no requests before any call g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) // Since we don't specify what version we expect, restmapper will fetch them all and search there. // To fetch a list of available versions // #1: GET https://host/api // #2: GET https://host/apis // Then, for all available versions: // #3: GET https://host/apis/inventory.example.com/v1alpha1 // #4: GET https://host/apis/inventory.example.com/v1 // This should fill the cache for apiGroups and versions. mapping, err := lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal(kind)) g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) crt.Reset() // We reset the counter to check how many additional requests are made later. // At this point v1alpha1 should be cached _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v1alpha1") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) // We update the CRD to only have v1 version. g.Expect(c.Get(ctx, types.NamespacedName{Name: crdName}, crd)).To(gmg.Succeed()) for _, version := range crd.Spec.Versions { if version.Name == "v1" { v1 = version break } } crd.Spec.Versions = []apiextensionsv1.CustomResourceDefinitionVersion{v1} g.Expect(c.Update(ctx, crd)).To(gmg.Succeed()) // We wait until v1alpha1 is not available anymore. g.Eventually(func(g gmg.Gomega) { _, err = discClient.ServerResourcesForGroupVersion(group + "/v1alpha1") g.Expect(apierrors.IsNotFound(err)).To(gmg.BeTrue(), "v1alpha1 should not be available anymore") }).Should(gmg.Succeed()) // Although v1alpha1 is not available anymore, the cache is not invalidated yet so it should return a mapping. _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v1alpha1") g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) // We request Limo, which is not in the mapper because it doesn't exist. // This will trigger a reload of the lazy mapper cache. // Reloading the cache will read v2 again and since it's not available anymore, it should invalidate the cache. // #1: GET https://host/apis/inventory.example.com/v1alpha1 // #2: GET https://host/apis/inventory.example.com/v1 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: "Limo"}) g.Expect(err).To(beNoMatchError()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(2)) crt.Reset() // Now we request v1alpha1 again and it should return an error since the cache was invalidated. // #1: GET https://host/apis/inventory.example.com/v1alpha1 _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}, "v1alpha1") g.Expect(err).To(beNoMatchError()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) // Verify that when requesting the mapping without a version, it doesn't error // and it returns v1. mapping, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: group, Kind: kind}) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(mapping.Resource.Version).To(gmg.Equal("v1")) }) } // createNewCRD creates a new CRD with the given group, kind, and plural and returns it. func createNewCRD(ctx context.Context, g gmg.Gomega, c client.Client, group, kind, plural string) *apiextensionsv1.CustomResourceDefinition { newCRD := newCRD(ctx, g, c, group, kind, plural) g.Expect(c.Create(ctx, newCRD)).To(gmg.Succeed()) return newCRD } // newCRD returns a new CRD with the given group, kind, and plural. func newCRD(ctx context.Context, g gmg.Gomega, c client.Client, group, kind, plural string) *apiextensionsv1.CustomResourceDefinition { crd := &apiextensionsv1.CustomResourceDefinition{} err := c.Get(ctx, types.NamespacedName{Name: "drivers.crew.example.com"}, crd) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crd.Spec.Names.Kind).To(gmg.Equal("Driver")) newCRD := &apiextensionsv1.CustomResourceDefinition{} crd.DeepCopyInto(newCRD) newCRD.Spec.Group = group newCRD.Name = plural + "." + group newCRD.Spec.Names = apiextensionsv1.CustomResourceDefinitionNames{ Kind: kind, Plural: plural, } newCRD.ResourceVersion = "" return newCRD } func beNoMatchError() gomegatypes.GomegaMatcher { return &errorMatcher{ checkFunc: meta.IsNoMatchError, message: "NoMatch", } } type errorMatcher struct { checkFunc func(error) bool message string } func (e *errorMatcher) Match(actual interface{}) (success bool, err error) { if actual == nil { return false, nil } actualErr, actualOk := actual.(error) if !actualOk { return false, fmt.Errorf("expected an error-type. got:\n%s", format.Object(actual, 1)) } return e.checkFunc(actualErr), nil } func (e *errorMatcher) FailureMessage(actual interface{}) (message string) { return format.Message(actual, fmt.Sprintf("to be %s error", e.message)) } func (e *errorMatcher) NegatedFailureMessage(actual interface{}) (message string) { return format.Message(actual, fmt.Sprintf("not to be %s error", e.message)) }