Merge pull request #129109 from pohly/log-client-go-restmapper-discovery

restmapper + discovery: add context-aware APIs

Kubernetes-commit: b308e6c5067486765de78de1dc809fb30b0b68c3
This commit is contained in:
Kubernetes Publisher
2026-06-30 18:10:10 +00:00
19 changed files with 1735 additions and 206 deletions

View File

@@ -17,6 +17,7 @@ limitations under the License.
package disk
import (
"context"
"errors"
"io"
"net/http"
@@ -42,7 +43,7 @@ import (
// CachedDiscoveryClient implements the functions that discovery server-supported API groups,
// versions and resources.
type CachedDiscoveryClient struct {
delegate discovery.DiscoveryInterface
delegate discovery.DiscoveryInterfaceWithContext
// cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well.
cacheDirectory string
@@ -61,72 +62,104 @@ type CachedDiscoveryClient struct {
fresh bool
// caching openapi v3 client which wraps the delegate's client
openapiClient openapi.Client
openapiClient *cachedopenapi.Client
}
var _ discovery.CachedDiscoveryInterface = &CachedDiscoveryClient{}
var (
//nolint:staticcheck // Intentionally using the old interface here.
_ discovery.CachedDiscoveryInterface = &CachedDiscoveryClient{}
_ discovery.CachedDiscoveryInterfaceWithContext = &CachedDiscoveryClient{}
)
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
//
// ServerResourcesForGroupVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerResourcesForGroupVersionWithContext instead.
func (d *CachedDiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
return d.ServerResourcesForGroupVersionWithContext(context.Background(), groupVersion)
}
// ServerResourcesForGroupVersionWithContext returns the supported resources for a group and version.
func (d *CachedDiscoveryClient) ServerResourcesForGroupVersionWithContext(ctx context.Context, groupVersion string) (*metav1.APIResourceList, error) {
filename := filepath.Join(d.cacheDirectory, groupVersion, "serverresources.json")
cachedBytes, err := d.getCachedFile(filename)
// don't fail on errors, we either don't have a file or won't be able to run the cached check. Either way we can fallback.
if err == nil {
cachedResources := &metav1.APIResourceList{}
if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedResources); err == nil {
klog.V(10).Infof("returning cached discovery info from %v", filename)
klog.FromContext(ctx).V(10).Info("Returning cached discovery info", "fileName", filename)
return cachedResources, nil
}
}
liveResources, err := d.delegate.ServerResourcesForGroupVersion(groupVersion)
liveResources, err := d.delegate.ServerResourcesForGroupVersionWithContext(ctx, groupVersion)
if err != nil {
klog.V(3).Infof("skipped caching discovery info due to %v", err)
klog.FromContext(ctx).V(3).Info("Skipped caching discovery info due to error", "err", err)
return liveResources, err
}
if liveResources == nil || len(liveResources.APIResources) == 0 {
klog.V(3).Infof("skipped caching discovery info, no resources found")
klog.FromContext(ctx).V(3).Info("skipped caching discovery info, no resources found")
return liveResources, err
}
if err := d.writeCachedFile(filename, liveResources); err != nil {
klog.V(1).Infof("failed to write cache to %v due to %v", filename, err)
klog.FromContext(ctx).V(1).Info("Failed to write cache", "fileName", filename, "err", err)
}
return liveResources, nil
}
// ServerGroupsAndResources returns the supported groups and resources for all groups and versions.
//
// ServerGroupsAndResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsAndResourcesWithContext instead.
func (d *CachedDiscoveryClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return discovery.ServerGroupsAndResources(d)
return d.ServerGroupsAndResourcesWithContext(context.Background())
}
// ServerGroupsAndResourcesWithContext returns the supported groups and resources for all groups and versions.
func (d *CachedDiscoveryClient) ServerGroupsAndResourcesWithContext(ctx context.Context) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return discovery.ServerGroupsAndResourcesWithContext(ctx, d)
}
// ServerGroups returns the supported groups, with information like supported versions and the
// preferred version.
//
// ServerGroupsWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsWithContext instead.
func (d *CachedDiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
return d.ServerGroupsWithContext(context.Background())
}
// ServerGroupsWithContext returns the supported groups, with information like supported versions and the
// preferred version.
func (d *CachedDiscoveryClient) ServerGroupsWithContext(ctx context.Context) (*metav1.APIGroupList, error) {
filename := filepath.Join(d.cacheDirectory, "servergroups.json")
cachedBytes, err := d.getCachedFile(filename)
// don't fail on errors, we either don't have a file or won't be able to run the cached check. Either way we can fallback.
if err == nil {
cachedGroups := &metav1.APIGroupList{}
if err := runtime.DecodeInto(scheme.Codecs.UniversalDecoder(), cachedBytes, cachedGroups); err == nil {
klog.V(10).Infof("returning cached discovery info from %v", filename)
klog.FromContext(ctx).V(10).Info("Returning cached discovery info", "fileName", filename)
return cachedGroups, nil
}
}
liveGroups, err := d.delegate.ServerGroups()
liveGroups, err := d.delegate.ServerGroupsWithContext(ctx)
if err != nil {
klog.V(3).Infof("skipped caching discovery info due to %v", err)
klog.FromContext(ctx).V(3).Info("Skipped caching discovery info due to error", "err", err)
return liveGroups, err
}
if liveGroups == nil || len(liveGroups.Groups) == 0 {
klog.V(3).Infof("skipped caching discovery info, no groups found")
klog.FromContext(ctx).V(3).Info("Skipped caching discovery info, no groups found")
return liveGroups, err
}
if err := d.writeCachedFile(filename, liveGroups); err != nil {
klog.V(1).Infof("failed to write cache to %v due to %v", filename, err)
klog.FromContext(ctx).V(1).Info("Failed to write cache", "fileName", filename, "err", err)
}
return liveGroups, nil
@@ -219,28 +252,75 @@ func (d *CachedDiscoveryClient) RESTClient() restclient.Interface {
// ServerPreferredResources returns the supported resources with the version preferred by the
// server.
//
// ServerPreferredResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredResourcesWithContext instead.
func (d *CachedDiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
return discovery.ServerPreferredResources(d)
return d.ServerPreferredResourcesWithContext(context.Background())
}
// ServerPreferredResourcesWithContext returns the supported resources with the version preferred by the
// server.
func (d *CachedDiscoveryClient) ServerPreferredResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return discovery.ServerPreferredResourcesWithContext(ctx, d)
}
// ServerPreferredNamespacedResources returns the supported namespaced resources with the
// version preferred by the server.
//
// ServerPreferredNamespacedResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredNamespacedResourcesWithContext instead.
func (d *CachedDiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
return discovery.ServerPreferredNamespacedResources(d)
return d.ServerPreferredNamespacedResourcesWithContext(context.Background())
}
// ServerPreferredNamespacedResourcesWithContext returns the supported namespaced resources with the
// version preferred by the server.
func (d *CachedDiscoveryClient) ServerPreferredNamespacedResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return discovery.ServerPreferredNamespacedResourcesWithContext(ctx, d)
}
// ServerVersion retrieves and parses the server's version (git version).
//
// ServerVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerVersionWithContext instead.
func (d *CachedDiscoveryClient) ServerVersion() (*version.Info, error) {
return d.delegate.ServerVersion()
return d.ServerVersionWithContext(context.Background())
}
// ServerVersionWithContext retrieves and parses the server's version (git version).
func (d *CachedDiscoveryClient) ServerVersionWithContext(ctx context.Context) (*version.Info, error) {
return d.delegate.ServerVersionWithContext(ctx)
}
// OpenAPISchema retrieves and parses the swagger API schema the server supports.
//
// OpenAPISchemaWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPISchemaWithContext instead.
func (d *CachedDiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
return d.delegate.OpenAPISchema()
return d.OpenAPISchemaWithContext(context.Background())
}
// OpenAPISchemaWithContext retrieves and parses the swagger API schema the server supports.
func (d *CachedDiscoveryClient) OpenAPISchemaWithContext(ctx context.Context) (*openapi_v2.Document, error) {
return d.delegate.OpenAPISchemaWithContext(ctx)
}
// OpenAPIV3 retrieves and parses the OpenAPIV3 specs exposed by the server
func (d *CachedDiscoveryClient) OpenAPIV3() openapi.Client {
return d.openAPIV3(context.Background())
}
// OpenAPIV3WithContext retrieves and parses the OpenAPIV3 specs exposed by the server
func (d *CachedDiscoveryClient) OpenAPIV3WithContext(ctx context.Context) openapi.ClientWithContext {
return d.openAPIV3(ctx)
}
func (d *CachedDiscoveryClient) openAPIV3(ctx context.Context) *cachedopenapi.Client {
// Must take lock since Invalidate call may modify openapiClient
d.mutex.Lock()
defer d.mutex.Unlock()
@@ -248,7 +328,7 @@ func (d *CachedDiscoveryClient) OpenAPIV3() openapi.Client {
if d.openapiClient == nil {
// Delegate is discovery client created with special HTTP client which
// respects E-Tag cache responses to serve cache from disk.
d.openapiClient = cachedopenapi.NewClient(d.delegate.OpenAPIV3())
d.openapiClient = cachedopenapi.NewClientWithContext(d.delegate.OpenAPIV3WithContext(ctx))
}
return d.openapiClient
@@ -256,7 +336,17 @@ func (d *CachedDiscoveryClient) OpenAPIV3() openapi.Client {
// 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).
//
// FreshWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use FreshWithContext instead.
func (d *CachedDiscoveryClient) Fresh() bool {
return d.FreshWithContext(context.Background())
}
// FreshWithContext 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).
func (d *CachedDiscoveryClient) FreshWithContext(ctx context.Context) bool {
d.mutex.Lock()
defer d.mutex.Unlock()
@@ -264,7 +354,16 @@ func (d *CachedDiscoveryClient) Fresh() bool {
}
// Invalidate enforces that no cached data is used in the future that is older than the current time.
//
// InvalidateWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use InvalidateWithContext instead.
func (d *CachedDiscoveryClient) Invalidate() {
d.InvalidateWithContext(context.Background())
}
// InvalidateWithContext enforces that no cached data is used in the future that is older than the current time.
func (d *CachedDiscoveryClient) InvalidateWithContext(ctx context.Context) {
d.mutex.Lock()
defer d.mutex.Unlock()
@@ -272,8 +371,16 @@ func (d *CachedDiscoveryClient) Invalidate() {
d.fresh = true
d.invalidated = true
d.openapiClient = nil
if ad, ok := d.delegate.(discovery.CachedDiscoveryInterface); ok {
ad.Invalidate()
ad, ok := d.delegate.(discovery.CachedDiscoveryInterfaceWithContext)
if !ok {
//nolint:staticcheck // Intentionally using the old interface here.
if ad2, ok2 := d.delegate.(discovery.CachedDiscoveryInterface); ok2 {
ad = discovery.ToCachedDiscoveryInterfaceWithContext(ad2)
ok = true
}
}
if ok {
ad.InvalidateWithContext(ctx)
}
}
@@ -283,6 +390,12 @@ func (d *CachedDiscoveryClient) WithLegacy() discovery.DiscoveryInterface {
return d
}
// WithLegacyWithContext returns current cached discovery client;
// current client does not support legacy-only discovery.
func (d *CachedDiscoveryClient) WithLegacyWithContext(ctx context.Context) discovery.DiscoveryInterfaceWithContext {
return d
}
// NewCachedDiscoveryClientForConfig creates a new DiscoveryClient for the given config, and wraps
// the created client in a CachedDiscoveryClient. The provided configuration is updated with a
// custom transport that understands cache responses.
@@ -310,11 +423,11 @@ func NewCachedDiscoveryClientForConfig(config *restclient.Config, discoveryCache
// The delegate caches the discovery groups and resources (memcache). "ServerGroups",
// which usually only returns (and caches) the groups, can now store the resources as
// well if the server supports the newer aggregated discovery format.
return newCachedDiscoveryClient(memory.NewMemCacheClient(discoveryClient), discoveryCacheDir, ttl), nil
return newCachedDiscoveryClient(memory.NewMemCacheClientWithContext(discoveryClient), discoveryCacheDir, ttl), nil
}
// NewCachedDiscoveryClient creates a new DiscoveryClient. cacheDirectory is the directory where discovery docs are held. It must be unique per host:port combination to work well.
func newCachedDiscoveryClient(delegate discovery.DiscoveryInterface, cacheDirectory string, ttl time.Duration) *CachedDiscoveryClient {
func newCachedDiscoveryClient(delegate discovery.DiscoveryInterfaceWithContext, cacheDirectory string, ttl time.Duration) *CachedDiscoveryClient {
return &CachedDiscoveryClient{
delegate: delegate,
cacheDirectory: cacheDirectory,

View File

@@ -52,7 +52,7 @@ func TestCachedDiscoveryClient_Fresh(t *testing.T) {
defer os.RemoveAll(d)
c := fakeDiscoveryClient{}
cdc := newCachedDiscoveryClient(&c, d, 60*time.Second)
cdc := newCachedDiscoveryClient(discovery.ToDiscoveryInterfaceWithContext(&c), d, 60*time.Second)
assert.True(cdc.Fresh(), "should be fresh after creation")
cdc.ServerGroups()
@@ -71,7 +71,7 @@ func TestCachedDiscoveryClient_Fresh(t *testing.T) {
assert.True(cdc.Fresh(), "should be fresh after another resources call")
assert.Equal(1, c.resourceCalls)
cdc = newCachedDiscoveryClient(&c, d, 60*time.Second)
cdc = newCachedDiscoveryClient(discovery.ToDiscoveryInterfaceWithContext(&c), d, 60*time.Second)
cdc.ServerGroups()
assert.False(cdc.Fresh(), "should NOT be fresh after recreation with existing groups cache")
assert.Equal(1, c.groupCalls)
@@ -96,7 +96,7 @@ func TestNewCachedDiscoveryClient_TTL(t *testing.T) {
defer os.RemoveAll(d)
c := fakeDiscoveryClient{}
cdc := newCachedDiscoveryClient(&c, d, 1*time.Nanosecond)
cdc := newCachedDiscoveryClient(discovery.ToDiscoveryInterfaceWithContext(&c), d, 1*time.Nanosecond)
cdc.ServerGroups()
assert.Equal(1, c.groupCalls)
@@ -115,7 +115,7 @@ func TestNewCachedDiscoveryClient_PathPerm(t *testing.T) {
defer os.RemoveAll(d)
c := fakeDiscoveryClient{}
cdc := newCachedDiscoveryClient(&c, d, 1*time.Nanosecond)
cdc := newCachedDiscoveryClient(discovery.ToDiscoveryInterfaceWithContext(&c), d, 1*time.Nanosecond)
cdc.ServerGroups()
err = filepath.Walk(d, func(path string, info os.FileInfo, err error) error {

View File

@@ -60,7 +60,7 @@ func (rt *cacheRoundTripper) CancelRequest(req *http.Request) {
if cr, ok := rt.rt.Transport.(canceler); ok {
cr.CancelRequest(req)
} else {
klog.Errorf("CancelRequest not implemented by %T", rt.rt.Transport)
klog.FromContext(req.Context()).Error(nil, "CancelRequest not implemented by round-tripper", "type", fmt.Sprintf("%T", rt.rt.Transport))
}
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
package memory
import (
"context"
"errors"
"fmt"
"sync"
@@ -47,13 +48,13 @@ type cacheEntry struct {
// TODO: Switch to a watch interface. Right now it will poll after each
// Invalidate() call.
type memCacheClient struct {
delegate discovery.DiscoveryInterface
delegate discovery.DiscoveryInterfaceWithContext
lock sync.RWMutex
groupToServerResources map[string]*cacheEntry
groupList *metav1.APIGroupList
cacheValid bool
openapiClient openapi.Client
openapiClient *cachedopenapi.Client
receivedAggregatedDiscovery bool
}
@@ -72,6 +73,7 @@ func (e *emptyResponseError) Error() string {
}
var _ discovery.CachedDiscoveryInterface = &memCacheClient{}
var _ discovery.CachedDiscoveryInterfaceWithContext = &memCacheClient{}
// isTransientConnectionError checks whether given error is "Connection refused" or
// "Connection reset" error which usually means that apiserver is temporarily
@@ -85,6 +87,11 @@ func isTransientConnectionError(err error) bool {
}
func isTransientError(err error) bool {
// Context cancelation is transient, retrying with a different context can work.
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return true
}
if isTransientConnectionError(err) {
return true
}
@@ -98,10 +105,15 @@ func isTransientError(err error) bool {
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
func (d *memCacheClient) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
return d.ServerResourcesForGroupVersionWithContext(context.Background(), groupVersion)
}
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
func (d *memCacheClient) ServerResourcesForGroupVersionWithContext(ctx context.Context, groupVersion string) (*metav1.APIResourceList, error) {
d.lock.Lock()
defer d.lock.Unlock()
if !d.cacheValid {
if err := d.refreshLocked(); err != nil {
if err := d.refreshLocked(ctx); err != nil {
return nil, err
}
}
@@ -111,14 +123,14 @@ func (d *memCacheClient) ServerResourcesForGroupVersion(groupVersion string) (*m
}
if cachedVal.err != nil && isTransientError(cachedVal.err) {
r, err := d.serverResourcesForGroupVersion(groupVersion)
r, err := d.serverResourcesForGroupVersion(ctx, groupVersion)
if err != nil {
// Don't log "empty response" as an error; it is a common response for metrics.
if _, emptyErr := err.(*emptyResponseError); emptyErr {
// Log at same verbosity as disk cache.
klog.V(3).Infof("%v", err)
klog.FromContext(ctx).V(3).Info(err.Error())
} else {
utilruntime.HandleError(fmt.Errorf("couldn't get resource list for %v: %v", groupVersion, err))
utilruntime.HandleErrorWithContext(ctx, err, "Couldn't get resource list", "gv", groupVersion)
}
}
cachedVal = &cacheEntry{r, err}
@@ -129,19 +141,43 @@ func (d *memCacheClient) ServerResourcesForGroupVersion(groupVersion string) (*m
}
// ServerGroupsAndResources returns the groups and supported resources for all groups and versions.
//
// ServerGroupsAndResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsAndResourcesWithContext instead.
func (d *memCacheClient) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return discovery.ServerGroupsAndResources(d)
return d.ServerGroupsAndResourcesWithContext(context.Background())
}
// ServerGroupsAndResources returns the groups and supported resources for all groups and versions.
func (d *memCacheClient) ServerGroupsAndResourcesWithContext(ctx context.Context) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return discovery.ServerGroupsAndResourcesWithContext(ctx, d)
}
// GroupsAndMaybeResources returns the list of APIGroups, and possibly the map of group/version
// to resources. The returned groups will never be nil, but the resources map can be nil
// if there are no cached resources.
//
// GroupsAndMaybeResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use GroupsAndMaybeResourcesWithContext instead.
func (d *memCacheClient) GroupsAndMaybeResources() (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error) {
return d.GroupsAndMaybeResourcesWithContext(context.Background())
}
// GroupsAndMaybeResourcesWithContext returns the list of APIGroups, and possibly the map of group/version
// to resources. The returned groups will never be nil, but the resources map can be nil
// if there are no cached resources.
func (d *memCacheClient) GroupsAndMaybeResourcesWithContext(ctx context.Context) (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error) {
if err := ctx.Err(); err != nil {
return nil, nil, nil, err
}
d.lock.Lock()
defer d.lock.Unlock()
if !d.cacheValid {
if err := d.refreshLocked(); err != nil {
if err := d.refreshLocked(ctx); err != nil {
return nil, nil, nil, err
}
}
@@ -166,8 +202,15 @@ func (d *memCacheClient) GroupsAndMaybeResources() (*metav1.APIGroupList, map[sc
return d.groupList, resourcesMap, failedGVs, nil
}
// ServerGroupsWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsWithContext instead.
func (d *memCacheClient) ServerGroups() (*metav1.APIGroupList, error) {
groups, _, _, err := d.GroupsAndMaybeResources()
return d.ServerGroupsWithContext(context.Background())
}
func (d *memCacheClient) ServerGroupsWithContext(ctx context.Context) (*metav1.APIGroupList, error) {
groups, _, _, err := d.GroupsAndMaybeResourcesWithContext(ctx)
if err != nil {
return nil, err
}
@@ -178,35 +221,81 @@ func (d *memCacheClient) RESTClient() restclient.Interface {
return d.delegate.RESTClient()
}
// ServerPreferredResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredResourcesWithContext instead.
func (d *memCacheClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
return discovery.ServerPreferredResources(d)
return d.ServerPreferredResourcesWithContext(context.Background())
}
func (d *memCacheClient) ServerPreferredResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return discovery.ServerPreferredResourcesWithContext(ctx, d)
}
// ServerPreferredNamespacedResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredNamespacedResourcesWithContext instead.
func (d *memCacheClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
return discovery.ServerPreferredNamespacedResources(d)
return d.ServerPreferredNamespacedResourcesWithContext(context.Background())
}
func (d *memCacheClient) ServerPreferredNamespacedResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return discovery.ServerPreferredNamespacedResourcesWithContext(ctx, d)
}
// ServerVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerVersionWithContext instead.
func (d *memCacheClient) ServerVersion() (*version.Info, error) {
return d.delegate.ServerVersion()
return d.ServerVersionWithContext(context.Background())
}
func (d *memCacheClient) ServerVersionWithContext(ctx context.Context) (*version.Info, error) {
return d.delegate.ServerVersionWithContext(ctx)
}
// OpenAPISchemaWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPISchemaWithContext instead.
func (d *memCacheClient) OpenAPISchema() (*openapi_v2.Document, error) {
return d.delegate.OpenAPISchema()
return d.OpenAPISchemaWithContext(context.Background())
}
func (d *memCacheClient) OpenAPISchemaWithContext(ctx context.Context) (*openapi_v2.Document, error) {
return d.delegate.OpenAPISchemaWithContext(ctx)
}
// OpenAPIV3WithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPIV3WithContext instead.
func (d *memCacheClient) OpenAPIV3() openapi.Client {
return d.openAPIV3(context.Background())
}
func (d *memCacheClient) OpenAPIV3WithContext(ctx context.Context) openapi.ClientWithContext {
return d.openAPIV3(ctx)
}
func (d *memCacheClient) openAPIV3(ctx context.Context) *cachedopenapi.Client {
// Must take lock since Invalidate call may modify openapiClient
d.lock.Lock()
defer d.lock.Unlock()
if d.openapiClient == nil {
d.openapiClient = cachedopenapi.NewClient(d.delegate.OpenAPIV3())
d.openapiClient = cachedopenapi.NewClientWithContext(d.delegate.OpenAPIV3WithContext(ctx))
}
return d.openapiClient
}
// FreshWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use FreshWithContext instead.
func (d *memCacheClient) Fresh() bool {
return d.FreshWithContext(context.Background())
}
func (d *memCacheClient) FreshWithContext(ctx context.Context) bool {
d.lock.RLock()
defer d.lock.RUnlock()
// Return whether the cache is populated at all. It is still possible that
@@ -217,7 +306,17 @@ func (d *memCacheClient) Fresh() bool {
// Invalidate enforces that no cached data that is older than the current time
// is used.
//
// InvalidateWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use InvalidateWithContext instead.
func (d *memCacheClient) Invalidate() {
d.InvalidateWithContext(context.Background())
}
// InvalidateWithContext enforces that no cached data that is older than the current time
// is used.
func (d *memCacheClient) InvalidateWithContext(ctx context.Context) {
d.lock.Lock()
defer d.lock.Unlock()
d.cacheValid = false
@@ -225,24 +324,41 @@ func (d *memCacheClient) Invalidate() {
d.groupList = nil
d.openapiClient = nil
d.receivedAggregatedDiscovery = false
if ad, ok := d.delegate.(discovery.CachedDiscoveryInterface); ok {
ad.Invalidate()
ad, ok := d.delegate.(discovery.CachedDiscoveryInterfaceWithContext)
if !ok {
//nolint:staticcheck // Intentionally using the old interface here.
ad2, ok2 := d.delegate.(discovery.CachedDiscoveryInterface)
if ok2 {
ad = discovery.ToCachedDiscoveryInterfaceWithContext(ad2)
ok = true
}
}
if ok {
ad.InvalidateWithContext(ctx)
}
}
// refreshLocked refreshes the state of cache. The caller must hold d.lock for
// writing.
func (d *memCacheClient) refreshLocked() error {
func (d *memCacheClient) refreshLocked(ctx context.Context) error {
// TODO: Could this multiplicative set of calls be replaced by a single call
// to ServerResources? If it's possible for more than one resulting
// APIResourceList to have the same GroupVersion, the lists would need merged.
var gl *metav1.APIGroupList
var err error
if ad, ok := d.delegate.(discovery.AggregatedDiscoveryInterface); ok {
ad, ok := d.delegate.(discovery.AggregatedDiscoveryInterfaceWithContext)
if !ok {
//nolint:staticcheck // Intentionally using the old interface here.
if ad2, ok2 := d.delegate.(discovery.AggregatedDiscoveryInterface); ok2 {
ad = discovery.ToAggregatedDiscoveryInterfaceWithContext(ad2)
ok = true
}
}
if ok {
var resources map[schema.GroupVersion]*metav1.APIResourceList
var failedGVs map[schema.GroupVersion]error
gl, resources, failedGVs, err = ad.GroupsAndMaybeResources()
gl, resources, failedGVs, err = ad.GroupsAndMaybeResourcesWithContext(ctx)
if resources != nil && err == nil {
// Cache the resources.
d.groupToServerResources = map[string]*cacheEntry{}
@@ -259,10 +375,10 @@ func (d *memCacheClient) refreshLocked() error {
return nil
}
} else {
gl, err = d.delegate.ServerGroups()
gl, err = d.delegate.ServerGroupsWithContext(ctx)
}
if err != nil || len(gl.Groups) == 0 {
utilruntime.HandleError(fmt.Errorf("couldn't get current server API group list: %v", err))
utilruntime.HandleErrorWithContext(ctx, err, "Couldn't get current server API group list")
return err
}
@@ -275,16 +391,16 @@ func (d *memCacheClient) refreshLocked() error {
wg.Add(1)
go func() {
defer wg.Done()
defer utilruntime.HandleCrash()
defer utilruntime.HandleCrashWithContext(ctx)
r, err := d.serverResourcesForGroupVersion(gv)
r, err := d.serverResourcesForGroupVersion(ctx, gv)
if err != nil {
// Don't log "empty response" as an error; it is a common response for metrics.
if _, emptyErr := err.(*emptyResponseError); emptyErr {
// Log at same verbosity as disk cache.
klog.V(3).Infof("%v", err)
klog.FromContext(ctx).V(3).Info(err.Error())
} else {
utilruntime.HandleError(fmt.Errorf("couldn't get resource list for %v: %v", gv, err))
utilruntime.HandleErrorWithContext(ctx, err, "Couldn't get resource list", "groupVersion", gv)
}
}
@@ -301,8 +417,8 @@ func (d *memCacheClient) refreshLocked() error {
return nil
}
func (d *memCacheClient) serverResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
r, err := d.delegate.ServerResourcesForGroupVersion(groupVersion)
func (d *memCacheClient) serverResourcesForGroupVersion(ctx context.Context, groupVersion string) (*metav1.APIResourceList, error) {
r, err := d.delegate.ServerResourcesForGroupVersionWithContext(ctx, groupVersion)
if err != nil {
return r, err
}
@@ -314,16 +430,43 @@ func (d *memCacheClient) serverResourcesForGroupVersion(groupVersion string) (*m
// WithLegacy returns current memory-cached discovery client;
// current client does not support legacy-only discovery.
//
// WithLegacyWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use WithLegacyWithContext instead.
func (d *memCacheClient) WithLegacy() discovery.DiscoveryInterface {
return d
}
// WithLegacyWithContext returns current memory-cached discovery client;
// current client does not support legacy-only discovery.
func (d *memCacheClient) WithLegacyWithContext(ctx context.Context) discovery.DiscoveryInterfaceWithContext {
return d
}
// NewMemCacheClient creates a new CachedDiscoveryInterface which caches
// discovery information in memory and will stay up-to-date if Invalidate is
// called with regularity.
//
// NOTE: The client will NOT resort to live lookups on cache misses.
//
// NewMemCacheClientWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use NewMemCacheClientWithContext instead.
func NewMemCacheClient(delegate discovery.DiscoveryInterface) discovery.CachedDiscoveryInterface {
return newMemCacheClient(discovery.ToDiscoveryInterfaceWithContext(delegate))
}
// NewMemCacheClientWithContext creates a new CachedDiscoveryInterfaceWithContext which caches
// discovery information in memory and will stay up-to-date if Invalidate is
// called with regularity.
//
// NOTE: The client will NOT resort to live lookups on cache misses.
func NewMemCacheClientWithContext(delegate discovery.DiscoveryInterfaceWithContext) discovery.CachedDiscoveryInterfaceWithContext {
return newMemCacheClient(delegate)
}
func newMemCacheClient(delegate discovery.DiscoveryInterfaceWithContext) *memCacheClient {
return &memCacheClient{
delegate: delegate,
groupToServerResources: map[string]*cacheEntry{},

View File

@@ -17,6 +17,7 @@ limitations under the License.
package memory
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -35,10 +36,10 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/fake"
"k8s.io/client-go/openapi"
"k8s.io/client-go/rest"
testutil "k8s.io/client-go/util/testing"
"k8s.io/klog/v2/ktesting"
)
type resourceMapEntry struct {
@@ -47,7 +48,10 @@ type resourceMapEntry struct {
}
type fakeDiscovery struct {
*fake.FakeDiscovery
// Intentionally limited to discovery.DiscoveryInterface because that is all that this
// code knows about and partly overrides. *fake.FakeDiscovery would inherit
// e.g. ServerGroupsWithContext which then would have to be overridden.
discovery.DiscoveryInterface
lock sync.Mutex
groupList *metav1.APIGroupList
@@ -73,34 +77,77 @@ func (c *fakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) {
return c.groupList, c.groupListErr
}
// fakeDiscoveryWithContext is a variant of fakeDiscovery which supports only the *WithContext methods.
type fakeDiscoveryWithContext struct {
*fakeDiscovery
discovery.DiscoveryInterfaceWithContext
// Invoked at the end of each ServerGroupsWithContext.
serverGroupsWithContextPostCallback func()
}
func (c *fakeDiscoveryWithContext) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
panic("fakeDiscoveryWithContext.ServerResourcesForGroupVersion should not have been called")
}
func (c *fakeDiscoveryWithContext) ServerGroups() (*metav1.APIGroupList, error) {
panic("fakeDiscoveryWithContext.ServerGroups should not have been called")
}
func (c *fakeDiscoveryWithContext) ServerResourcesForGroupVersionWithContext(ctx context.Context, groupVersion string) (*metav1.APIResourceList, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
return c.fakeDiscovery.ServerResourcesForGroupVersion(groupVersion)
}
func (c *fakeDiscoveryWithContext) ServerGroupsWithContext(ctx context.Context) (*metav1.APIGroupList, error) {
if c.serverGroupsWithContextPostCallback != nil {
defer c.serverGroupsWithContextPostCallback()
}
if err := ctx.Err(); err != nil {
return nil, err
}
return c.fakeDiscovery.ServerGroups()
}
func TestClient(t *testing.T) {
fake := &fakeDiscovery{
groupList: &metav1.APIGroupList{
Groups: []metav1.APIGroup{{
Name: "astronomy",
Versions: []metav1.GroupVersionForDiscovery{{
GroupVersion: "astronomy/v8beta1",
Version: "v8beta1",
}},
}},
},
resourceMap: map[string]*resourceMapEntry{
"astronomy/v8beta1": {
list: &metav1.APIResourceList{
GroupVersion: "astronomy/v8beta1",
APIResources: []metav1.APIResource{{
Name: "dwarfplanets",
SingularName: "dwarfplanet",
Namespaced: true,
Kind: "DwarfPlanet",
ShortNames: []string{"dp"},
newFake := func() *fakeDiscovery {
return &fakeDiscovery{
groupList: &metav1.APIGroupList{
Groups: []metav1.APIGroup{{
Name: "astronomy",
Versions: []metav1.GroupVersionForDiscovery{{
GroupVersion: "astronomy/v8beta1",
Version: "v8beta1",
}},
}},
},
resourceMap: map[string]*resourceMapEntry{
"astronomy/v8beta1": {
list: &metav1.APIResourceList{
GroupVersion: "astronomy/v8beta1",
APIResources: []metav1.APIResource{{
Name: "dwarfplanets",
SingularName: "dwarfplanet",
Namespaced: true,
Kind: "DwarfPlanet",
ShortNames: []string{"dp"},
}},
},
},
},
},
}
}
fake := newFake()
fakeWithContext := &fakeDiscoveryWithContext{fakeDiscovery: newFake()}
c := NewMemCacheClient(fake)
t.Run("DiscoveryInterface", func(t *testing.T) { testClient(t, fake, fake) })
t.Run("DiscoveryInterfaceWithContext", func(t *testing.T) { testClient(t, fakeWithContext.fakeDiscovery, fakeWithContext) })
}
func testClient(t *testing.T, fake *fakeDiscovery, delegate discovery.DiscoveryInterface) {
c := NewMemCacheClient(delegate)
if c.Fresh() {
t.Errorf("Expected not fresh.")
}
@@ -164,6 +211,62 @@ func TestClient(t *testing.T) {
}
}
func TestRetryAfterCancellation(t *testing.T) {
fake := &fakeDiscoveryWithContext{
fakeDiscovery: &fakeDiscovery{
groupList: &metav1.APIGroupList{
Groups: []metav1.APIGroup{{
Name: "astronomy",
Versions: []metav1.GroupVersionForDiscovery{{
GroupVersion: "astronomy/v8beta1",
Version: "v8beta1",
}},
}},
},
resourceMap: map[string]*resourceMapEntry{
"astronomy/v8beta1": {
list: &metav1.APIResourceList{
GroupVersion: "astronomy/v8beta1",
APIResources: []metav1.APIResource{{
Name: "dwarfplanets",
SingularName: "dwarfplanet",
Namespaced: true,
Kind: "DwarfPlanet",
ShortNames: []string{"dp"},
}},
},
},
},
},
}
ctx := context.Background()
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
c := NewMemCacheClientWithContext(fake)
// The context gets canceled in the middle of refreshLocked.
// This is important for code path which handles per-gv errors.
fake.serverGroupsWithContextPostCallback = cancel
_, err := c.ServerResourcesForGroupVersionWithContext(cancelCtx, "astronomy/v8beta1")
if err == nil {
t.Errorf("Expected error")
}
// This retries the lookup with a fresh context.
r, err := c.ServerResourcesForGroupVersionWithContext(ctx, "astronomy/v8beta1")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if e, a := fake.resourceMap["astronomy/v8beta1"].list, r; !reflect.DeepEqual(e, a) {
t.Errorf("Expected %#v, got %#v", e, a)
}
if !c.FreshWithContext(ctx) {
t.Errorf("Expected not fresh.")
}
}
func TestServerGroupsFails(t *testing.T) {
fake := &fakeDiscovery{
groupList: &metav1.APIGroupList{
@@ -435,6 +538,8 @@ func TestOpenAPIMemCache(t *testing.T) {
continue
}
// This is the original OpenAPI client. It is not affected
// by client.Invalidate() below.
pathsAgain, err := openapiClient.Paths()
if !assert.NoError(t, err) {
continue
@@ -445,7 +550,8 @@ func TestOpenAPIMemCache(t *testing.T) {
continue
}
assert.Equal(t, reflect.ValueOf(paths).Pointer(), reflect.ValueOf(pathsAgain).Pointer())
// The map itself might be different, but the content should not be.
assert.Equal(t, reflect.ValueOf(paths[k]).Pointer(), reflect.ValueOf(pathsAgain[k]).Pointer())
assert.Equal(t, reflect.ValueOf(original).Pointer(), reflect.ValueOf(schemaAgain).Pointer())
// Invalidate and try again. This time pointers should not be equal
@@ -461,6 +567,76 @@ func TestOpenAPIMemCache(t *testing.T) {
continue
}
assert.NotEqual(t, reflect.ValueOf(paths[k]).Pointer(), reflect.ValueOf(pathsAgain[k]).Pointer())
assert.NotEqual(t, reflect.ValueOf(original).Pointer(), reflect.ValueOf(schemaAgain).Pointer())
assert.Equal(t, original, schemaAgain)
}
})
}
}
// Tests that schema instances returned by openapi are cached and returned after
// successive calls.
func TestOpenAPIMemCacheWithContext(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
fakeServer, err := testutil.NewFakeOpenAPIV3Server("../../testdata")
require.NoError(t, err)
defer fakeServer.HttpServer.Close()
require.NotEmpty(t, fakeServer.ServedDocuments)
client := NewMemCacheClientWithContext(
discovery.NewDiscoveryClientForConfigOrDie(
&rest.Config{Host: fakeServer.HttpServer.URL},
),
)
openapiClient := client.OpenAPIV3WithContext(ctx)
paths, err := openapiClient.PathsWithContext(ctx)
require.NoError(t, err)
contentTypes := []string{
runtime.ContentTypeJSON, openapi.ContentTypeOpenAPIV3PB,
}
for _, contentType := range contentTypes {
t.Run(contentType, func(t *testing.T) {
//nolint:testifylint // "for error assertions use require" is a false positive here, we abort the iteration on failures.
for k, v := range paths {
original, err := v.SchemaWithContext(ctx, contentType)
if !assert.NoError(t, err) {
continue
}
// This is the original OpenAPI client. It is not affected
// by client.Invalidate() below.
pathsAgain, err := openapiClient.PathsWithContext(ctx)
if !assert.NoError(t, err) {
continue
}
schemaAgain, err := pathsAgain[k].SchemaWithContext(ctx, contentType)
if !assert.NoError(t, err) {
continue
}
// When using the *WithContext API, the map itself is cached.
assert.Equal(t, reflect.ValueOf(paths).Pointer(), reflect.ValueOf(pathsAgain).Pointer())
assert.Equal(t, reflect.ValueOf(original).Pointer(), reflect.ValueOf(schemaAgain).Pointer())
// Invalidate and try again. This time pointers should not be equal
client.InvalidateWithContext(ctx)
pathsAgain, err = client.OpenAPIV3WithContext(ctx).PathsWithContext(ctx)
if !assert.NoError(t, err) {
continue
}
schemaAgain, err = pathsAgain[k].SchemaWithContext(ctx, contentType)
if !assert.NoError(t, err) {
continue
}
assert.NotEqual(t, reflect.ValueOf(paths).Pointer(), reflect.ValueOf(pathsAgain).Pointer())
assert.NotEqual(t, reflect.ValueOf(original).Pointer(), reflect.ValueOf(schemaAgain).Pointer())
assert.Equal(t, original, schemaAgain)

View File

@@ -73,6 +73,10 @@ var v2GVK = schema.GroupVersionKind{Group: "apidiscovery.k8s.io", Version: "v2",
// DiscoveryInterface holds the methods that discover server-supported API groups,
// versions and resources.
//
// DiscoveryInterfaceWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use DiscoveryInterfaceWithContext instead.
type DiscoveryInterface interface {
RESTClient() restclient.Interface
ServerGroupsInterface
@@ -83,22 +87,123 @@ type DiscoveryInterface interface {
// 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.
//
// DiscoveryInterfaceWithContext.WithLegacyWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use DiscoveryInterfaceWithContext.WithLegacyWithContext instead.
WithLegacy() DiscoveryInterface
}
// DiscoveryInterfaceWithContext holds the methods that discover server-supported API groups,
// versions and resources.
type DiscoveryInterfaceWithContext interface {
RESTClient() restclient.Interface
ServerGroupsInterfaceWithContext
ServerResourcesInterfaceWithContext
ServerVersionInterfaceWithContext
OpenAPISchemaInterfaceWithContext
OpenAPIV3SchemaInterfaceWithContext
// 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.
WithLegacyWithContext(ctx context.Context) DiscoveryInterfaceWithContext
}
// DiscoveryInterfaces combines both interface variants.
type DiscoveryInterfaces interface {
DiscoveryInterface
DiscoveryInterfaceWithContext
}
func ToDiscoveryInterfaceWithContext(i DiscoveryInterface) DiscoveryInterfaceWithContext {
if i == nil {
return nil
}
if i, ok := i.(DiscoveryInterfaceWithContext); ok {
return i
}
return &discoveryInterfaceWrapper{
ServerGroupsInterfaceWithContext: ToServerGroupsInterfaceWithContext(i),
ServerResourcesInterfaceWithContext: ToServerResourcesInterfaceWithContext(i),
ServerVersionInterfaceWithContext: ToServerVersionInterfaceWithContext(i),
OpenAPISchemaInterfaceWithContext: ToOpenAPISchemaInterfaceWithContext(i),
OpenAPIV3SchemaInterfaceWithContext: OpenAPIV3ToSchemaInterfaceWithContext(i),
delegate: i,
}
}
type discoveryInterfaceWrapper struct {
ServerGroupsInterfaceWithContext
ServerResourcesInterfaceWithContext
ServerVersionInterfaceWithContext
OpenAPISchemaInterfaceWithContext
OpenAPIV3SchemaInterfaceWithContext
delegate DiscoveryInterface
}
func (i *discoveryInterfaceWrapper) RESTClient() restclient.Interface {
return i.delegate.RESTClient()
}
func (i *discoveryInterfaceWrapper) WithLegacyWithContext(ctx context.Context) DiscoveryInterfaceWithContext {
return ToDiscoveryInterfaceWithContext(i.delegate.WithLegacy())
}
// 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).
//
// AggregatedDiscoveryInterfaceWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use AggregatedDiscoveryInterfaceWithContext instead.
type AggregatedDiscoveryInterface interface {
DiscoveryInterface
// AggregatedDiscoveryInterfaceWithContext.GroupsAndMaybeResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use AggregatedDiscoveryInterfaceWithContext.GroupsAndMaybeResourcesWithContext instead.
GroupsAndMaybeResources() (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error)
}
// AggregatedDiscoveryInterfaceWithContext 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 AggregatedDiscoveryInterfaceWithContext interface {
DiscoveryInterfaceWithContext
GroupsAndMaybeResourcesWithContext(ctx context.Context) (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error)
}
func ToAggregatedDiscoveryInterfaceWithContext(i AggregatedDiscoveryInterface) AggregatedDiscoveryInterfaceWithContext {
if i == nil {
return nil
}
if i, ok := i.(AggregatedDiscoveryInterfaceWithContext); ok {
return i
}
return &aggregatedDiscoveryInterfaceWrapper{
DiscoveryInterfaceWithContext: ToDiscoveryInterfaceWithContext(i),
delegate: i,
}
}
type aggregatedDiscoveryInterfaceWrapper struct {
DiscoveryInterfaceWithContext
delegate AggregatedDiscoveryInterface
}
func (i *aggregatedDiscoveryInterfaceWrapper) GroupsAndMaybeResourcesWithContext(ctx context.Context) (*metav1.APIGroupList, map[schema.GroupVersion]*metav1.APIResourceList, map[schema.GroupVersion]error, error) {
return i.delegate.GroupsAndMaybeResources()
}
// 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.
//
// CachedDiscoveryInterfaceWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use CachedDiscoveryInterfaceWithContext instead.
type CachedDiscoveryInterface interface {
DiscoveryInterface
// Fresh is supposed to tell the caller whether or not to retry if the cache
@@ -106,58 +211,343 @@ type CachedDiscoveryInterface interface {
//
// TODO: this needs to be revisited, this interface can't be locked properly
// and doesn't make a lot of sense.
//
// CachedDiscoveryInterfaceWithContext.FreshWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use CachedDiscoveryInterfaceWithContext.FreshWithContext instead.
Fresh() bool
// Invalidate enforces that no cached data that is older than the current time
// is used.
//
// CachedDiscoveryInterfaceWithContext.InvalidateWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use CachedDiscoveryInterfaceWithContext.InvalidateWithContext instead.
Invalidate()
}
// CachedDiscoveryInterfaceWithContext is a DiscoveryInterfaceWithContext 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 CachedDiscoveryInterfaceWithContext interface {
DiscoveryInterfaceWithContext
// 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.
FreshWithContext(ctx context.Context) bool
// Invalidate enforces that no cached data that is older than the current time
// is used.
InvalidateWithContext(ctx context.Context)
}
func ToCachedDiscoveryInterfaceWithContext(i CachedDiscoveryInterface) CachedDiscoveryInterfaceWithContext {
if i == nil {
return nil
}
if i, ok := i.(CachedDiscoveryInterfaceWithContext); ok {
return i
}
return &cachedDiscoveryInterfaceWrapper{
DiscoveryInterfaceWithContext: ToDiscoveryInterfaceWithContext(i),
delegate: i,
}
}
type cachedDiscoveryInterfaceWrapper struct {
DiscoveryInterfaceWithContext
delegate CachedDiscoveryInterface
}
func (i *cachedDiscoveryInterfaceWrapper) FreshWithContext(ctx context.Context) bool {
return i.delegate.Fresh()
}
func (i *cachedDiscoveryInterfaceWrapper) InvalidateWithContext(ctx context.Context) {
i.delegate.Invalidate()
}
// ServerGroupsInterface has methods for obtaining supported groups on the API server
//
// ServerGroupsInterfaceWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsInterfaceWithContext instead.
type ServerGroupsInterface interface {
// ServerGroups returns the supported groups, with information like supported versions and the
// preferred version.
//
// ServerGroupsInterfaceWithContext.ServerGroups is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsInterfaceWithContext.ServerGroups instead.
ServerGroups() (*metav1.APIGroupList, error)
}
// ServerGroupsInterfaceWithContext has methods for obtaining supported groups on the API server
type ServerGroupsInterfaceWithContext interface {
// ServerGroups returns the supported groups, with information like supported versions and the
// preferred version.
ServerGroupsWithContext(ctx context.Context) (*metav1.APIGroupList, error)
}
func ToServerGroupsInterfaceWithContext(i ServerGroupsInterface) ServerGroupsInterfaceWithContext {
if i == nil {
return nil
}
if i, ok := i.(ServerGroupsInterfaceWithContext); ok {
return i
}
return &serverGroupsInterfaceWrapper{
delegate: i,
}
}
type serverGroupsInterfaceWrapper struct {
delegate ServerGroupsInterface
}
func (i *serverGroupsInterfaceWrapper) ServerGroupsWithContext(ctx context.Context) (*metav1.APIGroupList, error) {
return i.delegate.ServerGroups()
}
// ServerResourcesInterface has methods for obtaining supported resources on the API server
//
// ServerGroupsAndResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsAndResourcesWithContext instead.
type ServerResourcesInterface interface {
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
//
// ServerGroupsAndResourcesWithContext.ServerResourcesForGroupVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsAndResourcesWithContext.ServerResourcesForGroupVersionWithContext instead.
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.
//
// ServerGroupsAndResourcesWithContext.ServerGroupsAndResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsAndResourcesWithContext.ServerGroupsAndResourcesWithContext instead.
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.
//
// ServerResourcesInterfaceWithContext.ServerPreferredResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerResourcesInterfaceWithContext.ServerPreferredResourcesWithContext instead.
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.
//
// ServerResourcesInterfaceWithContext.ServerPreferredNamespacedResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerResourcesInterfaceWithContext.ServerPreferredNamespacedResourcesWithContext instead.
ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error)
}
// ServerResourcesInterfaceWithContext has methods for obtaining supported resources on the API server
type ServerResourcesInterfaceWithContext interface {
// ServerResourcesForGroupVersionWithContext returns the supported resources for a group and version.
ServerResourcesForGroupVersionWithContext(ctx context.Context, groupVersion string) (*metav1.APIResourceList, error)
// ServerGroupsAndResourcesWithContext 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.
ServerGroupsAndResourcesWithContext(ctx context.Context) ([]*metav1.APIGroup, []*metav1.APIResourceList, error)
// ServerPreferredResourcesWithContext 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.
ServerPreferredResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error)
// ServerPreferredNamespacedResourcesWithContext 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.
ServerPreferredNamespacedResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error)
}
func ToServerResourcesInterfaceWithContext(i ServerResourcesInterface) ServerResourcesInterfaceWithContext {
if i == nil {
return nil
}
if i, ok := i.(ServerResourcesInterfaceWithContext); ok {
return i
}
return &serverResourcesInterfaceWrapper{
delegate: i,
}
}
type serverResourcesInterfaceWrapper struct {
delegate ServerResourcesInterface
}
func (i *serverResourcesInterfaceWrapper) ServerResourcesForGroupVersionWithContext(ctx context.Context, groupVersion string) (*metav1.APIResourceList, error) {
return i.delegate.ServerResourcesForGroupVersion(groupVersion)
}
func (i *serverResourcesInterfaceWrapper) ServerGroupsAndResourcesWithContext(ctx context.Context) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return i.delegate.ServerGroupsAndResources()
}
func (i *serverResourcesInterfaceWrapper) ServerPreferredResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return i.delegate.ServerPreferredResources()
}
func (i *serverResourcesInterfaceWrapper) ServerPreferredNamespacedResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return i.delegate.ServerPreferredNamespacedResources()
}
// ServerVersionInterface has a method for retrieving the server's version.
//
// ServerVersionInterfaceWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerVersionInterfaceWithContext instead.
type ServerVersionInterface interface {
// ServerVersion retrieves and parses the server's version (git version).
//
// ServerVersionInterfaceWithContext.ServerVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerVersionInterfaceWithContext.ServerVersionWithContext instead.
ServerVersion() (*version.Info, error)
}
// ServerVersionInterface has a method for retrieving the server's version.
type ServerVersionInterfaceWithContext interface {
// ServerVersion retrieves and parses the server's version (git version).
ServerVersionWithContext(ctx context.Context) (*version.Info, error)
}
func ToServerVersionInterfaceWithContext(i ServerVersionInterface) ServerVersionInterfaceWithContext {
if i == nil {
return nil
}
if i, ok := i.(ServerVersionInterfaceWithContext); ok {
return i
}
return &serverVersionInterfaceWrapper{
delegate: i,
}
}
type serverVersionInterfaceWrapper struct {
delegate ServerVersionInterface
}
func (i *serverVersionInterfaceWrapper) ServerVersionWithContext(ctx context.Context) (*version.Info, error) {
return i.delegate.ServerVersion()
}
// OpenAPISchemaInterface has a method to retrieve the open API schema.
//
// OpenAPISchemaInterfaceWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPISchemaInterfaceWithContext instead.
type OpenAPISchemaInterface interface {
// OpenAPISchema retrieves and parses the swagger API schema the server supports.
//
// OpenAPISchemaInterfaceWithContext.OpenAPISchemaWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPISchemaInterfaceWithContext.OpenAPISchemaWithContext instead.
OpenAPISchema() (*openapi_v2.Document, error)
}
// OpenAPISchemaInterfaceWithContext has a method to retrieve the open API schema.
type OpenAPISchemaInterfaceWithContext interface {
// OpenAPISchemaWithContext retrieves and parses the swagger API schema the server supports.
OpenAPISchemaWithContext(ctx context.Context) (*openapi_v2.Document, error)
}
func ToOpenAPISchemaInterfaceWithContext(i OpenAPISchemaInterface) OpenAPISchemaInterfaceWithContext {
if i == nil {
return nil
}
if i, ok := i.(OpenAPISchemaInterfaceWithContext); ok {
return i
}
return &openAPISchemaInterfaceWrapper{
delegate: i,
}
}
type openAPISchemaInterfaceWrapper struct {
delegate OpenAPISchemaInterface
}
func (i *openAPISchemaInterfaceWrapper) OpenAPISchemaWithContext(ctx context.Context) (*openapi_v2.Document, error) {
return i.delegate.OpenAPISchema()
}
// OpenAPIV3SchemaInterfaceWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPIV3SchemaInterfaceWithContext instead.
type OpenAPIV3SchemaInterface interface {
// OpenAPIV3SchemaInterfaceWithContext.OpenAPIV3WithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPIV3SchemaInterfaceWithContext.OpenAPIV3WithContext instead.
OpenAPIV3() openapi.Client
}
type OpenAPIV3SchemaInterfaceWithContext interface {
OpenAPIV3WithContext(ctx context.Context) openapi.ClientWithContext
}
func OpenAPIV3ToSchemaInterfaceWithContext(i OpenAPIV3SchemaInterface) OpenAPIV3SchemaInterfaceWithContext {
if i == nil {
return nil
}
if i, ok := i.(OpenAPIV3SchemaInterfaceWithContext); ok {
return i
}
return &openAPIV3SchemaInterfaceWrapper{
delegate: i,
}
}
type openAPIV3SchemaInterfaceWrapper struct {
delegate OpenAPIV3SchemaInterface
}
func (i *openAPIV3SchemaInterfaceWrapper) OpenAPIV3WithContext(ctx context.Context) openapi.ClientWithContext {
return ToOpenAPIClientWithContext(i.delegate.OpenAPIV3())
}
func ToOpenAPIClientWithContext(i openapi.Client) openapi.ClientWithContext { //nolint:staticcheck // Intentionally using the old interface here.
if i == nil {
return nil
}
if i, ok := i.(openapi.ClientWithContext); ok {
return i
}
return &openAPIClientWrapper{
delegate: i,
}
}
type openAPIClientWrapper struct {
//nolint:staticcheck // Intentionally using the old interface here.
delegate openapi.Client
}
func (i *openAPIClientWrapper) PathsWithContext(ctx context.Context) (map[string]openapi.GroupVersionWithContext, error) {
resultWithoutContext, err := i.delegate.Paths()
result := make(map[string]openapi.GroupVersionWithContext, len(resultWithoutContext))
for key, entry := range resultWithoutContext {
result[key] = openapi.ToGroupVersionWithContext(entry)
}
return result, err
}
// DiscoveryClient implements the functions that discover server-supported API groups,
// versions and resources.
type DiscoveryClient struct {
@@ -199,19 +589,42 @@ func apiVersionsToAPIGroup(apiVersions *metav1.APIVersions) (apiGroup metav1.API
// 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.
//
// GroupsAndMaybeResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use GroupsAndMaybeResourcesWithContext instead.
func (d *DiscoveryClient) GroupsAndMaybeResources() (
*metav1.APIGroupList,
map[schema.GroupVersion]*metav1.APIResourceList,
map[schema.GroupVersion]error,
error) {
return d.GroupsAndMaybeResourcesWithContext(context.Background())
}
// 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) GroupsAndMaybeResourcesWithContext(ctx context.Context) (
*metav1.APIGroupList,
map[schema.GroupVersion]*metav1.APIResourceList,
map[schema.GroupVersion]error,
error) {
if err := ctx.Err(); err != nil {
return nil, nil, nil, err
}
// 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()
groups, resources, failedGVs, err := d.downloadLegacy(ctx)
if err != nil {
return nil, nil, nil, err
}
// Discovery groups and (possibly) resources downloaded from /apis.
apiGroups, apiResources, failedApisGVs, aerr := d.downloadAPIs()
apiGroups, apiResources, failedApisGVs, aerr := d.downloadAPIs(ctx)
if aerr != nil {
return nil, nil, nil, aerr
}
@@ -239,7 +652,7 @@ func (d *DiscoveryClient) GroupsAndMaybeResources() (
// 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() (
func (d *DiscoveryClient) downloadLegacy(ctx context.Context) (
*metav1.APIGroupList,
map[schema.GroupVersion]*metav1.APIResourceList,
map[schema.GroupVersion]error,
@@ -249,7 +662,7 @@ func (d *DiscoveryClient) downloadLegacy() (
body, err := d.restClient.Get().
AbsPath("/api").
SetHeader("Accept", accept).
Do(context.TODO()).
Do(ctx).
ContentType(&responseContentType).
Raw()
apiGroupList := &metav1.APIGroupList{}
@@ -295,7 +708,7 @@ func (d *DiscoveryClient) downloadLegacy() (
// 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() (
func (d *DiscoveryClient) downloadAPIs(ctx context.Context) (
*metav1.APIGroupList,
map[schema.GroupVersion]*metav1.APIResourceList,
map[schema.GroupVersion]error,
@@ -305,7 +718,7 @@ func (d *DiscoveryClient) downloadAPIs() (
body, err := d.restClient.Get().
AbsPath("/apis").
SetHeader("Accept", accept).
Do(context.TODO()).
Do(ctx).
ContentType(&responseContentType).
Raw()
if err != nil {
@@ -371,8 +784,18 @@ func ContentTypeIsGVK(contentType string, gvk schema.GroupVersionKind) (bool, er
// ServerGroups returns the supported groups, with information like supported versions and the
// preferred version.
//
// ServerGroupsWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsWithContext instead.
func (d *DiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
groups, _, _, err := d.GroupsAndMaybeResources()
return d.ServerGroupsWithContext(context.Background())
}
// ServerGroupsWithContext returns the supported groups, with information like supported versions and the
// preferred version.
func (d *DiscoveryClient) ServerGroupsWithContext(ctx context.Context) (*metav1.APIGroupList, error) {
groups, _, _, err := d.GroupsAndMaybeResourcesWithContext(ctx)
if err != nil {
return nil, err
}
@@ -380,7 +803,16 @@ func (d *DiscoveryClient) ServerGroups() (*metav1.APIGroupList, error) {
}
// ServerResourcesForGroupVersion returns the supported resources for a group and version.
//
// ServerResourcesForGroupVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerResourcesForGroupVersionWithContext instead.
func (d *DiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (resources *metav1.APIResourceList, err error) {
return d.ServerResourcesForGroupVersionWithContext(context.Background(), groupVersion)
}
// ServerResourcesForGroupVersionWithContext returns the supported resources for a group and version.
func (d *DiscoveryClient) ServerResourcesForGroupVersionWithContext(ctx context.Context, groupVersion string) (resources *metav1.APIResourceList, err error) {
url := url.URL{}
if len(groupVersion) == 0 {
return nil, fmt.Errorf("groupVersion shouldn't be empty")
@@ -393,7 +825,7 @@ func (d *DiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (r
resources = &metav1.APIResourceList{
GroupVersion: groupVersion,
}
err = d.restClient.Get().AbsPath(url.String()).Do(context.TODO()).Into(resources)
err = d.restClient.Get().AbsPath(url.String()).Do(ctx).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
@@ -408,8 +840,13 @@ func (d *DiscoveryClient) ServerResourcesForGroupVersion(groupVersion string) (r
// 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)
return d.ServerGroupsAndResourcesWithContext(context.Background())
}
// ServerGroupsAndResourcesWithContext returns the supported resources for all groups and versions.
func (d *DiscoveryClient) ServerGroupsAndResourcesWithContext(ctx context.Context) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return withRetries(ctx, defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return ServerGroupsAndResourcesWithContext(ctx, d)
})
}
@@ -452,7 +889,18 @@ func GroupDiscoveryFailedErrorGroups(err error) (map[schema.GroupVersion]error,
return nil, false
}
// ServerGroupsAndResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsAndResourcesWithContext instead.
func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
return ServerGroupsAndResourcesWithContext(context.Background(), ToDiscoveryInterfaceWithContext(d))
}
func ServerGroupsAndResourcesWithContext(ctx context.Context, d DiscoveryInterfaceWithContext) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
var sgs *metav1.APIGroupList
var resources []*metav1.APIResourceList
var failedGVs map[schema.GroupVersion]error
@@ -460,14 +908,21 @@ func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*meta
// 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 {
ad, ok := d.(AggregatedDiscoveryInterfaceWithContext)
if !ok {
if ad2, ok2 := d.(AggregatedDiscoveryInterface); ok2 {
ad = ToAggregatedDiscoveryInterfaceWithContext(ad2)
ok = true
}
}
if ok {
var resourcesByGV map[schema.GroupVersion]*metav1.APIResourceList
sgs, resourcesByGV, failedGVs, err = ad.GroupsAndMaybeResources()
sgs, resourcesByGV, failedGVs, err = ad.GroupsAndMaybeResourcesWithContext(ctx)
for _, resourceList := range resourcesByGV {
resources = append(resources, resourceList)
}
} else {
sgs, err = d.ServerGroups()
sgs, err = d.ServerGroupsWithContext(ctx)
}
if sgs == nil {
@@ -488,7 +943,7 @@ func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*meta
return resultGroups, resources, ferr
}
groupVersionResources, failedGroups := fetchGroupVersionResources(d, sgs)
groupVersionResources, failedGroups := fetchGroupVersionResources(ctx, d, sgs)
// order results by group/version discovery order
result := []*metav1.APIResourceList{}
@@ -509,7 +964,20 @@ func ServerGroupsAndResources(d DiscoveryInterface) ([]*metav1.APIGroup, []*meta
}
// ServerPreferredResources uses the provided discovery interface to look up preferred resources
//
// ServerPreferredResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredResourcesWithContext instead.
func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
return ServerPreferredResourcesWithContext(context.Background(), ToDiscoveryInterfaceWithContext(d))
}
// ServerPreferredResourcesWithContext uses the provided discovery interface to look up preferred resources
func ServerPreferredResourcesWithContext(ctx context.Context, d DiscoveryInterfaceWithContext) ([]*metav1.APIResourceList, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
var serverGroupList *metav1.APIGroupList
var failedGroups map[schema.GroupVersion]error
var groupVersionResources map[schema.GroupVersion]*metav1.APIResourceList
@@ -518,18 +986,24 @@ func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList,
// 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)
ad, ok := d.(AggregatedDiscoveryInterfaceWithContext)
if !ok {
if ad2, ok2 := d.(AggregatedDiscoveryInterface); ok2 {
ad = ToAggregatedDiscoveryInterfaceWithContext(ad2)
ok = true
}
}
if ok {
serverGroupList, groupVersionResources, failedGroups, err = ad.GroupsAndMaybeResources()
serverGroupList, groupVersionResources, failedGroups, err = ad.GroupsAndMaybeResourcesWithContext(ctx)
} else {
serverGroupList, err = d.ServerGroups()
serverGroupList, err = d.ServerGroupsWithContext(ctx)
}
if err != nil {
return nil, err
}
// Non-aggregated discovery must fetch resources from Groups.
if groupVersionResources == nil {
groupVersionResources, failedGroups = fetchGroupVersionResources(d, serverGroupList)
groupVersionResources, failedGroups = fetchGroupVersionResources(ctx, d, serverGroupList)
}
result := []*metav1.APIResourceList{}
@@ -585,7 +1059,7 @@ func ServerPreferredResources(d DiscoveryInterface) ([]*metav1.APIResourceList,
}
// 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) {
func fetchGroupVersionResources(ctx context.Context, d DiscoveryInterfaceWithContext, 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)
@@ -597,9 +1071,9 @@ func fetchGroupVersionResources(d DiscoveryInterface, apiGroups *metav1.APIGroup
wg.Add(1)
go func() {
defer wg.Done()
defer utilruntime.HandleCrash()
defer utilruntime.HandleCrashWithContext(ctx)
apiResourceList, err := d.ServerResourcesForGroupVersion(groupVersion.String())
apiResourceList, err := d.ServerResourcesForGroupVersionWithContext(ctx, groupVersion.String())
// lock to record results
resultLock.Lock()
@@ -623,23 +1097,56 @@ func fetchGroupVersionResources(d DiscoveryInterface, apiGroups *metav1.APIGroup
// ServerPreferredResources returns the supported resources with the version preferred by the
// server.
//
// ServerPreferredResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredResourcesWithContext instead.
func (d *DiscoveryClient) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
_, rs, err := withRetries(defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
_, rs, err := withRetries(context.Background(), defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
rs, err := ServerPreferredResources(d)
return nil, rs, err
})
return rs, err
}
// ServerPreferredResourcesWithContext returns the supported resources with the version preferred by the
// server.
func (d *DiscoveryClient) ServerPreferredResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
_, rs, err := withRetries(ctx, defaultRetries, func() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
rs, err := ServerPreferredResourcesWithContext(ctx, d)
return nil, rs, err
})
return rs, err
}
// ServerPreferredNamespacedResources returns the supported namespaced resources with the
// version preferred by the server.
//
// ServerPreferredNamespacedResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredNamespacedResourcesWithContext instead.
func (d *DiscoveryClient) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
return ServerPreferredNamespacedResources(d)
}
// ServerPreferredNamespacedResources returns the supported namespaced resources with the
// version preferred by the server.
func (d *DiscoveryClient) ServerPreferredNamespacedResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return ServerPreferredNamespacedResourcesWithContext(ctx, d)
}
// ServerPreferredNamespacedResources uses the provided discovery interface to look up preferred namespaced resources
//
// ServerPreferredNamespacedResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredNamespacedResourcesWithContext instead.
func ServerPreferredNamespacedResources(d DiscoveryInterface) ([]*metav1.APIResourceList, error) {
all, err := ServerPreferredResources(d)
return ServerPreferredNamespacedResourcesWithContext(context.Background(), ToDiscoveryInterfaceWithContext(d))
}
// ServerPreferredNamespacedResourcesWithContext uses the provided discovery interface to look up preferred namespaced resources
func ServerPreferredNamespacedResourcesWithContext(ctx context.Context, d DiscoveryInterfaceWithContext) ([]*metav1.APIResourceList, error) {
all, err := ServerPreferredResourcesWithContext(ctx, d)
return FilteredBy(ResourcePredicateFunc(func(groupVersion string, r *metav1.APIResource) bool {
return r.Namespaced
}), all), err
@@ -647,7 +1154,12 @@ func ServerPreferredNamespacedResources(d DiscoveryInterface) ([]*metav1.APIReso
// 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()
return d.ServerVersionWithContext(context.Background())
}
// ServerVersionWithContext retrieves and parses the server's version (git version).
func (d *DiscoveryClient) ServerVersionWithContext(ctx context.Context) (*version.Info, error) {
body, err := d.restClient.Get().AbsPath("/version").Do(ctx).Raw()
if err != nil {
return nil, err
}
@@ -661,7 +1173,12 @@ func (d *DiscoveryClient) ServerVersion() (*version.Info, error) {
// 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()
return d.OpenAPISchemaWithContext(context.Background())
}
// OpenAPISchemaWithContext fetches the open api v2 schema using a rest client and parses the proto.
func (d *DiscoveryClient) OpenAPISchemaWithContext(ctx context.Context) (*openapi_v2.Document, error) {
data, err := d.restClient.Get().AbsPath("/openapi/v2").SetHeader("Accept", openAPIV2mimePb).Do(ctx).Raw()
if err != nil {
return nil, err
}
@@ -673,20 +1190,40 @@ func (d *DiscoveryClient) OpenAPISchema() (*openapi_v2.Document, error) {
return document, nil
}
// OpenAPIV3WithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPIV3WithContext instead.
func (d *DiscoveryClient) OpenAPIV3() openapi.Client {
return openapi.NewClient(d.restClient)
}
func (d *DiscoveryClient) OpenAPIV3WithContext(ctx context.Context) openapi.ClientWithContext {
return openapi.NewClientWithContext(d.restClient)
}
// WithLegacy returns copy of current discovery client that will only
// receive the legacy discovery format.
//
// WithLegacyWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use WithLegacyWithContext instead.
func (d *DiscoveryClient) WithLegacy() DiscoveryInterface {
client := *d
client.UseLegacyDiscovery = true
return &client
}
// WithLegacyWithContext returns copy of current discovery client that will only
// receive the legacy discovery format.
func (d *DiscoveryClient) WithLegacyWithContext(ctx context.Context) DiscoveryInterfaceWithContext {
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) {
// It retries only if the context isn't canceled.
func withRetries(ctx context.Context, 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
@@ -698,6 +1235,10 @@ func withRetries(maxRetries int, f func() ([]*metav1.APIGroup, []*metav1.APIReso
if _, ok := err.(*ErrGroupDiscoveryFailed); !ok {
return nil, nil, err
}
if ctx.Err() != nil {
// Give up retrying because of context cancellation and propagate the error returned by f.
return nil, nil, err
}
}
return resultGroups, result, err
}

View File

@@ -17,6 +17,7 @@ limitations under the License.
package fake
import (
"context"
"fmt"
"net/http"
@@ -33,16 +34,33 @@ import (
"k8s.io/client-go/testing"
)
// FakeDiscovery implements discovery.DiscoveryInterface and sometimes calls testing.Fake.Invoke with an action,
// FakeDiscovery implements discovery.DiscoveryInterface and discovery.DiscoveryInterfaceWithContext.
// It sometimes calls testing.Fake.Invoke with an action,
// but doesn't respect the return value if any. There is a way to fake static values like ServerVersion by using the Faked... fields on the struct.
type FakeDiscovery struct {
*testing.Fake
FakedServerVersion *version.Info
}
var (
//nolint:staticcheck // Intentionally using the old interface here.
_ discovery.DiscoveryInterface = &FakeDiscovery{}
_ discovery.DiscoveryInterfaceWithContext = &FakeDiscovery{}
)
// ServerResourcesForGroupVersion returns the supported resources for a group
// and version.
//
// ServerResourcesForGroupVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerResourcesForGroupVersionWithContext instead.
func (c *FakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) {
return c.ServerResourcesForGroupVersionWithContext(context.Background(), groupVersion)
}
// ServerResourcesForGroupVersionWithContext returns the supported resources for a group
// and version.
func (c *FakeDiscovery) ServerResourcesForGroupVersionWithContext(ctx context.Context, groupVersion string) (*metav1.APIResourceList, error) {
action := testing.ActionImpl{
Verb: "get",
Resource: schema.GroupVersionResource{Resource: "resource"},
@@ -65,8 +83,17 @@ func (c *FakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*me
}
// ServerGroupsAndResources returns the supported groups and resources for all groups and versions.
//
// ServerGroupsAndResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsAndResourcesWithContext instead.
func (c *FakeDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
sgs, err := c.ServerGroups()
return c.ServerGroupsAndResourcesWithContext(context.Background())
}
// ServerGroupsAndResourcesWithContext returns the supported groups and resources for all groups and versions.
func (c *FakeDiscovery) ServerGroupsAndResourcesWithContext(ctx context.Context) ([]*metav1.APIGroup, []*metav1.APIResourceList, error) {
sgs, err := c.ServerGroupsWithContext(ctx)
if err != nil {
return nil, nil, err
}
@@ -87,19 +114,49 @@ func (c *FakeDiscovery) ServerGroupsAndResources() ([]*metav1.APIGroup, []*metav
// ServerPreferredResources returns the supported resources with the version
// preferred by the server.
//
// ServerPreferredResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredResourcesWithContext instead.
func (c *FakeDiscovery) ServerPreferredResources() ([]*metav1.APIResourceList, error) {
return c.ServerPreferredResourcesWithContext(context.Background())
}
// ServerPreferredResourcesWithContext returns the supported resources with the version
// preferred by the server.
func (c *FakeDiscovery) ServerPreferredResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return nil, nil
}
// ServerPreferredNamespacedResources returns the supported namespaced resources
// with the version preferred by the server.
//
// ServerPreferredNamespacedResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerPreferredNamespacedResourcesWithContext instead.
func (c *FakeDiscovery) ServerPreferredNamespacedResources() ([]*metav1.APIResourceList, error) {
return c.ServerPreferredNamespacedResourcesWithContext(context.Background())
}
// ServerPreferredNamespacedResourcesWithContext returns the supported namespaced resources
// with the version preferred by the server.
func (c *FakeDiscovery) ServerPreferredNamespacedResourcesWithContext(ctx context.Context) ([]*metav1.APIResourceList, error) {
return nil, nil
}
// ServerGroups returns the supported groups, with information like supported
// versions and the preferred version.
//
// ServerGroupsWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerGroupsWithContext instead.
func (c *FakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) {
return c.ServerGroupsWithContext(context.Background())
}
// ServerGroupsWithContext returns the supported groups, with information like supported
// versions and the preferred version.
func (c *FakeDiscovery) ServerGroupsWithContext(ctx context.Context) (*metav1.APIGroupList, error) {
action := testing.ActionImpl{
Verb: "get",
Resource: schema.GroupVersionResource{Resource: "group"},
@@ -143,7 +200,16 @@ func (c *FakeDiscovery) ServerGroups() (*metav1.APIGroupList, error) {
}
// ServerVersion retrieves and parses the server's version.
//
// ServerVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ServerVersionWithContext instead.
func (c *FakeDiscovery) ServerVersion() (*version.Info, error) {
return c.ServerVersionWithContext(context.Background())
}
// ServerVersionWithContext retrieves and parses the server's version.
func (c *FakeDiscovery) ServerVersionWithContext(ctx context.Context) (*version.Info, error) {
action := testing.ActionImpl{}
action.Verb = "get"
action.Resource = schema.GroupVersionResource{Resource: "version"}
@@ -161,20 +227,43 @@ func (c *FakeDiscovery) ServerVersion() (*version.Info, error) {
}
// OpenAPISchema retrieves and parses the swagger API schema the server supports.
//
// OpenAPISchemaWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPISchemaWithContext instead.
func (c *FakeDiscovery) OpenAPISchema() (*openapi_v2.Document, error) {
return c.OpenAPISchemaWithContext(context.Background())
}
// OpenAPISchemaWithContext retrieves and parses the swagger API schema the server supports.
func (c *FakeDiscovery) OpenAPISchemaWithContext(ctx context.Context) (*openapi_v2.Document, error) {
return &openapi_v2.Document{}, nil
}
// OpenAPIV3WithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use OpenAPIV3WithContext instead.
func (c *FakeDiscovery) OpenAPIV3() openapi.Client {
panic("unimplemented")
}
func (c *FakeDiscovery) OpenAPIV3WithContext(ctx context.Context) openapi.ClientWithContext {
panic("unimplemented")
}
// RESTClient returns a RESTClient that is used to communicate with API server
// by this client implementation.
func (c *FakeDiscovery) RESTClient() restclient.Interface {
return nil
}
// WithLegacyWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use WithLegacyWithContext instead.
func (c *FakeDiscovery) WithLegacy() discovery.DiscoveryInterface {
panic("unimplemented")
}
func (c *FakeDiscovery) WithLegacyWithContext(ctx context.Context) discovery.DiscoveryInterfaceWithContext {
panic("unimplemented")
}

2
go.mod
View File

@@ -24,7 +24,7 @@ require (
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
gopkg.in/evanphx/json-patch.v4 v4.13.0
k8s.io/api v0.0.0-20260626213116-2b6c2012d75f
k8s.io/apimachinery v0.0.0-20260626172716-6fa8dff7b19f
k8s.io/apimachinery v0.0.0-20260701082954-b16db0ad9a39
k8s.io/klog/v2 v2.140.0
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974
k8s.io/streaming v0.0.0-20260626172358-68009e8e6aa5

4
go.sum
View File

@@ -120,8 +120,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.0.0-20260626213116-2b6c2012d75f h1:WfCuhwj5sO/oWkTS3MovBWuwL7vBXPWn2ROuQf26qok=
k8s.io/api v0.0.0-20260626213116-2b6c2012d75f/go.mod h1:7NNJfcrPo0BNrOSUudMNZpLxWf/OYIRNbwS+/yAaJwg=
k8s.io/apimachinery v0.0.0-20260626172716-6fa8dff7b19f h1:WAFkshKyNvj5avlHoK0nhd0B0G+O+YUH3ntvUkUmDsE=
k8s.io/apimachinery v0.0.0-20260626172716-6fa8dff7b19f/go.mod h1:T9tvL1Yxf+TRVyTz+Q7KtLAncCr9xxxx1zrF6g/QuR0=
k8s.io/apimachinery v0.0.0-20260701082954-b16db0ad9a39 h1:2G7SZqq7fq1MXbKZEV85vdUI8Do8JzqM5dwfVDMqrWo=
k8s.io/apimachinery v0.0.0-20260701082954-b16db0ad9a39/go.mod h1:T9tvL1Yxf+TRVyTz+Q7KtLAncCr9xxxx1zrF6g/QuR0=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260618221249-bc653b64f974 h1:JVogoTvOj6gutlx8bUwGh0e8o8L4X8nDbTLyONmoVvk=

View File

@@ -81,7 +81,7 @@ import (
)
type Interface interface {
Discovery() discovery.DiscoveryInterface
Discovery() discovery.DiscoveryInterfaces
AdmissionregistrationV1() admissionregistrationv1.AdmissionregistrationV1Interface
AdmissionregistrationV1alpha1() admissionregistrationv1alpha1.AdmissionregistrationV1alpha1Interface
AdmissionregistrationV1beta1() admissionregistrationv1beta1.AdmissionregistrationV1beta1Interface
@@ -461,7 +461,7 @@ func (c *Clientset) StoragemigrationV1beta1() storagemigrationv1beta1.Storagemig
}
// Discovery retrieves the DiscoveryClient
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
func (c *Clientset) Discovery() discovery.DiscoveryInterfaces {
if c == nil {
return nil
}

View File

@@ -176,7 +176,7 @@ type Clientset struct {
tracker testing.ObjectTracker
}
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
func (c *Clientset) Discovery() discovery.DiscoveryInterfaces {
return c.discovery
}

View File

@@ -17,34 +17,71 @@ limitations under the License.
package cached
import (
"context"
"sync"
"k8s.io/client-go/openapi"
)
type client struct {
delegate openapi.Client
type Client struct {
delegate openapi.ClientWithContext
once sync.Once
result map[string]openapi.GroupVersion
result map[string]openapi.GroupVersionWithContext
err error
}
var (
//nolint:staticcheck // Intentionally using the old interface here.
_ openapi.Client = &Client{}
_ openapi.ClientWithContext = &Client{}
)
// NewClientWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use NewClientWithContext instead.
func NewClient(other openapi.Client) openapi.Client {
return &client{
return newClient(openapi.ToClientWithContext(other))
}
func NewClientWithContext(other openapi.ClientWithContext) *Client {
return newClient(other)
}
func newClient(other openapi.ClientWithContext) *Client {
return &Client{
delegate: other,
}
}
func (c *client) Paths() (map[string]openapi.GroupVersion, error) {
// PathsWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use PathsWithContext instead.
func (c *Client) Paths() (map[string]openapi.GroupVersion, error) {
// For efficiency reasons in the *WithContext case this is a map to
// openapi.GroupVersionWithContext. But we know that all entries
// also implement openapi.GroupVersion.
resultWithContext, err := c.PathsWithContext(context.Background())
result := make(map[string]openapi.GroupVersion, len(resultWithContext))
for key, entry := range resultWithContext {
result[key] = entry.(openapi.GroupVersion)
}
return result, err
}
func (c *Client) PathsWithContext(ctx context.Context) (map[string]openapi.GroupVersionWithContext, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
c.once.Do(func() {
uncached, err := c.delegate.Paths()
uncached, err := c.delegate.PathsWithContext(ctx)
if err != nil {
c.err = err
return
}
result := make(map[string]openapi.GroupVersion, len(uncached))
result := make(map[string]openapi.GroupVersionWithContext, len(uncached))
for k, v := range uncached {
result[k] = newGroupVersion(v)
}

View File

@@ -17,30 +17,48 @@ limitations under the License.
package cached
import (
"context"
"sync"
"k8s.io/client-go/openapi"
)
type groupversion struct {
delegate openapi.GroupVersion
delegate openapi.GroupVersionWithContext
lock sync.Mutex
docs map[string]docInfo
}
var (
//nolint:staticcheck // Intentionally using the old interface here.
_ openapi.GroupVersion = &groupversion{}
_ openapi.GroupVersionWithContext = &groupversion{}
)
type docInfo struct {
data []byte
err error
}
func newGroupVersion(delegate openapi.GroupVersion) *groupversion {
func newGroupVersion(delegate openapi.GroupVersionWithContext) *groupversion {
return &groupversion{
delegate: delegate,
}
}
// SchemaWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use SchemaWithContext instead.
func (g *groupversion) Schema(contentType string) ([]byte, error) {
return g.SchemaWithContext(context.Background(), contentType)
}
func (g *groupversion) SchemaWithContext(ctx context.Context, contentType string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
g.lock.Lock()
defer g.lock.Unlock()
@@ -50,7 +68,7 @@ func (g *groupversion) Schema(contentType string) ([]byte, error) {
g.docs = make(map[string]docInfo)
}
cachedInfo.data, cachedInfo.err = g.delegate.Schema(contentType)
cachedInfo.data, cachedInfo.err = g.delegate.SchemaWithContext(ctx, contentType)
g.docs[contentType] = cachedInfo
}

View File

@@ -25,25 +25,81 @@ import (
"k8s.io/kube-openapi/pkg/handler3"
)
// ClientWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ClientWithContext instead.
type Client interface {
Paths() (map[string]GroupVersion, error)
}
type ClientWithContext interface {
PathsWithContext(ctx context.Context) (map[string]GroupVersionWithContext, error)
}
func ToClientWithContext(c Client) ClientWithContext {
if c == nil {
return nil
}
if c, ok := c.(ClientWithContext); ok {
return c
}
return &clientWrapper{
delegate: c,
}
}
type clientWrapper struct {
delegate Client
}
func (c *clientWrapper) PathsWithContext(ctx context.Context) (map[string]GroupVersionWithContext, error) {
resultWithoutContext, err := c.delegate.Paths()
result := make(map[string]GroupVersionWithContext, len(resultWithoutContext))
for key, entry := range resultWithoutContext {
result[key] = ToGroupVersionWithContext(entry)
}
return result, err
}
type client struct {
// URL includes the `hash` query param to take advantage of cache busting
restClient rest.Interface
}
// NewClientWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use NewClientWithContext instead.
func NewClient(restClient rest.Interface) Client {
return newClient(restClient)
}
func NewClientWithContext(restClient rest.Interface) ClientWithContext {
return newClient(restClient)
}
func newClient(restClient rest.Interface) *client {
return &client{
restClient: restClient,
}
}
// PathsWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use PathsWithContext instead.
func (c *client) Paths() (map[string]GroupVersion, error) {
resultWithContext, err := c.PathsWithContext(context.Background())
result := make(map[string]GroupVersion, len(resultWithContext))
for key, entry := range resultWithContext {
// We know that this is a *groupversion which implements GroupVersion.
result[key] = entry.(GroupVersion)
}
return result, err
}
func (c *client) PathsWithContext(ctx context.Context) (map[string]GroupVersionWithContext, error) {
data, err := c.restClient.Get().
AbsPath("/openapi/v3").
Do(context.TODO()).
Do(ctx).
Raw()
if err != nil {
@@ -59,7 +115,7 @@ func (c *client) Paths() (map[string]GroupVersion, error) {
// Calculate the client-side prefix for a "root" request
rootPrefix := strings.TrimSuffix(c.restClient.Get().AbsPath("/").URL().Path, "/")
// Create GroupVersions for each element of the result
result := map[string]GroupVersion{}
result := make(map[string]GroupVersionWithContext, len(discoMap.Paths))
for k, v := range discoMap.Paths {
// Trim off the prefix that will always be added in client-side
v.ServerRelativeURL = strings.TrimPrefix(v.ServerRelativeURL, rootPrefix)

View File

@@ -25,6 +25,9 @@ import (
const ContentTypeOpenAPIV3PB = "application/com.github.proto-openapi.spec.v3@v1.0+protobuf"
// GroupVersionWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use GroupVersionWithContext instead.
type GroupVersion interface {
Schema(contentType string) ([]byte, error)
@@ -35,22 +38,61 @@ type GroupVersion interface {
ServerRelativeURL() string
}
type GroupVersionWithContext interface {
SchemaWithContext(ctx context.Context, contentType string) ([]byte, error)
// ServerRelativeURL. Returns the path and parameters used to fetch the schema.
// You should use the Schema method to fetch it, but this value can be used
// to key the current version of the schema in a cache since it contains a
// hash string which changes upon schema update.
ServerRelativeURL() string
}
func ToGroupVersionWithContext(gv GroupVersion) GroupVersionWithContext {
if gv == nil {
return nil
}
if gv, ok := gv.(GroupVersionWithContext); ok {
return gv
}
return &groupVersionWrapper{
GroupVersion: gv,
}
}
type groupVersionWrapper struct {
GroupVersion
}
func (gv *groupVersionWrapper) SchemaWithContext(ctx context.Context, contentType string) ([]byte, error) {
return gv.Schema(contentType)
}
type groupversion struct {
client *client
item handler3.OpenAPIV3DiscoveryGroupVersion
useClientPrefix bool
}
var (
_ GroupVersion = &groupversion{}
_ GroupVersionWithContext = &groupversion{}
)
func newGroupVersion(client *client, item handler3.OpenAPIV3DiscoveryGroupVersion, useClientPrefix bool) *groupversion {
return &groupversion{client: client, item: item, useClientPrefix: useClientPrefix}
}
func (g *groupversion) Schema(contentType string) ([]byte, error) {
return g.SchemaWithContext(context.Background(), contentType)
}
func (g *groupversion) SchemaWithContext(ctx context.Context, contentType string) ([]byte, error) {
if !g.useClientPrefix {
return g.client.restClient.Get().
RequestURI(g.item.ServerRelativeURL).
SetHeader("Accept", contentType).
Do(context.TODO()).
Do(ctx).
Raw()
}
@@ -72,7 +114,7 @@ func (g *groupversion) Schema(contentType string) ([]byte, error) {
}
}
return path.Do(context.TODO()).Raw()
return path.Do(ctx).Raw()
}
// URL used for fetching the schema. The URL includes a hash and can be used

View File

@@ -17,38 +17,97 @@ limitations under the License.
package restmapper
import (
"context"
"iter"
"slices"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery"
)
// CategoryExpander maps category strings to GroupResources.
// Categories are classification or 'tag' of a group of resources.
//
// CategoryExpanderWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use CategoryExpanderWithContext instead.
type CategoryExpander interface {
Expand(category string) ([]schema.GroupResource, bool)
}
// SimpleCategoryExpander implements CategoryExpander interface
// CategoryExpanderWithContext maps category strings to GroupResources.
// Categories are classification or 'tag' of a group of resources.
type CategoryExpanderWithContext interface {
ExpandWithContext(ctx context.Context, category string) ([]schema.GroupResource, bool)
}
func ToCategoryExpanderWithContext(c CategoryExpander) CategoryExpanderWithContext {
if c == nil {
return nil
}
if c, ok := c.(CategoryExpanderWithContext); ok {
return c
}
return &categoryExpanderWrapper{
delegate: c,
}
}
type categoryExpanderWrapper struct {
delegate CategoryExpander
}
func (c *categoryExpanderWrapper) ExpandWithContext(ctx context.Context, category string) ([]schema.GroupResource, bool) {
return c.delegate.Expand(category)
}
// SimpleCategoryExpander implements CategoryExpander and CategoryExpanderWithContext interface
// using a static mapping of categories to GroupResource mapping.
type SimpleCategoryExpander struct {
Expansions map[string][]schema.GroupResource
}
var (
_ CategoryExpander = SimpleCategoryExpander{}
_ CategoryExpanderWithContext = SimpleCategoryExpander{}
)
// Expand fulfills CategoryExpander
func (e SimpleCategoryExpander) Expand(category string) ([]schema.GroupResource, bool) {
ret, ok := e.Expansions[category]
return ret, ok
}
// ExpandWithContext fulfills CategoryExpanderWithContext
func (e SimpleCategoryExpander) ExpandWithContext(ctx context.Context, category string) ([]schema.GroupResource, bool) {
return e.Expand(category)
}
// discoveryCategoryExpander struct lets a REST Client wrapper (discoveryClient) to retrieve list of APIResourceList,
// and then convert to fallbackExpander
type discoveryCategoryExpander struct {
discoveryClient discovery.DiscoveryInterface
discoveryClient discovery.DiscoveryInterfaceWithContext
}
// NewDiscoveryCategoryExpander returns a category expander that makes use of the "categories" fields from
// the API, found through the discovery client. In case of any error or no category found (which likely
// means we're at a cluster prior to categories support, fallback to the expander provided.
//
// NewDiscoveryCategoryExpanderWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use NewDiscoveryCategoryExpanderWithContext instead.
func NewDiscoveryCategoryExpander(client discovery.DiscoveryInterface) CategoryExpander {
return newDiscoveryCategoryExpander(discovery.ToDiscoveryInterfaceWithContext(client))
}
// NewDiscoveryCategoryExpanderWithContext returns a category expander that makes use of the "categories" fields from
// the API, found through the discovery client. In case of any error or no category found (which likely
// means we're at a cluster prior to categories support, fallback to the expander provided.
func NewDiscoveryCategoryExpanderWithContext(client discovery.DiscoveryInterfaceWithContext) CategoryExpanderWithContext {
return newDiscoveryCategoryExpander(client)
}
func newDiscoveryCategoryExpander(client discovery.DiscoveryInterfaceWithContext) discoveryCategoryExpander {
if client == nil {
panic("Please provide discovery client to shortcut expander")
}
@@ -56,9 +115,18 @@ func NewDiscoveryCategoryExpander(client discovery.DiscoveryInterface) CategoryE
}
// Expand fulfills CategoryExpander
//
// ExpandWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ExpandWithContext instead.
func (e discoveryCategoryExpander) Expand(category string) ([]schema.GroupResource, bool) {
return e.ExpandWithContext(context.Background(), category)
}
// ExpandWithContext fulfills CategoryExpanderWithContext
func (e discoveryCategoryExpander) ExpandWithContext(ctx context.Context, category string) ([]schema.GroupResource, bool) {
// Get all supported resources for groups and versions from server, if no resource found, fallback anyway.
_, apiResourceLists, _ := e.discoveryClient.ServerGroupsAndResources()
_, apiResourceLists, _ := e.discoveryClient.ServerGroupsAndResourcesWithContext(ctx)
if len(apiResourceLists) == 0 {
return nil, false
}
@@ -89,16 +157,43 @@ func (e discoveryCategoryExpander) Expand(category string) ([]schema.GroupResour
// UnionCategoryExpander implements CategoryExpander interface.
// It maps given category string to union of expansions returned by all the CategoryExpanders in the list.
//
// UnionCategoryExpanderWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use UnionCategoryExpanderWithContext instead.
type UnionCategoryExpander []CategoryExpander
var _ CategoryExpander = UnionCategoryExpander{}
// Expand fulfills CategoryExpander
func (u UnionCategoryExpander) Expand(category string) ([]schema.GroupResource, bool) {
return unionExpand(context.Background(), func(yield func(i int, expander CategoryExpanderWithContext) bool) {
for i, expander := range u {
if !yield(i, ToCategoryExpanderWithContext(expander)) {
break
}
}
}, category)
}
// UnionCategoryExpanderWithContext implements CategoryExpanderWithContext interface.
// It maps given category string to union of expansions returned by all the CategoryExpanders in the list.
type UnionCategoryExpanderWithContext []CategoryExpanderWithContext
var _ CategoryExpanderWithContext = UnionCategoryExpanderWithContext{}
// ExpandWithContext fulfills CategoryExpanderWithContext
func (u UnionCategoryExpanderWithContext) ExpandWithContext(ctx context.Context, category string) ([]schema.GroupResource, bool) {
return unionExpand(ctx, slices.All(u), category)
}
func unionExpand(ctx context.Context, expanders iter.Seq2[int, CategoryExpanderWithContext], category string) ([]schema.GroupResource, bool) {
ret := []schema.GroupResource{}
ok := false
// Expand the category for each CategoryExpander in the list and merge/combine the results.
for _, expansion := range u {
curr, currOk := expansion.Expand(category)
for _, expansion := range expanders {
curr, currOk := expansion.ExpandWithContext(ctx, category)
for _, currGR := range curr {
found := false

View File

@@ -17,6 +17,7 @@ limitations under the License.
package restmapper
import (
"context"
"fmt"
"strings"
"sync"
@@ -40,8 +41,40 @@ type APIGroupResources struct {
// NewDiscoveryRESTMapper returns a PriorityRESTMapper based on the discovered
// groups and resources passed in.
//
// NewDiscoveryRESTMapperWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use NewDiscoveryRESTMapperWithContext instead.
func NewDiscoveryRESTMapper(groupResources []*APIGroupResources) meta.RESTMapper {
unionMapper := meta.MultiRESTMapper{}
mappers, resourcePriority, kindPriority := newDiscoveryRESTMapper(groupResources)
multiMapper := make(meta.MultiRESTMapper, len(mappers))
for i, m := range mappers {
multiMapper[i] = m
}
return &meta.PriorityRESTMapper{
Delegate: multiMapper,
ResourcePriority: resourcePriority,
KindPriority: kindPriority,
}
}
// NewDiscoveryRESTMapperWithContext returns a PriorityRESTMapper based on the discovered
// groups and resources passed in.
func NewDiscoveryRESTMapperWithContext(groupResources []*APIGroupResources) meta.RESTMapperWithContext {
mappers, resourcePriority, kindPriority := newDiscoveryRESTMapper(groupResources)
multiMapper := make(meta.MultiRESTMapperWithContext, len(mappers))
for i, m := range mappers {
multiMapper[i] = m
}
return &meta.PriorityRESTMapperWithContext{
Delegate: multiMapper,
ResourcePriority: resourcePriority,
KindPriority: kindPriority,
}
}
func newDiscoveryRESTMapper(groupResources []*APIGroupResources) ([]*meta.DefaultRESTMapper, []schema.GroupVersionResource, []schema.GroupVersionKind) {
var unionMapper []*meta.DefaultRESTMapper
var groupPriority []string
// /v1 is special. It should always come first
@@ -135,17 +168,23 @@ func NewDiscoveryRESTMapper(groupResources []*APIGroupResources) meta.RESTMapper
})
}
return meta.PriorityRESTMapper{
Delegate: unionMapper,
ResourcePriority: resourcePriority,
KindPriority: kindPriority,
}
return unionMapper, resourcePriority, kindPriority
}
// GetAPIGroupResources uses the provided discovery client to gather
// discovery information and populate a slice of APIGroupResources.
//
// GetAPIGroupResourcesWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use GetAPIGroupResourcesWithContext instead.
func GetAPIGroupResources(cl discovery.DiscoveryInterface) ([]*APIGroupResources, error) {
gs, rs, err := cl.ServerGroupsAndResources()
return GetAPIGroupResourcesWithContext(context.Background(), discovery.ToDiscoveryInterfaceWithContext(cl))
}
// GetAPIGroupResourcesWithContext uses the provided discovery client to gather
// discovery information and populate a slice of APIGroupResources.
func GetAPIGroupResourcesWithContext(ctx context.Context, cl discovery.DiscoveryInterfaceWithContext) ([]*APIGroupResources, error) {
gs, rs, err := cl.ServerGroupsAndResourcesWithContext(ctx)
if rs == nil || gs == nil {
return nil, err
// TODO track the errors and update callers to handle partial errors.
@@ -173,25 +212,45 @@ func GetAPIGroupResources(cl discovery.DiscoveryInterface) ([]*APIGroupResources
return result, nil
}
// DeferredDiscoveryRESTMapper is a RESTMapper that will defer
// DeferredDiscoveryRESTMapper is a RESTMapper and RESTMapperContext that will defer
// initialization of the RESTMapper until the first mapping is
// requested.
type DeferredDiscoveryRESTMapper struct {
initMu sync.Mutex
delegate meta.RESTMapper
cl discovery.CachedDiscoveryInterface
delegate meta.RESTMapperWithContext
cl discovery.CachedDiscoveryInterfaceWithContext
}
var (
//nolint:staticcheck // Intentionally using the old interface here.
_ meta.ResettableRESTMapper = &DeferredDiscoveryRESTMapper{}
_ meta.ResettableRESTMapperWithContext = &DeferredDiscoveryRESTMapper{}
_ fmt.Stringer = &DeferredDiscoveryRESTMapper{}
)
// NewDeferredDiscoveryRESTMapper returns a
// DeferredDiscoveryRESTMapper that will lazily query the provided
// client for discovery information to do REST mappings.
//
// NewDeferredDiscoveryRESTMapperWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use NewDeferredDiscoveryRESTMapperWithContext instead. NewDeferredDiscoveryRESTMapper will try to convert cl to discovery.CachedDiscoveryInterfaceWithContext and use a wrapper if that is not possible, but NewDeferredDiscoveryRESTMapperWithContext ensures that no such conversion is necessary.
func NewDeferredDiscoveryRESTMapper(cl discovery.CachedDiscoveryInterface) *DeferredDiscoveryRESTMapper {
return &DeferredDiscoveryRESTMapper{
cl: discovery.ToCachedDiscoveryInterfaceWithContext(cl),
}
}
// NewDeferredDiscoveryRESTMapperWithContext returns a
// DeferredDiscoveryRESTMapper that will lazily query the provided
// client for discovery information to do REST mappings.
func NewDeferredDiscoveryRESTMapperWithContext(cl discovery.CachedDiscoveryInterfaceWithContext) *DeferredDiscoveryRESTMapper {
return &DeferredDiscoveryRESTMapper{
cl: cl,
}
}
func (d *DeferredDiscoveryRESTMapper) getDelegate() (meta.RESTMapper, error) {
func (d *DeferredDiscoveryRESTMapper) getDelegate(ctx context.Context) (meta.RESTMapperWithContext, error) {
d.initMu.Lock()
defer d.initMu.Unlock()
@@ -199,98 +258,154 @@ func (d *DeferredDiscoveryRESTMapper) getDelegate() (meta.RESTMapper, error) {
return d.delegate, nil
}
groupResources, err := GetAPIGroupResources(d.cl)
groupResources, err := GetAPIGroupResourcesWithContext(ctx, d.cl)
if err != nil {
return nil, err
}
d.delegate = NewDiscoveryRESTMapper(groupResources)
d.delegate = NewDiscoveryRESTMapperWithContext(groupResources)
return d.delegate, nil
}
// Reset resets the internally cached Discovery information and will
// cause the next mapping request to re-discover.
func (d *DeferredDiscoveryRESTMapper) Reset() {
klog.V(5).Info("Invalidating discovery information")
d.ResetWithContext(context.Background())
}
// ResetWithContext resets the internally cached Discovery information and will
// cause the next mapping request to re-discover.
func (d *DeferredDiscoveryRESTMapper) ResetWithContext(ctx context.Context) {
klog.FromContext(ctx).V(5).Info("Invalidating discovery information")
d.initMu.Lock()
defer d.initMu.Unlock()
d.cl.Invalidate()
d.cl.InvalidateWithContext(ctx)
d.delegate = nil
}
// KindFor takes a partial resource and returns back the single match.
// It returns an error if there are multiple matches.
//
// KindForWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use KindForWithContext instead.
func (d *DeferredDiscoveryRESTMapper) KindFor(resource schema.GroupVersionResource) (gvk schema.GroupVersionKind, err error) {
del, err := d.getDelegate()
return d.KindForWithContext(context.Background(), resource)
}
// KindForWithContext takes a partial resource and returns back the single match.
// It returns an error if there are multiple matches.
func (d *DeferredDiscoveryRESTMapper) KindForWithContext(ctx context.Context, resource schema.GroupVersionResource) (gvk schema.GroupVersionKind, err error) {
del, err := d.getDelegate(ctx)
if err != nil {
return schema.GroupVersionKind{}, err
}
gvk, err = del.KindFor(resource)
if err != nil && !d.cl.Fresh() {
d.Reset()
gvk, err = d.KindFor(resource)
gvk, err = del.KindForWithContext(ctx, resource)
if err != nil && !d.cl.FreshWithContext(ctx) {
d.ResetWithContext(ctx)
gvk, err = d.KindForWithContext(ctx, resource)
}
return
}
// KindsFor takes a partial resource and returns back the list of
// potential kinds in priority order.
//
// KindsForWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use KindsForWithContext instead.
func (d *DeferredDiscoveryRESTMapper) KindsFor(resource schema.GroupVersionResource) (gvks []schema.GroupVersionKind, err error) {
del, err := d.getDelegate()
return d.KindsForWithContext(context.Background(), resource)
}
// KindsForWithContext takes a partial resource and returns back the list of
// potential kinds in priority order.
func (d *DeferredDiscoveryRESTMapper) KindsForWithContext(ctx context.Context, resource schema.GroupVersionResource) (gvks []schema.GroupVersionKind, err error) {
del, err := d.getDelegate(ctx)
if err != nil {
return nil, err
}
gvks, err = del.KindsFor(resource)
if len(gvks) == 0 && !d.cl.Fresh() {
d.Reset()
gvks, err = d.KindsFor(resource)
gvks, err = del.KindsForWithContext(ctx, resource)
if len(gvks) == 0 && !d.cl.FreshWithContext(ctx) {
d.ResetWithContext(ctx)
gvks, err = d.KindsForWithContext(ctx, resource)
}
return
}
// ResourceFor takes a partial resource and returns back the single
// match. It returns an error if there are multiple matches.
//
// ResourceForWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ResourceForWithContext instead.
func (d *DeferredDiscoveryRESTMapper) ResourceFor(input schema.GroupVersionResource) (gvr schema.GroupVersionResource, err error) {
del, err := d.getDelegate()
return d.ResourceForWithContext(context.Background(), input)
}
// ResourceForWithContext takes a partial resource and returns back the single
// match. It returns an error if there are multiple matches.
func (d *DeferredDiscoveryRESTMapper) ResourceForWithContext(ctx context.Context, input schema.GroupVersionResource) (gvr schema.GroupVersionResource, err error) {
del, err := d.getDelegate(ctx)
if err != nil {
return schema.GroupVersionResource{}, err
}
gvr, err = del.ResourceFor(input)
if err != nil && !d.cl.Fresh() {
d.Reset()
gvr, err = d.ResourceFor(input)
gvr, err = del.ResourceForWithContext(ctx, input)
if err != nil && !d.cl.FreshWithContext(ctx) {
d.ResetWithContext(ctx)
gvr, err = d.ResourceForWithContext(ctx, input)
}
return
}
// ResourcesFor takes a partial resource and returns back the list of
// potential resource in priority order.
//
// ResourcesForWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ResourcesForWithContext instead.
func (d *DeferredDiscoveryRESTMapper) ResourcesFor(input schema.GroupVersionResource) (gvrs []schema.GroupVersionResource, err error) {
del, err := d.getDelegate()
return d.ResourcesForWithContext(context.Background(), input)
}
// ResourcesForWithContext takes a partial resource and returns back the list of
// potential resource in priority order.
func (d *DeferredDiscoveryRESTMapper) ResourcesForWithContext(ctx context.Context, input schema.GroupVersionResource) (gvrs []schema.GroupVersionResource, err error) {
del, err := d.getDelegate(ctx)
if err != nil {
return nil, err
}
gvrs, err = del.ResourcesFor(input)
if len(gvrs) == 0 && !d.cl.Fresh() {
d.Reset()
gvrs, err = d.ResourcesFor(input)
gvrs, err = del.ResourcesForWithContext(ctx, input)
if len(gvrs) == 0 && !d.cl.FreshWithContext(ctx) {
d.ResetWithContext(ctx)
gvrs, err = d.ResourcesForWithContext(ctx, input)
}
return
}
// RESTMapping identifies a preferred resource mapping for the
// provided group kind.
//
// RESTMappingWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use RESTMappingWithContext instead.
func (d *DeferredDiscoveryRESTMapper) RESTMapping(gk schema.GroupKind, versions ...string) (m *meta.RESTMapping, err error) {
del, err := d.getDelegate()
return d.RESTMappingWithContext(context.Background(), gk, versions...)
}
// RESTMappingWithContext identifies a preferred resource mapping for the
// provided group kind.
func (d *DeferredDiscoveryRESTMapper) RESTMappingWithContext(ctx context.Context, gk schema.GroupKind, versions ...string) (m *meta.RESTMapping, err error) {
del, err := d.getDelegate(ctx)
if err != nil {
return nil, err
}
m, err = del.RESTMapping(gk, versions...)
if err != nil && !d.cl.Fresh() {
d.Reset()
m, err = d.RESTMapping(gk, versions...)
m, err = del.RESTMappingWithContext(ctx, gk, versions...)
if err != nil && !d.cl.FreshWithContext(ctx) {
d.ResetWithContext(ctx)
m, err = d.RESTMappingWithContext(ctx, gk, versions...)
}
return
}
@@ -298,41 +413,59 @@ func (d *DeferredDiscoveryRESTMapper) RESTMapping(gk schema.GroupKind, versions
// RESTMappings returns the RESTMappings for the provided group kind
// in a rough internal preferred order. If no kind is found, it will
// return a NoResourceMatchError.
//
// RESTMappingsWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use RESTMappingsWithContext instead.
func (d *DeferredDiscoveryRESTMapper) RESTMappings(gk schema.GroupKind, versions ...string) (ms []*meta.RESTMapping, err error) {
del, err := d.getDelegate()
return d.RESTMappingsWithContext(context.Background(), gk, versions...)
}
// RESTMappingsWithContext returns the RESTMappings for the provided group kind
// in a rough internal preferred order. If no kind is found, it will
// return a NoResourceMatchError.
func (d *DeferredDiscoveryRESTMapper) RESTMappingsWithContext(ctx context.Context, gk schema.GroupKind, versions ...string) (ms []*meta.RESTMapping, err error) {
del, err := d.getDelegate(ctx)
if err != nil {
return nil, err
}
ms, err = del.RESTMappings(gk, versions...)
if len(ms) == 0 && !d.cl.Fresh() {
d.Reset()
ms, err = d.RESTMappings(gk, versions...)
ms, err = del.RESTMappingsWithContext(ctx, gk, versions...)
if len(ms) == 0 && !d.cl.FreshWithContext(ctx) {
d.ResetWithContext(ctx)
ms, err = d.RESTMappingsWithContext(ctx, gk, versions...)
}
return
}
// ResourceSingularizer converts a resource name from plural to
// singular (e.g., from pods to pod).
//
// ResourceSingularizerWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ResourceSingularizerWithContext instead.
func (d *DeferredDiscoveryRESTMapper) ResourceSingularizer(resource string) (singular string, err error) {
del, err := d.getDelegate()
return d.ResourceSingularizerWithContext(context.Background(), resource)
}
// ResourceSingularizerWithContext converts a resource name from plural to
// singular (e.g., from pods to pod).
func (d *DeferredDiscoveryRESTMapper) ResourceSingularizerWithContext(ctx context.Context, resource string) (singular string, err error) {
del, err := d.getDelegate(ctx)
if err != nil {
return resource, err
}
singular, err = del.ResourceSingularizer(resource)
if err != nil && !d.cl.Fresh() {
d.Reset()
singular, err = d.ResourceSingularizer(resource)
singular, err = del.ResourceSingularizerWithContext(ctx, resource)
if err != nil && !d.cl.FreshWithContext(ctx) {
d.ResetWithContext(ctx)
singular, err = d.ResourceSingularizerWithContext(ctx, resource)
}
return
}
func (d *DeferredDiscoveryRESTMapper) String() string {
del, err := d.getDelegate()
del, err := d.getDelegate(context.Background() /* hopefully we already have a delegate and don't need the context */)
if err != nil {
return fmt.Sprintf("DeferredDiscoveryRESTMapper{%v}", err)
}
return fmt.Sprintf("DeferredDiscoveryRESTMapper{\n\t%v\n}", del)
}
// Make sure it satisfies the interface
var _ meta.ResettableRESTMapper = &DeferredDiscoveryRESTMapper{}

View File

@@ -17,6 +17,7 @@ limitations under the License.
package restmapper
import (
"context"
"fmt"
"strings"
@@ -30,22 +31,41 @@ import (
// shortcutExpander is a RESTMapper that can be used for Kubernetes resources. It expands the resource first, then invokes the wrapped
type shortcutExpander struct {
RESTMapper meta.RESTMapper
RESTMapper meta.RESTMapperWithContext
discoveryClient discovery.DiscoveryInterface
discoveryClient discovery.DiscoveryInterfaceWithContext
warningHandler func(string)
}
var _ meta.ResettableRESTMapper = shortcutExpander{}
var _ meta.ResettableRESTMapperWithContext = shortcutExpander{}
// NewShortcutExpander wraps a restmapper in a layer that expands shortcuts found via discovery
//
// NewShortcutExpanderWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use NewShortcutExpanderWithContext instead.
func NewShortcutExpander(delegate meta.RESTMapper, client discovery.DiscoveryInterface, warningHandler func(string)) meta.RESTMapper {
return newShortcutExpander(meta.ToRESTMapperWithContext(delegate), discovery.ToDiscoveryInterfaceWithContext(client), warningHandler)
}
// NewShortcutExpanderWithContext wraps a restmapper in a layer that expands shortcuts found via discovery
func NewShortcutExpanderWithContext(delegate meta.RESTMapperWithContext, client discovery.DiscoveryInterfaceWithContext, warningHandler func(string)) meta.RESTMapperWithContext {
return newShortcutExpander(delegate, client, warningHandler)
}
func newShortcutExpander(delegate meta.RESTMapperWithContext, client discovery.DiscoveryInterfaceWithContext, warningHandler func(string)) shortcutExpander {
return shortcutExpander{RESTMapper: delegate, discoveryClient: client, warningHandler: warningHandler}
}
// KindFor fulfills meta.RESTMapper
func (e shortcutExpander) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
return e.KindForWithContext(context.Background(), resource)
}
// KindForWithContext fulfills meta.RESTMapperWithContext
func (e shortcutExpander) KindForWithContext(ctx context.Context, resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
// expandResourceShortcut works with current API resources as read from discovery cache.
// In case of new CRDs this means we potentially don't have current state of discovery.
// In the current wiring in k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go#toRESTMapper,
@@ -53,59 +73,113 @@ func (e shortcutExpander) KindFor(resource schema.GroupVersionResource) (schema.
// cache and fetch all data from a cluster (see k8s.io/client-go/restmapper/discovery.go#KindFor).
// Thus another call to expandResourceShortcut, after a NoMatchError should successfully
// read Kind to the user or an error.
gvk, err := e.RESTMapper.KindFor(e.expandResourceShortcut(resource))
gvk, err := e.RESTMapper.KindForWithContext(ctx, e.expandResourceShortcut(ctx, resource))
if meta.IsNoMatchError(err) {
return e.RESTMapper.KindFor(e.expandResourceShortcut(resource))
return e.RESTMapper.KindForWithContext(ctx, e.expandResourceShortcut(ctx, resource))
}
return gvk, err
}
// KindsFor fulfills meta.RESTMapper
//
// KindsForWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use KindsForWithContext instead.
func (e shortcutExpander) KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
return e.RESTMapper.KindsFor(e.expandResourceShortcut(resource))
return e.KindsForWithContext(context.Background(), resource)
}
// KindsForWithContext fulfills meta.RESTMapperWithContext
func (e shortcutExpander) KindsForWithContext(ctx context.Context, resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error) {
return e.RESTMapper.KindsForWithContext(ctx, e.expandResourceShortcut(ctx, resource))
}
// ResourcesFor fulfills meta.RESTMapper
//
// ResourcesForWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ResourcesForWithContext instead.
func (e shortcutExpander) ResourcesFor(resource schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
return e.RESTMapper.ResourcesFor(e.expandResourceShortcut(resource))
return e.ResourcesForWithContext(context.Background(), resource)
}
// ResourcesForWithContext fulfills meta.RESTMapperWithContext
func (e shortcutExpander) ResourcesForWithContext(ctx context.Context, resource schema.GroupVersionResource) ([]schema.GroupVersionResource, error) {
return e.RESTMapper.ResourcesForWithContext(ctx, e.expandResourceShortcut(ctx, resource))
}
// ResourceFor fulfills meta.RESTMapper
//
// ResourceForWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ResourceForWithContext instead.
func (e shortcutExpander) ResourceFor(resource schema.GroupVersionResource) (schema.GroupVersionResource, error) {
return e.RESTMapper.ResourceFor(e.expandResourceShortcut(resource))
return e.ResourceForWithContext(context.Background(), resource)
}
// ResourceForWithContext fulfills meta.RESTMapperWithContext
func (e shortcutExpander) ResourceForWithContext(ctx context.Context, resource schema.GroupVersionResource) (schema.GroupVersionResource, error) {
return e.RESTMapper.ResourceForWithContext(ctx, e.expandResourceShortcut(ctx, resource))
}
// ResourceSingularizer fulfills meta.RESTMapper
//
// ResourceSingularizerWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ResourceSingularizerWithContext instead.
func (e shortcutExpander) ResourceSingularizer(resource string) (string, error) {
return e.RESTMapper.ResourceSingularizer(e.expandResourceShortcut(schema.GroupVersionResource{Resource: resource}).Resource)
return e.ResourceSingularizerWithContext(context.Background(), resource)
}
// ResourceSingularizerWithContext fulfills meta.RESTMapperWithContext
func (e shortcutExpander) ResourceSingularizerWithContext(ctx context.Context, resource string) (string, error) {
return e.RESTMapper.ResourceSingularizerWithContext(ctx, e.expandResourceShortcut(ctx, schema.GroupVersionResource{Resource: resource}).Resource)
}
// RESTMapping fulfills meta.RESTMapper
//
// RESTMappingWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use RESTMappingWithContext instead.
func (e shortcutExpander) RESTMapping(gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
return e.RESTMapper.RESTMapping(gk, versions...)
return e.RESTMappingWithContext(context.Background(), gk, versions...)
}
// RESTMappingWithContext fulfills meta.RESTMapperWithContext
func (e shortcutExpander) RESTMappingWithContext(ctx context.Context, gk schema.GroupKind, versions ...string) (*meta.RESTMapping, error) {
return e.RESTMapper.RESTMappingWithContext(ctx, gk, versions...)
}
// RESTMappings fulfills meta.RESTMapper
//
// RESTMappingsWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use RESTMappingsWithContext instead.
func (e shortcutExpander) RESTMappings(gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
return e.RESTMapper.RESTMappings(gk, versions...)
return e.RESTMappingsWithContext(context.Background(), gk, versions...)
}
// RESTMappingsWithContext fulfills meta.RESTMapperWithContext
func (e shortcutExpander) RESTMappingsWithContext(ctx context.Context, gk schema.GroupKind, versions ...string) ([]*meta.RESTMapping, error) {
return e.RESTMapper.RESTMappingsWithContext(ctx, gk, versions...)
}
// getShortcutMappings returns a set of tuples which holds short names for resources.
// First the list of potential resources will be taken from the API server.
// Next we will append the hardcoded list of resources - to be backward compatible with old servers.
// NOTE that the list is ordered by group priority.
func (e shortcutExpander) getShortcutMappings() ([]*metav1.APIResourceList, []resourceShortcuts, error) {
func (e shortcutExpander) getShortcutMappings(ctx context.Context) ([]*metav1.APIResourceList, []resourceShortcuts, error) {
res := []resourceShortcuts{}
// get server resources
// This can return an error *and* the results it was able to find. We don't need to fail on the error.
_, apiResList, err := e.discoveryClient.ServerGroupsAndResources()
_, apiResList, err := e.discoveryClient.ServerGroupsAndResourcesWithContext(ctx)
if err != nil {
klog.V(1).Infof("Error loading discovery information: %v", err)
klog.FromContext(ctx).V(1).Info("Error loading discovery information", "err", err)
}
for _, apiResources := range apiResList {
gv, err := schema.ParseGroupVersion(apiResources.GroupVersion)
if err != nil {
klog.V(1).Infof("Unable to parse groupversion = %s due to = %s", apiResources.GroupVersion, err.Error())
klog.FromContext(ctx).V(1).Info("Unable to parse group/version", "groupVersion", apiResources.GroupVersion, "err", err.Error())
continue
}
for _, apiRes := range apiResources.APIResources {
@@ -126,9 +200,9 @@ func (e shortcutExpander) getShortcutMappings() ([]*metav1.APIResourceList, []re
// (something that a pkg/api/meta.RESTMapper can understand), if it is
// indeed a shortcut. If no match has been found, we will match on group prefixing.
// Lastly we will return resource unmodified.
func (e shortcutExpander) expandResourceShortcut(resource schema.GroupVersionResource) schema.GroupVersionResource {
func (e shortcutExpander) expandResourceShortcut(ctx context.Context, resource schema.GroupVersionResource) schema.GroupVersionResource {
// get the shortcut mappings and return on first match.
if allResources, shortcutResources, err := e.getShortcutMappings(); err == nil {
if allResources, shortcutResources, err := e.getShortcutMappings(ctx); err == nil {
// avoid expanding if there's an exact match to a full resource name
for _, apiResources := range allResources {
gv, err := schema.ParseGroupVersion(apiResources.GroupVersion)
@@ -199,8 +273,15 @@ func (e shortcutExpander) expandResourceShortcut(resource schema.GroupVersionRes
return resource
}
// ResetWithContext is a better alternative because it supports contextual logging and cancellation.
//
// Contextual logging: Use ResetWithContext instead.
func (e shortcutExpander) Reset() {
meta.MaybeResetRESTMapper(e.RESTMapper)
e.ResetWithContext(context.Background())
}
func (e shortcutExpander) ResetWithContext(ctx context.Context) {
meta.MaybeResetRESTMapperWithContext(ctx, e.RESTMapper)
}
// ResourceShortcuts represents a structure that holds the information how to

View File

@@ -31,9 +31,12 @@ import (
"k8s.io/client-go/openapi"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/rest/fake"
"k8s.io/klog/v2/ktesting"
)
func TestReplaceAliases(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
tests := []struct {
name string
arg string
@@ -135,7 +138,7 @@ func TestReplaceAliases(t *testing.T) {
}
mapper := NewShortcutExpander(&fakeRESTMapper{}, ds, nil).(shortcutExpander)
actual := mapper.expandResourceShortcut(schema.GroupVersionResource{Resource: test.arg})
actual := mapper.expandResourceShortcut(ctx, schema.GroupVersionResource{Resource: test.arg})
if actual != test.expected {
t.Errorf("%s: unexpected argument: expected %s, got %s", test.name, test.expected, actual)
}
@@ -260,6 +263,8 @@ func TestKindForWithNewCRDs(t *testing.T) {
}
func TestWarnAmbigious(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
tests := []struct {
name string
arg string
@@ -439,7 +444,7 @@ func TestWarnAmbigious(t *testing.T) {
actualWarnings = append(actualWarnings, a)
}).(shortcutExpander)
actual := mapper.expandResourceShortcut(schema.GroupVersionResource{Resource: test.arg})
actual := mapper.expandResourceShortcut(ctx, schema.GroupVersionResource{Resource: test.arg})
if actual != test.expected {
t.Errorf("%s: unexpected argument: expected %s, got %s", test.name, test.expected, actual)
}