1/*
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}
517