blob: 6c8e87e2327464c462478cd9497ffa0d642e718a [file] [log] [blame]
Matteo Scandoloa4285862020-12-01 18:10:10 -08001/*
2Copyright 2015 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package discovery
18
19import (
20 "context"
21 "encoding/json"
22 "fmt"
23 "net/url"
24 "sort"
25 "strings"
26 "sync"
27 "time"
28
29 "github.com/golang/protobuf/proto"
30 openapi_v2 "github.com/googleapis/gnostic/openapiv2"
31
32 "k8s.io/apimachinery/pkg/api/errors"
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/apimachinery/pkg/runtime"
35 "k8s.io/apimachinery/pkg/runtime/schema"
36 "k8s.io/apimachinery/pkg/runtime/serializer"
37 utilruntime "k8s.io/apimachinery/pkg/util/runtime"
38 "k8s.io/apimachinery/pkg/version"
39 "k8s.io/client-go/kubernetes/scheme"
40 restclient "k8s.io/client-go/rest"
41)
42
43const (
44 // defaultRetries is the number of times a resource discovery is repeated if an api group disappears on the fly (e.g. ThirdPartyResources).
45 defaultRetries = 2
46 // protobuf mime type
47 mimePb = "application/com.github.proto-openapi.spec.v2@v1.0+protobuf"
48 // defaultTimeout is the maximum amount of time per request when no timeout has been set on a RESTClient.
49 // Defaults to 32s in order to have a distinguishable length of time, relative to other timeouts that exist.
50 defaultTimeout = 32 * time.Second
51)
52
53// DiscoveryInterface holds the methods that discover server-supported API groups,
54// versions and resources.
55type DiscoveryInterface interface {
56 RESTClient() restclient.Interface
57 ServerGroupsInterface
58 ServerResourcesInterface
59 ServerVersionInterface
60 OpenAPISchemaInterface
61}
62
63// CachedDiscoveryInterface is a DiscoveryInterface with cache invalidation and freshness.
64// Note that If the ServerResourcesForGroupVersion method returns a cache miss
65// error, the user needs to explicitly call Invalidate to clear the cache,
66// otherwise the same cache miss error will be returned next time.
67type CachedDiscoveryInterface interface {
68 DiscoveryInterface
69 // Fresh is supposed to tell the caller whether or not to retry if the cache
70 // fails to find something (false = retry, true = no need to retry).
71 //
72 // TODO: this needs to be revisited, this interface can't be locked properly
73 // and doesn't make a lot of sense.
74 Fresh() bool
75 // Invalidate enforces that no cached data that is older than the current time
76 // is used.
77 Invalidate()
78}
79
80// ServerGroupsInterface has methods for obtaining supported groups on the API server
81type ServerGroupsInterface interface {
82 // ServerGroups returns the supported groups, with information like supported versions and the
83 // preferred version.
84 ServerGroups() (*metav1.APIGroupList, error)
85}
86
87// ServerResourcesInterface has methods for obtaining supported resources on the API server
88type ServerResourcesInterface interface {
89 // ServerResourcesForGroupVersion returns the supported resources for a group and version.
90 ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error)
91 // ServerResources returns the supported resources for all groups and versions.
92 //
93 // The returned resource list might be non-nil with partial results even in the case of
94 // non-nil error.
95 //
96 // Deprecated: use ServerGroupsAndResources instead.
97 ServerResources() ([]*metav1.APIResourceList, error)
98 // ServerResources returns the supported groups and resources for all groups and versions.
99 //
100 // The returned group and resource lists might be non-nil with partial results even in the
101 // case of non-nil error.
102 ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)
103 // ServerPreferredResources returns the supported resources with the version preferred by the
104 // server.
105 //
106 // The returned group and resource lists might be non-nil with partial results even in the
107 // case of non-nil error.
108 ServerPreferredResources() ([]*metav1.APIResourceList, error)
109 // ServerPreferredNamespacedResources returns the supported namespaced resources with the
110 // version preferred by the server.
111 //
112 // The returned resource list might be non-nil with partial results even in the case of
113 // non-nil error.
114 ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error)
115}
116
117// ServerVersionInterface has a method for retrieving the server's version.
118type ServerVersionInterface interface {
119 // ServerVersion retrieves and parses the server's version (git version).
120 ServerVersion() (*version.Info, error)
121}
122
123// OpenAPISchemaInterface has a method to retrieve the open API schema.
124type OpenAPISchemaInterface interface {
125 // OpenAPISchema retrieves and parses the swagger API schema the server supports.
126 OpenAPISchema() (*openapi_v2.Document, error)
127}
128
129// DiscoveryClient implements the functions that discover server-supported API groups,
130// versions and resources.
131type DiscoveryClient struct {
132 restClient restclient.Interface
133
134 LegacyPrefix string
135}
136
137// Convert metav1.APIVersions to metav1.APIGroup. APIVersions is used by legacy v1, so
138// group would be "".
139func apiVersionsToAPIGroup(apiVersions *metav1.APIVersions) (apiGroup metav1.APIGroup) {
140 groupVersions := []metav1.GroupVersionForDiscovery{}
141 for _, version := range apiVersions.Versions {
142 groupVersion := metav1.GroupVersionForDiscovery{
143 GroupVersion: version,
144 Version: version,
145 }
146 groupVersions = append(groupVersions, groupVersion)
147 }
148 apiGroup.Versions = groupVersions
149 // There should be only one groupVersion returned at /api
150 apiGroup.PreferredVersion = groupVersions[0]
151 return
152}
153
154// ServerGroups returns the supported groups, with information like supported versions and the
155// preferred version.
156func (d *DiscoveryClient) ServerGroups() (apiGroupList *metav1.APIGroupList, err error) {
157 // Get the groupVersions exposed at /api
158 v := &metav1.APIVersions{}
159 err = d.restClient.Get().AbsPath(d.LegacyPrefix).Do(context.TODO()).Into(v)
160 apiGroup := metav1.APIGroup{}
161 if err == nil && len(v.Versions) != 0 {
162 apiGroup = apiVersionsToAPIGroup(v)
163 }
164 if err != nil && !errors.IsNotFound(err) && !errors.IsForbidden(err) {
165 return nil, err
166 }
167
168 // Get the groupVersions exposed at /apis
169 apiGroupList = &metav1.APIGroupList{}
170 err = d.restClient.Get().AbsPath("/apis").Do(context.TODO()).Into(apiGroupList)
171 if err != nil && !errors.IsNotFound(err) && !errors.IsForbidden(err) {
172 return nil, err
173 }
174 // to be compatible with a v1.0 server, if it's a 403 or 404, ignore and return whatever we got from /api
175 if err != nil && (errors.IsNotFound(err) || errors.IsForbidden(err)) {
176 apiGroupList = &metav1.APIGroupList{}
177 }
178
179 // prepend the group retrieved from /api to the list if not empty
180 if len(v.Versions) != 0 {
181 apiGroupList.Groups = append([]metav1.APIGroup{apiGroup}, apiGroupList.Groups...)
182 }
183 return apiGroupList, nil
184}
185
186// ServerResourcesForGroupVersion returns the supported resources for a group and version.
187func (d *DiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (resources *metav1.APIResourceList, err error) {
188 url := url.URL{}
189 if len(groupVersion) == 0 {
190 return nil, fmt.Errorf("groupVersion shouldn't be empty")
191 }
192 if len(d.LegacyPrefix) > 0 && groupVersion == "v1" {
193 url.Path = d.LegacyPrefix + "/" + groupVersion
194 } else {
195 url.Path = "/apis/" + groupVersion
196 }
197 resources = &metav1.APIResourceList{
198 GroupVersion: groupVersion,
199 }
200 err = d.restClient.Get().AbsPath(url.String()).Do(context.TODO()).Into(resources)
201 if err != nil {
202 // ignore 403 or 404 error to be compatible with an v1.0 server.
203 if groupVersion == "v1" && (errors.IsNotFound(err) || errors.IsForbidden(err)) {
204 return resources, nil
205 }
206 return nil, err
207 }
208 return resources, nil
209}
210
211// ServerResources returns the supported resources for all groups and versions.
212// Deprecated: use ServerGroupsAndResources instead.
213func (d *DiscoveryClient) ServerResources() ([]*metav1.APIResourceList, error) {
214 _, rs, err := d.ServerGroupsAndResources()
215 return rs, err
216}
217
218// ServerGroupsAndResources returns the supported resources for all groups and versions.
219func (d *DiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
220 return withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
221 return ServerGroupsAndResources(d)
222 })
223}
224
225// ErrGroupDiscoveryFailed is returned if one or more API groups fail to load.
226type ErrGroupDiscoveryFailed struct {
227 // Groups is a list of the groups that failed to load and the error cause
228 Groups map[schema.GroupVersion]error
229}
230
231// Error implements the error interface
232func (e *ErrGroupDiscoveryFailed) Error() string {
233 var groups []string
234 for k, v := range e.Groups {
235 groups = append(groups, fmt.Sprintf("%s: %v", k, v))
236 }
237 sort.Strings(groups)
238 return fmt.Sprintf("unable to retrieve the complete list of server APIs: %s", strings.Join(groups, ", "))
239}
240
241// IsGroupDiscoveryFailedError returns true if the provided error indicates the server was unable to discover
242// a complete list of APIs for the client to use.
243func IsGroupDiscoveryFailedError(err error) bool {
244 _, ok := err.(*ErrGroupDiscoveryFailed)
245 return err != nil && ok
246}
247
248// ServerResources uses the provided discovery interface to look up supported resources for all groups and versions.
249// Deprecated: use ServerGroupsAndResources instead.
250func ServerResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
251 _, rs, err := ServerGroupsAndResources(d)
252 return rs, err
253}
254
255func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
256 sgs, err := d.ServerGroups()
257 if sgs == nil {
258 return nil, nil, err
259 }
260 resultGroups := []*metav1.APIGroup{}
261 for i := range sgs.Groups {
262 resultGroups = append(resultGroups, &sgs.Groups[i])
263 }
264
265 groupVersionResources, failedGroups := fetchGroupVersionResources(d, sgs)
266
267 // order results by group/version discovery order
268 result := []*metav1.APIResourceList{}
269 for _, apiGroup := range sgs.Groups {
270 for _, version := range apiGroup.Versions {
271 gv := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
272 if resources, ok := groupVersionResources[gv]; ok {
273 result = append(result, resources)
274 }
275 }
276 }
277
278 if len(failedGroups) == 0 {
279 return resultGroups, result, nil
280 }
281
282 return resultGroups, result, &ErrGroupDiscoveryFailed{Groups: failedGroups}
283}
284
285// ServerPreferredResources uses the provided discovery interface to look up preferred resources
286func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
287 serverGroupList, err := d.ServerGroups()
288 if err != nil {
289 return nil, err
290 }
291
292 groupVersionResources, failedGroups := fetchGroupVersionResources(d, serverGroupList)
293
294 result := []*metav1.APIResourceList{}
295 grVersions := map[schema.GroupResource]string{} // selected version of a GroupResource
296 grAPIResources := map[schema.GroupResource]*metav1.APIResource{} // selected APIResource for a GroupResource
297 gvAPIResourceLists := map[schema.GroupVersion]*metav1.APIResourceList{} // blueprint for a APIResourceList for later grouping
298
299 for _, apiGroup := range serverGroupList.Groups {
300 for _, version := range apiGroup.Versions {
301 groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
302
303 apiResourceList, ok := groupVersionResources[groupVersion]
304 if !ok {
305 continue
306 }
307
308 // create empty list which is filled later in another loop
309 emptyAPIResourceList := metav1.APIResourceList{
310 GroupVersion: version.GroupVersion,
311 }
312 gvAPIResourceLists[groupVersion] = &emptyAPIResourceList
313 result = append(result, &emptyAPIResourceList)
314
315 for i := range apiResourceList.APIResources {
316 apiResource := &apiResourceList.APIResources[i]
317 if strings.Contains(apiResource.Name, "/") {
318 continue
319 }
320 gv := schema.GroupResource{Group: apiGroup.Name, Resource: apiResource.Name}
321 if _, ok := grAPIResources[gv]; ok && version.Version != apiGroup.PreferredVersion.Version {
322 // only override with preferred version
323 continue
324 }
325 grVersions[gv] = version.Version
326 grAPIResources[gv] = apiResource
327 }
328 }
329 }
330
331 // group selected APIResources according to GroupVersion into APIResourceLists
332 for groupResource, apiResource := range grAPIResources {
333 version := grVersions[groupResource]
334 groupVersion := schema.GroupVersion{Group: groupResource.Group, Version: version}
335 apiResourceList := gvAPIResourceLists[groupVersion]
336 apiResourceList.APIResources = append(apiResourceList.APIResources, *apiResource)
337 }
338
339 if len(failedGroups) == 0 {
340 return result, nil
341 }
342
343 return result, &ErrGroupDiscoveryFailed{Groups: failedGroups}
344}
345
346// fetchServerResourcesForGroupVersions uses the discovery client to fetch the resources for the specified groups in parallel.
347func fetchGroupVersionResources(d DiscoveryInterface, apiGroups *metav1.APIGroupList) (map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error) {
348 groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
349 failedGroups := make(map[schema.GroupVersion]error)
350
351 wg := &sync.WaitGroup{}
352 resultLock := &sync.Mutex{}
353 for _, apiGroup := range apiGroups.Groups {
354 for _, version := range apiGroup.Versions {
355 groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
356 wg.Add(1)
357 go func() {
358 defer wg.Done()
359 defer utilruntime.HandleCrash()
360
361 apiResourceList, err := d.ServerResourcesForGroupVersion(groupVersion.String())
362
363 // lock to record results
364 resultLock.Lock()
365 defer resultLock.Unlock()
366
367 if err != nil {
368 // TODO: maybe restrict this to NotFound errors
369 failedGroups[groupVersion] = err
370 }
371 if apiResourceList != nil {
372 // even in case of error, some fallback might have been returned
373 groupVersionResources[groupVersion] = apiResourceList
374 }
375 }()
376 }
377 }
378 wg.Wait()
379
380 return groupVersionResources, failedGroups
381}
382
383// ServerPreferredResources returns the supported resources with the version preferred by the
384// server.
385func (d *DiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
386 _, rs, err := withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
387 rs, err := ServerPreferredResources(d)
388 return nil, rs, err
389 })
390 return rs, err
391}
392
393// ServerPreferredNamespacedResources returns the supported namespaced resources with the
394// version preferred by the server.
395func (d *DiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
396 return ServerPreferredNamespacedResources(d)
397}
398
399// ServerPreferredNamespacedResources uses the provided discovery interface to look up preferred namespaced resources
400func ServerPreferredNamespacedResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
401 all, err := ServerPreferredResources(d)
402 return FilteredBy(ResourcePredicateFunc(func(groupVersion string, r *metav1.APIResource) bool {
403 return r.Namespaced
404 }), all), err
405}
406
407// ServerVersion retrieves and parses the server's version (git version).
408func (d *DiscoveryClient) ServerVersion() (*version.Info, error) {
409 body, err := d.restClient.Get().AbsPath("/version").Do(context.TODO()).Raw()
410 if err != nil {
411 return nil, err
412 }
413 var info version.Info
414 err = json.Unmarshal(body, &info)
415 if err != nil {
416 return nil, fmt.Errorf("unable to parse the server version: %v", err)
417 }
418 return &info, nil
419}
420
421// OpenAPISchema fetches the open api schema using a rest client and parses the proto.
422func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
423 data, err := d.restClient.Get().AbsPath("/openapi/v2").SetHeader("Accept", mimePb).Do(context.TODO()).Raw()
424 if err != nil {
425 if errors.IsForbidden(err) || errors.IsNotFound(err) || errors.IsNotAcceptable(err) {
426 // single endpoint not found/registered in old server, try to fetch old endpoint
427 // TODO: remove this when kubectl/client-go don't work with 1.9 server
428 data, err = d.restClient.Get().AbsPath("/swagger-2.0.0.pb-v1").Do(context.TODO()).Raw()
429 if err != nil {
430 return nil, err
431 }
432 } else {
433 return nil, err
434 }
435 }
436 document := &openapi_v2.Document{}
437 err = proto.Unmarshal(data, document)
438 if err != nil {
439 return nil, err
440 }
441 return document, nil
442}
443
444// withRetries retries the given recovery function in case the groups supported by the server change after ServerGroup() returns.
445func withRetries(maxRetries int, f func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
446 var result []*metav1.APIResourceList
447 var resultGroups []*metav1.APIGroup
448 var err error
449 for i := 0; i < maxRetries; i++ {
450 resultGroups, result, err = f()
451 if err == nil {
452 return resultGroups, result, nil
453 }
454 if _, ok := err.(*ErrGroupDiscoveryFailed); !ok {
455 return nil, nil, err
456 }
457 }
458 return resultGroups, result, err
459}
460
461func setDiscoveryDefaults(config *restclient.Config) error {
462 config.APIPath = ""
463 config.GroupVersion = nil
464 if config.Timeout == 0 {
465 config.Timeout = defaultTimeout
466 }
467 if config.Burst == 0 && config.QPS < 100 {
468 // discovery is expected to be bursty, increase the default burst
469 // to accommodate looking up resource info for many API groups.
470 // matches burst set by ConfigFlags#ToDiscoveryClient().
471 // see https://issue.k8s.io/86149
472 config.Burst = 100
473 }
474 codec := runtime.NoopEncoder{Decoder: scheme.Codecs.UniversalDecoder()}
475 config.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec})
476 if len(config.UserAgent) == 0 {
477 config.UserAgent = restclient.DefaultKubernetesUserAgent()
478 }
479 return nil
480}
481
482// NewDiscoveryClientForConfig creates a new DiscoveryClient for the given config. This client
483// can be used to discover supported resources in the API server.
484func NewDiscoveryClientForConfig(c *restclient.Config) (*DiscoveryClient, error) {
485 config := *c
486 if err := setDiscoveryDefaults(&config); err != nil {
487 return nil, err
488 }
489 client, err := restclient.UnversionedRESTClientFor(&config)
490 return &DiscoveryClient{restClient: client, LegacyPrefix: "/api"}, err
491}
492
493// NewDiscoveryClientForConfigOrDie creates a new DiscoveryClient for the given config. If
494// there is an error, it panics.
495func NewDiscoveryClientForConfigOrDie(c *restclient.Config) *DiscoveryClient {
496 client, err := NewDiscoveryClientForConfig(c)
497 if err != nil {
498 panic(err)
499 }
500 return client
501
502}
503
504// NewDiscoveryClient returns a new DiscoveryClient for the given RESTClient.
505func NewDiscoveryClient(c restclient.Interface) *DiscoveryClient {
506 return &DiscoveryClient{restClient: c, LegacyPrefix: "/api"}
507}
508
509// RESTClient returns a RESTClient that is used to communicate
510// with API server by this client implementation.
511func (d *DiscoveryClient) RESTClient() restclient.Interface {
512 if d == nil {
513 return nil
514 }
515 return d.restClient
516}