Skip to content

Commit ac5cb85

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 ac5cb85

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed

pkg/client/apiutil/lazyrestmapper.go

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

0 commit comments

Comments
 (0)