diff --git a/discovery/cached/disk/cached_discovery.go b/discovery/cached/disk/cached_discovery.go index 158810ee5..eb9ce79c1 100644 --- a/discovery/cached/disk/cached_discovery.go +++ b/discovery/cached/disk/cached_discovery.go @@ -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, diff --git a/discovery/cached/disk/cached_discovery_test.go b/discovery/cached/disk/cached_discovery_test.go index 94cbfc8b7..e3b78b909 100644 --- a/discovery/cached/disk/cached_discovery_test.go +++ b/discovery/cached/disk/cached_discovery_test.go @@ -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 { diff --git a/discovery/cached/disk/round_tripper.go b/discovery/cached/disk/round_tripper.go index b978a2bed..db26773b7 100644 --- a/discovery/cached/disk/round_tripper.go +++ b/discovery/cached/disk/round_tripper.go @@ -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)) } } diff --git a/discovery/cached/memory/memcache.go b/discovery/cached/memory/memcache.go index 3829b3cc0..9f2420929 100644 --- a/discovery/cached/memory/memcache.go +++ b/discovery/cached/memory/memcache.go @@ -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,39 @@ 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) { 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 +198,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 +217,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 +302,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 +320,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 +371,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 +387,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 +413,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 +426,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{}, diff --git a/discovery/cached/memory/memcache_test.go b/discovery/cached/memory/memcache_test.go index 1412a961b..a6348d49d 100644 --- a/discovery/cached/memory/memcache_test.go +++ b/discovery/cached/memory/memcache_test.go @@ -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) diff --git a/discovery/discovery_client.go b/discovery/discovery_client.go index 5811cb68c..f89196e18 100644 --- a/discovery/discovery_client.go +++ b/discovery/discovery_client.go @@ -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,117 @@ 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 +} + +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 +205,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 +583,38 @@ 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) { // 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 +642,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 +652,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 +698,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 +708,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 +774,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 +793,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 +815,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 +830,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 +879,14 @@ 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) { var sgs *metav1.APIGroupList var resources []*metav1.APIResourceList var failedGVs map[schema.GroupVersion]error @@ -460,14 +894,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 +929,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 +950,16 @@ 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) { var serverGroupList *metav1.APIGroupList var failedGroups map[schema.GroupVersion]error var groupVersionResources map[schema.GroupVersion]*metav1.APIResourceList @@ -518,18 +968,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 +1041,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 +1053,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 +1079,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 +1136,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 +1155,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 +1172,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 +1217,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 } diff --git a/discovery/fake/discovery.go b/discovery/fake/discovery.go index e5d9e7f80..02b2416e1 100644 --- a/discovery/fake/discovery.go +++ b/discovery/fake/discovery.go @@ -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") +} diff --git a/openapi/cached/client.go b/openapi/cached/client.go index 17f63ed26..919883311 100644 --- a/openapi/cached/client.go +++ b/openapi/cached/client.go @@ -17,34 +17,67 @@ 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) { 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) } diff --git a/openapi/cached/groupversion.go b/openapi/cached/groupversion.go index 73730c51b..2b1dcf86b 100644 --- a/openapi/cached/groupversion.go +++ b/openapi/cached/groupversion.go @@ -17,30 +17,44 @@ 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) { g.lock.Lock() defer g.lock.Unlock() @@ -50,7 +64,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 } diff --git a/openapi/client.go b/openapi/client.go index d32895a2f..6e9a55e84 100644 --- a/openapi/client.go +++ b/openapi/client.go @@ -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) diff --git a/openapi/groupversion.go b/openapi/groupversion.go index 40d91b9a5..0e0b59399 100644 --- a/openapi/groupversion.go +++ b/openapi/groupversion.go @@ -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 diff --git a/restmapper/category_expansion.go b/restmapper/category_expansion.go index 484e4c839..265cc7c6a 100644 --- a/restmapper/category_expansion.go +++ b/restmapper/category_expansion.go @@ -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 diff --git a/restmapper/discovery.go b/restmapper/discovery.go index 3505178b6..9457cccd3 100644 --- a/restmapper/discovery.go +++ b/restmapper/discovery.go @@ -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{} diff --git a/restmapper/shortcut.go b/restmapper/shortcut.go index 0afc8689d..4dbeea07d 100644 --- a/restmapper/shortcut.go +++ b/restmapper/shortcut.go @@ -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 diff --git a/restmapper/shortcut_test.go b/restmapper/shortcut_test.go index e1f87ae26..0a47fad06 100644 --- a/restmapper/shortcut_test.go +++ b/restmapper/shortcut_test.go @@ -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) }