Skip to content

Commit 23b3137

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 fc423fc commit 23b3137

File tree

3 files changed

+752
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)