Skip to content

Commit cc59ac4

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 cc59ac4

File tree

2 files changed

+489
-0
lines changed

2 files changed

+489
-0
lines changed
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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+
"sync"
21+
22+
"k8s.io/apimachinery/pkg/api/meta"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/runtime/schema"
25+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
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+
38+
// mutex to provide thread-safe mapper reloading
39+
mu sync.Mutex
40+
}
41+
42+
// NewLazyRESTMapper initializes a LazyRESTMapper
43+
func NewLazyRESTMapper(c *rest.Config) (meta.RESTMapper, error) {
44+
dc, err := discovery.NewDiscoveryClientForConfig(c)
45+
if err != nil {
46+
return nil, err
47+
}
48+
49+
return &LazyRESTMapper{
50+
mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
51+
client: dc,
52+
knownGroups: map[string]*restmapper.APIGroupResources{},
53+
}, nil
54+
}
55+
56+
// NewLazyRESTMapperWithClient initializes a LazyRESTMapper
57+
func NewLazyRESTMapperWithClient(dc *discovery.DiscoveryClient) meta.RESTMapper {
58+
return &LazyRESTMapper{
59+
mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
60+
client: dc,
61+
knownGroups: map[string]*restmapper.APIGroupResources{},
62+
}
63+
}
64+
65+
func (m *LazyRESTMapper) addKnownGroupAndReload(groupName string) error {
66+
m.mu.Lock()
67+
defer m.mu.Unlock()
68+
69+
groupResources, err := getFilteredAPIGroupResources(m.client, groupName)
70+
if err != nil || groupResources == nil {
71+
return err
72+
}
73+
74+
m.knownGroups[groupName] = groupResources
75+
76+
updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
77+
for _, v := range m.knownGroups {
78+
updatedGroupResources = append(updatedGroupResources, v)
79+
}
80+
81+
m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
82+
83+
return nil
84+
}
85+
86+
// KindFor implements Mapper.KindFor
87+
func (m *LazyRESTMapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
88+
res, err := m.mapper.KindFor(resource)
89+
if meta.IsNoMatchError(err) {
90+
if err = m.addKnownGroupAndReload(resource.Group); err != nil {
91+
return res, err
92+
}
93+
res, err = m.mapper.KindFor(resource)
94+
}
95+
return res, err
96+
}
97+
98+
// KindsFor implements Mapper.KindsFor
99+
func (m *LazyRESTMapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
100+
res, err := m.mapper.KindsFor(resource)
101+
if meta.IsNoMatchError(err) {
102+
if err = m.addKnownGroupAndReload(resource.Group); err != nil {
103+
return res, err
104+
}
105+
res, err = m.mapper.KindsFor(resource)
106+
}
107+
return res, err
108+
}
109+
110+
// ResourceFor implements Mapper.ResourceFor
111+
func (m *LazyRESTMapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
112+
res, err := m.mapper.ResourceFor(input)
113+
if meta.IsNoMatchError(err) {
114+
if err = m.addKnownGroupAndReload(input.Group); err != nil {
115+
return res, err
116+
}
117+
res, err = m.mapper.ResourceFor(input)
118+
}
119+
return res, err
120+
}
121+
122+
// ResourcesFor implements Mapper.ResourcesFor
123+
func (m *LazyRESTMapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
124+
res, err := m.mapper.ResourcesFor(input)
125+
if meta.IsNoMatchError(err) {
126+
if err = m.addKnownGroupAndReload(input.Group); err != nil {
127+
return res, err
128+
}
129+
res, err = m.mapper.ResourcesFor(input)
130+
}
131+
return res, err
132+
}
133+
134+
// RESTMapping implements Mapper.RESTMapping
135+
func (m *LazyRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
136+
res, err := m.mapper.RESTMapping(gk, versions...)
137+
if meta.IsNoMatchError(err) {
138+
if err = m.addKnownGroupAndReload(gk.Group); err != nil {
139+
return res, err
140+
}
141+
res, err = m.mapper.RESTMapping(gk, versions...)
142+
}
143+
return res, err
144+
}
145+
146+
// RESTMappings implements Mapper.RESTMappings
147+
func (m *LazyRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
148+
res, err := m.mapper.RESTMappings(gk, versions...)
149+
if meta.IsNoMatchError(err) {
150+
if err = m.addKnownGroupAndReload(gk.Group); err != nil {
151+
return res, err
152+
}
153+
res, err = m.mapper.RESTMappings(gk, versions...)
154+
}
155+
return res, err
156+
}
157+
158+
// ResourceSingularizer implements Mapper.ResourceSingularizer
159+
func (m *LazyRESTMapper) ResourceSingularizer(resource string) (string, error) {
160+
return m.mapper.ResourceSingularizer(resource)
161+
}
162+
163+
// fetchGroupVersionResources uses the discovery client to fetch the resources for the specified group in parallel.
164+
// Mainly replicates the same named function from the client-go internals aside from the changed `apiGroups` argument type (uses slice instead of a single group).
165+
// ref: https://github.com/kubernetes/kubernetes/blob/a84d877310ba5cf9237c8e8e3218229c202d3a1e/staging/src/k8s.io/client-go/discovery/discovery_client.go#L506
166+
func fetchGroupVersionResources(d discovery.DiscoveryInterface, apiGroup metav1.APIGroup) (map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error) {
167+
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
168+
failedGroups := make(map[schema.GroupVersion]error)
169+
170+
wg := &sync.WaitGroup{}
171+
resultLock := &sync.Mutex{}
172+
for _, version := range apiGroup.Versions {
173+
groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
174+
wg.Add(1)
175+
go func() {
176+
defer wg.Done()
177+
defer utilruntime.HandleCrash()
178+
179+
apiResourceList, err := d.ServerResourcesForGroupVersion(groupVersion.String())
180+
181+
// lock to record results
182+
resultLock.Lock()
183+
defer resultLock.Unlock()
184+
185+
if err != nil {
186+
// TODO: maybe restrict this to NotFound errors
187+
failedGroups[groupVersion] = err
188+
}
189+
if apiResourceList != nil {
190+
// even in case of error, some fallback might have been returned
191+
groupVersionResources[groupVersion] = apiResourceList
192+
}
193+
}()
194+
}
195+
wg.Wait()
196+
197+
return groupVersionResources, failedGroups
198+
}
199+
200+
// filteredServerGroupsAndResources returns the supported resources for groups filtered by passed predicate and versions.
201+
// Mainly replicate ServerGroupsAndResources function from the client-go. The difference is that this function takes
202+
// a group name as an argument for filtering out unwanted groups.
203+
// ref: https://github.com/kubernetes/kubernetes/blob/a84d877310ba5cf9237c8e8e3218229c202d3a1e/staging/src/k8s.io/client-go/discovery/discovery_client.go#L383
204+
func filteredServerGroupsAndResources(d discovery.DiscoveryInterface, groupName string) (*metav1.APIGroup, []*metav1.APIResourceList, error) {
205+
sgs, err := d.ServerGroups()
206+
if sgs == nil {
207+
return nil, nil, err
208+
}
209+
210+
apiGroup := metav1.APIGroup{}
211+
for i := range sgs.Groups {
212+
if groupName == (&sgs.Groups[i]).Name {
213+
apiGroup = sgs.Groups[i]
214+
}
215+
}
216+
217+
groupVersionResources, failedGroups := fetchGroupVersionResources(d, apiGroup)
218+
219+
// order results by group/version discovery order
220+
result := []*metav1.APIResourceList{}
221+
for _, version := range apiGroup.Versions {
222+
gv := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
223+
if resources, ok := groupVersionResources[gv]; ok {
224+
result = append(result, resources)
225+
}
226+
}
227+
228+
if len(failedGroups) == 0 {
229+
return &apiGroup, result, nil
230+
}
231+
232+
return &apiGroup, result, &discovery.ErrGroupDiscoveryFailed{Groups: failedGroups}
233+
}
234+
235+
// getFilteredAPIGroupResources uses the provided discovery client to gather
236+
// discovery information and populate a slice of APIGroupResources.
237+
func getFilteredAPIGroupResources(cl discovery.DiscoveryInterface, groupName string) (*restmapper.APIGroupResources, error) {
238+
group, rs, err := filteredServerGroupsAndResources(cl, groupName)
239+
if rs == nil || group == nil {
240+
return nil, err
241+
// TODO track the errors and update callers to handle partial errors.
242+
}
243+
rsm := map[string]*metav1.APIResourceList{}
244+
for _, r := range rs {
245+
rsm[r.GroupVersion] = r
246+
}
247+
248+
groupResources := &restmapper.APIGroupResources{
249+
Group: *group,
250+
VersionedResources: make(map[string][]metav1.APIResource),
251+
}
252+
for _, version := range group.Versions {
253+
resources, ok := rsm[version.GroupVersion]
254+
if !ok {
255+
continue
256+
}
257+
groupResources.VersionedResources[version.Version] = resources.APIResources
258+
}
259+
260+
return groupResources, nil
261+
}

0 commit comments

Comments
 (0)