...

Source file src/k8s.io/client-go/discovery/discovery_client.go

Documentation: k8s.io/client-go/discovery

     1  /*
     2  Copyright 2015 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 discovery
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	goerrors "errors"
    23  	"fmt"
    24  	"mime"
    25  	"net/http"
    26  	"net/url"
    27  	"sort"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  
    32  	//nolint:staticcheck // SA1019 Keep using module since it's still being maintained and the api of google.golang.org/protobuf/proto differs
    33  	"github.com/golang/protobuf/proto"
    34  	openapi_v2 "github.com/google/gnostic-models/openapiv2"
    35  
    36  	apidiscoveryv2 "k8s.io/api/apidiscovery/v2"
    37  	apidiscoveryv2beta1 "k8s.io/api/apidiscovery/v2beta1"
    38  	"k8s.io/apimachinery/pkg/api/errors"
    39  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    40  	"k8s.io/apimachinery/pkg/runtime"
    41  	"k8s.io/apimachinery/pkg/runtime/schema"
    42  	"k8s.io/apimachinery/pkg/runtime/serializer"
    43  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    44  	"k8s.io/apimachinery/pkg/version"
    45  	"k8s.io/client-go/kubernetes/scheme"
    46  	"k8s.io/client-go/openapi"
    47  	restclient "k8s.io/client-go/rest"
    48  )
    49  
    50  const (
    51  	// defaultRetries is the number of times a resource discovery is repeated if an api group disappears on the fly (e.g. CustomResourceDefinitions).
    52  	defaultRetries = 2
    53  	// protobuf mime type
    54  	openAPIV2mimePb = "application/com.github.proto-openapi.spec.v2@v1.0+protobuf"
    55  
    56  	// defaultTimeout is the maximum amount of time per request when no timeout has been set on a RESTClient.
    57  	// Defaults to 32s in order to have a distinguishable length of time, relative to other timeouts that exist.
    58  	defaultTimeout = 32 * time.Second
    59  
    60  	// defaultBurst is the default burst to be used with the discovery client's token bucket rate limiter
    61  	defaultBurst = 300
    62  
    63  	AcceptV1 = runtime.ContentTypeJSON
    64  	// Aggregated discovery content-type (v2beta1). NOTE: content-type parameters
    65  	// MUST be ordered (g, v, as) for server in "Accept" header (BUT we are resilient
    66  	// to ordering when comparing returned values in "Content-Type" header).
    67  	AcceptV2Beta1 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList"
    68  	AcceptV2      = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2;as=APIGroupDiscoveryList"
    69  	// Prioritize aggregated discovery by placing first in the order of discovery accept types.
    70  	acceptDiscoveryFormats = AcceptV2 + "," + AcceptV2Beta1 + "," + AcceptV1
    71  )
    72  
    73  // Aggregated discovery content-type GVK.
    74  var v2Beta1GVK = schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2beta1", Kind: "APIGroupDiscoveryList"}
    75  var v2GVK = schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2", Kind: "APIGroupDiscoveryList"}
    76  
    77  // DiscoveryInterface holds the methods that discover server-supported API groups,
    78  // versions and resources.
    79  type DiscoveryInterface interface {
    80  	RESTClient() restclient.Interface
    81  	ServerGroupsInterface
    82  	ServerResourcesInterface
    83  	ServerVersionInterface
    84  	OpenAPISchemaInterface
    85  	OpenAPIV3SchemaInterface
    86  	// Returns copy of current discovery client that will only
    87  	// receive the legacy discovery format, or pointer to current
    88  	// discovery client if it does not support legacy-only discovery.
    89  	WithLegacy() DiscoveryInterface
    90  }
    91  
    92  // AggregatedDiscoveryInterface extends DiscoveryInterface to include a method to possibly
    93  // return discovery resources along with the discovery groups, which is what the newer
    94  // aggregated discovery format does (APIGroupDiscoveryList).
    95  type AggregatedDiscoveryInterface interface {
    96  	DiscoveryInterface
    97  
    98  	GroupsAndMaybeResources() (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error)
    99  }
   100  
   101  // CachedDiscoveryInterface is a DiscoveryInterface with cache invalidation and freshness.
   102  // Note that If the ServerResourcesForGroupVersion method returns a cache miss
   103  // error, the user needs to explicitly call Invalidate to clear the cache,
   104  // otherwise the same cache miss error will be returned next time.
   105  type CachedDiscoveryInterface interface {
   106  	DiscoveryInterface
   107  	// Fresh is supposed to tell the caller whether or not to retry if the cache
   108  	// fails to find something (false = retry, true = no need to retry).
   109  	//
   110  	// TODO: this needs to be revisited, this interface can't be locked properly
   111  	// and doesn't make a lot of sense.
   112  	Fresh() bool
   113  	// Invalidate enforces that no cached data that is older than the current time
   114  	// is used.
   115  	Invalidate()
   116  }
   117  
   118  // ServerGroupsInterface has methods for obtaining supported groups on the API server
   119  type ServerGroupsInterface interface {
   120  	// ServerGroups returns the supported groups, with information like supported versions and the
   121  	// preferred version.
   122  	ServerGroups() (*metav1.APIGroupList, error)
   123  }
   124  
   125  // ServerResourcesInterface has methods for obtaining supported resources on the API server
   126  type ServerResourcesInterface interface {
   127  	// ServerResourcesForGroupVersion returns the supported resources for a group and version.
   128  	ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error)
   129  	// ServerGroupsAndResources returns the supported groups and resources for all groups and versions.
   130  	//
   131  	// The returned group and resource lists might be non-nil with partial results even in the
   132  	// case of non-nil error.
   133  	ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)
   134  	// ServerPreferredResources returns the supported resources with the version preferred by the
   135  	// server.
   136  	//
   137  	// The returned group and resource lists might be non-nil with partial results even in the
   138  	// case of non-nil error.
   139  	ServerPreferredResources() ([]*metav1.APIResourceList, error)
   140  	// ServerPreferredNamespacedResources returns the supported namespaced resources with the
   141  	// version preferred by the server.
   142  	//
   143  	// The returned resource list might be non-nil with partial results even in the case of
   144  	// non-nil error.
   145  	ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error)
   146  }
   147  
   148  // ServerVersionInterface has a method for retrieving the server's version.
   149  type ServerVersionInterface interface {
   150  	// ServerVersion retrieves and parses the server's version (git version).
   151  	ServerVersion() (*version.Info, error)
   152  }
   153  
   154  // OpenAPISchemaInterface has a method to retrieve the open API schema.
   155  type OpenAPISchemaInterface interface {
   156  	// OpenAPISchema retrieves and parses the swagger API schema the server supports.
   157  	OpenAPISchema() (*openapi_v2.Document, error)
   158  }
   159  
   160  type OpenAPIV3SchemaInterface interface {
   161  	OpenAPIV3() openapi.Client
   162  }
   163  
   164  // DiscoveryClient implements the functions that discover server-supported API groups,
   165  // versions and resources.
   166  type DiscoveryClient struct {
   167  	restClient restclient.Interface
   168  
   169  	LegacyPrefix string
   170  	// Forces the client to request only "unaggregated" (legacy) discovery.
   171  	UseLegacyDiscovery bool
   172  }
   173  
   174  var _ AggregatedDiscoveryInterface = &DiscoveryClient{}
   175  
   176  // Convert metav1.APIVersions to metav1.APIGroup. APIVersions is used by legacy v1, so
   177  // group would be "".
   178  func apiVersionsToAPIGroup(apiVersions *metav1.APIVersions) (apiGroup metav1.APIGroup) {
   179  	groupVersions := []metav1.GroupVersionForDiscovery{}
   180  	for _, version := range apiVersions.Versions {
   181  		groupVersion := metav1.GroupVersionForDiscovery{
   182  			GroupVersion: version,
   183  			Version:      version,
   184  		}
   185  		groupVersions = append(groupVersions, groupVersion)
   186  	}
   187  	apiGroup.Versions = groupVersions
   188  	// There should be only one groupVersion returned at /api
   189  	apiGroup.PreferredVersion = groupVersions[0]
   190  	return
   191  }
   192  
   193  // GroupsAndMaybeResources returns the discovery groups, and (if new aggregated
   194  // discovery format) the resources keyed by group/version. Merges discovery groups
   195  // and resources from /api and /apis (either aggregated or not). Legacy groups
   196  // must be ordered first. The server will either return both endpoints (/api, /apis)
   197  // as aggregated discovery format or legacy format. For safety, resources will only
   198  // be returned if both endpoints returned resources. Returned "failedGVs" can be
   199  // empty, but will only be nil in the case an error is returned.
   200  func (d *DiscoveryClient) GroupsAndMaybeResources() (
   201  	*metav1.APIGroupList,
   202  	map[schema.GroupVersion]*metav1.APIResourceList,
   203  	map[schema.GroupVersion]error,
   204  	error) {
   205  	// Legacy group ordered first (there is only one -- core/v1 group). Returned groups must
   206  	// be non-nil, but it could be empty. Returned resources, apiResources map could be nil.
   207  	groups, resources, failedGVs, err := d.downloadLegacy()
   208  	if err != nil {
   209  		return nil, nil, nil, err
   210  	}
   211  	// Discovery groups and (possibly) resources downloaded from /apis.
   212  	apiGroups, apiResources, failedApisGVs, aerr := d.downloadAPIs()
   213  	if aerr != nil {
   214  		return nil, nil, nil, aerr
   215  	}
   216  	// Merge apis groups into the legacy groups.
   217  	for _, group := range apiGroups.Groups {
   218  		groups.Groups = append(groups.Groups, group)
   219  	}
   220  	// For safety, only return resources if both endpoints returned resources.
   221  	if resources != nil && apiResources != nil {
   222  		for gv, resourceList := range apiResources {
   223  			resources[gv] = resourceList
   224  		}
   225  	} else if resources != nil {
   226  		resources = nil
   227  	}
   228  	// Merge failed GroupVersions from /api and /apis
   229  	for gv, err := range failedApisGVs {
   230  		failedGVs[gv] = err
   231  	}
   232  	return groups, resources, failedGVs, err
   233  }
   234  
   235  // downloadLegacy returns the discovery groups and possibly resources
   236  // for the legacy v1 GVR at /api, or an error if one occurred. It is
   237  // possible for the resource map to be nil if the server returned
   238  // the unaggregated discovery. Returned "failedGVs" can be empty, but
   239  // will only be nil in the case of a returned error.
   240  func (d *DiscoveryClient) downloadLegacy() (
   241  	*metav1.APIGroupList,
   242  	map[schema.GroupVersion]*metav1.APIResourceList,
   243  	map[schema.GroupVersion]error,
   244  	error) {
   245  	accept := acceptDiscoveryFormats
   246  	if d.UseLegacyDiscovery {
   247  		accept = AcceptV1
   248  	}
   249  	var responseContentType string
   250  	body, err := d.restClient.Get().
   251  		AbsPath("/api").
   252  		SetHeader("Accept", accept).
   253  		Do(context.TODO()).
   254  		ContentType(&responseContentType).
   255  		Raw()
   256  	apiGroupList := &metav1.APIGroupList{}
   257  	failedGVs := map[schema.GroupVersion]error{}
   258  	if err != nil {
   259  		// Tolerate 404, since aggregated api servers can return it.
   260  		if errors.IsNotFound(err) {
   261  			// Return empty structures and no error.
   262  			emptyGVMap := map[schema.GroupVersion]*metav1.APIResourceList{}
   263  			return apiGroupList, emptyGVMap, failedGVs, nil
   264  		} else {
   265  			return nil, nil, nil, err
   266  		}
   267  	}
   268  
   269  	var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
   270  	// Based on the content-type server responded with: aggregated or unaggregated.
   271  	if isGVK, _ := ContentTypeIsGVK(responseContentType, v2GVK); isGVK {
   272  		var aggregatedDiscovery apidiscoveryv2.APIGroupDiscoveryList
   273  		err = json.Unmarshal(body, &aggregatedDiscovery)
   274  		if err != nil {
   275  			return nil, nil, nil, err
   276  		}
   277  		apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery)
   278  	} else if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK {
   279  		var aggregatedDiscovery apidiscoveryv2beta1.APIGroupDiscoveryList
   280  		err = json.Unmarshal(body, &aggregatedDiscovery)
   281  		if err != nil {
   282  			return nil, nil, nil, err
   283  		}
   284  		apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResourcesV2Beta1(aggregatedDiscovery)
   285  	} else {
   286  		// Default is unaggregated discovery v1.
   287  		var v metav1.APIVersions
   288  		err = json.Unmarshal(body, &v)
   289  		if err != nil {
   290  			return nil, nil, nil, err
   291  		}
   292  		apiGroup := metav1.APIGroup{}
   293  		if len(v.Versions) != 0 {
   294  			apiGroup = apiVersionsToAPIGroup(&v)
   295  		}
   296  		apiGroupList.Groups = []metav1.APIGroup{apiGroup}
   297  	}
   298  
   299  	return apiGroupList, resourcesByGV, failedGVs, nil
   300  }
   301  
   302  // downloadAPIs returns the discovery groups and (if aggregated format) the
   303  // discovery resources. The returned groups will always exist, but the
   304  // resources map may be nil. Returned "failedGVs" can be empty, but will
   305  // only be nil in the case of a returned error.
   306  func (d *DiscoveryClient) downloadAPIs() (
   307  	*metav1.APIGroupList,
   308  	map[schema.GroupVersion]*metav1.APIResourceList,
   309  	map[schema.GroupVersion]error,
   310  	error) {
   311  	accept := acceptDiscoveryFormats
   312  	if d.UseLegacyDiscovery {
   313  		accept = AcceptV1
   314  	}
   315  	var responseContentType string
   316  	body, err := d.restClient.Get().
   317  		AbsPath("/apis").
   318  		SetHeader("Accept", accept).
   319  		Do(context.TODO()).
   320  		ContentType(&responseContentType).
   321  		Raw()
   322  	if err != nil {
   323  		return nil, nil, nil, err
   324  	}
   325  
   326  	apiGroupList := &metav1.APIGroupList{}
   327  	failedGVs := map[schema.GroupVersion]error{}
   328  	var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
   329  	// Based on the content-type server responded with: aggregated or unaggregated.
   330  	if isGVK, _ := ContentTypeIsGVK(responseContentType, v2GVK); isGVK {
   331  		var aggregatedDiscovery apidiscoveryv2.APIGroupDiscoveryList
   332  		err = json.Unmarshal(body, &aggregatedDiscovery)
   333  		if err != nil {
   334  			return nil, nil, nil, err
   335  		}
   336  		apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery)
   337  	} else if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK {
   338  		var aggregatedDiscovery apidiscoveryv2beta1.APIGroupDiscoveryList
   339  		err = json.Unmarshal(body, &aggregatedDiscovery)
   340  		if err != nil {
   341  			return nil, nil, nil, err
   342  		}
   343  		apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResourcesV2Beta1(aggregatedDiscovery)
   344  	} else {
   345  		// Default is unaggregated discovery v1.
   346  		err = json.Unmarshal(body, apiGroupList)
   347  		if err != nil {
   348  			return nil, nil, nil, err
   349  		}
   350  	}
   351  
   352  	return apiGroupList, resourcesByGV, failedGVs, nil
   353  }
   354  
   355  // ContentTypeIsGVK checks of the content-type string is both
   356  // "application/json" and matches the provided GVK. An error
   357  // is returned if the content type string is malformed.
   358  // NOTE: This function is resilient to the ordering of the
   359  // content-type parameters, as well as parameters added by
   360  // intermediaries such as proxies or gateways. Examples:
   361  //
   362  //	("application/json; g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil)
   363  //	("application/json; as=APIGroupDiscoveryList;v=v2beta1;g=apidiscovery.k8s.io", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil)
   364  //	("application/json; as=APIGroupDiscoveryList;v=v2beta1;g=apidiscovery.k8s.io;charset=utf-8", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil)
   365  //	("application/json", any GVK) = (false, nil)
   366  //	("application/json; charset=UTF-8", any GVK) = (false, nil)
   367  //	("malformed content type string", any GVK) = (false, error)
   368  func ContentTypeIsGVK(contentType string, gvk schema.GroupVersionKind) (bool, error) {
   369  	base, params, err := mime.ParseMediaType(contentType)
   370  	if err != nil {
   371  		return false, err
   372  	}
   373  	gvkMatch := runtime.ContentTypeJSON == base &&
   374  		params["g"] == gvk.Group &&
   375  		params["v"] == gvk.Version &&
   376  		params["as"] == gvk.Kind
   377  	return gvkMatch, nil
   378  }
   379  
   380  // ServerGroups returns the supported groups, with information like supported versions and the
   381  // preferred version.
   382  func (d *DiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
   383  	groups, _, _, err := d.GroupsAndMaybeResources()
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  	return groups, nil
   388  }
   389  
   390  // ServerResourcesForGroupVersion returns the supported resources for a group and version.
   391  func (d *DiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (resources *metav1.APIResourceList, err error) {
   392  	url := url.URL{}
   393  	if len(groupVersion) == 0 {
   394  		return nil, fmt.Errorf("groupVersion shouldn't be empty")
   395  	}
   396  	if len(d.LegacyPrefix) > 0 && groupVersion == "v1" {
   397  		url.Path = d.LegacyPrefix + "/" + groupVersion
   398  	} else {
   399  		url.Path = "/apis/" + groupVersion
   400  	}
   401  	resources = &metav1.APIResourceList{
   402  		GroupVersion: groupVersion,
   403  	}
   404  	err = d.restClient.Get().AbsPath(url.String()).Do(context.TODO()).Into(resources)
   405  	if err != nil {
   406  		// Tolerate core/v1 not found response by returning empty resource list;
   407  		// this probably should not happen. But we should verify all callers are
   408  		// not depending on this toleration before removal.
   409  		if groupVersion == "v1" && errors.IsNotFound(err) {
   410  			return resources, nil
   411  		}
   412  		return nil, err
   413  	}
   414  	return resources, nil
   415  }
   416  
   417  // ServerGroupsAndResources returns the supported resources for all groups and versions.
   418  func (d *DiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
   419  	return withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
   420  		return ServerGroupsAndResources(d)
   421  	})
   422  }
   423  
   424  // ErrGroupDiscoveryFailed is returned if one or more API groups fail to load.
   425  type ErrGroupDiscoveryFailed struct {
   426  	// Groups is a list of the groups that failed to load and the error cause
   427  	Groups map[schema.GroupVersion]error
   428  }
   429  
   430  // Error implements the error interface
   431  func (e *ErrGroupDiscoveryFailed) Error() string {
   432  	var groups []string
   433  	for k, v := range e.Groups {
   434  		groups = append(groups, fmt.Sprintf("%s: %v", k, v))
   435  	}
   436  	sort.Strings(groups)
   437  	return fmt.Sprintf("unable to retrieve the complete list of server APIs: %s", strings.Join(groups, ", "))
   438  }
   439  
   440  // Is makes it possible for the callers to use `errors.Is(` helper on errors wrapped with ErrGroupDiscoveryFailed error.
   441  func (e *ErrGroupDiscoveryFailed) Is(target error) bool {
   442  	_, ok := target.(*ErrGroupDiscoveryFailed)
   443  	return ok
   444  }
   445  
   446  // IsGroupDiscoveryFailedError returns true if the provided error indicates the server was unable to discover
   447  // a complete list of APIs for the client to use.
   448  func IsGroupDiscoveryFailedError(err error) bool {
   449  	_, ok := err.(*ErrGroupDiscoveryFailed)
   450  	return err != nil && ok
   451  }
   452  
   453  // GroupDiscoveryFailedErrorGroups returns true if the error is an ErrGroupDiscoveryFailed error,
   454  // along with the map of group versions that failed discovery.
   455  func GroupDiscoveryFailedErrorGroups(err error) (map[schema.GroupVersion]error, bool) {
   456  	var groupDiscoveryError *ErrGroupDiscoveryFailed
   457  	if err != nil && goerrors.As(err, &groupDiscoveryError) {
   458  		return groupDiscoveryError.Groups, true
   459  	}
   460  	return nil, false
   461  }
   462  
   463  func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
   464  	var sgs *metav1.APIGroupList
   465  	var resources []*metav1.APIResourceList
   466  	var failedGVs map[schema.GroupVersion]error
   467  	var err error
   468  
   469  	// If the passed discovery object implements the wider AggregatedDiscoveryInterface,
   470  	// then attempt to retrieve aggregated discovery with both groups and the resources.
   471  	if ad, ok := d.(AggregatedDiscoveryInterface); ok {
   472  		var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
   473  		sgs, resourcesByGV, failedGVs, err = ad.GroupsAndMaybeResources()
   474  		for _, resourceList := range resourcesByGV {
   475  			resources = append(resources, resourceList)
   476  		}
   477  	} else {
   478  		sgs, err = d.ServerGroups()
   479  	}
   480  
   481  	if sgs == nil {
   482  		return nil, nil, err
   483  	}
   484  	resultGroups := []*metav1.APIGroup{}
   485  	for i := range sgs.Groups {
   486  		resultGroups = append(resultGroups, &sgs.Groups[i])
   487  	}
   488  	// resources is non-nil if aggregated discovery succeeded.
   489  	if resources != nil {
   490  		// Any stale Group/Versions returned by aggregated discovery
   491  		// must be surfaced to the caller as failed Group/Versions.
   492  		var ferr error
   493  		if len(failedGVs) > 0 {
   494  			ferr = &ErrGroupDiscoveryFailed{Groups: failedGVs}
   495  		}
   496  		return resultGroups, resources, ferr
   497  	}
   498  
   499  	groupVersionResources, failedGroups := fetchGroupVersionResources(d, sgs)
   500  
   501  	// order results by group/version discovery order
   502  	result := []*metav1.APIResourceList{}
   503  	for _, apiGroup := range sgs.Groups {
   504  		for _, version := range apiGroup.Versions {
   505  			gv := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
   506  			if resources, ok := groupVersionResources[gv]; ok {
   507  				result = append(result, resources)
   508  			}
   509  		}
   510  	}
   511  
   512  	if len(failedGroups) == 0 {
   513  		return resultGroups, result, nil
   514  	}
   515  
   516  	return resultGroups, result, &ErrGroupDiscoveryFailed{Groups: failedGroups}
   517  }
   518  
   519  // ServerPreferredResources uses the provided discovery interface to look up preferred resources
   520  func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
   521  	var serverGroupList *metav1.APIGroupList
   522  	var failedGroups map[schema.GroupVersion]error
   523  	var groupVersionResources map[schema.GroupVersion]*metav1.APIResourceList
   524  	var err error
   525  
   526  	// If the passed discovery object implements the wider AggregatedDiscoveryInterface,
   527  	// then it is attempt to retrieve both the groups and the resources. "failedGroups"
   528  	// are Group/Versions returned as stale in AggregatedDiscovery format.
   529  	ad, ok := d.(AggregatedDiscoveryInterface)
   530  	if ok {
   531  		serverGroupList, groupVersionResources, failedGroups, err = ad.GroupsAndMaybeResources()
   532  	} else {
   533  		serverGroupList, err = d.ServerGroups()
   534  	}
   535  	if err != nil {
   536  		return nil, err
   537  	}
   538  	// Non-aggregated discovery must fetch resources from Groups.
   539  	if groupVersionResources == nil {
   540  		groupVersionResources, failedGroups = fetchGroupVersionResources(d, serverGroupList)
   541  	}
   542  
   543  	result := []*metav1.APIResourceList{}
   544  	grVersions := map[schema.GroupResource]string{}                         // selected version of a GroupResource
   545  	grAPIResources := map[schema.GroupResource]*metav1.APIResource{}        // selected APIResource for a GroupResource
   546  	gvAPIResourceLists := map[schema.GroupVersion]*metav1.APIResourceList{} // blueprint for a APIResourceList for later grouping
   547  
   548  	for _, apiGroup := range serverGroupList.Groups {
   549  		for _, version := range apiGroup.Versions {
   550  			groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
   551  
   552  			apiResourceList, ok := groupVersionResources[groupVersion]
   553  			if !ok {
   554  				continue
   555  			}
   556  
   557  			// create empty list which is filled later in another loop
   558  			emptyAPIResourceList := metav1.APIResourceList{
   559  				GroupVersion: version.GroupVersion,
   560  			}
   561  			gvAPIResourceLists[groupVersion] = &emptyAPIResourceList
   562  			result = append(result, &emptyAPIResourceList)
   563  
   564  			for i := range apiResourceList.APIResources {
   565  				apiResource := &apiResourceList.APIResources[i]
   566  				if strings.Contains(apiResource.Name, "/") {
   567  					continue
   568  				}
   569  				gv := schema.GroupResource{Group: apiGroup.Name, Resource: apiResource.Name}
   570  				if _, ok := grAPIResources[gv]; ok && version.Version != apiGroup.PreferredVersion.Version {
   571  					// only override with preferred version
   572  					continue
   573  				}
   574  				grVersions[gv] = version.Version
   575  				grAPIResources[gv] = apiResource
   576  			}
   577  		}
   578  	}
   579  
   580  	// group selected APIResources according to GroupVersion into APIResourceLists
   581  	for groupResource, apiResource := range grAPIResources {
   582  		version := grVersions[groupResource]
   583  		groupVersion := schema.GroupVersion{Group: groupResource.Group, Version: version}
   584  		apiResourceList := gvAPIResourceLists[groupVersion]
   585  		apiResourceList.APIResources = append(apiResourceList.APIResources, *apiResource)
   586  	}
   587  
   588  	if len(failedGroups) == 0 {
   589  		return result, nil
   590  	}
   591  
   592  	return result, &ErrGroupDiscoveryFailed{Groups: failedGroups}
   593  }
   594  
   595  // fetchServerResourcesForGroupVersions uses the discovery client to fetch the resources for the specified groups in parallel.
   596  func fetchGroupVersionResources(d DiscoveryInterface, apiGroups *metav1.APIGroupList) (map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error) {
   597  	groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
   598  	failedGroups := make(map[schema.GroupVersion]error)
   599  
   600  	wg := &sync.WaitGroup{}
   601  	resultLock := &sync.Mutex{}
   602  	for _, apiGroup := range apiGroups.Groups {
   603  		for _, version := range apiGroup.Versions {
   604  			groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
   605  			wg.Add(1)
   606  			go func() {
   607  				defer wg.Done()
   608  				defer utilruntime.HandleCrash()
   609  
   610  				apiResourceList, err := d.ServerResourcesForGroupVersion(groupVersion.String())
   611  
   612  				// lock to record results
   613  				resultLock.Lock()
   614  				defer resultLock.Unlock()
   615  
   616  				if err != nil {
   617  					// TODO: maybe restrict this to NotFound errors
   618  					failedGroups[groupVersion] = err
   619  				}
   620  				if apiResourceList != nil {
   621  					// even in case of error, some fallback might have been returned
   622  					groupVersionResources[groupVersion] = apiResourceList
   623  				}
   624  			}()
   625  		}
   626  	}
   627  	wg.Wait()
   628  
   629  	return groupVersionResources, failedGroups
   630  }
   631  
   632  // ServerPreferredResources returns the supported resources with the version preferred by the
   633  // server.
   634  func (d *DiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
   635  	_, rs, err := withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
   636  		rs, err := ServerPreferredResources(d)
   637  		return nil, rs, err
   638  	})
   639  	return rs, err
   640  }
   641  
   642  // ServerPreferredNamespacedResources returns the supported namespaced resources with the
   643  // version preferred by the server.
   644  func (d *DiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
   645  	return ServerPreferredNamespacedResources(d)
   646  }
   647  
   648  // ServerPreferredNamespacedResources uses the provided discovery interface to look up preferred namespaced resources
   649  func ServerPreferredNamespacedResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
   650  	all, err := ServerPreferredResources(d)
   651  	return FilteredBy(ResourcePredicateFunc(func(groupVersion string, r *metav1.APIResource) bool {
   652  		return r.Namespaced
   653  	}), all), err
   654  }
   655  
   656  // ServerVersion retrieves and parses the server's version (git version).
   657  func (d *DiscoveryClient) ServerVersion() (*version.Info, error) {
   658  	body, err := d.restClient.Get().AbsPath("/version").Do(context.TODO()).Raw()
   659  	if err != nil {
   660  		return nil, err
   661  	}
   662  	var info version.Info
   663  	err = json.Unmarshal(body, &info)
   664  	if err != nil {
   665  		return nil, fmt.Errorf("unable to parse the server version: %v", err)
   666  	}
   667  	return &info, nil
   668  }
   669  
   670  // OpenAPISchema fetches the open api v2 schema using a rest client and parses the proto.
   671  func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
   672  	data, err := d.restClient.Get().AbsPath("/openapi/v2").SetHeader("Accept", openAPIV2mimePb).Do(context.TODO()).Raw()
   673  	if err != nil {
   674  		return nil, err
   675  	}
   676  	document := &openapi_v2.Document{}
   677  	err = proto.Unmarshal(data, document)
   678  	if err != nil {
   679  		return nil, err
   680  	}
   681  	return document, nil
   682  }
   683  
   684  func (d *DiscoveryClient) OpenAPIV3() openapi.Client {
   685  	return openapi.NewClient(d.restClient)
   686  }
   687  
   688  // WithLegacy returns copy of current discovery client that will only
   689  // receive the legacy discovery format.
   690  func (d *DiscoveryClient) WithLegacy() DiscoveryInterface {
   691  	client := *d
   692  	client.UseLegacyDiscovery = true
   693  	return &client
   694  }
   695  
   696  // withRetries retries the given recovery function in case the groups supported by the server change after ServerGroup() returns.
   697  func withRetries(maxRetries int, f func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
   698  	var result []*metav1.APIResourceList
   699  	var resultGroups []*metav1.APIGroup
   700  	var err error
   701  	for i := 0; i < maxRetries; i++ {
   702  		resultGroups, result, err = f()
   703  		if err == nil {
   704  			return resultGroups, result, nil
   705  		}
   706  		if _, ok := err.(*ErrGroupDiscoveryFailed); !ok {
   707  			return nil, nil, err
   708  		}
   709  	}
   710  	return resultGroups, result, err
   711  }
   712  
   713  func setDiscoveryDefaults(config *restclient.Config) error {
   714  	config.APIPath = ""
   715  	config.GroupVersion = nil
   716  	if config.Timeout == 0 {
   717  		config.Timeout = defaultTimeout
   718  	}
   719  	// if a burst limit is not already configured
   720  	if config.Burst == 0 {
   721  		// discovery is expected to be bursty, increase the default burst
   722  		// to accommodate looking up resource info for many API groups.
   723  		// matches burst set by ConfigFlags#ToDiscoveryClient().
   724  		// see https://issue.k8s.io/86149
   725  		config.Burst = defaultBurst
   726  	}
   727  	codec := runtime.NoopEncoder{Decoder: scheme.Codecs.UniversalDecoder()}
   728  	config.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec})
   729  	if len(config.UserAgent) == 0 {
   730  		config.UserAgent = restclient.DefaultKubernetesUserAgent()
   731  	}
   732  	return nil
   733  }
   734  
   735  // NewDiscoveryClientForConfig creates a new DiscoveryClient for the given config. This client
   736  // can be used to discover supported resources in the API server.
   737  // NewDiscoveryClientForConfig is equivalent to NewDiscoveryClientForConfigAndClient(c, httpClient),
   738  // where httpClient was generated with rest.HTTPClientFor(c).
   739  func NewDiscoveryClientForConfig(c *restclient.Config) (*DiscoveryClient, error) {
   740  	config := *c
   741  	if err := setDiscoveryDefaults(&config); err != nil {
   742  		return nil, err
   743  	}
   744  	httpClient, err := restclient.HTTPClientFor(&config)
   745  	if err != nil {
   746  		return nil, err
   747  	}
   748  	return NewDiscoveryClientForConfigAndClient(&config, httpClient)
   749  }
   750  
   751  // NewDiscoveryClientForConfigAndClient creates a new DiscoveryClient for the given config. This client
   752  // can be used to discover supported resources in the API server.
   753  // Note the http client provided takes precedence over the configured transport values.
   754  func NewDiscoveryClientForConfigAndClient(c *restclient.Config, httpClient *http.Client) (*DiscoveryClient, error) {
   755  	config := *c
   756  	if err := setDiscoveryDefaults(&config); err != nil {
   757  		return nil, err
   758  	}
   759  	client, err := restclient.UnversionedRESTClientForConfigAndClient(&config, httpClient)
   760  	return &DiscoveryClient{restClient: client, LegacyPrefix: "/api", UseLegacyDiscovery: false}, err
   761  }
   762  
   763  // NewDiscoveryClientForConfigOrDie creates a new DiscoveryClient for the given config. If
   764  // there is an error, it panics.
   765  func NewDiscoveryClientForConfigOrDie(c *restclient.Config) *DiscoveryClient {
   766  	client, err := NewDiscoveryClientForConfig(c)
   767  	if err != nil {
   768  		panic(err)
   769  	}
   770  	return client
   771  
   772  }
   773  
   774  // NewDiscoveryClient returns a new DiscoveryClient for the given RESTClient.
   775  func NewDiscoveryClient(c restclient.Interface) *DiscoveryClient {
   776  	return &DiscoveryClient{restClient: c, LegacyPrefix: "/api", UseLegacyDiscovery: false}
   777  }
   778  
   779  // RESTClient returns a RESTClient that is used to communicate
   780  // with API server by this client implementation.
   781  func (d *DiscoveryClient) RESTClient() restclient.Interface {
   782  	if d == nil {
   783  		return nil
   784  	}
   785  	return d.restClient
   786  }
   787  

View as plain text