From b3f4882283cc03a5dc56bbab0eb499feca57d37e Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Tue, 3 Jan 2023 13:10:55 +0100 Subject: [PATCH 1/3] Provide a truly lazy restmapper This commit adds a rest mapper that will lazily query the provided client for discovery information to do REST mappings. --- pkg/client/apiutil/lazyrestmapper.go | 259 +++++++++++++ pkg/client/apiutil/lazyrestmapper_test.go | 429 ++++++++++++++++++++++ pkg/client/apiutil/testdata/crd.yaml | 62 ++++ 3 files changed, 750 insertions(+) create mode 100644 pkg/client/apiutil/lazyrestmapper.go create mode 100644 pkg/client/apiutil/lazyrestmapper_test.go create mode 100644 pkg/client/apiutil/testdata/crd.yaml diff --git a/pkg/client/apiutil/lazyrestmapper.go b/pkg/client/apiutil/lazyrestmapper.go new file mode 100644 index 0000000000..a845167995 --- /dev/null +++ b/pkg/client/apiutil/lazyrestmapper.go @@ -0,0 +1,259 @@ +/* +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 + +import ( + "fmt" + "sync" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "k8s.io/client-go/restmapper" +) + +// LazyRESTMapper is a RESTMapper that will lazily query the provided +// client for discovery information to do REST mappings. +type LazyRESTMapper struct { + mapper meta.RESTMapper + client *discovery.DiscoveryClient + knownGroups map[string]*restmapper.APIGroupResources + apiGroups *metav1.APIGroupList + + // mutex to provide thread-safe mapper reloading. + mu sync.Mutex +} + +// NewLazyRESTMapper initializes a LazyRESTMapper. +func NewLazyRESTMapper(c *rest.Config) (meta.RESTMapper, error) { + discoveryClient, err := discovery.NewDiscoveryClientForConfig(c) + if err != nil { + return nil, fmt.Errorf("failed to create discovery client: %w", err) + } + + return NewLazyRESTMapperWithClient(discoveryClient) +} + +// NewLazyRESTMapperWithClient initializes a LazyRESTMapper with a custom discovery client. +func NewLazyRESTMapperWithClient(discoveryClient *discovery.DiscoveryClient) (meta.RESTMapper, error) { + return &LazyRESTMapper{ + mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}), + client: discoveryClient, + knownGroups: map[string]*restmapper.APIGroupResources{}, + }, nil +} + +// KindFor implements Mapper.KindFor. +func (m *LazyRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { + res, err := m.mapper.KindFor(resource) + if meta.IsNoMatchError(err) { + if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil { + return res, err + } + + res, err = m.mapper.KindFor(resource) + } + + return res, err +} + +// KindsFor implements Mapper.KindsFor. +func (m *LazyRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { + res, err := m.mapper.KindsFor(resource) + if meta.IsNoMatchError(err) { + if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil { + return res, err + } + + res, err = m.mapper.KindsFor(resource) + } + + return res, err +} + +// ResourceFor implements Mapper.ResourceFor. +func (m *LazyRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { + res, err := m.mapper.ResourceFor(input) + if meta.IsNoMatchError(err) { + if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil { + return res, err + } + + res, err = m.mapper.ResourceFor(input) + } + + return res, err +} + +// ResourcesFor implements Mapper.ResourcesFor. +func (m *LazyRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { + res, err := m.mapper.ResourcesFor(input) + if meta.IsNoMatchError(err) { + if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil { + return res, err + } + + res, err = m.mapper.ResourcesFor(input) + } + + return res, err +} + +// RESTMapping implements Mapper.RESTMapping. +func (m *LazyRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { + res, err := m.mapper.RESTMapping(gk, versions...) + if meta.IsNoMatchError(err) { + if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil { + return res, err + } + + res, err = m.mapper.RESTMapping(gk, versions...) + } + + return res, err +} + +// RESTMappings implements Mapper.RESTMappings. +func (m *LazyRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { + res, err := m.mapper.RESTMappings(gk, versions...) + if meta.IsNoMatchError(err) { + if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil { + return res, err + } + + res, err = m.mapper.RESTMappings(gk, versions...) + } + + return res, err +} + +// ResourceSingularizer implements Mapper.ResourceSingularizer. +func (m *LazyRESTMapper) ResourceSingularizer(resource string) (string, error) { + return m.mapper.ResourceSingularizer(resource) +} + +// addKnownGroupAndReload reloads the mapper with updated information about missing API group. +// versions can be specified for partial updates, for instance for v1beta1 version only. +func (m *LazyRESTMapper) addKnownGroupAndReload(groupName string, versions ...string) error { + m.mu.Lock() + defer m.mu.Unlock() + + // If no specific versions are set by user, we will scan all available ones for the API group. + // This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls + // this data will be taken from cache. + if len(versions) == 0 { + apiGroup, err := m.findAPIGroupByName(groupName) + if err != nil { + return err + } + for _, version := range apiGroup.Versions { + versions = append(versions, version.Version) + } + } + + // Create or fetch group resources from cache. + groupResources := &restmapper.APIGroupResources{ + Group: metav1.APIGroup{Name: groupName}, + VersionedResources: make(map[string][]metav1.APIResource), + } + if _, ok := m.knownGroups[groupName]; ok { + groupResources = m.knownGroups[groupName] + } + + // Update information for group resources about versioned resources. + // The number of API calls is equal to the number of versions: /apis//. + groupVersionResources, err := m.fetchGroupVersionResources(groupName, versions...) + if err != nil { + return fmt.Errorf("failed to get API group resources: %w", err) + } + for version, resources := range groupVersionResources { + groupResources.VersionedResources[version.Version] = resources.APIResources + } + + // Update information for group resources about the API group by adding new versions. + for _, version := range versions { + groupResources.Group.Versions = append(groupResources.Group.Versions, metav1.GroupVersionForDiscovery{ + GroupVersion: metav1.GroupVersion{Group: groupName, Version: version}.String(), + Version: version, + }) + } + + // Update data in the cache. + m.knownGroups[groupName] = groupResources + + // Finally, update the group with received information and regenerate the mapper. + updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups)) + for _, agr := range m.knownGroups { + updatedGroupResources = append(updatedGroupResources, agr) + } + + m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources) + + return nil +} + +// findAPIGroupByName returns API group by its name. +func (m *LazyRESTMapper) findAPIGroupByName(groupName string) (metav1.APIGroup, error) { + // Ensure that required info about existing API groups is received and stored in the mapper. + // It will make 2 API calls to /api and /apis, but only once. + if m.apiGroups == nil { + apiGroups, err := m.client.ServerGroups() + if err != nil { + return metav1.APIGroup{}, fmt.Errorf("failed to get server groups: %w", err) + } + if len(apiGroups.Groups) == 0 { + return metav1.APIGroup{}, fmt.Errorf("received an empty API groups list") + } + + m.apiGroups = apiGroups + } + + for i := range m.apiGroups.Groups { + if groupName == (&m.apiGroups.Groups[i]).Name { + return m.apiGroups.Groups[i], nil + } + } + + return metav1.APIGroup{}, fmt.Errorf("failed to find API group %s", groupName) +} + +// fetchGroupVersionResources fetches the resources for the specified group and its versions. +func (m *LazyRESTMapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) { + groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList) + failedGroups := make(map[schema.GroupVersion]error) + + for _, version := range versions { + groupVersion := schema.GroupVersion{Group: groupName, Version: version} + + apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String()) + if err != nil { + failedGroups[groupVersion] = err + } + if apiResourceList != nil { + // even in case of error, some fallback might have been returned. + groupVersionResources[groupVersion] = apiResourceList + } + } + + if len(failedGroups) > 0 { + return nil, &discovery.ErrGroupDiscoveryFailed{Groups: failedGroups} + } + + return groupVersionResources, nil +} diff --git a/pkg/client/apiutil/lazyrestmapper_test.go b/pkg/client/apiutil/lazyrestmapper_test.go new file mode 100644 index 0000000000..e9f4feadbb --- /dev/null +++ b/pkg/client/apiutil/lazyrestmapper_test.go @@ -0,0 +1,429 @@ +/* +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 ( + "net/http" + "testing" + + _ "github.com/onsi/ginkgo/v2" + gmg "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + "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) + + // To initialize mapper does 2 requests: + // GET https://host/api + // GET https://host/apis + // Then, 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 + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + 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"}) + g.Expect(err).NotTo(gmg.HaveOccurred()) + g.Expect(mapping.GroupVersionKind.Kind).To(gmg.Equal("deployment")) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) + + mappings, err := lazyRestMapper.RESTMappings(schema.GroupKind{Group: "", Kind: "pod"}) + g.Expect(err).NotTo(gmg.HaveOccurred()) + g.Expect(len(mappings)).To(gmg.Equal(1)) + g.Expect(mappings[0].GroupVersionKind.Kind).To(gmg.Equal("pod")) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) + + 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(5)) + + kinds, err := lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Version: "v1", Resource: "tokenreviews"}) + g.Expect(err).NotTo(gmg.HaveOccurred()) + g.Expect(len(kinds)).To(gmg.Equal(1)) + g.Expect(kinds[0].Kind).To(gmg.Equal("TokenReview")) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) + + 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(7)) + + resources, err := lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Version: "v1", Resource: "poddisruptionbudgets"}) + g.Expect(err).NotTo(gmg.HaveOccurred()) + g.Expect(len(resources)).To(gmg.Equal(1)) + g.Expect(resources[0].Resource).To(gmg.Equal("poddisruptionbudgets")) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(8)) + }) + + 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 + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + 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 + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + 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 + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + 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 + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + 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 + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "INVALID1"}, "v1") + g.Expect(err).To(gmg.HaveOccurred()) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(1)) + + _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "INVALID2"}, "v1") + g.Expect(err).To(gmg.HaveOccurred()) + 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(crt.GetRequestCount()).To(gmg.Equal(3)) + + _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "INVALID4", Version: "v1"}) + g.Expect(err).To(gmg.HaveOccurred()) + 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(crt.GetRequestCount()).To(gmg.Equal(5)) + + _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "INVALID6", Version: "v1"}) + g.Expect(err).To(gmg.HaveOccurred()) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) + }) + + t.Run("LazyRESTMapper should return an error if a resource doesn't exist", func(t *testing.T) { + g := gmg.NewWithT(t) + + // After initialization 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 + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "INVALID"}) + g.Expect(err).To(gmg.HaveOccurred()) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(3)) + + _, err = lazyRestMapper.RESTMappings(schema.GroupKind{Group: "", Kind: "INVALID"}) + g.Expect(err).To(gmg.HaveOccurred()) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(4)) + + _, err = lazyRestMapper.KindFor(schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "INVALID"}) + g.Expect(err).To(gmg.HaveOccurred()) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(5)) + + _, err = lazyRestMapper.KindsFor(schema.GroupVersionResource{Group: "authentication.k8s.io", Version: "v1", Resource: "INVALID"}) + g.Expect(err).To(gmg.HaveOccurred()) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(6)) + + _, err = lazyRestMapper.ResourceFor(schema.GroupVersionResource{Group: "scheduling.k8s.io", Version: "v1", Resource: "INVALID"}) + g.Expect(err).To(gmg.HaveOccurred()) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(7)) + + _, err = lazyRestMapper.ResourcesFor(schema.GroupVersionResource{Group: "policy", Version: "v1", Resource: "INVALID"}) + g.Expect(err).To(gmg.HaveOccurred()) + g.Expect(crt.GetRequestCount()).To(gmg.Equal(8)) + }) + + 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 + + discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + g.Expect(err).NotTo(gmg.HaveOccurred()) + + _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}, "INVALID") + g.Expect(err).To(gmg.HaveOccurred()) + 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(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(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(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(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(crt.GetRequestCount()).To(gmg.Equal(6)) + }) +} diff --git a/pkg/client/apiutil/testdata/crd.yaml b/pkg/client/apiutil/testdata/crd.yaml new file mode 100644 index 0000000000..5bb2d73f69 --- /dev/null +++ b/pkg/client/apiutil/testdata/crd.yaml @@ -0,0 +1,62 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + creationTimestamp: null + name: drivers.crew.example.com +spec: + group: crew.example.com + names: + kind: Driver + plural: drivers + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + schema: + openAPIV3Schema: + description: Driver is the Schema for the drivers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' + type: string + spec: + type: object + status: + type: object + type: object + - name: v2 + served: true + storage: false + schema: + openAPIV3Schema: + description: Driver is the Schema for the drivers API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' + type: string + spec: + type: object + status: + type: object + type: object +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] From f4d10a3e96a11de9fb6e06be94808bc56452f428 Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Mon, 6 Feb 2023 01:05:13 +0100 Subject: [PATCH 2/3] Use DynamicRESTMapperOption to enable Lazy Restmapper Instead of creating the instance of the mapper directly, we will use WithExperimentalLazyMapper option for Dynamic Restmapper. --- pkg/client/apiutil/dynamicrestmapper.go | 11 ++++++ pkg/client/apiutil/lazyrestmapper.go | 41 +++++++++-------------- pkg/client/apiutil/lazyrestmapper_test.go | 41 +++++------------------ 3 files changed, 34 insertions(+), 59 deletions(-) diff --git a/pkg/client/apiutil/dynamicrestmapper.go b/pkg/client/apiutil/dynamicrestmapper.go index 8b7c1c4b68..6b9dcf68ad 100644 --- a/pkg/client/apiutil/dynamicrestmapper.go +++ b/pkg/client/apiutil/dynamicrestmapper.go @@ -40,6 +40,8 @@ type dynamicRESTMapper struct { // Used for lazy init. inited uint32 initMtx sync.Mutex + + useLazyRestmapper bool } // DynamicRESTMapperOption is a functional option on the dynamicRESTMapper. @@ -60,6 +62,12 @@ var WithLazyDiscovery DynamicRESTMapperOption = func(drm *dynamicRESTMapper) err return nil } +// WithExperimentalLazyMapper enables experimental more advanced Lazy Restmapping mechanism. +var WithExperimentalLazyMapper DynamicRESTMapperOption = func(drm *dynamicRESTMapper) error { + drm.useLazyRestmapper = true + return nil +} + // WithCustomMapper supports setting a custom RESTMapper refresher instead of // the default method, which uses a discovery client. // @@ -95,6 +103,9 @@ func NewDynamicRESTMapper(cfg *rest.Config, opts ...DynamicRESTMapperOption) (me return nil, err } } + if drm.useLazyRestmapper { + return newLazyRESTMapperWithClient(client) + } if !drm.lazy { if err := drm.setStaticMapper(); err != nil { return nil, err diff --git a/pkg/client/apiutil/lazyrestmapper.go b/pkg/client/apiutil/lazyrestmapper.go index a845167995..70c6a11dbc 100644 --- a/pkg/client/apiutil/lazyrestmapper.go +++ b/pkg/client/apiutil/lazyrestmapper.go @@ -24,13 +24,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" - "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" ) -// LazyRESTMapper is a RESTMapper that will lazily query the provided +// lazyRESTMapper is a RESTMapper that will lazily query the provided // client for discovery information to do REST mappings. -type LazyRESTMapper struct { +type lazyRESTMapper struct { mapper meta.RESTMapper client *discovery.DiscoveryClient knownGroups map[string]*restmapper.APIGroupResources @@ -40,19 +39,9 @@ type LazyRESTMapper struct { mu sync.Mutex } -// NewLazyRESTMapper initializes a LazyRESTMapper. -func NewLazyRESTMapper(c *rest.Config) (meta.RESTMapper, error) { - discoveryClient, err := discovery.NewDiscoveryClientForConfig(c) - if err != nil { - return nil, fmt.Errorf("failed to create discovery client: %w", err) - } - - return NewLazyRESTMapperWithClient(discoveryClient) -} - -// NewLazyRESTMapperWithClient initializes a LazyRESTMapper with a custom discovery client. -func NewLazyRESTMapperWithClient(discoveryClient *discovery.DiscoveryClient) (meta.RESTMapper, error) { - return &LazyRESTMapper{ +// newLazyRESTMapperWithClient initializes a LazyRESTMapper with a custom discovery client. +func newLazyRESTMapperWithClient(discoveryClient *discovery.DiscoveryClient) (meta.RESTMapper, error) { + return &lazyRESTMapper{ mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}), client: discoveryClient, knownGroups: map[string]*restmapper.APIGroupResources{}, @@ -60,7 +49,7 @@ func NewLazyRESTMapperWithClient(discoveryClient *discovery.DiscoveryClient) (me } // KindFor implements Mapper.KindFor. -func (m *LazyRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { +func (m *lazyRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) { res, err := m.mapper.KindFor(resource) if meta.IsNoMatchError(err) { if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil { @@ -74,7 +63,7 @@ func (m *LazyRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.G } // KindsFor implements Mapper.KindsFor. -func (m *LazyRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { +func (m *lazyRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) { res, err := m.mapper.KindsFor(resource) if meta.IsNoMatchError(err) { if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil { @@ -88,7 +77,7 @@ func (m *LazyRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schem } // ResourceFor implements Mapper.ResourceFor. -func (m *LazyRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { +func (m *lazyRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) { res, err := m.mapper.ResourceFor(input) if meta.IsNoMatchError(err) { if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil { @@ -102,7 +91,7 @@ func (m *LazyRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema. } // ResourcesFor implements Mapper.ResourcesFor. -func (m *LazyRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { +func (m *lazyRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) { res, err := m.mapper.ResourcesFor(input) if meta.IsNoMatchError(err) { if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil { @@ -116,7 +105,7 @@ func (m *LazyRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]sche } // RESTMapping implements Mapper.RESTMapping. -func (m *LazyRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { +func (m *lazyRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) { res, err := m.mapper.RESTMapping(gk, versions...) if meta.IsNoMatchError(err) { if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil { @@ -130,7 +119,7 @@ func (m *LazyRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (* } // RESTMappings implements Mapper.RESTMappings. -func (m *LazyRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { +func (m *lazyRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) { res, err := m.mapper.RESTMappings(gk, versions...) if meta.IsNoMatchError(err) { if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil { @@ -144,13 +133,13 @@ func (m *LazyRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ( } // ResourceSingularizer implements Mapper.ResourceSingularizer. -func (m *LazyRESTMapper) ResourceSingularizer(resource string) (string, error) { +func (m *lazyRESTMapper) ResourceSingularizer(resource string) (string, error) { return m.mapper.ResourceSingularizer(resource) } // addKnownGroupAndReload reloads the mapper with updated information about missing API group. // versions can be specified for partial updates, for instance for v1beta1 version only. -func (m *LazyRESTMapper) addKnownGroupAndReload(groupName string, versions ...string) error { +func (m *lazyRESTMapper) addKnownGroupAndReload(groupName string, versions ...string) error { m.mu.Lock() defer m.mu.Unlock() @@ -209,7 +198,7 @@ func (m *LazyRESTMapper) addKnownGroupAndReload(groupName string, versions ...st } // findAPIGroupByName returns API group by its name. -func (m *LazyRESTMapper) findAPIGroupByName(groupName string) (metav1.APIGroup, error) { +func (m *lazyRESTMapper) findAPIGroupByName(groupName string) (metav1.APIGroup, error) { // Ensure that required info about existing API groups is received and stored in the mapper. // It will make 2 API calls to /api and /apis, but only once. if m.apiGroups == nil { @@ -234,7 +223,7 @@ func (m *LazyRESTMapper) findAPIGroupByName(groupName string) (metav1.APIGroup, } // fetchGroupVersionResources fetches the resources for the specified group and its versions. -func (m *LazyRESTMapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) { +func (m *lazyRESTMapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) { groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList) failedGroups := make(map[schema.GroupVersion]error) diff --git a/pkg/client/apiutil/lazyrestmapper_test.go b/pkg/client/apiutil/lazyrestmapper_test.go index e9f4feadbb..a0027e77ff 100644 --- a/pkg/client/apiutil/lazyrestmapper_test.go +++ b/pkg/client/apiutil/lazyrestmapper_test.go @@ -24,7 +24,6 @@ import ( gmg "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/client-go/discovery" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/envtest" @@ -96,10 +95,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt - discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) // There are no requests before any call @@ -148,10 +144,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt - discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) @@ -188,10 +181,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt - discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) @@ -227,10 +217,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt - discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) @@ -265,10 +252,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt - discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) @@ -312,10 +296,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt - discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "INVALID1"}, "v1") @@ -354,10 +335,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt - discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "INVALID"}) @@ -396,10 +374,7 @@ func TestLazyRestMapperProvider(t *testing.T) { crt := newCountingRoundTripper(httpClient.Transport) httpClient.Transport = crt - discoveryClient, err := discovery.NewDiscoveryClientForConfigAndClient(restCfg, httpClient) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - lazyRestMapper, err := apiutil.NewLazyRESTMapperWithClient(discoveryClient) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}, "INVALID") From c31cb14431acc3fbf09dcb04597543dee112f131 Mon Sep 17 00:00:00 2001 From: Alvaro Aleman Date: Sun, 5 Feb 2023 20:15:39 -0500 Subject: [PATCH 3/3] Update tests for 0.14 --- pkg/client/apiutil/lazyrestmapper_test.go | 108 ++++++++++++---------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/pkg/client/apiutil/lazyrestmapper_test.go b/pkg/client/apiutil/lazyrestmapper_test.go index a0027e77ff..6282370164 100644 --- a/pkg/client/apiutil/lazyrestmapper_test.go +++ b/pkg/client/apiutil/lazyrestmapper_test.go @@ -89,13 +89,14 @@ func TestLazyRestMapperProvider(t *testing.T) { // Then, 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 + var crt *countingRoundTripper + restCfg := rest.CopyConfig(restCfg) + restCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + crt = newCountingRoundTripper(rt) + return crt + } - lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) // There are no requests before any call @@ -138,13 +139,14 @@ func TestLazyRestMapperProvider(t *testing.T) { 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()) + var crt *countingRoundTripper + restCfg := rest.CopyConfig(restCfg) + restCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + crt = newCountingRoundTripper(rt) + return crt + } - crt := newCountingRoundTripper(httpClient.Transport) - httpClient.Transport = crt - - lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) @@ -175,13 +177,14 @@ func TestLazyRestMapperProvider(t *testing.T) { 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 + var crt *countingRoundTripper + restCfg := rest.CopyConfig(restCfg) + restCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + crt = newCountingRoundTripper(rt) + return crt + } - lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) @@ -211,13 +214,14 @@ func TestLazyRestMapperProvider(t *testing.T) { 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 + var crt *countingRoundTripper + restCfg := rest.CopyConfig(restCfg) + restCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + crt = newCountingRoundTripper(rt) + return crt + } - lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) @@ -246,13 +250,14 @@ func TestLazyRestMapperProvider(t *testing.T) { 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 + var crt *countingRoundTripper + restCfg := rest.CopyConfig(restCfg) + restCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + crt = newCountingRoundTripper(rt) + return crt + } - lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) g.Expect(crt.GetRequestCount()).To(gmg.Equal(0)) @@ -290,13 +295,14 @@ func TestLazyRestMapperProvider(t *testing.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 + var crt *countingRoundTripper + restCfg := rest.CopyConfig(restCfg) + restCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + crt = newCountingRoundTripper(rt) + return crt + } - lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "INVALID1"}, "v1") @@ -328,14 +334,14 @@ func TestLazyRestMapperProvider(t *testing.T) { g := gmg.NewWithT(t) // After initialization for each invalid resource the mapper performs just 1 request to the API server. + var crt *countingRoundTripper + restCfg := rest.CopyConfig(restCfg) + restCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + crt = newCountingRoundTripper(rt) + return crt + } - httpClient, err := rest.HTTPClientFor(restCfg) - g.Expect(err).NotTo(gmg.HaveOccurred()) - - crt := newCountingRoundTripper(httpClient.Transport) - httpClient.Transport = crt - - lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, httpClient, apiutil.WithExperimentalLazyMapper) + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "INVALID"}) @@ -367,14 +373,14 @@ func TestLazyRestMapperProvider(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, apiutil.WithExperimentalLazyMapper) + var crt *countingRoundTripper + restCfg := rest.CopyConfig(restCfg) + restCfg.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + crt = newCountingRoundTripper(rt) + return crt + } + + lazyRestMapper, err := apiutil.NewDynamicRESTMapper(restCfg, apiutil.WithExperimentalLazyMapper) g.Expect(err).NotTo(gmg.HaveOccurred()) _, err = lazyRestMapper.RESTMapping(schema.GroupKind{Group: "apps", Kind: "deployment"}, "INVALID")