Skip to content

Commit b9940ed

Browse files
k8s-infra-cherrypick-robotFedosinalvaroaleman
authored
✨ Provide a truly lazy restmapper (#2179)
* 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. * Use DynamicRESTMapperOption to enable Lazy Restmapper Instead of creating the instance of the mapper directly, we will use WithExperimentalLazyMapper option for Dynamic Restmapper. * Update tests for 0.14 --------- Co-authored-by: Mike Fedosin <[email protected]> Co-authored-by: Alvaro Aleman <[email protected]>
1 parent 5055a52 commit b9940ed

File tree

4 files changed

+731
-0
lines changed

4 files changed

+731
-0
lines changed

pkg/client/apiutil/dynamicrestmapper.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ type dynamicRESTMapper struct {
4040
// Used for lazy init.
4141
inited uint32
4242
initMtx sync.Mutex
43+
44+
useLazyRestmapper bool
4345
}
4446

4547
// DynamicRESTMapperOption is a functional option on the dynamicRESTMapper.
@@ -60,6 +62,12 @@ var WithLazyDiscovery DynamicRESTMapperOption = func(drm *dynamicRESTMapper) err
6062
return nil
6163
}
6264

65+
// WithExperimentalLazyMapper enables experimental more advanced Lazy Restmapping mechanism.
66+
var WithExperimentalLazyMapper DynamicRESTMapperOption = func(drm *dynamicRESTMapper) error {
67+
drm.useLazyRestmapper = true
68+
return nil
69+
}
70+
6371
// WithCustomMapper supports setting a custom RESTMapper refresher instead of
6472
// the default method, which uses a discovery client.
6573
//
@@ -95,6 +103,9 @@ func NewDynamicRESTMapper(cfg *rest.Config, opts ...DynamicRESTMapperOption) (me
95103
return nil, err
96104
}
97105
}
106+
if drm.useLazyRestmapper {
107+
return newLazyRESTMapperWithClient(client)
108+
}
98109
if !drm.lazy {
99110
if err := drm.setStaticMapper(); err != nil {
100111
return nil, err

pkg/client/apiutil/lazyrestmapper.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package apiutil
18+
19+
import (
20+
"fmt"
21+
"sync"
22+
23+
"k8s.io/apimachinery/pkg/api/meta"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
"k8s.io/client-go/discovery"
27+
"k8s.io/client-go/restmapper"
28+
)
29+
30+
// lazyRESTMapper is a RESTMapper that will lazily query the provided
31+
// client for discovery information to do REST mappings.
32+
type lazyRESTMapper struct {
33+
mapper meta.RESTMapper
34+
client *discovery.DiscoveryClient
35+
knownGroups map[string]*restmapper.APIGroupResources
36+
apiGroups *metav1.APIGroupList
37+
38+
// mutex to provide thread-safe mapper reloading.
39+
mu sync.Mutex
40+
}
41+
42+
// newLazyRESTMapperWithClient initializes a LazyRESTMapper with a custom discovery client.
43+
func newLazyRESTMapperWithClient(discoveryClient *discovery.DiscoveryClient) (meta.RESTMapper, error) {
44+
return &lazyRESTMapper{
45+
mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
46+
client: discoveryClient,
47+
knownGroups: map[string]*restmapper.APIGroupResources{},
48+
}, nil
49+
}
50+
51+
// KindFor implements Mapper.KindFor.
52+
func (m *lazyRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
53+
res, err := m.mapper.KindFor(resource)
54+
if meta.IsNoMatchError(err) {
55+
if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
56+
return res, err
57+
}
58+
59+
res, err = m.mapper.KindFor(resource)
60+
}
61+
62+
return res, err
63+
}
64+
65+
// KindsFor implements Mapper.KindsFor.
66+
func (m *lazyRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
67+
res, err := m.mapper.KindsFor(resource)
68+
if meta.IsNoMatchError(err) {
69+
if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
70+
return res, err
71+
}
72+
73+
res, err = m.mapper.KindsFor(resource)
74+
}
75+
76+
return res, err
77+
}
78+
79+
// ResourceFor implements Mapper.ResourceFor.
80+
func (m *lazyRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
81+
res, err := m.mapper.ResourceFor(input)
82+
if meta.IsNoMatchError(err) {
83+
if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
84+
return res, err
85+
}
86+
87+
res, err = m.mapper.ResourceFor(input)
88+
}
89+
90+
return res, err
91+
}
92+
93+
// ResourcesFor implements Mapper.ResourcesFor.
94+
func (m *lazyRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
95+
res, err := m.mapper.ResourcesFor(input)
96+
if meta.IsNoMatchError(err) {
97+
if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
98+
return res, err
99+
}
100+
101+
res, err = m.mapper.ResourcesFor(input)
102+
}
103+
104+
return res, err
105+
}
106+
107+
// RESTMapping implements Mapper.RESTMapping.
108+
func (m *lazyRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
109+
res, err := m.mapper.RESTMapping(gk, versions...)
110+
if meta.IsNoMatchError(err) {
111+
if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
112+
return res, err
113+
}
114+
115+
res, err = m.mapper.RESTMapping(gk, versions...)
116+
}
117+
118+
return res, err
119+
}
120+
121+
// RESTMappings implements Mapper.RESTMappings.
122+
func (m *lazyRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
123+
res, err := m.mapper.RESTMappings(gk, versions...)
124+
if meta.IsNoMatchError(err) {
125+
if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
126+
return res, err
127+
}
128+
129+
res, err = m.mapper.RESTMappings(gk, versions...)
130+
}
131+
132+
return res, err
133+
}
134+
135+
// ResourceSingularizer implements Mapper.ResourceSingularizer.
136+
func (m *lazyRESTMapper) ResourceSingularizer(resource string) (string, error) {
137+
return m.mapper.ResourceSingularizer(resource)
138+
}
139+
140+
// addKnownGroupAndReload reloads the mapper with updated information about missing API group.
141+
// versions can be specified for partial updates, for instance for v1beta1 version only.
142+
func (m *lazyRESTMapper) addKnownGroupAndReload(groupName string, versions ...string) error {
143+
m.mu.Lock()
144+
defer m.mu.Unlock()
145+
146+
// If no specific versions are set by user, we will scan all available ones for the API group.
147+
// This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls
148+
// this data will be taken from cache.
149+
if len(versions) == 0 {
150+
apiGroup, err := m.findAPIGroupByName(groupName)
151+
if err != nil {
152+
return err
153+
}
154+
for _, version := range apiGroup.Versions {
155+
versions = append(versions, version.Version)
156+
}
157+
}
158+
159+
// Create or fetch group resources from cache.
160+
groupResources := &restmapper.APIGroupResources{
161+
Group: metav1.APIGroup{Name: groupName},
162+
VersionedResources: make(map[string][]metav1.APIResource),
163+
}
164+
if _, ok := m.knownGroups[groupName]; ok {
165+
groupResources = m.knownGroups[groupName]
166+
}
167+
168+
// Update information for group resources about versioned resources.
169+
// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
170+
groupVersionResources, err := m.fetchGroupVersionResources(groupName, versions...)
171+
if err != nil {
172+
return fmt.Errorf("failed to get API group resources: %w", err)
173+
}
174+
for version, resources := range groupVersionResources {
175+
groupResources.VersionedResources[version.Version] = resources.APIResources
176+
}
177+
178+
// Update information for group resources about the API group by adding new versions.
179+
for _, version := range versions {
180+
groupResources.Group.Versions = append(groupResources.Group.Versions, metav1.GroupVersionForDiscovery{
181+
GroupVersion: metav1.GroupVersion{Group: groupName, Version: version}.String(),
182+
Version: version,
183+
})
184+
}
185+
186+
// Update data in the cache.
187+
m.knownGroups[groupName] = groupResources
188+
189+
// Finally, update the group with received information and regenerate the mapper.
190+
updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
191+
for _, agr := range m.knownGroups {
192+
updatedGroupResources = append(updatedGroupResources, agr)
193+
}
194+
195+
m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
196+
197+
return nil
198+
}
199+
200+
// findAPIGroupByName returns API group by its name.
201+
func (m *lazyRESTMapper) findAPIGroupByName(groupName string) (metav1.APIGroup, error) {
202+
// Ensure that required info about existing API groups is received and stored in the mapper.
203+
// It will make 2 API calls to /api and /apis, but only once.
204+
if m.apiGroups == nil {
205+
apiGroups, err := m.client.ServerGroups()
206+
if err != nil {
207+
return metav1.APIGroup{}, fmt.Errorf("failed to get server groups: %w", err)
208+
}
209+
if len(apiGroups.Groups) == 0 {
210+
return metav1.APIGroup{}, fmt.Errorf("received an empty API groups list")
211+
}
212+
213+
m.apiGroups = apiGroups
214+
}
215+
216+
for i := range m.apiGroups.Groups {
217+
if groupName == (&m.apiGroups.Groups[i]).Name {
218+
return m.apiGroups.Groups[i], nil
219+
}
220+
}
221+
222+
return metav1.APIGroup{}, fmt.Errorf("failed to find API group %s", groupName)
223+
}
224+
225+
// fetchGroupVersionResources fetches the resources for the specified group and its versions.
226+
func (m *lazyRESTMapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
227+
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
228+
failedGroups := make(map[schema.GroupVersion]error)
229+
230+
for _, version := range versions {
231+
groupVersion := schema.GroupVersion{Group: groupName, Version: version}
232+
233+
apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
234+
if err != nil {
235+
failedGroups[groupVersion] = err
236+
}
237+
if apiResourceList != nil {
238+
// even in case of error, some fallback might have been returned.
239+
groupVersionResources[groupVersion] = apiResourceList
240+
}
241+
}
242+
243+
if len(failedGroups) > 0 {
244+
return nil, &discovery.ErrGroupDiscoveryFailed{Groups: failedGroups}
245+
}
246+
247+
return groupVersionResources, nil
248+
}

0 commit comments

Comments
 (0)