working commit
This commit is contained in:
+240
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
Copyright 2018 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 apiutil contains utilities for working with raw Kubernetes
|
||||
// API machinery, such as creating RESTMappers and raw REST clients,
|
||||
// and extracting the GVK of an object.
|
||||
package apiutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
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"
|
||||
"k8s.io/client-go/dynamic"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var (
|
||||
protobufScheme = runtime.NewScheme()
|
||||
protobufSchemeLock sync.RWMutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Currently only enabled for built-in resources which are guaranteed to implement Protocol Buffers.
|
||||
// For custom resources, CRDs can not support Protocol Buffers but Aggregated API can.
|
||||
// See doc: https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/#advanced-features-and-flexibility
|
||||
if err := clientgoscheme.AddToScheme(protobufScheme); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// AddToProtobufScheme add the given SchemeBuilder into protobufScheme, which should
|
||||
// be additional types that do support protobuf.
|
||||
func AddToProtobufScheme(addToScheme func(*runtime.Scheme) error) error {
|
||||
protobufSchemeLock.Lock()
|
||||
defer protobufSchemeLock.Unlock()
|
||||
return addToScheme(protobufScheme)
|
||||
}
|
||||
|
||||
// IsObjectNamespaced returns true if the object is namespace scoped.
|
||||
// For unstructured objects the gvk is found from the object itself.
|
||||
func IsObjectNamespaced(obj runtime.Object, scheme *runtime.Scheme, restmapper meta.RESTMapper) (bool, error) {
|
||||
gvk, err := GVKForObject(obj, scheme)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return IsGVKNamespaced(gvk, restmapper)
|
||||
}
|
||||
|
||||
// IsGVKNamespaced returns true if the object having the provided
|
||||
// GVK is namespace scoped.
|
||||
func IsGVKNamespaced(gvk schema.GroupVersionKind, restmapper meta.RESTMapper) (bool, error) {
|
||||
// Fetch the RESTMapping using the complete GVK. If we exclude the Version, the Version set
|
||||
// will be populated using the cached Group if available. This can lead to failures updating
|
||||
// the cache with new Versions of CRDs registered at runtime.
|
||||
restmapping, err := restmapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get restmapping: %w", err)
|
||||
}
|
||||
|
||||
scope := restmapping.Scope.Name()
|
||||
if scope == "" {
|
||||
return false, errors.New("scope cannot be identified, empty scope returned")
|
||||
}
|
||||
|
||||
if scope != meta.RESTScopeNameRoot {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// GVKForObject finds the GroupVersionKind associated with the given object, if there is only a single such GVK.
|
||||
func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) {
|
||||
// TODO(directxman12): do we want to generalize this to arbitrary container types?
|
||||
// I think we'd need a generalized form of scheme or something. It's a
|
||||
// shame there's not a reliable "GetGVK" interface that works by default
|
||||
// for unpopulated static types and populated "dynamic" types
|
||||
// (unstructured, partial, etc)
|
||||
|
||||
// check for PartialObjectMetadata, which is analogous to unstructured, but isn't handled by ObjectKinds
|
||||
_, isPartial := obj.(*metav1.PartialObjectMetadata)
|
||||
_, isPartialList := obj.(*metav1.PartialObjectMetadataList)
|
||||
if isPartial || isPartialList {
|
||||
// we require that the GVK be populated in order to recognize the object
|
||||
gvk := obj.GetObjectKind().GroupVersionKind()
|
||||
if len(gvk.Kind) == 0 {
|
||||
return schema.GroupVersionKind{}, runtime.NewMissingKindErr("unstructured object has no kind")
|
||||
}
|
||||
if len(gvk.Version) == 0 {
|
||||
return schema.GroupVersionKind{}, runtime.NewMissingVersionErr("unstructured object has no version")
|
||||
}
|
||||
return gvk, nil
|
||||
}
|
||||
|
||||
// Use the given scheme to retrieve all the GVKs for the object.
|
||||
gvks, isUnversioned, err := scheme.ObjectKinds(obj)
|
||||
if err != nil {
|
||||
return schema.GroupVersionKind{}, err
|
||||
}
|
||||
if isUnversioned {
|
||||
return schema.GroupVersionKind{}, fmt.Errorf("cannot create group-version-kind for unversioned type %T", obj)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(gvks) < 1:
|
||||
// If the object has no GVK, the object might not have been registered with the scheme.
|
||||
// or it's not a valid object.
|
||||
return schema.GroupVersionKind{}, fmt.Errorf("no GroupVersionKind associated with Go type %T, was the type registered with the Scheme?", obj)
|
||||
case len(gvks) > 1:
|
||||
err := fmt.Errorf("multiple GroupVersionKinds associated with Go type %T within the Scheme, this can happen when a type is registered for multiple GVKs at the same time", obj)
|
||||
|
||||
// We've found multiple GVKs for the object.
|
||||
currentGVK := obj.GetObjectKind().GroupVersionKind()
|
||||
if !currentGVK.Empty() {
|
||||
// If the base object has a GVK, check if it's in the list of GVKs before using it.
|
||||
for _, gvk := range gvks {
|
||||
if gvk == currentGVK {
|
||||
return gvk, nil
|
||||
}
|
||||
}
|
||||
|
||||
return schema.GroupVersionKind{}, fmt.Errorf(
|
||||
"%w: the object's supplied GroupVersionKind %q was not found in the Scheme's list; refusing to guess at one: %q", err, currentGVK, gvks)
|
||||
}
|
||||
|
||||
// This should only trigger for things like metav1.XYZ --
|
||||
// normal versioned types should be fine.
|
||||
//
|
||||
// See https://github.com/kubernetes-sigs/controller-runtime/issues/362
|
||||
// for more information.
|
||||
return schema.GroupVersionKind{}, fmt.Errorf(
|
||||
"%w: callers can either fix their type registration to only register it once, or specify the GroupVersionKind to use for object passed in; refusing to guess at one: %q", err, gvks)
|
||||
default:
|
||||
// In any other case, we've found a single GVK for the object.
|
||||
return gvks[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
// RESTClientForGVK constructs a new rest.Interface capable of accessing the resource associated
|
||||
// with the given GroupVersionKind. The REST client will be configured to use the negotiated serializer from
|
||||
// baseConfig, if set, otherwise a default serializer will be set.
|
||||
func RESTClientForGVK(
|
||||
gvk schema.GroupVersionKind,
|
||||
forceDisableProtoBuf bool,
|
||||
isUnstructured bool,
|
||||
baseConfig *rest.Config,
|
||||
codecs serializer.CodecFactory,
|
||||
httpClient *http.Client,
|
||||
) (rest.Interface, error) {
|
||||
if httpClient == nil {
|
||||
return nil, fmt.Errorf("httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client")
|
||||
}
|
||||
return rest.RESTClientForConfigAndClient(createRestConfig(gvk, forceDisableProtoBuf, isUnstructured, baseConfig, codecs), httpClient)
|
||||
}
|
||||
|
||||
// createRestConfig copies the base config and updates needed fields for a new rest config.
|
||||
func createRestConfig(gvk schema.GroupVersionKind,
|
||||
forceDisableProtoBuf bool,
|
||||
isUnstructured bool,
|
||||
baseConfig *rest.Config,
|
||||
codecs serializer.CodecFactory,
|
||||
) *rest.Config {
|
||||
gv := gvk.GroupVersion()
|
||||
|
||||
cfg := rest.CopyConfig(baseConfig)
|
||||
cfg.GroupVersion = &gv
|
||||
if gvk.Group == "" {
|
||||
cfg.APIPath = "/api"
|
||||
} else {
|
||||
cfg.APIPath = "/apis"
|
||||
}
|
||||
if cfg.UserAgent == "" {
|
||||
cfg.UserAgent = rest.DefaultKubernetesUserAgent()
|
||||
}
|
||||
// TODO(FillZpp): In the long run, we want to check discovery or something to make sure that this is actually true.
|
||||
if cfg.ContentType == "" && !forceDisableProtoBuf {
|
||||
protobufSchemeLock.RLock()
|
||||
if protobufScheme.Recognizes(gvk) {
|
||||
cfg.ContentType = runtime.ContentTypeProtobuf
|
||||
}
|
||||
protobufSchemeLock.RUnlock()
|
||||
}
|
||||
|
||||
if isUnstructured {
|
||||
// If the object is unstructured, we use the client-go dynamic serializer.
|
||||
cfg = dynamic.ConfigFor(cfg)
|
||||
} else {
|
||||
cfg.NegotiatedSerializer = serializerWithTargetZeroingDecode{NegotiatedSerializer: serializer.WithoutConversionCodecFactory{CodecFactory: codecs}}
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
type serializerWithTargetZeroingDecode struct {
|
||||
runtime.NegotiatedSerializer
|
||||
}
|
||||
|
||||
func (s serializerWithTargetZeroingDecode) DecoderToVersion(serializer runtime.Decoder, r runtime.GroupVersioner) runtime.Decoder {
|
||||
return targetZeroingDecoder{upstream: s.NegotiatedSerializer.DecoderToVersion(serializer, r)}
|
||||
}
|
||||
|
||||
type targetZeroingDecoder struct {
|
||||
upstream runtime.Decoder
|
||||
}
|
||||
|
||||
func (t targetZeroingDecoder) Decode(data []byte, defaults *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) {
|
||||
zero(into)
|
||||
return t.upstream.Decode(data, defaults, into)
|
||||
}
|
||||
|
||||
// zero zeros the value of a pointer.
|
||||
func zero(x any) {
|
||||
if x == nil {
|
||||
return
|
||||
}
|
||||
res := reflect.ValueOf(x).Elem()
|
||||
res.Set(reflect.Zero(res.Type()))
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2023 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 apiutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// ErrResourceDiscoveryFailed is returned if the RESTMapper cannot discover supported resources for some GroupVersions.
|
||||
// It wraps the errors encountered, except "NotFound" errors are replaced with meta.NoResourceMatchError, for
|
||||
// backwards compatibility with code that uses meta.IsNoMatchError() to check for unsupported APIs.
|
||||
type ErrResourceDiscoveryFailed map[schema.GroupVersion]error
|
||||
|
||||
// Error implements the error interface.
|
||||
func (e *ErrResourceDiscoveryFailed) Error() string {
|
||||
subErrors := []string{}
|
||||
for k, v := range *e {
|
||||
subErrors = append(subErrors, fmt.Sprintf("%s: %v", k, v))
|
||||
}
|
||||
slices.Sort(subErrors)
|
||||
return fmt.Sprintf("unable to retrieve the complete list of server APIs: %s", strings.Join(subErrors, ", "))
|
||||
}
|
||||
|
||||
func (e *ErrResourceDiscoveryFailed) Unwrap() []error {
|
||||
subErrors := []error{}
|
||||
for gv, err := range *e {
|
||||
if apierrors.IsNotFound(err) {
|
||||
err = &meta.NoResourceMatchError{PartialResource: gv.WithResource("")}
|
||||
}
|
||||
subErrors = append(subErrors, err)
|
||||
}
|
||||
return subErrors
|
||||
}
|
||||
+372
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
Copyright 2023 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 apiutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/restmapper"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// NewDynamicRESTMapper returns a dynamic RESTMapper for cfg. The dynamic
|
||||
// RESTMapper dynamically discovers resource types at runtime.
|
||||
func NewDynamicRESTMapper(cfg *rest.Config, httpClient *http.Client) (meta.RESTMapper, error) {
|
||||
if httpClient == nil {
|
||||
return nil, fmt.Errorf("httpClient must not be nil, consider using rest.HTTPClientFor(c) to create a client")
|
||||
}
|
||||
|
||||
client, err := discovery.NewDiscoveryClientForConfigAndClient(cfg, httpClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mapper{
|
||||
mapper: restmapper.NewDiscoveryRESTMapper([]*restmapper.APIGroupResources{}),
|
||||
client: client,
|
||||
knownGroups: map[string]*restmapper.APIGroupResources{},
|
||||
apiGroups: map[string]*metav1.APIGroup{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// mapper is a RESTMapper that will lazily query the provided
|
||||
// client for discovery information to do REST mappings.
|
||||
type mapper struct {
|
||||
mapper meta.RESTMapper
|
||||
client discovery.AggregatedDiscoveryInterface
|
||||
knownGroups map[string]*restmapper.APIGroupResources
|
||||
apiGroups map[string]*metav1.APIGroup
|
||||
|
||||
initialDiscoveryDone bool
|
||||
|
||||
// mutex to provide thread-safe mapper reloading.
|
||||
// It protects all fields in the mapper as well as methods
|
||||
// that have the `Locked` suffix.
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// KindFor implements Mapper.KindFor.
|
||||
func (m *mapper) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
|
||||
res, err := m.getMapper().KindFor(resource)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
|
||||
return schema.GroupVersionKind{}, err
|
||||
}
|
||||
res, err = m.getMapper().KindFor(resource)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// KindsFor implements Mapper.KindsFor.
|
||||
func (m *mapper) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
|
||||
res, err := m.getMapper().KindsFor(resource)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(resource.Group, resource.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = m.getMapper().KindsFor(resource)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ResourceFor implements Mapper.ResourceFor.
|
||||
func (m *mapper) ResourceFor(input schema.GroupVersionResource) (schema.GroupVersionResource, error) {
|
||||
res, err := m.getMapper().ResourceFor(input)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
|
||||
return schema.GroupVersionResource{}, err
|
||||
}
|
||||
res, err = m.getMapper().ResourceFor(input)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ResourcesFor implements Mapper.ResourcesFor.
|
||||
func (m *mapper) ResourcesFor(input schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
|
||||
res, err := m.getMapper().ResourcesFor(input)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(input.Group, input.Version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = m.getMapper().ResourcesFor(input)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// RESTMapping implements Mapper.RESTMapping.
|
||||
func (m *mapper) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
|
||||
res, err := m.getMapper().RESTMapping(gk, versions...)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = m.getMapper().RESTMapping(gk, versions...)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// RESTMappings implements Mapper.RESTMappings.
|
||||
func (m *mapper) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
|
||||
res, err := m.getMapper().RESTMappings(gk, versions...)
|
||||
if meta.IsNoMatchError(err) {
|
||||
if err := m.addKnownGroupAndReload(gk.Group, versions...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err = m.getMapper().RESTMappings(gk, versions...)
|
||||
}
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
// ResourceSingularizer implements Mapper.ResourceSingularizer.
|
||||
func (m *mapper) ResourceSingularizer(resource string) (string, error) {
|
||||
return m.getMapper().ResourceSingularizer(resource)
|
||||
}
|
||||
|
||||
func (m *mapper) getMapper() meta.RESTMapper {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
return m.mapper
|
||||
}
|
||||
|
||||
// addKnownGroupAndReload reloads the mapper with updated information about missing API group.
|
||||
// versions can be specified for partial updates, for instance for v1beta1 version only.
|
||||
func (m *mapper) addKnownGroupAndReload(groupName string, versions ...string) error {
|
||||
// versions will here be [""] if the forwarded Version value of
|
||||
// GroupVersionResource (in calling method) was not specified.
|
||||
if len(versions) == 1 && versions[0] == "" {
|
||||
versions = nil
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
// If no specific versions are set by user, we will scan all available ones for the API group.
|
||||
// This operation requires 2 requests: /api and /apis, but only once. For all subsequent calls
|
||||
// this data will be taken from cache.
|
||||
//
|
||||
// We always run this once, because if the server supports aggregated discovery, this will
|
||||
// load everything with two api calls which we assume is overall cheaper.
|
||||
if len(versions) == 0 || !m.initialDiscoveryDone {
|
||||
apiGroup, didAggregatedDiscovery, err := m.findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked(groupName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if apiGroup != nil && len(versions) == 0 {
|
||||
for _, version := range apiGroup.Versions {
|
||||
versions = append(versions, version.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// No need to do anything further if aggregatedDiscovery is supported and we did a lookup
|
||||
if didAggregatedDiscovery {
|
||||
failedGroups := make(map[schema.GroupVersion]error)
|
||||
for _, version := range versions {
|
||||
if m.knownGroups[groupName] == nil || m.knownGroups[groupName].VersionedResources[version] == nil {
|
||||
failedGroups[schema.GroupVersion{Group: groupName, Version: version}] = &meta.NoResourceMatchError{
|
||||
PartialResource: schema.GroupVersionResource{
|
||||
Group: groupName,
|
||||
Version: version,
|
||||
}}
|
||||
}
|
||||
}
|
||||
if len(failedGroups) > 0 {
|
||||
return ptr.To(ErrResourceDiscoveryFailed(failedGroups))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Update information for group resources about versioned resources.
|
||||
// The number of API calls is equal to the number of versions: /apis/<group>/<version>.
|
||||
// If we encounter a missing API version (NotFound error), we will remove the group from
|
||||
// the m.apiGroups and m.knownGroups caches.
|
||||
// If this happens, in the next call the group will be added back to apiGroups
|
||||
// and only the existing versions will be loaded in knownGroups.
|
||||
groupVersionResources, err := m.fetchGroupVersionResourcesLocked(groupName, versions...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get API group resources: %w", err)
|
||||
}
|
||||
|
||||
m.addGroupVersionResourcesToCacheAndReloadLocked(groupVersionResources)
|
||||
return nil
|
||||
}
|
||||
|
||||
// addGroupVersionResourcesToCacheAndReloadLocked does what the name suggests. The mutex must be held when
|
||||
// calling it.
|
||||
func (m *mapper) addGroupVersionResourcesToCacheAndReloadLocked(gvr map[schema.GroupVersion]*metav1.APIResourceList) {
|
||||
// Update information for group resources about the API group by adding new versions.
|
||||
// Ignore the versions that are already registered
|
||||
for groupVersion, resources := range gvr {
|
||||
var groupResources *restmapper.APIGroupResources
|
||||
if _, ok := m.knownGroups[groupVersion.Group]; ok {
|
||||
groupResources = m.knownGroups[groupVersion.Group]
|
||||
} else {
|
||||
groupResources = &restmapper.APIGroupResources{
|
||||
Group: metav1.APIGroup{Name: groupVersion.Group},
|
||||
VersionedResources: make(map[string][]metav1.APIResource),
|
||||
}
|
||||
}
|
||||
|
||||
version := groupVersion.Version
|
||||
|
||||
groupResources.VersionedResources[version] = resources.APIResources
|
||||
found := false
|
||||
for _, v := range groupResources.Group.Versions {
|
||||
if v.Version == version {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
gv := metav1.GroupVersionForDiscovery{
|
||||
GroupVersion: metav1.GroupVersion{Group: groupVersion.Group, Version: version}.String(),
|
||||
Version: version,
|
||||
}
|
||||
|
||||
// Prepend if preferred version, else append. The upstream DiscoveryRestMappper assumes
|
||||
// the first version is the preferred one: https://github.com/kubernetes/kubernetes/blob/ef54ac803b712137871c1a1f8d635d50e69ffa6c/staging/src/k8s.io/apimachinery/pkg/api/meta/restmapper.go#L458-L461
|
||||
if group, ok := m.apiGroups[groupVersion.Group]; ok && group.PreferredVersion.Version == version {
|
||||
groupResources.Group.Versions = append([]metav1.GroupVersionForDiscovery{gv}, groupResources.Group.Versions...)
|
||||
} else {
|
||||
groupResources.Group.Versions = append(groupResources.Group.Versions, gv)
|
||||
}
|
||||
}
|
||||
|
||||
// Update data in the cache.
|
||||
m.knownGroups[groupVersion.Group] = groupResources
|
||||
}
|
||||
|
||||
// Finally, reload the mapper.
|
||||
updatedGroupResources := make([]*restmapper.APIGroupResources, 0, len(m.knownGroups))
|
||||
for _, agr := range m.knownGroups {
|
||||
updatedGroupResources = append(updatedGroupResources, agr)
|
||||
}
|
||||
|
||||
m.mapper = restmapper.NewDiscoveryRESTMapper(updatedGroupResources)
|
||||
}
|
||||
|
||||
// findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked tries to find the passed apiGroup.
|
||||
// If the server supports aggregated discovery, it will always perform that.
|
||||
func (m *mapper) findAPIGroupByNameAndMaybeAggregatedDiscoveryLocked(groupName string) (_ *metav1.APIGroup, didAggregatedDiscovery bool, _ error) {
|
||||
// Looking in the cache first
|
||||
group, ok := m.apiGroups[groupName]
|
||||
if ok {
|
||||
return group, false, nil
|
||||
}
|
||||
|
||||
// Update the cache if nothing was found.
|
||||
apiGroups, maybeResources, _, err := m.client.GroupsAndMaybeResources()
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to get server groups: %w", err)
|
||||
}
|
||||
if len(apiGroups.Groups) == 0 {
|
||||
return nil, false, fmt.Errorf("received an empty API groups list")
|
||||
}
|
||||
|
||||
m.initialDiscoveryDone = true
|
||||
for i := range apiGroups.Groups {
|
||||
group := &apiGroups.Groups[i]
|
||||
m.apiGroups[group.Name] = group
|
||||
}
|
||||
if len(maybeResources) > 0 {
|
||||
didAggregatedDiscovery = true
|
||||
m.addGroupVersionResourcesToCacheAndReloadLocked(maybeResources)
|
||||
}
|
||||
|
||||
// Looking in the cache again.
|
||||
// Don't return an error here if the API group is not present.
|
||||
// The reloaded RESTMapper will take care of returning a NoMatchError.
|
||||
return m.apiGroups[groupName], didAggregatedDiscovery, nil
|
||||
}
|
||||
|
||||
// fetchGroupVersionResourcesLocked fetches the resources for the specified group and its versions.
|
||||
// This method might modify the cache so it needs to be called under the lock.
|
||||
func (m *mapper) fetchGroupVersionResourcesLocked(groupName string, versions ...string) (map[schema.GroupVersion]*metav1.APIResourceList, error) {
|
||||
groupVersionResources := make(map[schema.GroupVersion]*metav1.APIResourceList)
|
||||
failedGroups := make(map[schema.GroupVersion]error)
|
||||
|
||||
for _, version := range versions {
|
||||
groupVersion := schema.GroupVersion{Group: groupName, Version: version}
|
||||
|
||||
apiResourceList, err := m.client.ServerResourcesForGroupVersion(groupVersion.String())
|
||||
if apierrors.IsNotFound(err) {
|
||||
// If the version is not found, we remove the group from the cache
|
||||
// so it gets refreshed on the next call.
|
||||
if m.isAPIGroupCachedLocked(groupVersion) {
|
||||
delete(m.apiGroups, groupName)
|
||||
}
|
||||
if m.isGroupVersionCachedLocked(groupVersion) {
|
||||
delete(m.knownGroups, groupName)
|
||||
}
|
||||
continue
|
||||
} else if err != nil {
|
||||
failedGroups[groupVersion] = err
|
||||
}
|
||||
|
||||
if apiResourceList != nil {
|
||||
// even in case of error, some fallback might have been returned.
|
||||
groupVersionResources[groupVersion] = apiResourceList
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedGroups) > 0 {
|
||||
err := ErrResourceDiscoveryFailed(failedGroups)
|
||||
return nil, &err
|
||||
}
|
||||
|
||||
return groupVersionResources, nil
|
||||
}
|
||||
|
||||
// isGroupVersionCachedLocked checks if a version for a group is cached in the known groups cache.
|
||||
func (m *mapper) isGroupVersionCachedLocked(gv schema.GroupVersion) bool {
|
||||
if cachedGroup, ok := m.knownGroups[gv.Group]; ok {
|
||||
_, cached := cachedGroup.VersionedResources[gv.Version]
|
||||
return cached
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isAPIGroupCachedLocked checks if a version for a group is cached in the api groups cache.
|
||||
func (m *mapper) isAPIGroupCachedLocked(gv schema.GroupVersion) bool {
|
||||
cachedGroup, ok := m.apiGroups[gv.Group]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, version := range cachedGroup.Versions {
|
||||
if version.Version == gv.Version {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user