Skip to content

Commit 3f3c8e0

Browse files
committed
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.
1 parent 3c4deba commit 3f3c8e0

File tree

3 files changed

+703
-0
lines changed

3 files changed

+703
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
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 lazyrestmapper
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+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
27+
"k8s.io/client-go/discovery"
28+
"k8s.io/client-go/rest"
29+
"k8s.io/client-go/restmapper"
30+
)
31+
32+
// LazyRESTMapper is a RESTMapper that will lazily query the provided
33+
// client for discovery information to do REST mappings.
34+
type LazyRESTMapper struct {
35+
mapper meta.RESTMapper
36+
client *discovery.DiscoveryClient
37+
knownGroups map[string]*restmapper.APIGroupResources
38+
apiGroups *metav1.APIGroupList
39+
40+
// mutex to provide thread-safe mapper reloading
41+
mu sync.Mutex
42+
}
43+
44+
// NewLazyRESTMapper initializes a LazyRESTMapper.
45+
func NewLazyRESTMapper(c *rest.Config) (meta.RESTMapper, error) {
46+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(c)
47+
if err != nil {
48+
return nil, fmt.Errorf("failed to create discovery client: %w", err)
49+
}
50+
51+
return NewLazyRESTMapperWithClient(discoveryClient)
52+
}
53+
54+
// NewLazyRESTMapperWithClient initializes a LazyRESTMapper with a custom discovery client.
55+
func NewLazyRESTMapperWithClient(discoveryClient *discovery.DiscoveryClient) (meta.RESTMapper, error) {
56+
return &LazyRESTMapper{
57+
mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
58+
client: discoveryClient,
59+
knownGroups: map[string]*restmapper.APIGroupResources{},
60+
}, nil
61+
}
62+
63+
// KindFor implements Mapper.KindFor.
64+
func (m *LazyRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
65+
res, err := m.mapper.KindFor(resource)
66+
if meta.IsNoMatchError(err) {
67+
if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
68+
return res, err
69+
}
70+
71+
res, err = m.mapper.KindFor(resource)
72+
}
73+
74+
return res, err
75+
}
76+
77+
// KindsFor implements Mapper.KindsFor.
78+
func (m *LazyRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
79+
res, err := m.mapper.KindsFor(resource)
80+
if meta.IsNoMatchError(err) {
81+
if err = m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
82+
return res, err
83+
}
84+
85+
res, err = m.mapper.KindsFor(resource)
86+
}
87+
88+
return res, err
89+
}
90+
91+
// ResourceFor implements Mapper.ResourceFor.
92+
func (m *LazyRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
93+
res, err := m.mapper.ResourceFor(input)
94+
if meta.IsNoMatchError(err) {
95+
if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
96+
return res, err
97+
}
98+
99+
res, err = m.mapper.ResourceFor(input)
100+
}
101+
102+
return res, err
103+
}
104+
105+
// ResourcesFor implements Mapper.ResourcesFor.
106+
func (m *LazyRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
107+
res, err := m.mapper.ResourcesFor(input)
108+
if meta.IsNoMatchError(err) {
109+
if err = m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
110+
return res, err
111+
}
112+
113+
res, err = m.mapper.ResourcesFor(input)
114+
}
115+
116+
return res, err
117+
}
118+
119+
// RESTMapping implements Mapper.RESTMapping.
120+
func (m *LazyRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
121+
res, err := m.mapper.RESTMapping(gk, versions...)
122+
if meta.IsNoMatchError(err) {
123+
if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
124+
return res, err
125+
}
126+
127+
res, err = m.mapper.RESTMapping(gk, versions...)
128+
}
129+
130+
return res, err
131+
}
132+
133+
// RESTMappings implements Mapper.RESTMappings.
134+
func (m *LazyRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
135+
res, err := m.mapper.RESTMappings(gk, versions...)
136+
if meta.IsNoMatchError(err) {
137+
if err = m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
138+
return res, err
139+
}
140+
141+
res, err = m.mapper.RESTMappings(gk, versions...)
142+
}
143+
144+
return res, err
145+
}
146+
147+
// ResourceSingularizer implements Mapper.ResourceSingularizer.
148+
func (m *LazyRESTMapper) ResourceSingularizer(resource string) (string, error) {
149+
return m.mapper.ResourceSingularizer(resource)
150+
}
151+
152+
// addKnownGroupAndReload reloads the mapper with updated information about missing API group.
153+
// versions can be specified for partial updates, for instance for v1beta1 version only.
154+
func (m *LazyRESTMapper) addKnownGroupAndReload(groupName string, versions ...string) error {
155+
m.mu.Lock()
156+
defer m.mu.Unlock()
157+
158+
// First, find information about requested group and its versions. Fail immediately if there is no such group.
159+
apiGroup, err := m.findAPIGroupByName(groupName)
160+
if err != nil {
161+
return err
162+
}
163+
164+
// If no specific versions are set by user, we will scan all available ones for the API group.
165+
if len(versions) == 0 {
166+
for _, version := range apiGroup.Versions {
167+
versions = append(versions, version.Version)
168+
}
169+
}
170+
171+
// Second, get resources. The number of API calls is equal to the number of versions: /apis/<group>/<version>.
172+
groupVersionResources, err := m.fetchGroupVersionResources(apiGroup.Name, versions...)
173+
if err != nil {
174+
return fmt.Errorf("failed to get API group resources: %w", err)
175+
}
176+
177+
groupResources := &restmapper.APIGroupResources{
178+
Group: apiGroup,
179+
VersionedResources: make(map[string][]metav1.APIResource),
180+
}
181+
for version, resources := range groupVersionResources {
182+
groupResources.VersionedResources[version.Version] = resources.APIResources
183+
}
184+
185+
// Add new known API group or just append the resources to the existing group.
186+
if _, ok := m.knownGroups[groupName]; !ok {
187+
m.knownGroups[groupName] = groupResources
188+
} else {
189+
for version, resources := range groupResources.VersionedResources {
190+
m.knownGroups[groupName].VersionedResources[version] = resources
191+
}
192+
}
193+
194+
// Finally, update the group with received information and regenerate the mapper.
195+
updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
196+
for _, v := range m.knownGroups {
197+
updatedGroupResources = append(updatedGroupResources, v)
198+
}
199+
200+
m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
201+
202+
return nil
203+
}
204+
205+
// findAPIGroupByName returns API group by its name.
206+
func (m *LazyRESTMapper) findAPIGroupByName(groupName string) (metav1.APIGroup, error) {
207+
// Ensure that required info about existing API groups is received and stored in the mapper.
208+
// It will make 2 API calls to /api and /apis, but only once.
209+
if m.apiGroups == nil {
210+
apiGroups, err := m.client.ServerGroups()
211+
if err != nil {
212+
return metav1.APIGroup{}, fmt.Errorf("failed to get server groups: %w", err)
213+
}
214+
if len(apiGroups.Groups) == 0 {
215+
return metav1.APIGroup{}, fmt.Errorf("received an empty API groups list")
216+
}
217+
218+
m.apiGroups = apiGroups
219+
}
220+
221+
for i := range m.apiGroups.Groups {
222+
if groupName == (&m.apiGroups.Groups[i]).Name {
223+
return m.apiGroups.Groups[i], nil
224+
}
225+
}
226+
227+
return metav1.APIGroup{}, fmt.Errorf("failed to find API group %s", groupName)
228+
}
229+
230+
// fetchGroupVersionResources fetchs the resources for the specified group and its versions in parallel.
231+
func (m *LazyRESTMapper) fetchGroupVersionResources(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
232+
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
233+
failedGroups := make(map[schema.GroupVersion]error)
234+
235+
wg := &sync.WaitGroup{}
236+
resultLock := &sync.Mutex{}
237+
238+
for _, version := range versions {
239+
groupVersion := schema.GroupVersion{Group: groupName, Version: version}
240+
241+
wg.Add(1)
242+
243+
go func() {
244+
defer wg.Done()
245+
defer utilruntime.HandleCrash()
246+
247+
apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
248+
249+
// lock to record results
250+
resultLock.Lock()
251+
defer resultLock.Unlock()
252+
253+
if err != nil {
254+
failedGroups[groupVersion] = err
255+
}
256+
if apiResourceList != nil {
257+
// even in case of error, some fallback might have been returned
258+
groupVersionResources[groupVersion] = apiResourceList
259+
}
260+
}()
261+
}
262+
263+
wg.Wait()
264+
265+
if len(failedGroups) > 0 {
266+
return nil, &discovery.ErrGroupDiscoveryFailed{Groups: failedGroups}
267+
}
268+
269+
return groupVersionResources, nil
270+
}

0 commit comments

Comments
 (0)