mirror of
https://github.com/kubernetes/client-go.git
synced 2026-07-16 08:48:13 +00:00
restmapper + discovery: add context-aware APIs
The main purpose is to replace context.TODO with a context provided by the caller. A secondary purpose is to enable contextual logging. Modifying the existing interfaces and APIs would have a big impact on the ecosystem. This is a no-go. Instead, the following approach was taken: - All interfaces get duplicated in a *WithContext variant where the methods also have a *WithContext suffix and the ctx parameter. All methods are treated this way except for obvious local get methods (like RESTClient) because it cannot be ruled out entirely that some implementation may need a context. - Implementations of these interfaces implement both method variants which is possible because the method names are different. The old methods are implemented as thin wrappers around the updated code which is now the body of the new methods or shared helpers. In some cases there is additional overhead (type checks, potentially additional allocations) when using the old methods. - To*WithContext helpers bridge from the old to the new interfaces. They try a type cast first. Because the in-tree implementations implement both, they can be used directly. For other implementations wrappers are used. - None of old APIs and interfaces are marked as deprecated. Instead doc comments inform consumers about the new and better alternatives. This approach avoids causing turmoil in the ecosystem when linters pick up on a formal deprecation and developers act on those linter warnings without properly thinking about the implications. "// Contextual logging: " is used as placeholder for "//logcheck:context". Once all relevant consumers have been updated to the new API, a search/replace can enable the logcheck linter to warn about usage of the old API. Implementations also get marked this way even if probably nothing ever calls them directly because it shows which code, at least theoretically, could get removed. - Existing unit tests do not get updated to the new APIs. This gives us unit test coverage of the old and new API because the old APIs call the new ones. - In-tree consumers will be updated in follow-up PRs. This is likely to be a longer process. Because of the deprecation comment, `hack/golangci-lint.sh -n` can be used to find code which needs to be updated. Kubernetes-commit: ab9f9f2a0e2f06d29464beb1f131190c91b6655e
This commit is contained in:
committed by
Kubernetes Publisher
parent
48ec1022ee
commit
0ff074ca35
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user