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:
Patrick Ohly
2024-12-06 17:38:06 +01:00
committed by Kubernetes Publisher
parent 48ec1022ee
commit 0ff074ca35
15 changed files with 1699 additions and 200 deletions

View File

@@ -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)
}

View File

@@ -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
}

View File

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

View File

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