1
16
17 package apiutil
18
19 import (
20 "fmt"
21 "net/http"
22 "sync"
23
24 apierrors "k8s.io/apimachinery/pkg/api/errors"
25 "k8s.io/apimachinery/pkg/api/meta"
26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27 "k8s.io/apimachinery/pkg/runtime/schema"
28 "k8s.io/client-go/discovery"
29 "k8s.io/client-go/rest"
30 "k8s.io/client-go/restmapper"
31 )
32
33
34
35 func NewDynamicRESTMapper(cfg *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) {
36 if httpClient == nil {
37 return nil, fmt.Errorf("httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client")
38 }
39
40 client, err := discovery.NewDiscoveryClientForConfigAndClient(cfg, httpClient)
41 if err != nil {
42 return nil, err
43 }
44 return &mapper{
45 mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
46 client: client,
47 knownGroups: map[string]*restmapper.APIGroupResources{},
48 apiGroups: map[string]*metav1.APIGroup{},
49 }, nil
50 }
51
52
53
54 type mapper struct {
55 mapper meta.RESTMapper
56 client discovery.DiscoveryInterface
57 knownGroups map[string]*restmapper.APIGroupResources
58 apiGroups map[string]*metav1.APIGroup
59
60
61 mu sync.RWMutex
62 }
63
64
65 func (m *mapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
66 res, err := m.getMapper().KindFor(resource)
67 if meta.IsNoMatchError(err) {
68 if err := m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
69 return schema.GroupVersionKind{}, err
70 }
71 res, err = m.getMapper().KindFor(resource)
72 }
73
74 return res, err
75 }
76
77
78 func (m *mapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
79 res, err := m.getMapper().KindsFor(resource)
80 if meta.IsNoMatchError(err) {
81 if err := m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
82 return nil, err
83 }
84 res, err = m.getMapper().KindsFor(resource)
85 }
86
87 return res, err
88 }
89
90
91 func (m *mapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
92 res, err := m.getMapper().ResourceFor(input)
93 if meta.IsNoMatchError(err) {
94 if err := m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
95 return schema.GroupVersionResource{}, err
96 }
97 res, err = m.getMapper().ResourceFor(input)
98 }
99
100 return res, err
101 }
102
103
104 func (m *mapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
105 res, err := m.getMapper().ResourcesFor(input)
106 if meta.IsNoMatchError(err) {
107 if err := m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
108 return nil, err
109 }
110 res, err = m.getMapper().ResourcesFor(input)
111 }
112
113 return res, err
114 }
115
116
117 func (m *mapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
118 res, err := m.getMapper().RESTMapping(gk, versions...)
119 if meta.IsNoMatchError(err) {
120 if err := m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
121 return nil, err
122 }
123 res, err = m.getMapper().RESTMapping(gk, versions...)
124 }
125
126 return res, err
127 }
128
129
130 func (m *mapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
131 res, err := m.getMapper().RESTMappings(gk, versions...)
132 if meta.IsNoMatchError(err) {
133 if err := m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
134 return nil, err
135 }
136 res, err = m.getMapper().RESTMappings(gk, versions...)
137 }
138
139 return res, err
140 }
141
142
143 func (m *mapper) ResourceSingularizer(resource string) (string, error) {
144 return m.getMapper().ResourceSingularizer(resource)
145 }
146
147 func (m *mapper) getMapper() meta.RESTMapper {
148 m.mu.RLock()
149 defer m.mu.RUnlock()
150 return m.mapper
151 }
152
153
154
155 func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) error {
156
157
158 if len(versions) == 1 && versions[0] == "" {
159 versions = nil
160 }
161
162
163
164
165 if len(versions) == 0 {
166 apiGroup, err := m.findAPIGroupByName(groupName)
167 if err != nil {
168 return err
169 }
170 if apiGroup != nil {
171 for _, version := range apiGroup.Versions {
172 versions = append(versions, version.Version)
173 }
174 }
175 }
176
177 m.mu.Lock()
178 defer m.mu.Unlock()
179
180
181 groupResources := &restmapper.APIGroupResources{
182 Group: metav1.APIGroup{Name: groupName},
183 VersionedResources: make(map[string][]metav1.APIResource),
184 }
185
186
187
188
189
190
191
192 groupVersionResources, err := m.fetchGroupVersionResourcesLocked(groupName, versions...)
193 if err != nil {
194 return fmt.Errorf("failed to get API group resources: %w", err)
195 }
196
197 if _, ok := m.knownGroups[groupName]; ok {
198 groupResources = m.knownGroups[groupName]
199 }
200
201
202
203 for groupVersion, resources := range groupVersionResources {
204 version := groupVersion.Version
205
206 groupResources.VersionedResources[version] = resources.APIResources
207 found := false
208 for _, v := range groupResources.Group.Versions {
209 if v.Version == version {
210 found = true
211 break
212 }
213 }
214
215 if !found {
216 groupResources.Group.Versions = append(groupResources.Group.Versions, metav1.GroupVersionForDiscovery{
217 GroupVersion: metav1.GroupVersion{Group: groupName, Version: version}.String(),
218 Version: version,
219 })
220 }
221 }
222
223
224 m.knownGroups[groupName] = groupResources
225
226
227 updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
228 for _, agr := range m.knownGroups {
229 updatedGroupResources = append(updatedGroupResources, agr)
230 }
231
232 m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
233 return nil
234 }
235
236
237 func (m *mapper) findAPIGroupByName(groupName string) (*metav1.APIGroup, error) {
238
239 {
240 m.mu.RLock()
241 group, ok := m.apiGroups[groupName]
242 m.mu.RUnlock()
243 if ok {
244 return group, nil
245 }
246 }
247
248
249 apiGroups, err := m.client.ServerGroups()
250 if err != nil {
251 return nil, fmt.Errorf("failed to get server groups: %w", err)
252 }
253 if len(apiGroups.Groups) == 0 {
254 return nil, fmt.Errorf("received an empty API groups list")
255 }
256
257 m.mu.Lock()
258 for i := range apiGroups.Groups {
259 group := &apiGroups.Groups[i]
260 m.apiGroups[group.Name] = group
261 }
262 m.mu.Unlock()
263
264
265 m.mu.RLock()
266 defer m.mu.RUnlock()
267
268
269
270 return m.apiGroups[groupName], nil
271 }
272
273
274
275 func (m *mapper) fetchGroupVersionResourcesLocked(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
276 groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
277 failedGroups := make(map[schema.GroupVersion]error)
278
279 for _, version := range versions {
280 groupVersion := schema.GroupVersion{Group: groupName, Version: version}
281
282 apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
283 if apierrors.IsNotFound(err) {
284
285
286 if m.isAPIGroupCached(groupVersion) {
287 delete(m.apiGroups, groupName)
288 }
289 if m.isGroupVersionCached(groupVersion) {
290 delete(m.knownGroups, groupName)
291 }
292 continue
293 } else if err != nil {
294 failedGroups[groupVersion] = err
295 }
296
297 if apiResourceList != nil {
298
299 groupVersionResources[groupVersion] = apiResourceList
300 }
301 }
302
303 if len(failedGroups) > 0 {
304 err := ErrResourceDiscoveryFailed(failedGroups)
305 return nil, &err
306 }
307
308 return groupVersionResources, nil
309 }
310
311
312 func (m *mapper) isGroupVersionCached(gv schema.GroupVersion) bool {
313 if cachedGroup, ok := m.knownGroups[gv.Group]; ok {
314 _, cached := cachedGroup.VersionedResources[gv.Version]
315 return cached
316 }
317
318 return false
319 }
320
321
322 func (m *mapper) isAPIGroupCached(gv schema.GroupVersion) bool {
323 cachedGroup, ok := m.apiGroups[gv.Group]
324 if !ok {
325 return false
326 }
327
328 for _, version := range cachedGroup.Versions {
329 if version.Version == gv.Version {
330 return true
331 }
332 }
333
334 return false
335 }
336
View as plain text