mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-12-23 13:30:23 +00:00
f080b9e0c9
Signed-off-by: Niels de Vos <ndevos@ibm.com>
770 lines
28 KiB
Go
770 lines
28 KiB
Go
/*
|
|
Copyright 2015 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
goerrors "errors"
|
|
"fmt"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
//nolint:staticcheck // SA1019 Keep using module since it's still being maintained and the api of google.golang.org/protobuf/proto differs
|
|
"github.com/golang/protobuf/proto"
|
|
openapi_v2 "github.com/google/gnostic-models/openapiv2"
|
|
|
|
apidiscovery "k8s.io/api/apidiscovery/v2beta1"
|
|
"k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
"k8s.io/apimachinery/pkg/version"
|
|
"k8s.io/client-go/kubernetes/scheme"
|
|
"k8s.io/client-go/openapi"
|
|
restclient "k8s.io/client-go/rest"
|
|
)
|
|
|
|
const (
|
|
// defaultRetries is the number of times a resource discovery is repeated if an api group disappears on the fly (e.g. CustomResourceDefinitions).
|
|
defaultRetries = 2
|
|
// protobuf mime type
|
|
openAPIV2mimePb = "application/com.github.proto-openapi.spec.v2@v1.0+protobuf"
|
|
|
|
// defaultTimeout is the maximum amount of time per request when no timeout has been set on a RESTClient.
|
|
// Defaults to 32s in order to have a distinguishable length of time, relative to other timeouts that exist.
|
|
defaultTimeout = 32 * time.Second
|
|
|
|
// defaultBurst is the default burst to be used with the discovery client's token bucket rate limiter
|
|
defaultBurst = 300
|
|
|
|
AcceptV1 = runtime.ContentTypeJSON
|
|
// Aggregated discovery content-type (v2beta1). NOTE: content-type parameters
|
|
// MUST be ordered (g, v, as) for server in "Accept" header (BUT we are resilient
|
|
// to ordering when comparing returned values in "Content-Type" header).
|
|
AcceptV2Beta1 = runtime.ContentTypeJSON + ";" + "g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList"
|
|
// Prioritize aggregated discovery by placing first in the order of discovery accept types.
|
|
acceptDiscoveryFormats = AcceptV2Beta1 + "," + AcceptV1
|
|
)
|
|
|
|
// Aggregated discovery content-type GVK.
|
|
var v2Beta1GVK = schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2beta1", Kind: "APIGroupDiscoveryList"}
|
|
|
|
// DiscoveryInterface holds the methods that discover server-supported API groups,
|
|
// versions and resources.
|
|
type DiscoveryInterface interface {
|
|
RESTClient() restclient.Interface
|
|
ServerGroupsInterface
|
|
ServerResourcesInterface
|
|
ServerVersionInterface
|
|
OpenAPISchemaInterface
|
|
OpenAPIV3SchemaInterface
|
|
// Returns copy of current discovery client that will only
|
|
// receive the legacy discovery format, or pointer to current
|
|
// discovery client if it does not support legacy-only discovery.
|
|
WithLegacy() DiscoveryInterface
|
|
}
|
|
|
|
// AggregatedDiscoveryInterface extends DiscoveryInterface to include a method to possibly
|
|
// return discovery resources along with the discovery groups, which is what the newer
|
|
// aggregated discovery format does (APIGroupDiscoveryList).
|
|
type AggregatedDiscoveryInterface interface {
|
|
DiscoveryInterface
|
|
|
|
GroupsAndMaybeResources() (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error)
|
|
}
|
|
|
|
// CachedDiscoveryInterface is a DiscoveryInterface with cache invalidation and freshness.
|
|
// Note that If the ServerResourcesForGroupVersion method returns a cache miss
|
|
// error, the user needs to explicitly call Invalidate to clear the cache,
|
|
// otherwise the same cache miss error will be returned next time.
|
|
type CachedDiscoveryInterface interface {
|
|
DiscoveryInterface
|
|
// Fresh is supposed to tell the caller whether or not to retry if the cache
|
|
// fails to find something (false = retry, true = no need to retry).
|
|
//
|
|
// TODO: this needs to be revisited, this interface can't be locked properly
|
|
// and doesn't make a lot of sense.
|
|
Fresh() bool
|
|
// Invalidate enforces that no cached data that is older than the current time
|
|
// is used.
|
|
Invalidate()
|
|
}
|
|
|
|
// ServerGroupsInterface has methods for obtaining supported groups on the API server
|
|
type ServerGroupsInterface interface {
|
|
// ServerGroups returns the supported groups, with information like supported versions and the
|
|
// preferred version.
|
|
ServerGroups() (*metav1.APIGroupList, error)
|
|
}
|
|
|
|
// ServerResourcesInterface has methods for obtaining supported resources on the API server
|
|
type ServerResourcesInterface interface {
|
|
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
|
|
ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error)
|
|
// ServerGroupsAndResources returns the supported groups and resources for all groups and versions.
|
|
//
|
|
// The returned group and resource lists might be non-nil with partial results even in the
|
|
// case of non-nil error.
|
|
ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)
|
|
// ServerPreferredResources returns the supported resources with the version preferred by the
|
|
// server.
|
|
//
|
|
// The returned group and resource lists might be non-nil with partial results even in the
|
|
// case of non-nil error.
|
|
ServerPreferredResources() ([]*metav1.APIResourceList, error)
|
|
// ServerPreferredNamespacedResources returns the supported namespaced resources with the
|
|
// version preferred by the server.
|
|
//
|
|
// The returned resource list might be non-nil with partial results even in the case of
|
|
// non-nil error.
|
|
ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error)
|
|
}
|
|
|
|
// ServerVersionInterface has a method for retrieving the server's version.
|
|
type ServerVersionInterface interface {
|
|
// ServerVersion retrieves and parses the server's version (git version).
|
|
ServerVersion() (*version.Info, error)
|
|
}
|
|
|
|
// OpenAPISchemaInterface has a method to retrieve the open API schema.
|
|
type OpenAPISchemaInterface interface {
|
|
// OpenAPISchema retrieves and parses the swagger API schema the server supports.
|
|
OpenAPISchema() (*openapi_v2.Document, error)
|
|
}
|
|
|
|
type OpenAPIV3SchemaInterface interface {
|
|
OpenAPIV3() openapi.Client
|
|
}
|
|
|
|
// DiscoveryClient implements the functions that discover server-supported API groups,
|
|
// versions and resources.
|
|
type DiscoveryClient struct {
|
|
restClient restclient.Interface
|
|
|
|
LegacyPrefix string
|
|
// Forces the client to request only "unaggregated" (legacy) discovery.
|
|
UseLegacyDiscovery bool
|
|
}
|
|
|
|
var _ AggregatedDiscoveryInterface = &DiscoveryClient{}
|
|
|
|
// Convert metav1.APIVersions to metav1.APIGroup. APIVersions is used by legacy v1, so
|
|
// group would be "".
|
|
func apiVersionsToAPIGroup(apiVersions *metav1.APIVersions) (apiGroup metav1.APIGroup) {
|
|
groupVersions := []metav1.GroupVersionForDiscovery{}
|
|
for _, version := range apiVersions.Versions {
|
|
groupVersion := metav1.GroupVersionForDiscovery{
|
|
GroupVersion: version,
|
|
Version: version,
|
|
}
|
|
groupVersions = append(groupVersions, groupVersion)
|
|
}
|
|
apiGroup.Versions = groupVersions
|
|
// There should be only one groupVersion returned at /api
|
|
apiGroup.PreferredVersion = groupVersions[0]
|
|
return
|
|
}
|
|
|
|
// GroupsAndMaybeResources returns the discovery groups, and (if new aggregated
|
|
// discovery format) the resources keyed by group/version. Merges discovery groups
|
|
// and resources from /api and /apis (either aggregated or not). Legacy groups
|
|
// must be ordered first. The server will either return both endpoints (/api, /apis)
|
|
// as aggregated discovery format or legacy format. For safety, resources will only
|
|
// be returned if both endpoints returned resources. Returned "failedGVs" can be
|
|
// empty, but will only be nil in the case an error is returned.
|
|
func (d *DiscoveryClient) GroupsAndMaybeResources() (
|
|
*metav1.APIGroupList,
|
|
map[schema.GroupVersion]*metav1.APIResourceList,
|
|
map[schema.GroupVersion]error,
|
|
error) {
|
|
// Legacy group ordered first (there is only one -- core/v1 group). Returned groups must
|
|
// be non-nil, but it could be empty. Returned resources, apiResources map could be nil.
|
|
groups, resources, failedGVs, err := d.downloadLegacy()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
// Discovery groups and (possibly) resources downloaded from /apis.
|
|
apiGroups, apiResources, failedApisGVs, aerr := d.downloadAPIs()
|
|
if aerr != nil {
|
|
return nil, nil, nil, aerr
|
|
}
|
|
// Merge apis groups into the legacy groups.
|
|
for _, group := range apiGroups.Groups {
|
|
groups.Groups = append(groups.Groups, group)
|
|
}
|
|
// For safety, only return resources if both endpoints returned resources.
|
|
if resources != nil && apiResources != nil {
|
|
for gv, resourceList := range apiResources {
|
|
resources[gv] = resourceList
|
|
}
|
|
} else if resources != nil {
|
|
resources = nil
|
|
}
|
|
// Merge failed GroupVersions from /api and /apis
|
|
for gv, err := range failedApisGVs {
|
|
failedGVs[gv] = err
|
|
}
|
|
return groups, resources, failedGVs, err
|
|
}
|
|
|
|
// downloadLegacy returns the discovery groups and possibly resources
|
|
// for the legacy v1 GVR at /api, or an error if one occurred. It is
|
|
// possible for the resource map to be nil if the server returned
|
|
// the unaggregated discovery. Returned "failedGVs" can be empty, but
|
|
// will only be nil in the case of a returned error.
|
|
func (d *DiscoveryClient) downloadLegacy() (
|
|
*metav1.APIGroupList,
|
|
map[schema.GroupVersion]*metav1.APIResourceList,
|
|
map[schema.GroupVersion]error,
|
|
error) {
|
|
accept := acceptDiscoveryFormats
|
|
if d.UseLegacyDiscovery {
|
|
accept = AcceptV1
|
|
}
|
|
var responseContentType string
|
|
body, err := d.restClient.Get().
|
|
AbsPath("/api").
|
|
SetHeader("Accept", accept).
|
|
Do(context.TODO()).
|
|
ContentType(&responseContentType).
|
|
Raw()
|
|
apiGroupList := &metav1.APIGroupList{}
|
|
failedGVs := map[schema.GroupVersion]error{}
|
|
if err != nil {
|
|
// Tolerate 404, since aggregated api servers can return it.
|
|
if errors.IsNotFound(err) {
|
|
// Return empty structures and no error.
|
|
emptyGVMap := map[schema.GroupVersion]*metav1.APIResourceList{}
|
|
return apiGroupList, emptyGVMap, failedGVs, nil
|
|
} else {
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
|
|
var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
|
|
// Based on the content-type server responded with: aggregated or unaggregated.
|
|
if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK {
|
|
var aggregatedDiscovery apidiscovery.APIGroupDiscoveryList
|
|
err = json.Unmarshal(body, &aggregatedDiscovery)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery)
|
|
} else {
|
|
// Default is unaggregated discovery v1.
|
|
var v metav1.APIVersions
|
|
err = json.Unmarshal(body, &v)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
apiGroup := metav1.APIGroup{}
|
|
if len(v.Versions) != 0 {
|
|
apiGroup = apiVersionsToAPIGroup(&v)
|
|
}
|
|
apiGroupList.Groups = []metav1.APIGroup{apiGroup}
|
|
}
|
|
|
|
return apiGroupList, resourcesByGV, failedGVs, nil
|
|
}
|
|
|
|
// downloadAPIs returns the discovery groups and (if aggregated format) the
|
|
// discovery resources. The returned groups will always exist, but the
|
|
// resources map may be nil. Returned "failedGVs" can be empty, but will
|
|
// only be nil in the case of a returned error.
|
|
func (d *DiscoveryClient) downloadAPIs() (
|
|
*metav1.APIGroupList,
|
|
map[schema.GroupVersion]*metav1.APIResourceList,
|
|
map[schema.GroupVersion]error,
|
|
error) {
|
|
accept := acceptDiscoveryFormats
|
|
if d.UseLegacyDiscovery {
|
|
accept = AcceptV1
|
|
}
|
|
var responseContentType string
|
|
body, err := d.restClient.Get().
|
|
AbsPath("/apis").
|
|
SetHeader("Accept", accept).
|
|
Do(context.TODO()).
|
|
ContentType(&responseContentType).
|
|
Raw()
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
apiGroupList := &metav1.APIGroupList{}
|
|
failedGVs := map[schema.GroupVersion]error{}
|
|
var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
|
|
// Based on the content-type server responded with: aggregated or unaggregated.
|
|
if isGVK, _ := ContentTypeIsGVK(responseContentType, v2Beta1GVK); isGVK {
|
|
var aggregatedDiscovery apidiscovery.APIGroupDiscoveryList
|
|
err = json.Unmarshal(body, &aggregatedDiscovery)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
apiGroupList, resourcesByGV, failedGVs = SplitGroupsAndResources(aggregatedDiscovery)
|
|
} else {
|
|
// Default is unaggregated discovery v1.
|
|
err = json.Unmarshal(body, apiGroupList)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
}
|
|
|
|
return apiGroupList, resourcesByGV, failedGVs, nil
|
|
}
|
|
|
|
// ContentTypeIsGVK checks of the content-type string is both
|
|
// "application/json" and matches the provided GVK. An error
|
|
// is returned if the content type string is malformed.
|
|
// NOTE: This function is resilient to the ordering of the
|
|
// content-type parameters, as well as parameters added by
|
|
// intermediaries such as proxies or gateways. Examples:
|
|
//
|
|
// ("application/json; g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil)
|
|
// ("application/json; as=APIGroupDiscoveryList;v=v2beta1;g=apidiscovery.k8s.io", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil)
|
|
// ("application/json; as=APIGroupDiscoveryList;v=v2beta1;g=apidiscovery.k8s.io;charset=utf-8", {apidiscovery.k8s.io, v2beta1, APIGroupDiscoveryList}) = (true, nil)
|
|
// ("application/json", any GVK) = (false, nil)
|
|
// ("application/json; charset=UTF-8", any GVK) = (false, nil)
|
|
// ("malformed content type string", any GVK) = (false, error)
|
|
func ContentTypeIsGVK(contentType string, gvk schema.GroupVersionKind) (bool, error) {
|
|
base, params, err := mime.ParseMediaType(contentType)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
gvkMatch := runtime.ContentTypeJSON == base &&
|
|
params["g"] == gvk.Group &&
|
|
params["v"] == gvk.Version &&
|
|
params["as"] == gvk.Kind
|
|
return gvkMatch, nil
|
|
}
|
|
|
|
// ServerGroups returns the supported groups, with information like supported versions and the
|
|
// preferred version.
|
|
func (d *DiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
|
|
groups, _, _, err := d.GroupsAndMaybeResources()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return groups, nil
|
|
}
|
|
|
|
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
|
|
func (d *DiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (resources *metav1.APIResourceList, err error) {
|
|
url := url.URL{}
|
|
if len(groupVersion) == 0 {
|
|
return nil, fmt.Errorf("groupVersion shouldn't be empty")
|
|
}
|
|
if len(d.LegacyPrefix) > 0 && groupVersion == "v1" {
|
|
url.Path = d.LegacyPrefix + "/" + groupVersion
|
|
} else {
|
|
url.Path = "/apis/" + groupVersion
|
|
}
|
|
resources = &metav1.APIResourceList{
|
|
GroupVersion: groupVersion,
|
|
}
|
|
err = d.restClient.Get().AbsPath(url.String()).Do(context.TODO()).Into(resources)
|
|
if err != nil {
|
|
// Tolerate core/v1 not found response by returning empty resource list;
|
|
// this probably should not happen. But we should verify all callers are
|
|
// not depending on this toleration before removal.
|
|
if groupVersion == "v1" && errors.IsNotFound(err) {
|
|
return resources, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return resources, nil
|
|
}
|
|
|
|
// ServerGroupsAndResources returns the supported resources for all groups and versions.
|
|
func (d *DiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
|
|
return withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
|
|
return ServerGroupsAndResources(d)
|
|
})
|
|
}
|
|
|
|
// ErrGroupDiscoveryFailed is returned if one or more API groups fail to load.
|
|
type ErrGroupDiscoveryFailed struct {
|
|
// Groups is a list of the groups that failed to load and the error cause
|
|
Groups map[schema.GroupVersion]error
|
|
}
|
|
|
|
// Error implements the error interface
|
|
func (e *ErrGroupDiscoveryFailed) Error() string {
|
|
var groups []string
|
|
for k, v := range e.Groups {
|
|
groups = append(groups, fmt.Sprintf("%s: %v", k, v))
|
|
}
|
|
sort.Strings(groups)
|
|
return fmt.Sprintf("unable to retrieve the complete list of server APIs: %s", strings.Join(groups, ", "))
|
|
}
|
|
|
|
// Is makes it possible for the callers to use `errors.Is(` helper on errors wrapped with ErrGroupDiscoveryFailed error.
|
|
func (e *ErrGroupDiscoveryFailed) Is(target error) bool {
|
|
_, ok := target.(*ErrGroupDiscoveryFailed)
|
|
return ok
|
|
}
|
|
|
|
// IsGroupDiscoveryFailedError returns true if the provided error indicates the server was unable to discover
|
|
// a complete list of APIs for the client to use.
|
|
func IsGroupDiscoveryFailedError(err error) bool {
|
|
_, ok := err.(*ErrGroupDiscoveryFailed)
|
|
return err != nil && ok
|
|
}
|
|
|
|
// GroupDiscoveryFailedErrorGroups returns true if the error is an ErrGroupDiscoveryFailed error,
|
|
// along with the map of group versions that failed discovery.
|
|
func GroupDiscoveryFailedErrorGroups(err error) (map[schema.GroupVersion]error, bool) {
|
|
var groupDiscoveryError *ErrGroupDiscoveryFailed
|
|
if err != nil && goerrors.As(err, &groupDiscoveryError) {
|
|
return groupDiscoveryError.Groups, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
|
|
var sgs *metav1.APIGroupList
|
|
var resources []*metav1.APIResourceList
|
|
var failedGVs map[schema.GroupVersion]error
|
|
var err error
|
|
|
|
// If the passed discovery object implements the wider AggregatedDiscoveryInterface,
|
|
// then attempt to retrieve aggregated discovery with both groups and the resources.
|
|
if ad, ok := d.(AggregatedDiscoveryInterface); ok {
|
|
var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
|
|
sgs, resourcesByGV, failedGVs, err = ad.GroupsAndMaybeResources()
|
|
for _, resourceList := range resourcesByGV {
|
|
resources = append(resources, resourceList)
|
|
}
|
|
} else {
|
|
sgs, err = d.ServerGroups()
|
|
}
|
|
|
|
if sgs == nil {
|
|
return nil, nil, err
|
|
}
|
|
resultGroups := []*metav1.APIGroup{}
|
|
for i := range sgs.Groups {
|
|
resultGroups = append(resultGroups, &sgs.Groups[i])
|
|
}
|
|
// resources is non-nil if aggregated discovery succeeded.
|
|
if resources != nil {
|
|
// Any stale Group/Versions returned by aggregated discovery
|
|
// must be surfaced to the caller as failed Group/Versions.
|
|
var ferr error
|
|
if len(failedGVs) > 0 {
|
|
ferr = &ErrGroupDiscoveryFailed{Groups: failedGVs}
|
|
}
|
|
return resultGroups, resources, ferr
|
|
}
|
|
|
|
groupVersionResources, failedGroups := fetchGroupVersionResources(d, sgs)
|
|
|
|
// order results by group/version discovery order
|
|
result := []*metav1.APIResourceList{}
|
|
for _, apiGroup := range sgs.Groups {
|
|
for _, version := range apiGroup.Versions {
|
|
gv := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
|
|
if resources, ok := groupVersionResources[gv]; ok {
|
|
result = append(result, resources)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(failedGroups) == 0 {
|
|
return resultGroups, result, nil
|
|
}
|
|
|
|
return resultGroups, result, &ErrGroupDiscoveryFailed{Groups: failedGroups}
|
|
}
|
|
|
|
// ServerPreferredResources uses the provided discovery interface to look up preferred resources
|
|
func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
|
|
var serverGroupList *metav1.APIGroupList
|
|
var failedGroups map[schema.GroupVersion]error
|
|
var groupVersionResources map[schema.GroupVersion]*metav1.APIResourceList
|
|
var err error
|
|
|
|
// If the passed discovery object implements the wider AggregatedDiscoveryInterface,
|
|
// then it is attempt to retrieve both the groups and the resources. "failedGroups"
|
|
// are Group/Versions returned as stale in AggregatedDiscovery format.
|
|
ad, ok := d.(AggregatedDiscoveryInterface)
|
|
if ok {
|
|
serverGroupList, groupVersionResources, failedGroups, err = ad.GroupsAndMaybeResources()
|
|
} else {
|
|
serverGroupList, err = d.ServerGroups()
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Non-aggregated discovery must fetch resources from Groups.
|
|
if groupVersionResources == nil {
|
|
groupVersionResources, failedGroups = fetchGroupVersionResources(d, serverGroupList)
|
|
}
|
|
|
|
result := []*metav1.APIResourceList{}
|
|
grVersions := map[schema.GroupResource]string{} // selected version of a GroupResource
|
|
grAPIResources := map[schema.GroupResource]*metav1.APIResource{} // selected APIResource for a GroupResource
|
|
gvAPIResourceLists := map[schema.GroupVersion]*metav1.APIResourceList{} // blueprint for a APIResourceList for later grouping
|
|
|
|
for _, apiGroup := range serverGroupList.Groups {
|
|
for _, version := range apiGroup.Versions {
|
|
groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
|
|
|
|
apiResourceList, ok := groupVersionResources[groupVersion]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// create empty list which is filled later in another loop
|
|
emptyAPIResourceList := metav1.APIResourceList{
|
|
GroupVersion: version.GroupVersion,
|
|
}
|
|
gvAPIResourceLists[groupVersion] = &emptyAPIResourceList
|
|
result = append(result, &emptyAPIResourceList)
|
|
|
|
for i := range apiResourceList.APIResources {
|
|
apiResource := &apiResourceList.APIResources[i]
|
|
if strings.Contains(apiResource.Name, "/") {
|
|
continue
|
|
}
|
|
gv := schema.GroupResource{Group: apiGroup.Name, Resource: apiResource.Name}
|
|
if _, ok := grAPIResources[gv]; ok && version.Version != apiGroup.PreferredVersion.Version {
|
|
// only override with preferred version
|
|
continue
|
|
}
|
|
grVersions[gv] = version.Version
|
|
grAPIResources[gv] = apiResource
|
|
}
|
|
}
|
|
}
|
|
|
|
// group selected APIResources according to GroupVersion into APIResourceLists
|
|
for groupResource, apiResource := range grAPIResources {
|
|
version := grVersions[groupResource]
|
|
groupVersion := schema.GroupVersion{Group: groupResource.Group, Version: version}
|
|
apiResourceList := gvAPIResourceLists[groupVersion]
|
|
apiResourceList.APIResources = append(apiResourceList.APIResources, *apiResource)
|
|
}
|
|
|
|
if len(failedGroups) == 0 {
|
|
return result, nil
|
|
}
|
|
|
|
return result, &ErrGroupDiscoveryFailed{Groups: failedGroups}
|
|
}
|
|
|
|
// fetchServerResourcesForGroupVersions uses the discovery client to fetch the resources for the specified groups in parallel.
|
|
func fetchGroupVersionResources(d DiscoveryInterface, apiGroups *metav1.APIGroupList) (map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error) {
|
|
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
|
|
failedGroups := make(map[schema.GroupVersion]error)
|
|
|
|
wg := &sync.WaitGroup{}
|
|
resultLock := &sync.Mutex{}
|
|
for _, apiGroup := range apiGroups.Groups {
|
|
for _, version := range apiGroup.Versions {
|
|
groupVersion := schema.GroupVersion{Group: apiGroup.Name, Version: version.Version}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
defer utilruntime.HandleCrash()
|
|
|
|
apiResourceList, err := d.ServerResourcesForGroupVersion(groupVersion.String())
|
|
|
|
// lock to record results
|
|
resultLock.Lock()
|
|
defer resultLock.Unlock()
|
|
|
|
if err != nil {
|
|
// TODO: maybe restrict this to NotFound errors
|
|
failedGroups[groupVersion] = err
|
|
}
|
|
if apiResourceList != nil {
|
|
// even in case of error, some fallback might have been returned
|
|
groupVersionResources[groupVersion] = apiResourceList
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
wg.Wait()
|
|
|
|
return groupVersionResources, failedGroups
|
|
}
|
|
|
|
// ServerPreferredResources returns the supported resources with the version preferred by the
|
|
// server.
|
|
func (d *DiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
|
|
_, rs, err := withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
|
|
rs, err := ServerPreferredResources(d)
|
|
return nil, rs, err
|
|
})
|
|
return rs, err
|
|
}
|
|
|
|
// ServerPreferredNamespacedResources returns the supported namespaced resources with the
|
|
// version preferred by the server.
|
|
func (d *DiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
|
|
return ServerPreferredNamespacedResources(d)
|
|
}
|
|
|
|
// ServerPreferredNamespacedResources uses the provided discovery interface to look up preferred namespaced resources
|
|
func ServerPreferredNamespacedResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
|
|
all, err := ServerPreferredResources(d)
|
|
return FilteredBy(ResourcePredicateFunc(func(groupVersion string, r *metav1.APIResource) bool {
|
|
return r.Namespaced
|
|
}), all), err
|
|
}
|
|
|
|
// ServerVersion retrieves and parses the server's version (git version).
|
|
func (d *DiscoveryClient) ServerVersion() (*version.Info, error) {
|
|
body, err := d.restClient.Get().AbsPath("/version").Do(context.TODO()).Raw()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var info version.Info
|
|
err = json.Unmarshal(body, &info)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse the server version: %v", err)
|
|
}
|
|
return &info, nil
|
|
}
|
|
|
|
// OpenAPISchema fetches the open api v2 schema using a rest client and parses the proto.
|
|
func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
|
|
data, err := d.restClient.Get().AbsPath("/openapi/v2").SetHeader("Accept", openAPIV2mimePb).Do(context.TODO()).Raw()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
document := &openapi_v2.Document{}
|
|
err = proto.Unmarshal(data, document)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return document, nil
|
|
}
|
|
|
|
func (d *DiscoveryClient) OpenAPIV3() openapi.Client {
|
|
return openapi.NewClient(d.restClient)
|
|
}
|
|
|
|
// WithLegacy returns copy of current discovery client that will only
|
|
// receive the legacy discovery format.
|
|
func (d *DiscoveryClient) WithLegacy() DiscoveryInterface {
|
|
client := *d
|
|
client.UseLegacyDiscovery = true
|
|
return &client
|
|
}
|
|
|
|
// withRetries retries the given recovery function in case the groups supported by the server change after ServerGroup() returns.
|
|
func withRetries(maxRetries int, f func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error)) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
|
|
var result []*metav1.APIResourceList
|
|
var resultGroups []*metav1.APIGroup
|
|
var err error
|
|
for i := 0; i < maxRetries; i++ {
|
|
resultGroups, result, err = f()
|
|
if err == nil {
|
|
return resultGroups, result, nil
|
|
}
|
|
if _, ok := err.(*ErrGroupDiscoveryFailed); !ok {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
return resultGroups, result, err
|
|
}
|
|
|
|
func setDiscoveryDefaults(config *restclient.Config) error {
|
|
config.APIPath = ""
|
|
config.GroupVersion = nil
|
|
if config.Timeout == 0 {
|
|
config.Timeout = defaultTimeout
|
|
}
|
|
// if a burst limit is not already configured
|
|
if config.Burst == 0 {
|
|
// discovery is expected to be bursty, increase the default burst
|
|
// to accommodate looking up resource info for many API groups.
|
|
// matches burst set by ConfigFlags#ToDiscoveryClient().
|
|
// see https://issue.k8s.io/86149
|
|
config.Burst = defaultBurst
|
|
}
|
|
codec := runtime.NoopEncoder{Decoder: scheme.Codecs.UniversalDecoder()}
|
|
config.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec})
|
|
if len(config.UserAgent) == 0 {
|
|
config.UserAgent = restclient.DefaultKubernetesUserAgent()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewDiscoveryClientForConfig creates a new DiscoveryClient for the given config. This client
|
|
// can be used to discover supported resources in the API server.
|
|
// NewDiscoveryClientForConfig is equivalent to NewDiscoveryClientForConfigAndClient(c, httpClient),
|
|
// where httpClient was generated with rest.HTTPClientFor(c).
|
|
func NewDiscoveryClientForConfig(c *restclient.Config) (*DiscoveryClient, error) {
|
|
config := *c
|
|
if err := setDiscoveryDefaults(&config); err != nil {
|
|
return nil, err
|
|
}
|
|
httpClient, err := restclient.HTTPClientFor(&config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewDiscoveryClientForConfigAndClient(&config, httpClient)
|
|
}
|
|
|
|
// NewDiscoveryClientForConfigAndClient creates a new DiscoveryClient for the given config. This client
|
|
// can be used to discover supported resources in the API server.
|
|
// Note the http client provided takes precedence over the configured transport values.
|
|
func NewDiscoveryClientForConfigAndClient(c *restclient.Config, httpClient *http.Client) (*DiscoveryClient, error) {
|
|
config := *c
|
|
if err := setDiscoveryDefaults(&config); err != nil {
|
|
return nil, err
|
|
}
|
|
client, err := restclient.UnversionedRESTClientForConfigAndClient(&config, httpClient)
|
|
return &DiscoveryClient{restClient: client, LegacyPrefix: "/api", UseLegacyDiscovery: false}, err
|
|
}
|
|
|
|
// NewDiscoveryClientForConfigOrDie creates a new DiscoveryClient for the given config. If
|
|
// there is an error, it panics.
|
|
func NewDiscoveryClientForConfigOrDie(c *restclient.Config) *DiscoveryClient {
|
|
client, err := NewDiscoveryClientForConfig(c)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return client
|
|
|
|
}
|
|
|
|
// NewDiscoveryClient returns a new DiscoveryClient for the given RESTClient.
|
|
func NewDiscoveryClient(c restclient.Interface) *DiscoveryClient {
|
|
return &DiscoveryClient{restClient: c, LegacyPrefix: "/api", UseLegacyDiscovery: false}
|
|
}
|
|
|
|
// RESTClient returns a RESTClient that is used to communicate
|
|
// with API server by this client implementation.
|
|
func (d *DiscoveryClient) RESTClient() restclient.Interface {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
return d.restClient
|
|
}
|