Merge pull request #127709 from pohly/log-client-go-rest

client-go/rest: finish context support
This commit is contained in:
Kubernetes Prow Robot 2025-01-22 11:12:37 -08:00 committed by GitHub
commit 427cd18f72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 819 additions and 157 deletions

View File

@ -143,6 +143,7 @@ linters-settings: # please keep this alphabetized
contextual k8s.io/api/.* contextual k8s.io/api/.*
contextual k8s.io/apimachinery/pkg/util/runtime/.* contextual k8s.io/apimachinery/pkg/util/runtime/.*
contextual k8s.io/client-go/metadata/.* contextual k8s.io/client-go/metadata/.*
contextual k8s.io/client-go/rest/.*
contextual k8s.io/client-go/tools/cache/.* contextual k8s.io/client-go/tools/cache/.*
contextual k8s.io/client-go/tools/events/.* contextual k8s.io/client-go/tools/events/.*
contextual k8s.io/client-go/tools/record/.* contextual k8s.io/client-go/tools/record/.*

View File

@ -189,6 +189,7 @@ linters-settings: # please keep this alphabetized
contextual k8s.io/api/.* contextual k8s.io/api/.*
contextual k8s.io/apimachinery/pkg/util/runtime/.* contextual k8s.io/apimachinery/pkg/util/runtime/.*
contextual k8s.io/client-go/metadata/.* contextual k8s.io/client-go/metadata/.*
contextual k8s.io/client-go/rest/.*
contextual k8s.io/client-go/tools/cache/.* contextual k8s.io/client-go/tools/cache/.*
contextual k8s.io/client-go/tools/events/.* contextual k8s.io/client-go/tools/events/.*
contextual k8s.io/client-go/tools/record/.* contextual k8s.io/client-go/tools/record/.*

View File

@ -191,6 +191,7 @@ linters-settings: # please keep this alphabetized
contextual k8s.io/api/.* contextual k8s.io/api/.*
contextual k8s.io/apimachinery/pkg/util/runtime/.* contextual k8s.io/apimachinery/pkg/util/runtime/.*
contextual k8s.io/client-go/metadata/.* contextual k8s.io/client-go/metadata/.*
contextual k8s.io/client-go/rest/.*
contextual k8s.io/client-go/tools/cache/.* contextual k8s.io/client-go/tools/cache/.*
contextual k8s.io/client-go/tools/events/.* contextual k8s.io/client-go/tools/events/.*
contextual k8s.io/client-go/tools/record/.* contextual k8s.io/client-go/tools/record/.*

View File

@ -27,6 +27,7 @@ structured k8s.io/apiserver/pkg/server/options/encryptionconfig/.*
contextual k8s.io/api/.* contextual k8s.io/api/.*
contextual k8s.io/apimachinery/pkg/util/runtime/.* contextual k8s.io/apimachinery/pkg/util/runtime/.*
contextual k8s.io/client-go/metadata/.* contextual k8s.io/client-go/metadata/.*
contextual k8s.io/client-go/rest/.*
contextual k8s.io/client-go/tools/cache/.* contextual k8s.io/client-go/tools/cache/.*
contextual k8s.io/client-go/tools/events/.* contextual k8s.io/client-go/tools/events/.*
contextual k8s.io/client-go/tools/record/.* contextual k8s.io/client-go/tools/record/.*

View File

@ -318,6 +318,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

View File

@ -320,6 +320,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=

View File

@ -58,6 +58,7 @@ require (
github.com/onsi/ginkgo/v2 v2.21.0 // indirect github.com/onsi/ginkgo/v2 v2.21.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.28.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect golang.org/x/text v0.21.0 // indirect

View File

@ -88,6 +88,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -0,0 +1,10 @@
---
dir: .
filename: "mock_{{.InterfaceName | snakecase}}_test.go"
boilerplate-file: ../../../../../hack/boilerplate/boilerplate.generatego.txt
outpkg: rest
with-expecter: true
packages:
k8s.io/client-go/rest:
interfaces:
BackoffManager:

View File

@ -93,7 +93,7 @@ type RESTClient struct {
content requestClientContentConfigProvider content requestClientContentConfigProvider
// creates BackoffManager that is passed to requests. // creates BackoffManager that is passed to requests.
createBackoffMgr func() BackoffManager createBackoffMgr func() BackoffManagerWithContext
// rateLimiter is shared among all requests created by this client unless specifically // rateLimiter is shared among all requests created by this client unless specifically
// overridden. // overridden.
@ -101,7 +101,7 @@ type RESTClient struct {
// warningHandler is shared among all requests created by this client. // warningHandler is shared among all requests created by this client.
// If not set, defaultWarningHandler is used. // If not set, defaultWarningHandler is used.
warningHandler WarningHandler warningHandler WarningHandlerWithContext
// Set specific behavior of the client. If not set http.DefaultClient will be used. // Set specific behavior of the client. If not set http.DefaultClient will be used.
Client *http.Client Client *http.Client
@ -178,7 +178,7 @@ func (c *RESTClient) GetRateLimiter() flowcontrol.RateLimiter {
// readExpBackoffConfig handles the internal logic of determining what the // readExpBackoffConfig handles the internal logic of determining what the
// backoff policy is. By default if no information is available, NoBackoff. // backoff policy is. By default if no information is available, NoBackoff.
// TODO Generalize this see #17727 . // TODO Generalize this see #17727 .
func readExpBackoffConfig() BackoffManager { func readExpBackoffConfig() BackoffManagerWithContext {
backoffBase := os.Getenv(envBackoffBase) backoffBase := os.Getenv(envBackoffBase)
backoffDuration := os.Getenv(envBackoffDuration) backoffDuration := os.Getenv(envBackoffDuration)

View File

@ -35,6 +35,7 @@ import (
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
utiltesting "k8s.io/client-go/util/testing" utiltesting "k8s.io/client-go/util/testing"
"k8s.io/klog/v2/ktesting"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
) )
@ -335,26 +336,26 @@ func TestHTTPProxy(t *testing.T) {
} }
func TestCreateBackoffManager(t *testing.T) { func TestCreateBackoffManager(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
theUrl, _ := url.Parse("http://localhost") theUrl, _ := url.Parse("http://localhost")
// 1 second base backoff + duration of 2 seconds -> exponential backoff for requests. // 1 second base backoff + duration of 2 seconds -> exponential backoff for requests.
t.Setenv(envBackoffBase, "1") t.Setenv(envBackoffBase, "1")
t.Setenv(envBackoffDuration, "2") t.Setenv(envBackoffDuration, "2")
backoff := readExpBackoffConfig() backoff := readExpBackoffConfig()
backoff.UpdateBackoff(theUrl, nil, 500) backoff.UpdateBackoffWithContext(ctx, theUrl, nil, 500)
backoff.UpdateBackoff(theUrl, nil, 500) backoff.UpdateBackoffWithContext(ctx, theUrl, nil, 500)
if backoff.CalculateBackoff(theUrl)/time.Second != 2 { if backoff.CalculateBackoffWithContext(ctx, theUrl)/time.Second != 2 {
t.Errorf("Backoff env not working.") t.Errorf("Backoff env not working.")
} }
// 0 duration -> no backoff. // 0 duration -> no backoff.
t.Setenv(envBackoffBase, "1") t.Setenv(envBackoffBase, "1")
t.Setenv(envBackoffDuration, "0") t.Setenv(envBackoffDuration, "0")
backoff.UpdateBackoff(theUrl, nil, 500) backoff.UpdateBackoffWithContext(ctx, theUrl, nil, 500)
backoff.UpdateBackoff(theUrl, nil, 500) backoff.UpdateBackoffWithContext(ctx, theUrl, nil, 500)
backoff = readExpBackoffConfig() backoff = readExpBackoffConfig()
if backoff.CalculateBackoff(theUrl)/time.Second != 0 { if backoff.CalculateBackoffWithContext(ctx, theUrl)/time.Second != 0 {
t.Errorf("Zero backoff duration, but backoff still occurring.") t.Errorf("Zero backoff duration, but backoff still occurring.")
} }
@ -362,9 +363,9 @@ func TestCreateBackoffManager(t *testing.T) {
t.Setenv(envBackoffBase, "") t.Setenv(envBackoffBase, "")
t.Setenv(envBackoffDuration, "") t.Setenv(envBackoffDuration, "")
backoff = readExpBackoffConfig() backoff = readExpBackoffConfig()
backoff.UpdateBackoff(theUrl, nil, 500) backoff.UpdateBackoffWithContext(ctx, theUrl, nil, 500)
backoff.UpdateBackoff(theUrl, nil, 500) backoff.UpdateBackoffWithContext(ctx, theUrl, nil, 500)
if backoff.CalculateBackoff(theUrl)/time.Second != 0 { if backoff.CalculateBackoffWithContext(ctx, theUrl)/time.Second != 0 {
t.Errorf("Backoff should have been 0.") t.Errorf("Backoff should have been 0.")
} }

View File

@ -129,10 +129,23 @@ type Config struct {
RateLimiter flowcontrol.RateLimiter RateLimiter flowcontrol.RateLimiter
// WarningHandler handles warnings in server responses. // WarningHandler handles warnings in server responses.
// If not set, the default warning handler is used. // If this and WarningHandlerWithContext are not set, the
// See documentation for SetDefaultWarningHandler() for details. // default warning handler is used. If both are set,
// WarningHandlerWithContext is used.
//
// See documentation for [SetDefaultWarningHandler] for details.
//
//logcheck:context // WarningHandlerWithContext should be used instead of WarningHandler in code which supports contextual logging.
WarningHandler WarningHandler WarningHandler WarningHandler
// WarningHandlerWithContext handles warnings in server responses.
// If this and WarningHandler are not set, the
// default warning handler is used. If both are set,
// WarningHandlerWithContext is used.
//
// See documentation for [SetDefaultWarningHandler] for details.
WarningHandlerWithContext WarningHandlerWithContext
// The maximum length of time to wait before giving up on a server request. A value of zero means no timeout. // The maximum length of time to wait before giving up on a server request. A value of zero means no timeout.
Timeout time.Duration Timeout time.Duration
@ -381,12 +394,27 @@ func RESTClientForConfigAndClient(config *Config, httpClient *http.Client) (*RES
} }
restClient, err := NewRESTClient(baseURL, versionedAPIPath, clientContent, rateLimiter, httpClient) restClient, err := NewRESTClient(baseURL, versionedAPIPath, clientContent, rateLimiter, httpClient)
if err == nil && config.WarningHandler != nil { maybeSetWarningHandler(restClient, config.WarningHandler, config.WarningHandlerWithContext)
restClient.warningHandler = config.WarningHandler
}
return restClient, err return restClient, err
} }
// maybeSetWarningHandler sets the handlerWithContext if non-nil,
// otherwise the handler with a wrapper if non-nil,
// and does nothing if both are nil.
//
// May be called for a nil client.
func maybeSetWarningHandler(c *RESTClient, handler WarningHandler, handlerWithContext WarningHandlerWithContext) {
if c == nil {
return
}
switch {
case handlerWithContext != nil:
c.warningHandler = handlerWithContext
case handler != nil:
c.warningHandler = warningLoggerNopContext{l: handler}
}
}
// UnversionedRESTClientFor is the same as RESTClientFor, except that it allows // UnversionedRESTClientFor is the same as RESTClientFor, except that it allows
// the config.Version to be empty. // the config.Version to be empty.
func UnversionedRESTClientFor(config *Config) (*RESTClient, error) { func UnversionedRESTClientFor(config *Config) (*RESTClient, error) {
@ -448,9 +476,7 @@ func UnversionedRESTClientForConfigAndClient(config *Config, httpClient *http.Cl
} }
restClient, err := NewRESTClient(baseURL, versionedAPIPath, clientContent, rateLimiter, httpClient) restClient, err := NewRESTClient(baseURL, versionedAPIPath, clientContent, rateLimiter, httpClient)
if err == nil && config.WarningHandler != nil { maybeSetWarningHandler(restClient, config.WarningHandler, config.WarningHandlerWithContext)
restClient.warningHandler = config.WarningHandler
}
return restClient, err return restClient, err
} }
@ -532,6 +558,7 @@ func InClusterConfig() (*Config, error) {
tlsClientConfig := TLSClientConfig{} tlsClientConfig := TLSClientConfig{}
if _, err := certutil.NewPool(rootCAFile); err != nil { if _, err := certutil.NewPool(rootCAFile); err != nil {
//nolint:logcheck // The decision to log this instead of returning an error goes back to ~2016. It's part of the client-go API now, so not changing it just to support contextual logging.
klog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err) klog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err)
} else { } else {
tlsClientConfig.CAFile = rootCAFile tlsClientConfig.CAFile = rootCAFile
@ -616,15 +643,16 @@ func AnonymousClientConfig(config *Config) *Config {
CAData: config.TLSClientConfig.CAData, CAData: config.TLSClientConfig.CAData,
NextProtos: config.TLSClientConfig.NextProtos, NextProtos: config.TLSClientConfig.NextProtos,
}, },
RateLimiter: config.RateLimiter, RateLimiter: config.RateLimiter,
WarningHandler: config.WarningHandler, WarningHandler: config.WarningHandler,
UserAgent: config.UserAgent, WarningHandlerWithContext: config.WarningHandlerWithContext,
DisableCompression: config.DisableCompression, UserAgent: config.UserAgent,
QPS: config.QPS, DisableCompression: config.DisableCompression,
Burst: config.Burst, QPS: config.QPS,
Timeout: config.Timeout, Burst: config.Burst,
Dial: config.Dial, Timeout: config.Timeout,
Proxy: config.Proxy, Dial: config.Dial,
Proxy: config.Proxy,
} }
} }
@ -658,17 +686,18 @@ func CopyConfig(config *Config) *Config {
CAData: config.TLSClientConfig.CAData, CAData: config.TLSClientConfig.CAData,
NextProtos: config.TLSClientConfig.NextProtos, NextProtos: config.TLSClientConfig.NextProtos,
}, },
UserAgent: config.UserAgent, UserAgent: config.UserAgent,
DisableCompression: config.DisableCompression, DisableCompression: config.DisableCompression,
Transport: config.Transport, Transport: config.Transport,
WrapTransport: config.WrapTransport, WrapTransport: config.WrapTransport,
QPS: config.QPS, QPS: config.QPS,
Burst: config.Burst, Burst: config.Burst,
RateLimiter: config.RateLimiter, RateLimiter: config.RateLimiter,
WarningHandler: config.WarningHandler, WarningHandler: config.WarningHandler,
Timeout: config.Timeout, WarningHandlerWithContext: config.WarningHandlerWithContext,
Dial: config.Dial, Timeout: config.Timeout,
Proxy: config.Proxy, Dial: config.Dial,
Proxy: config.Proxy,
} }
if config.ExecProvider != nil && config.ExecProvider.Config != nil { if config.ExecProvider != nil && config.ExecProvider.Config != nil {
c.ExecProvider.Config = config.ExecProvider.Config.DeepCopyObject() c.ExecProvider.Config = config.ExecProvider.Config.DeepCopyObject()

View File

@ -41,6 +41,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
fuzz "github.com/google/gofuzz" fuzz "github.com/google/gofuzz"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestIsConfigTransportTLS(t *testing.T) { func TestIsConfigTransportTLS(t *testing.T) {
@ -266,6 +267,19 @@ type fakeWarningHandler struct{}
func (f fakeWarningHandler) HandleWarningHeader(code int, agent string, message string) {} func (f fakeWarningHandler) HandleWarningHeader(code int, agent string, message string) {}
type fakeWarningHandlerWithLogging struct {
messages []string
}
func (f *fakeWarningHandlerWithLogging) HandleWarningHeader(code int, agent string, message string) {
f.messages = append(f.messages, message)
}
type fakeWarningHandlerWithContext struct{}
func (f fakeWarningHandlerWithContext) HandleWarningHeaderWithContext(ctx context.Context, code int, agent string, message string) {
}
type fakeNegotiatedSerializer struct{} type fakeNegotiatedSerializer struct{}
func (n *fakeNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo { func (n *fakeNegotiatedSerializer) SupportedMediaTypes() []runtime.SerializerInfo {
@ -330,6 +344,9 @@ func TestAnonymousAuthConfig(t *testing.T) {
func(h *WarningHandler, f fuzz.Continue) { func(h *WarningHandler, f fuzz.Continue) {
*h = &fakeWarningHandler{} *h = &fakeWarningHandler{}
}, },
func(h *WarningHandlerWithContext, f fuzz.Continue) {
*h = &fakeWarningHandlerWithContext{}
},
// Authentication does not require fuzzer // Authentication does not require fuzzer
func(r *AuthProviderConfigPersister, f fuzz.Continue) {}, func(r *AuthProviderConfigPersister, f fuzz.Continue) {},
func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) { func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) {
@ -428,6 +445,9 @@ func TestCopyConfig(t *testing.T) {
func(h *WarningHandler, f fuzz.Continue) { func(h *WarningHandler, f fuzz.Continue) {
*h = &fakeWarningHandler{} *h = &fakeWarningHandler{}
}, },
func(h *WarningHandlerWithContext, f fuzz.Continue) {
*h = &fakeWarningHandlerWithContext{}
},
func(r *AuthProviderConfigPersister, f fuzz.Continue) { func(r *AuthProviderConfigPersister, f fuzz.Continue) {
*r = fakeAuthProviderConfigPersister{} *r = fakeAuthProviderConfigPersister{}
}, },
@ -619,25 +639,69 @@ func TestConfigSprint(t *testing.T) {
KeyData: []byte("fake key"), KeyData: []byte("fake key"),
NextProtos: []string{"h2", "http/1.1"}, NextProtos: []string{"h2", "http/1.1"},
}, },
UserAgent: "gobot", UserAgent: "gobot",
Transport: &fakeRoundTripper{}, Transport: &fakeRoundTripper{},
WrapTransport: fakeWrapperFunc, WrapTransport: fakeWrapperFunc,
QPS: 1, QPS: 1,
Burst: 2, Burst: 2,
RateLimiter: &fakeLimiter{}, RateLimiter: &fakeLimiter{},
WarningHandler: fakeWarningHandler{}, WarningHandler: fakeWarningHandler{},
Timeout: 3 * time.Second, WarningHandlerWithContext: fakeWarningHandlerWithContext{},
Dial: fakeDialFunc, Timeout: 3 * time.Second,
Proxy: fakeProxyFunc, Dial: fakeDialFunc,
Proxy: fakeProxyFunc,
} }
want := fmt.Sprintf( want := fmt.Sprintf(
`&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", UID:"uid123", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.ExecConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: "", ProvideClusterInfo: true, Config: runtime.Object(--- REDACTED ---), StdinUnavailable: false}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), WarningHandler:rest.fakeWarningHandler{}, Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`, `&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", UID:"uid123", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.ExecConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: "", ProvideClusterInfo: true, Config: runtime.Object(--- REDACTED ---), StdinUnavailable: false}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), WarningHandler:rest.fakeWarningHandler{}, WarningHandlerWithContext:rest.fakeWarningHandlerWithContext{}, Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`,
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc, c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc,
) )
for _, f := range []string{"%s", "%v", "%+v", "%#v"} { for _, f := range []string{"%s", "%v", "%+v", "%#v"} {
if got := fmt.Sprintf(f, c); want != got { if got := fmt.Sprintf(f, c); want != got {
t.Errorf("fmt.Sprintf(%q, c)\ngot: %q\nwant: %q", f, got, want) t.Errorf("fmt.Sprintf(%q, c)\ngot: %q\nwant: %q\ndiff: %s", f, got, want, cmp.Diff(want, got))
} }
} }
} }
func TestConfigWarningHandler(t *testing.T) {
config := &Config{}
config.GroupVersion = &schema.GroupVersion{}
config.NegotiatedSerializer = &fakeNegotiatedSerializer{}
handlerNoContext := &fakeWarningHandler{}
handlerWithContext := &fakeWarningHandlerWithContext{}
t.Run("none", func(t *testing.T) {
client, err := RESTClientForConfigAndClient(config, nil)
require.NoError(t, err)
assert.Nil(t, client.warningHandler)
})
t.Run("no-context", func(t *testing.T) {
config := CopyConfig(config)
handler := &fakeWarningHandlerWithLogging{}
config.WarningHandler = handler
client, err := RESTClientForConfigAndClient(config, nil)
require.NoError(t, err)
client.warningHandler.HandleWarningHeaderWithContext(context.Background(), 0, "", "message")
assert.Equal(t, []string{"message"}, handler.messages)
})
t.Run("with-context", func(t *testing.T) {
config := CopyConfig(config)
config.WarningHandlerWithContext = handlerWithContext
client, err := RESTClientForConfigAndClient(config, nil)
require.NoError(t, err)
assert.Equal(t, handlerWithContext, client.warningHandler)
})
t.Run("both", func(t *testing.T) {
config := CopyConfig(config)
config.WarningHandler = handlerNoContext
config.WarningHandlerWithContext = handlerWithContext
client, err := RESTClientForConfigAndClient(config, nil)
require.NoError(t, err)
assert.NotNil(t, client.warningHandler)
assert.Equal(t, handlerWithContext, client.warningHandler)
})
}

View File

@ -242,6 +242,9 @@ func TestConfigToExecClusterRoundtrip(t *testing.T) {
func(h *WarningHandler, f fuzz.Continue) { func(h *WarningHandler, f fuzz.Continue) {
*h = &fakeWarningHandler{} *h = &fakeWarningHandler{}
}, },
func(h *WarningHandlerWithContext, f fuzz.Continue) {
*h = &fakeWarningHandlerWithContext{}
},
// Authentication does not require fuzzer // Authentication does not require fuzzer
func(r *AuthProviderConfigPersister, f fuzz.Continue) {}, func(r *AuthProviderConfigPersister, f fuzz.Continue) {},
func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) { func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) {
@ -289,6 +292,7 @@ func TestConfigToExecClusterRoundtrip(t *testing.T) {
expected.Burst = 0 expected.Burst = 0
expected.RateLimiter = nil expected.RateLimiter = nil
expected.WarningHandler = nil expected.WarningHandler = nil
expected.WarningHandlerWithContext = nil
expected.Timeout = 0 expected.Timeout = 0
expected.Dial = nil expected.Dial = nil

View File

@ -0,0 +1,168 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by mockery v2.40.3. DO NOT EDIT.
package rest
import (
mock "github.com/stretchr/testify/mock"
time "time"
url "net/url"
)
// MockBackoffManager is an autogenerated mock type for the BackoffManager type
type MockBackoffManager struct {
mock.Mock
}
type MockBackoffManager_Expecter struct {
mock *mock.Mock
}
func (_m *MockBackoffManager) EXPECT() *MockBackoffManager_Expecter {
return &MockBackoffManager_Expecter{mock: &_m.Mock}
}
// CalculateBackoff provides a mock function with given fields: actualURL
func (_m *MockBackoffManager) CalculateBackoff(actualURL *url.URL) time.Duration {
ret := _m.Called(actualURL)
if len(ret) == 0 {
panic("no return value specified for CalculateBackoff")
}
var r0 time.Duration
if rf, ok := ret.Get(0).(func(*url.URL) time.Duration); ok {
r0 = rf(actualURL)
} else {
r0 = ret.Get(0).(time.Duration)
}
return r0
}
// MockBackoffManager_CalculateBackoff_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CalculateBackoff'
type MockBackoffManager_CalculateBackoff_Call struct {
*mock.Call
}
// CalculateBackoff is a helper method to define mock.On call
// - actualURL *url.URL
func (_e *MockBackoffManager_Expecter) CalculateBackoff(actualURL interface{}) *MockBackoffManager_CalculateBackoff_Call {
return &MockBackoffManager_CalculateBackoff_Call{Call: _e.mock.On("CalculateBackoff", actualURL)}
}
func (_c *MockBackoffManager_CalculateBackoff_Call) Run(run func(actualURL *url.URL)) *MockBackoffManager_CalculateBackoff_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*url.URL))
})
return _c
}
func (_c *MockBackoffManager_CalculateBackoff_Call) Return(_a0 time.Duration) *MockBackoffManager_CalculateBackoff_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackoffManager_CalculateBackoff_Call) RunAndReturn(run func(*url.URL) time.Duration) *MockBackoffManager_CalculateBackoff_Call {
_c.Call.Return(run)
return _c
}
// Sleep provides a mock function with given fields: d
func (_m *MockBackoffManager) Sleep(d time.Duration) {
_m.Called(d)
}
// MockBackoffManager_Sleep_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Sleep'
type MockBackoffManager_Sleep_Call struct {
*mock.Call
}
// Sleep is a helper method to define mock.On call
// - d time.Duration
func (_e *MockBackoffManager_Expecter) Sleep(d interface{}) *MockBackoffManager_Sleep_Call {
return &MockBackoffManager_Sleep_Call{Call: _e.mock.On("Sleep", d)}
}
func (_c *MockBackoffManager_Sleep_Call) Run(run func(d time.Duration)) *MockBackoffManager_Sleep_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(time.Duration))
})
return _c
}
func (_c *MockBackoffManager_Sleep_Call) Return() *MockBackoffManager_Sleep_Call {
_c.Call.Return()
return _c
}
func (_c *MockBackoffManager_Sleep_Call) RunAndReturn(run func(time.Duration)) *MockBackoffManager_Sleep_Call {
_c.Call.Return(run)
return _c
}
// UpdateBackoff provides a mock function with given fields: actualURL, err, responseCode
func (_m *MockBackoffManager) UpdateBackoff(actualURL *url.URL, err error, responseCode int) {
_m.Called(actualURL, err, responseCode)
}
// MockBackoffManager_UpdateBackoff_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateBackoff'
type MockBackoffManager_UpdateBackoff_Call struct {
*mock.Call
}
// UpdateBackoff is a helper method to define mock.On call
// - actualURL *url.URL
// - err error
// - responseCode int
func (_e *MockBackoffManager_Expecter) UpdateBackoff(actualURL interface{}, err interface{}, responseCode interface{}) *MockBackoffManager_UpdateBackoff_Call {
return &MockBackoffManager_UpdateBackoff_Call{Call: _e.mock.On("UpdateBackoff", actualURL, err, responseCode)}
}
func (_c *MockBackoffManager_UpdateBackoff_Call) Run(run func(actualURL *url.URL, err error, responseCode int)) *MockBackoffManager_UpdateBackoff_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(*url.URL), args[1].(error), args[2].(int))
})
return _c
}
func (_c *MockBackoffManager_UpdateBackoff_Call) Return() *MockBackoffManager_UpdateBackoff_Call {
_c.Call.Return()
return _c
}
func (_c *MockBackoffManager_UpdateBackoff_Call) RunAndReturn(run func(*url.URL, error, int)) *MockBackoffManager_UpdateBackoff_Call {
_c.Call.Return(run)
return _c
}
// NewMockBackoffManager creates a new instance of MockBackoffManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockBackoffManager(t interface {
mock.TestingT
Cleanup(func())
}) *MockBackoffManager {
mock := &MockBackoffManager{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -21,8 +21,6 @@ import (
"net/http" "net/http"
"sync" "sync"
"k8s.io/klog/v2"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api" clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
) )
@ -65,7 +63,10 @@ func RegisterAuthProviderPlugin(name string, plugin Factory) error {
if _, found := plugins[name]; found { if _, found := plugins[name]; found {
return fmt.Errorf("auth Provider Plugin %q was registered twice", name) return fmt.Errorf("auth Provider Plugin %q was registered twice", name)
} }
klog.V(4).Infof("Registered Auth Provider Plugin %q", name) // RegisterAuthProviderPlugin gets called during the init phase before
// logging is initialized and therefore should not emit logs. If you
// need this message for debugging something, then uncomment it.
// klog.V(4).Infof("Registered Auth Provider Plugin %q", name)
plugins[name] = plugin plugins[name] = plugin
return nil return nil
} }

View File

@ -54,7 +54,7 @@ import (
"k8s.io/utils/clock" "k8s.io/utils/clock"
) )
var ( const (
// longThrottleLatency defines threshold for logging requests. All requests being // longThrottleLatency defines threshold for logging requests. All requests being
// throttled (via the provided rateLimiter) for more than longThrottleLatency will // throttled (via the provided rateLimiter) for more than longThrottleLatency will
// be logged. // be logged.
@ -103,10 +103,10 @@ type Request struct {
contentConfig ClientContentConfig contentConfig ClientContentConfig
contentTypeNotSet bool contentTypeNotSet bool
warningHandler WarningHandler warningHandler WarningHandlerWithContext
rateLimiter flowcontrol.RateLimiter rateLimiter flowcontrol.RateLimiter
backoff BackoffManager backoff BackoffManagerWithContext
timeout time.Duration timeout time.Duration
maxRetries int maxRetries int
@ -136,7 +136,7 @@ type Request struct {
// NewRequest creates a new request helper object for accessing runtime.Objects on a server. // NewRequest creates a new request helper object for accessing runtime.Objects on a server.
func NewRequest(c *RESTClient) *Request { func NewRequest(c *RESTClient) *Request {
var backoff BackoffManager var backoff BackoffManagerWithContext
if c.createBackoffMgr != nil { if c.createBackoffMgr != nil {
backoff = c.createBackoffMgr() backoff = c.createBackoffMgr()
} }
@ -259,20 +259,47 @@ func (r *Request) Resource(resource string) *Request {
} }
// BackOff sets the request's backoff manager to the one specified, // BackOff sets the request's backoff manager to the one specified,
// or defaults to the stub implementation if nil is provided // or defaults to the stub implementation if nil is provided.
//
// Deprecated: BackoffManager.Sleep ignores the caller's context. Use BackOffWithContext and BackoffManagerWithContext instead.
func (r *Request) BackOff(manager BackoffManager) *Request { func (r *Request) BackOff(manager BackoffManager) *Request {
if manager == nil { if manager == nil {
r.backoff = &NoBackoff{} r.backoff = &NoBackoff{}
return r return r
} }
r.backoff = &backoffManagerNopContext{BackoffManager: manager}
return r
}
// BackOffWithContext sets the request's backoff manager to the one specified,
// or defaults to the stub implementation if nil is provided.
func (r *Request) BackOffWithContext(manager BackoffManagerWithContext) *Request {
if manager == nil {
r.backoff = &NoBackoff{}
return r
}
r.backoff = manager r.backoff = manager
return r return r
} }
// WarningHandler sets the handler this client uses when warning headers are encountered. // WarningHandler sets the handler this client uses when warning headers are encountered.
// If set to nil, this client will use the default warning handler (see SetDefaultWarningHandler). // If set to nil, this client will use the default warning handler (see [SetDefaultWarningHandler]).
//
//logcheck:context // WarningHandlerWithContext should be used instead of WarningHandler in code which supports contextual logging.
func (r *Request) WarningHandler(handler WarningHandler) *Request { func (r *Request) WarningHandler(handler WarningHandler) *Request {
if handler == nil {
r.warningHandler = nil
return r
}
r.warningHandler = warningLoggerNopContext{l: handler}
return r
}
// WarningHandlerWithContext sets the handler this client uses when warning headers are encountered.
// If set to nil, this client will use the default warning handler (see [SetDefaultWarningHandlerWithContext]).
func (r *Request) WarningHandlerWithContext(handler WarningHandlerWithContext) *Request {
r.warningHandler = handler r.warningHandler = handler
return r return r
} }
@ -649,21 +676,17 @@ func (r *Request) tryThrottleWithInfo(ctx context.Context, retryInfo string) err
} }
latency := time.Since(now) latency := time.Since(now)
var message string
switch {
case len(retryInfo) > 0:
message = fmt.Sprintf("Waited for %v, %s - request: %s:%s", latency, retryInfo, r.verb, r.URL().String())
default:
message = fmt.Sprintf("Waited for %v due to client-side throttling, not priority and fairness, request: %s:%s", latency, r.verb, r.URL().String())
}
if latency > longThrottleLatency { if latency > longThrottleLatency {
klog.V(3).Info(message) if retryInfo == "" {
} retryInfo = "client-side throttling, not priority and fairness"
if latency > extraLongThrottleLatency { }
// If the rate limiter latency is very high, the log message should be printed at a higher log level, klog.FromContext(ctx).V(3).Info("Waited before sending request", "delay", latency, "reason", retryInfo, "verb", r.verb, "URL", r.URL())
// but we use a throttled logger to prevent spamming.
globalThrottledLogger.Infof("%s", message) if latency > extraLongThrottleLatency {
// If the rate limiter latency is very high, the log message should be printed at a higher log level,
// but we use a throttled logger to prevent spamming.
globalThrottledLogger.info(klog.FromContext(ctx), "Waited before sending request", "delay", latency, "reason", retryInfo, "verb", r.verb, "URL", r.URL())
}
} }
metrics.RateLimiterLatency.Observe(ctx, r.verb, r.finalURLTemplate(), latency) metrics.RateLimiterLatency.Observe(ctx, r.verb, r.finalURLTemplate(), latency)
@ -675,7 +698,7 @@ func (r *Request) tryThrottle(ctx context.Context) error {
} }
type throttleSettings struct { type throttleSettings struct {
logLevel klog.Level logLevel int
minLogInterval time.Duration minLogInterval time.Duration
lastLogTime time.Time lastLogTime time.Time
@ -700,9 +723,9 @@ var globalThrottledLogger = &throttledLogger{
}, },
} }
func (b *throttledLogger) attemptToLog() (klog.Level, bool) { func (b *throttledLogger) attemptToLog(logger klog.Logger) (int, bool) {
for _, setting := range b.settings { for _, setting := range b.settings {
if bool(klog.V(setting.logLevel).Enabled()) { if bool(logger.V(setting.logLevel).Enabled()) {
// Return early without write locking if possible. // Return early without write locking if possible.
if func() bool { if func() bool {
setting.lock.RLock() setting.lock.RLock()
@ -724,9 +747,9 @@ func (b *throttledLogger) attemptToLog() (klog.Level, bool) {
// Infof will write a log message at each logLevel specified by the receiver's throttleSettings // Infof will write a log message at each logLevel specified by the receiver's throttleSettings
// as long as it hasn't written a log message more recently than minLogInterval. // as long as it hasn't written a log message more recently than minLogInterval.
func (b *throttledLogger) Infof(message string, args ...interface{}) { func (b *throttledLogger) info(logger klog.Logger, message string, kv ...any) {
if logLevel, ok := b.attemptToLog(); ok { if logLevel, ok := b.attemptToLog(logger); ok {
klog.V(logLevel).Infof(message, args...) logger.V(logLevel).Info(message, kv...)
} }
} }
@ -739,7 +762,7 @@ func (r *Request) Watch(ctx context.Context) (watch.Interface, error) {
func (r *Request) watchInternal(ctx context.Context) (watch.Interface, runtime.Decoder, error) { func (r *Request) watchInternal(ctx context.Context) (watch.Interface, runtime.Decoder, error) {
if r.body == nil { if r.body == nil {
logBody(ctx, 2, "Request Body", r.bodyBytes) logBody(klog.FromContext(ctx), 2, "Request Body", r.bodyBytes)
} }
// We specifically don't want to rate limit watches, so we // We specifically don't want to rate limit watches, so we
@ -776,7 +799,7 @@ func (r *Request) watchInternal(ctx context.Context) (watch.Interface, runtime.D
resp, err := client.Do(req) resp, err := client.Do(req)
retry.After(ctx, r, resp, err) retry.After(ctx, r, resp, err)
if err == nil && resp.StatusCode == http.StatusOK { if err == nil && resp.StatusCode == http.StatusOK {
return r.newStreamWatcher(resp) return r.newStreamWatcher(ctx, resp)
} }
done, transformErr := func() (bool, error) { done, transformErr := func() (bool, error) {
@ -898,7 +921,7 @@ func (r WatchListResult) Into(obj runtime.Object) error {
// to see what parameters are currently required. // to see what parameters are currently required.
func (r *Request) WatchList(ctx context.Context) WatchListResult { func (r *Request) WatchList(ctx context.Context) WatchListResult {
if r.body == nil { if r.body == nil {
logBody(ctx, 2, "Request Body", r.bodyBytes) logBody(klog.FromContext(ctx), 2, "Request Body", r.bodyBytes)
} }
if !clientfeatures.FeatureGates().Enabled(clientfeatures.WatchListClient) { if !clientfeatures.FeatureGates().Enabled(clientfeatures.WatchListClient) {
@ -969,18 +992,18 @@ func (r *Request) handleWatchList(ctx context.Context, w watch.Interface, negoti
} }
} }
func (r *Request) newStreamWatcher(resp *http.Response) (watch.Interface, runtime.Decoder, error) { func (r *Request) newStreamWatcher(ctx context.Context, resp *http.Response) (watch.Interface, runtime.Decoder, error) {
contentType := resp.Header.Get("Content-Type") contentType := resp.Header.Get("Content-Type")
mediaType, params, err := mime.ParseMediaType(contentType) mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil { if err != nil {
klog.V(4).Infof("Unexpected content type from the server: %q: %v", contentType, err) klog.FromContext(ctx).V(4).Info("Unexpected content type from the server", "contentType", contentType, "err", err)
} }
objectDecoder, streamingSerializer, framer, err := r.contentConfig.Negotiator.StreamDecoder(mediaType, params) objectDecoder, streamingSerializer, framer, err := r.contentConfig.Negotiator.StreamDecoder(mediaType, params)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
handleWarnings(resp.Header, r.warningHandler) handleWarnings(ctx, resp.Header, r.warningHandler)
frameReader := framer.NewFrameReader(resp.Body) frameReader := framer.NewFrameReader(resp.Body)
watchEventDecoder := streaming.NewDecoder(frameReader, streamingSerializer) watchEventDecoder := streaming.NewDecoder(frameReader, streamingSerializer)
@ -1031,7 +1054,7 @@ func sanitize(req *Request, resp *http.Response, err error) (string, string) {
// If we can, we return that as an error. Otherwise, we create an error that lists the http status and the content of the response. // If we can, we return that as an error. Otherwise, we create an error that lists the http status and the content of the response.
func (r *Request) Stream(ctx context.Context) (io.ReadCloser, error) { func (r *Request) Stream(ctx context.Context) (io.ReadCloser, error) {
if r.body == nil { if r.body == nil {
logBody(ctx, 2, "Request Body", r.bodyBytes) logBody(klog.FromContext(ctx), 2, "Request Body", r.bodyBytes)
} }
if r.err != nil { if r.err != nil {
@ -1067,7 +1090,7 @@ func (r *Request) Stream(ctx context.Context) (io.ReadCloser, error) {
switch { switch {
case (resp.StatusCode >= 200) && (resp.StatusCode < 300): case (resp.StatusCode >= 200) && (resp.StatusCode < 300):
handleWarnings(resp.Header, r.warningHandler) handleWarnings(ctx, resp.Header, r.warningHandler)
return resp.Body, nil return resp.Body, nil
default: default:
@ -1175,7 +1198,7 @@ func (r *Request) request(ctx context.Context, fn func(*http.Request, *http.Resp
}() }()
if r.err != nil { if r.err != nil {
klog.V(4).Infof("Error in request: %v", r.err) klog.FromContext(ctx).V(4).Info("Error in request", "err", r.err)
return r.err return r.err
} }
@ -1267,8 +1290,9 @@ func (r *Request) request(ctx context.Context, fn func(*http.Request, *http.Resp
// - If the server responds with a status: *errors.StatusError or *errors.UnexpectedObjectError // - If the server responds with a status: *errors.StatusError or *errors.UnexpectedObjectError
// - http.Client.Do errors are returned directly. // - http.Client.Do errors are returned directly.
func (r *Request) Do(ctx context.Context) Result { func (r *Request) Do(ctx context.Context) Result {
logger := klog.FromContext(ctx)
if r.body == nil { if r.body == nil {
logBody(ctx, 2, "Request Body", r.bodyBytes) logBody(logger, 2, "Request Body", r.bodyBytes)
} }
var result Result var result Result
@ -1276,7 +1300,7 @@ func (r *Request) Do(ctx context.Context) Result {
result = r.transformResponse(ctx, resp, req) result = r.transformResponse(ctx, resp, req)
}) })
if err != nil { if err != nil {
return Result{err: err} return Result{err: err, logger: logger}
} }
if result.err == nil || len(result.body) > 0 { if result.err == nil || len(result.body) > 0 {
metrics.ResponseSize.Observe(ctx, r.verb, r.URL().Host, float64(len(result.body))) metrics.ResponseSize.Observe(ctx, r.verb, r.URL().Host, float64(len(result.body)))
@ -1286,14 +1310,15 @@ func (r *Request) Do(ctx context.Context) Result {
// DoRaw executes the request but does not process the response body. // DoRaw executes the request but does not process the response body.
func (r *Request) DoRaw(ctx context.Context) ([]byte, error) { func (r *Request) DoRaw(ctx context.Context) ([]byte, error) {
logger := klog.FromContext(ctx)
if r.body == nil { if r.body == nil {
logBody(ctx, 2, "Request Body", r.bodyBytes) logBody(logger, 2, "Request Body", r.bodyBytes)
} }
var result Result var result Result
err := r.request(ctx, func(req *http.Request, resp *http.Response) { err := r.request(ctx, func(req *http.Request, resp *http.Response) {
result.body, result.err = io.ReadAll(resp.Body) result.body, result.err = io.ReadAll(resp.Body)
logBody(ctx, 2, "Response Body", result.body) logBody(logger, 2, "Response Body", result.body)
if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent { if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent {
result.err = r.transformUnstructuredResponseError(resp, req, result.body) result.err = r.transformUnstructuredResponseError(resp, req, result.body)
} }
@ -1309,6 +1334,7 @@ func (r *Request) DoRaw(ctx context.Context) ([]byte, error) {
// transformResponse converts an API response into a structured API object // transformResponse converts an API response into a structured API object
func (r *Request) transformResponse(ctx context.Context, resp *http.Response, req *http.Request) Result { func (r *Request) transformResponse(ctx context.Context, resp *http.Response, req *http.Request) Result {
logger := klog.FromContext(ctx)
var body []byte var body []byte
if resp.Body != nil { if resp.Body != nil {
data, err := io.ReadAll(resp.Body) data, err := io.ReadAll(resp.Body)
@ -1323,22 +1349,24 @@ func (r *Request) transformResponse(ctx context.Context, resp *http.Response, re
// 2. Apiserver sends back the headers and then part of the body // 2. Apiserver sends back the headers and then part of the body
// 3. Apiserver closes connection. // 3. Apiserver closes connection.
// 4. client-go should catch this and return an error. // 4. client-go should catch this and return an error.
klog.V(2).Infof("Stream error %#v when reading response body, may be caused by closed connection.", err) logger.V(2).Info("Stream error when reading response body, may be caused by closed connection", "err", err)
streamErr := fmt.Errorf("stream error when reading response body, may be caused by closed connection. Please retry. Original error: %w", err) streamErr := fmt.Errorf("stream error when reading response body, may be caused by closed connection. Please retry. Original error: %w", err)
return Result{ return Result{
err: streamErr, err: streamErr,
logger: logger,
} }
default: default:
klog.Errorf("Unexpected error when reading response body: %v", err) logger.Error(err, "Unexpected error when reading response body")
unexpectedErr := fmt.Errorf("unexpected error when reading response body. Please retry. Original error: %w", err) unexpectedErr := fmt.Errorf("unexpected error when reading response body. Please retry. Original error: %w", err)
return Result{ return Result{
err: unexpectedErr, err: unexpectedErr,
logger: logger,
} }
} }
} }
// Call depth is tricky. This one is okay for Do and DoRaw. // Call depth is tricky. This one is okay for Do and DoRaw.
logBody(ctx, 7, "Response Body", body) logBody(logger, 7, "Response Body", body)
// verify the content type is accurate // verify the content type is accurate
var decoder runtime.Decoder var decoder runtime.Decoder
@ -1350,7 +1378,7 @@ func (r *Request) transformResponse(ctx context.Context, resp *http.Response, re
var err error var err error
mediaType, params, err := mime.ParseMediaType(contentType) mediaType, params, err := mime.ParseMediaType(contentType)
if err != nil { if err != nil {
return Result{err: errors.NewInternalError(err)} return Result{err: errors.NewInternalError(err), logger: logger}
} }
decoder, err = r.contentConfig.Negotiator.Decoder(mediaType, params) decoder, err = r.contentConfig.Negotiator.Decoder(mediaType, params)
if err != nil { if err != nil {
@ -1359,13 +1387,14 @@ func (r *Request) transformResponse(ctx context.Context, resp *http.Response, re
case resp.StatusCode == http.StatusSwitchingProtocols: case resp.StatusCode == http.StatusSwitchingProtocols:
// no-op, we've been upgraded // no-op, we've been upgraded
case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent: case resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent:
return Result{err: r.transformUnstructuredResponseError(resp, req, body)} return Result{err: r.transformUnstructuredResponseError(resp, req, body), logger: logger}
} }
return Result{ return Result{
body: body, body: body,
contentType: contentType, contentType: contentType,
statusCode: resp.StatusCode, statusCode: resp.StatusCode,
warnings: handleWarnings(resp.Header, r.warningHandler), warnings: handleWarnings(ctx, resp.Header, r.warningHandler),
logger: logger,
} }
} }
} }
@ -1384,7 +1413,8 @@ func (r *Request) transformResponse(ctx context.Context, resp *http.Response, re
statusCode: resp.StatusCode, statusCode: resp.StatusCode,
decoder: decoder, decoder: decoder,
err: err, err: err,
warnings: handleWarnings(resp.Header, r.warningHandler), warnings: handleWarnings(ctx, resp.Header, r.warningHandler),
logger: logger,
} }
} }
@ -1393,7 +1423,8 @@ func (r *Request) transformResponse(ctx context.Context, resp *http.Response, re
contentType: contentType, contentType: contentType,
statusCode: resp.StatusCode, statusCode: resp.StatusCode,
decoder: decoder, decoder: decoder,
warnings: handleWarnings(resp.Header, r.warningHandler), warnings: handleWarnings(ctx, resp.Header, r.warningHandler),
logger: logger,
} }
} }
@ -1421,8 +1452,7 @@ func truncateBody(logger klog.Logger, body string) string {
// whether the body is printable. // whether the body is printable.
// //
// It needs to be called by all functions which send or receive the data. // It needs to be called by all functions which send or receive the data.
func logBody(ctx context.Context, callDepth int, prefix string, body []byte) { func logBody(logger klog.Logger, callDepth int, prefix string, body []byte) {
logger := klog.FromContext(ctx)
if loggerV := logger.V(8); loggerV.Enabled() { if loggerV := logger.V(8); loggerV.Enabled() {
loggerV := loggerV.WithCallDepth(callDepth) loggerV := loggerV.WithCallDepth(callDepth)
if bytes.IndexFunc(body, func(r rune) bool { if bytes.IndexFunc(body, func(r rune) bool {
@ -1524,6 +1554,7 @@ type Result struct {
contentType string contentType string
err error err error
statusCode int statusCode int
logger klog.Logger
decoder runtime.Decoder decoder runtime.Decoder
} }
@ -1629,7 +1660,7 @@ func (r Result) Error() error {
// to be backwards compatible with old servers that do not return a version, default to "v1" // to be backwards compatible with old servers that do not return a version, default to "v1"
out, _, err := r.decoder.Decode(r.body, &schema.GroupVersionKind{Version: "v1"}, nil) out, _, err := r.decoder.Decode(r.body, &schema.GroupVersionKind{Version: "v1"}, nil)
if err != nil { if err != nil {
klog.V(5).Infof("body was not decodable (unable to check for Status): %v", err) r.logger.V(5).Info("Body was not decodable (unable to check for Status)", "err", err)
return r.err return r.err
} }
switch t := out.(type) { switch t := out.(type) {

View File

@ -1489,6 +1489,7 @@ func TestDoRequestNewWay(t *testing.T) {
// This test assumes that the client implementation backs off exponentially, for an individual request. // This test assumes that the client implementation backs off exponentially, for an individual request.
func TestBackoffLifecycle(t *testing.T) { func TestBackoffLifecycle(t *testing.T) {
_, ctx := ktesting.NewTestContext(t)
count := 0 count := 0
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
count++ count++
@ -1508,22 +1509,30 @@ func TestBackoffLifecycle(t *testing.T) {
seconds := []int{0, 1, 2, 4, 8, 0, 1, 2, 4, 0} seconds := []int{0, 1, 2, 4, 8, 0, 1, 2, 4, 0}
request := c.Verb("POST").Prefix("backofftest").Suffix("abc") request := c.Verb("POST").Prefix("backofftest").Suffix("abc")
clock := testingclock.FakeClock{} clock := testingclock.FakeClock{}
request.backoff = &URLBackoff{ request.backoff = stepClockDuringSleep{
// Use a fake backoff here to avoid flakes and speed the test up. BackoffManagerWithContext: &URLBackoff{
Backoff: flowcontrol.NewFakeBackOff( // Use a fake backoff here to avoid flakes and speed the test up.
time.Duration(1)*time.Second, Backoff: flowcontrol.NewFakeBackOff(
time.Duration(200)*time.Second, time.Duration(1)*time.Second,
&clock, time.Duration(200)*time.Second,
)} &clock,
),
},
clock: &clock,
}
for _, sec := range seconds { for _, sec := range seconds {
thisBackoff := request.backoff.CalculateBackoff(request.URL()) thisBackoff := request.backoff.CalculateBackoffWithContext(ctx, request.URL())
t.Logf("Current backoff %v", thisBackoff) t.Logf("Current backoff %v", thisBackoff)
if thisBackoff != time.Duration(sec)*time.Second { if thisBackoff != time.Duration(sec)*time.Second {
t.Errorf("Backoff is %v instead of %v", thisBackoff, sec) t.Errorf("Backoff is %v instead of %v", thisBackoff, sec)
} }
// This relies on advancing the fake clock by exactly the duration
// that SleepWithContext is being called for while DoRaw is executing.
// stepClockDuringSleep.SleepWithContext ensures that this happens.
now := clock.Now() now := clock.Now()
request.DoRaw(context.Background()) request.DoRaw(ctx)
elapsed := clock.Since(now) elapsed := clock.Since(now)
if clock.Since(now) != thisBackoff { if clock.Since(now) != thisBackoff {
t.Errorf("CalculatedBackoff not honored by clock: Expected time of %v, but got %v ", thisBackoff, elapsed) t.Errorf("CalculatedBackoff not honored by clock: Expected time of %v, but got %v ", thisBackoff, elapsed)
@ -1531,18 +1540,51 @@ func TestBackoffLifecycle(t *testing.T) {
} }
} }
type stepClockDuringSleep struct {
BackoffManagerWithContext
clock *testingclock.FakeClock
}
// SleepWithContext wraps the underlying SleepWithContext and ensures that once
// that is sleeping, the fake clock advances by exactly the duration that
// it is sleeping for.
func (s stepClockDuringSleep) SleepWithContext(ctx context.Context, d time.Duration) {
// This code is sensitive to both the implementation of
// URLBackoff.SleepWithContext and of FakeClock.NewTimer:
// - SleepWithContext must be a no-op when the duration is zero
// => no need to step the fake clock
// - SleepWithContext must use FakeClock.NewTimer, not FakeClock.Sleep
// because the latter would advance time itself
if d != 0 {
go func() {
// Poll until the caller is sleeping.
for {
if s.clock.HasWaiters() {
s.clock.Step(d)
return
}
if ctx.Err() != nil {
return
}
time.Sleep(time.Millisecond)
}
}()
}
s.BackoffManagerWithContext.SleepWithContext(ctx, d)
}
type testBackoffManager struct { type testBackoffManager struct {
sleeps []time.Duration sleeps []time.Duration
} }
func (b *testBackoffManager) UpdateBackoff(actualUrl *url.URL, err error, responseCode int) { func (b *testBackoffManager) UpdateBackoffWithContext(ctx context.Context, actualURL *url.URL, err error, responseCode int) {
} }
func (b *testBackoffManager) CalculateBackoff(actualUrl *url.URL) time.Duration { func (b *testBackoffManager) CalculateBackoffWithContext(ctx context.Context, actualURL *url.URL) time.Duration {
return time.Duration(0) return time.Duration(0)
} }
func (b *testBackoffManager) Sleep(d time.Duration) { func (b *testBackoffManager) SleepWithContext(ctx context.Context, d time.Duration) {
b.sleeps = append(b.sleeps, d) b.sleeps = append(b.sleeps, d)
} }
@ -1568,7 +1610,7 @@ func TestCheckRetryClosesBody(t *testing.T) {
expectedSleeps := []time.Duration{0, time.Second, time.Second, time.Second, time.Second} expectedSleeps := []time.Duration{0, time.Second, time.Second, time.Second, time.Second}
c := testRESTClient(t, testServer) c := testRESTClient(t, testServer)
c.createBackoffMgr = func() BackoffManager { return backoff } c.createBackoffMgr = func() BackoffManagerWithContext { return backoff }
_, err := c.Verb("POST"). _, err := c.Verb("POST").
Prefix("foo", "bar"). Prefix("foo", "bar").
Suffix("baz"). Suffix("baz").
@ -2434,6 +2476,7 @@ func TestRequestPreflightCheck(t *testing.T) {
} }
func TestThrottledLogger(t *testing.T) { func TestThrottledLogger(t *testing.T) {
logger := klog.Background()
now := time.Now() now := time.Now()
oldClock := globalThrottledLogger.clock oldClock := globalThrottledLogger.clock
defer func() { defer func() {
@ -2448,7 +2491,7 @@ func TestThrottledLogger(t *testing.T) {
wg.Add(10) wg.Add(10)
for j := 0; j < 10; j++ { for j := 0; j < 10; j++ {
go func() { go func() {
if _, ok := globalThrottledLogger.attemptToLog(); ok { if _, ok := globalThrottledLogger.attemptToLog(logger); ok {
logMessages++ logMessages++
} }
wg.Done() wg.Done()
@ -2612,6 +2655,8 @@ type noSleepBackOff struct {
func (n *noSleepBackOff) Sleep(d time.Duration) {} func (n *noSleepBackOff) Sleep(d time.Duration) {}
func (n *noSleepBackOff) SleepWithContext(ctx context.Context, d time.Duration) {}
func TestRequestWithRetry(t *testing.T) { func TestRequestWithRetry(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
@ -2997,7 +3042,6 @@ const retryTestKey retryTestKeyType = iota
// metric calls are invoked appropriately in right order. // metric calls are invoked appropriately in right order.
type withRateLimiterBackoffManagerAndMetrics struct { type withRateLimiterBackoffManagerAndMetrics struct {
flowcontrol.RateLimiter flowcontrol.RateLimiter
*NoBackoff
metrics.ResultMetric metrics.ResultMetric
calculateBackoffSeq int64 calculateBackoffSeq int64
calculateBackoffFn func(i int64) time.Duration calculateBackoffFn func(i int64) time.Duration
@ -3013,7 +3057,7 @@ func (lb *withRateLimiterBackoffManagerAndMetrics) Wait(ctx context.Context) err
return nil return nil
} }
func (lb *withRateLimiterBackoffManagerAndMetrics) CalculateBackoff(actualUrl *url.URL) time.Duration { func (lb *withRateLimiterBackoffManagerAndMetrics) CalculateBackoffWithContext(ctx context.Context, actualURL *url.URL) time.Duration {
lb.invokeOrderGot = append(lb.invokeOrderGot, "BackoffManager.CalculateBackoff") lb.invokeOrderGot = append(lb.invokeOrderGot, "BackoffManager.CalculateBackoff")
waitFor := lb.calculateBackoffFn(lb.calculateBackoffSeq) waitFor := lb.calculateBackoffFn(lb.calculateBackoffSeq)
@ -3021,11 +3065,11 @@ func (lb *withRateLimiterBackoffManagerAndMetrics) CalculateBackoff(actualUrl *u
return waitFor return waitFor
} }
func (lb *withRateLimiterBackoffManagerAndMetrics) UpdateBackoff(actualUrl *url.URL, err error, responseCode int) { func (lb *withRateLimiterBackoffManagerAndMetrics) UpdateBackoffWithContext(ctx context.Context, actualURL *url.URL, err error, responseCode int) {
lb.invokeOrderGot = append(lb.invokeOrderGot, "BackoffManager.UpdateBackoff") lb.invokeOrderGot = append(lb.invokeOrderGot, "BackoffManager.UpdateBackoff")
} }
func (lb *withRateLimiterBackoffManagerAndMetrics) Sleep(d time.Duration) { func (lb *withRateLimiterBackoffManagerAndMetrics) SleepWithContext(ctx context.Context, d time.Duration) {
lb.invokeOrderGot = append(lb.invokeOrderGot, "BackoffManager.Sleep") lb.invokeOrderGot = append(lb.invokeOrderGot, "BackoffManager.Sleep")
lb.sleepsGot = append(lb.sleepsGot, d.String()) lb.sleepsGot = append(lb.sleepsGot, d.String())
} }
@ -3206,7 +3250,6 @@ func testRetryWithRateLimiterBackoffAndMetrics(t *testing.T, key string, doFunc
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
interceptor := &withRateLimiterBackoffManagerAndMetrics{ interceptor := &withRateLimiterBackoffManagerAndMetrics{
RateLimiter: flowcontrol.NewFakeAlwaysRateLimiter(), RateLimiter: flowcontrol.NewFakeAlwaysRateLimiter(),
NoBackoff: &NoBackoff{},
calculateBackoffFn: test.calculateBackoffFn, calculateBackoffFn: test.calculateBackoffFn,
} }
@ -4066,15 +4109,24 @@ func TestRequestLogging(t *testing.T) {
testcases := map[string]struct { testcases := map[string]struct {
v int v int
body any body any
response *http.Response
expectedOutput string expectedOutput string
}{ }{
"no-output": { "no-output": {
v: 7, v: 7,
body: []byte("ping"), body: []byte("ping"),
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("pong")),
},
}, },
"output": { "output": {
v: 8, v: 8,
body: []byte("ping"), body: []byte("ping"),
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("pong")),
},
expectedOutput: `<location>] "Request Body" logger="TestLogger" body="ping" expectedOutput: `<location>] "Request Body" logger="TestLogger" body="ping"
<location>] "Response Body" logger="TestLogger" body="pong" <location>] "Response Body" logger="TestLogger" body="pong"
`, `,
@ -4082,6 +4134,10 @@ func TestRequestLogging(t *testing.T) {
"io-reader": { "io-reader": {
v: 8, v: 8,
body: strings.NewReader("ping"), body: strings.NewReader("ping"),
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("pong")),
},
// Cannot log the request body! // Cannot log the request body!
expectedOutput: `<location>] "Response Body" logger="TestLogger" body="pong" expectedOutput: `<location>] "Response Body" logger="TestLogger" body="pong"
`, `,
@ -4089,13 +4145,38 @@ func TestRequestLogging(t *testing.T) {
"truncate": { "truncate": {
v: 8, v: 8,
body: []byte(strings.Repeat("a", 2000)), body: []byte(strings.Repeat("a", 2000)),
response: &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("pong")),
},
expectedOutput: fmt.Sprintf(`<location>] "Request Body" logger="TestLogger" body="%s [truncated 976 chars]" expectedOutput: fmt.Sprintf(`<location>] "Request Body" logger="TestLogger" body="%s [truncated 976 chars]"
<location>] "Response Body" logger="TestLogger" body="pong" <location>] "Response Body" logger="TestLogger" body="pong"
`, strings.Repeat("a", 1024)), `, strings.Repeat("a", 1024)),
}, },
"warnings": {
v: 8,
body: []byte("ping"),
response: &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{
"Warning": []string{
`299 request-test "warning 1"`,
`299 request-test-2 "warning 2"`,
`300 request-test-3 "ignore code 300"`,
},
},
Body: io.NopCloser(strings.NewReader("pong")),
},
expectedOutput: `<location>] "Request Body" logger="TestLogger" body="ping"
<location>] "Response Body" logger="TestLogger" body="pong"
warnings.go] "Warning: warning 1" logger="TestLogger"
warnings.go] "Warning: warning 2" logger="TestLogger"
`,
},
} }
for name, tc := range testcases { for name, tc := range testcases {
//nolint:logcheck // Intentionally testing with plain klog here.
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
state := klog.CaptureState() state := klog.CaptureState()
defer state.Restore() defer state.Restore()
@ -4106,12 +4187,10 @@ func TestRequestLogging(t *testing.T) {
var fs flag.FlagSet var fs flag.FlagSet
klog.InitFlags(&fs) klog.InitFlags(&fs)
require.NoError(t, fs.Set("v", fmt.Sprintf("%d", tc.v)), "set verbosity") require.NoError(t, fs.Set("v", fmt.Sprintf("%d", tc.v)), "set verbosity")
require.NoError(t, fs.Set("one_output", "true"), "set one_output")
client := clientForFunc(func(req *http.Request) (*http.Response, error) { client := clientForFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{ return tc.response, nil
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("pong")),
}, nil
}) })
req := NewRequestWithClient(nil, "", defaultContentConfig(), client). req := NewRequestWithClient(nil, "", defaultContentConfig(), client).
@ -4128,11 +4207,49 @@ func TestRequestLogging(t *testing.T) {
// Compare log output: // Compare log output:
// - strip date/time/pid from each line (fixed length header) // - strip date/time/pid from each line (fixed length header)
// - replace <location> with the actual call location // - replace <location> with the actual call location
// - strip line number from warnings.go (might change)
state.Restore() state.Restore()
expectedOutput := strings.ReplaceAll(tc.expectedOutput, "<location>", fmt.Sprintf("%s:%d", path.Base(file), line+1)) expectedOutput := strings.ReplaceAll(tc.expectedOutput, "<location>", fmt.Sprintf("%s:%d", path.Base(file), line+1))
actualOutput := buffer.String() actualOutput := buffer.String()
actualOutput = regexp.MustCompile(`(?m)^.{30}`).ReplaceAllString(actualOutput, "") actualOutput = regexp.MustCompile(`(?m)^.{30}`).ReplaceAllString(actualOutput, "")
actualOutput = regexp.MustCompile(`(?m)^warnings\.go:\d+`).ReplaceAllString(actualOutput, "warnings.go")
assert.Equal(t, expectedOutput, actualOutput) assert.Equal(t, expectedOutput, actualOutput)
}) })
} }
} }
func TestRequestWarningHandler(t *testing.T) {
t.Run("no-context", func(t *testing.T) {
request := &Request{}
handler := &fakeWarningHandlerWithLogging{}
//nolint:logcheck
assert.Equal(t, request, request.WarningHandler(handler))
assert.NotNil(t, request.warningHandler)
request.warningHandler.HandleWarningHeaderWithContext(context.Background(), 0, "", "message")
assert.Equal(t, []string{"message"}, handler.messages)
})
t.Run("with-context", func(t *testing.T) {
request := &Request{}
handler := &fakeWarningHandlerWithContext{}
assert.Equal(t, request, request.WarningHandlerWithContext(handler))
assert.Equal(t, request.warningHandler, handler)
})
t.Run("nil-no-context", func(t *testing.T) {
request := &Request{
warningHandler: &fakeWarningHandlerWithContext{},
}
//nolint:logcheck
assert.Equal(t, request, request.WarningHandler(nil))
assert.Nil(t, request.warningHandler)
})
t.Run("nil-with-context", func(t *testing.T) {
request := &Request{
warningHandler: &fakeWarningHandlerWithContext{},
}
assert.Equal(t, request, request.WarningHandlerWithContext(nil))
assert.Nil(t, request.warningHandler)
})
}

View File

@ -17,6 +17,8 @@ limitations under the License.
package rest package rest
import ( import (
"context"
"fmt"
"net/url" "net/url"
"time" "time"
@ -32,12 +34,24 @@ import (
var serverIsOverloadedSet = sets.NewInt(429) var serverIsOverloadedSet = sets.NewInt(429)
var maxResponseCode = 499 var maxResponseCode = 499
//go:generate mockery
// Deprecated: BackoffManager.Sleep ignores the caller's context. Use BackoffManagerWithContext instead.
type BackoffManager interface { type BackoffManager interface {
UpdateBackoff(actualUrl *url.URL, err error, responseCode int) UpdateBackoff(actualURL *url.URL, err error, responseCode int)
CalculateBackoff(actualUrl *url.URL) time.Duration CalculateBackoff(actualURL *url.URL) time.Duration
Sleep(d time.Duration) Sleep(d time.Duration)
} }
type BackoffManagerWithContext interface {
UpdateBackoffWithContext(ctx context.Context, actualURL *url.URL, err error, responseCode int)
CalculateBackoffWithContext(ctx context.Context, actualURL *url.URL) time.Duration
SleepWithContext(ctx context.Context, d time.Duration)
}
var _ BackoffManager = &URLBackoff{}
var _ BackoffManagerWithContext = &URLBackoff{}
// URLBackoff struct implements the semantics on top of Backoff which // URLBackoff struct implements the semantics on top of Backoff which
// we need for URL specific exponential backoff. // we need for URL specific exponential backoff.
type URLBackoff struct { type URLBackoff struct {
@ -49,11 +63,19 @@ type URLBackoff struct {
type NoBackoff struct { type NoBackoff struct {
} }
func (n *NoBackoff) UpdateBackoff(actualUrl *url.URL, err error, responseCode int) { func (n *NoBackoff) UpdateBackoff(actualURL *url.URL, err error, responseCode int) {
// do nothing. // do nothing.
} }
func (n *NoBackoff) CalculateBackoff(actualUrl *url.URL) time.Duration { func (n *NoBackoff) UpdateBackoffWithContext(ctx context.Context, actualURL *url.URL, err error, responseCode int) {
// do nothing.
}
func (n *NoBackoff) CalculateBackoff(actualURL *url.URL) time.Duration {
return 0 * time.Second
}
func (n *NoBackoff) CalculateBackoffWithContext(ctx context.Context, actualURL *url.URL) time.Duration {
return 0 * time.Second return 0 * time.Second
} }
@ -61,10 +83,21 @@ func (n *NoBackoff) Sleep(d time.Duration) {
time.Sleep(d) time.Sleep(d)
} }
func (n *NoBackoff) SleepWithContext(ctx context.Context, d time.Duration) {
if d == 0 {
return
}
t := time.NewTimer(d)
defer t.Stop()
select {
case <-ctx.Done():
case <-t.C:
}
}
// Disable makes the backoff trivial, i.e., sets it to zero. This might be used // Disable makes the backoff trivial, i.e., sets it to zero. This might be used
// by tests which want to run 1000s of mock requests without slowing down. // by tests which want to run 1000s of mock requests without slowing down.
func (b *URLBackoff) Disable() { func (b *URLBackoff) Disable() {
klog.V(4).Infof("Disabling backoff strategy")
b.Backoff = flowcontrol.NewBackOff(0*time.Second, 0*time.Second) b.Backoff = flowcontrol.NewBackOff(0*time.Second, 0*time.Second)
} }
@ -76,32 +109,74 @@ func (b *URLBackoff) baseUrlKey(rawurl *url.URL) string {
// in the future. // in the future.
host, err := url.Parse(rawurl.String()) host, err := url.Parse(rawurl.String())
if err != nil { if err != nil {
klog.V(4).Infof("Error extracting url: %v", rawurl) panic(fmt.Sprintf("Error parsing bad URL %q: %v", rawurl, err))
panic("bad url!")
} }
return host.Host return host.Host
} }
// UpdateBackoff updates backoff metadata // UpdateBackoff updates backoff metadata
func (b *URLBackoff) UpdateBackoff(actualUrl *url.URL, err error, responseCode int) { func (b *URLBackoff) UpdateBackoff(actualURL *url.URL, err error, responseCode int) {
b.UpdateBackoffWithContext(context.Background(), actualURL, err, responseCode)
}
// UpdateBackoffWithContext updates backoff metadata
func (b *URLBackoff) UpdateBackoffWithContext(ctx context.Context, actualURL *url.URL, err error, responseCode int) {
// range for retry counts that we store is [0,13] // range for retry counts that we store is [0,13]
if responseCode > maxResponseCode || serverIsOverloadedSet.Has(responseCode) { if responseCode > maxResponseCode || serverIsOverloadedSet.Has(responseCode) {
b.Backoff.Next(b.baseUrlKey(actualUrl), b.Backoff.Clock.Now()) b.Backoff.Next(b.baseUrlKey(actualURL), b.Backoff.Clock.Now())
return return
} else if responseCode >= 300 || err != nil { } else if responseCode >= 300 || err != nil {
klog.V(4).Infof("Client is returning errors: code %v, error %v", responseCode, err) klog.FromContext(ctx).V(4).Info("Client is returning errors", "code", responseCode, "err", err)
} }
//If we got this far, there is no backoff required for this URL anymore. //If we got this far, there is no backoff required for this URL anymore.
b.Backoff.Reset(b.baseUrlKey(actualUrl)) b.Backoff.Reset(b.baseUrlKey(actualURL))
} }
// CalculateBackoff takes a url and back's off exponentially, // CalculateBackoff takes a url and back's off exponentially,
// based on its knowledge of existing failures. // based on its knowledge of existing failures.
func (b *URLBackoff) CalculateBackoff(actualUrl *url.URL) time.Duration { func (b *URLBackoff) CalculateBackoff(actualURL *url.URL) time.Duration {
return b.Backoff.Get(b.baseUrlKey(actualUrl)) return b.Backoff.Get(b.baseUrlKey(actualURL))
}
// CalculateBackoffWithContext takes a url and back's off exponentially,
// based on its knowledge of existing failures.
func (b *URLBackoff) CalculateBackoffWithContext(ctx context.Context, actualURL *url.URL) time.Duration {
return b.Backoff.Get(b.baseUrlKey(actualURL))
} }
func (b *URLBackoff) Sleep(d time.Duration) { func (b *URLBackoff) Sleep(d time.Duration) {
b.Backoff.Clock.Sleep(d) b.Backoff.Clock.Sleep(d)
} }
func (b *URLBackoff) SleepWithContext(ctx context.Context, d time.Duration) {
if d == 0 {
return
}
t := b.Backoff.Clock.NewTimer(d)
defer t.Stop()
select {
case <-ctx.Done():
case <-t.C():
}
}
// backoffManagerNopContext wraps a BackoffManager and adds the *WithContext methods.
type backoffManagerNopContext struct {
BackoffManager
}
var _ BackoffManager = &backoffManagerNopContext{}
var _ BackoffManagerWithContext = &backoffManagerNopContext{}
func (b *backoffManagerNopContext) UpdateBackoffWithContext(ctx context.Context, actualURL *url.URL, err error, responseCode int) {
b.UpdateBackoff(actualURL, err, responseCode)
}
func (b *backoffManagerNopContext) CalculateBackoffWithContext(ctx context.Context, actualURL *url.URL) time.Duration {
return b.CalculateBackoff(actualURL)
}
func (b *backoffManagerNopContext) SleepWithContext(ctx context.Context, d time.Duration) {
b.Sleep(d)
}

View File

@ -17,10 +17,14 @@ limitations under the License.
package rest package rest
import ( import (
"context"
"errors"
"net/url" "net/url"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
"k8s.io/client-go/util/flowcontrol" "k8s.io/client-go/util/flowcontrol"
) )
@ -77,3 +81,38 @@ func TestURLBackoffFunctionality(t *testing.T) {
t.Errorf("The final return code %v should have resulted in a backoff ! ", returnCodes[7]) t.Errorf("The final return code %v should have resulted in a backoff ! ", returnCodes[7])
} }
} }
func TestBackoffManagerNopContext(t *testing.T) {
mock := NewMockBackoffManager(t)
sleepDuration := 42 * time.Second
mock.On("Sleep", sleepDuration).Return()
url := &url.URL{}
mock.On("CalculateBackoff", url).Return(time.Second)
err := errors.New("fake error")
responseCode := 404
mock.On("UpdateBackoff", url, err, responseCode).Return()
ctx := context.Background()
wrapper := backoffManagerNopContext{BackoffManager: mock}
wrapper.SleepWithContext(ctx, sleepDuration)
wrapper.CalculateBackoffWithContext(ctx, url)
wrapper.UpdateBackoffWithContext(ctx, url, err, responseCode)
}
func TestNoBackoff(t *testing.T) {
var backoff NoBackoff
assert.Equal(t, 0*time.Second, backoff.CalculateBackoff(nil))
assert.Equal(t, 0*time.Second, backoff.CalculateBackoffWithContext(context.Background(), nil))
start := time.Now()
backoff.Sleep(0 * time.Second)
assert.WithinDuration(t, start, time.Now(), time.Minute /* pretty generous, but we don't want to flake */, time.Since(start), "backoff.Sleep")
// Cancel right away to prevent sleeping.
ctx, cancel := context.WithCancel(context.Background())
cancel()
start = time.Now()
backoff.SleepWithContext(ctx, 10*time.Minute)
assert.WithinDuration(t, start, time.Now(), time.Minute /* pretty generous, but we don't want to flake */, time.Since(start), "backoff.SleepWithContext")
}

View File

@ -17,6 +17,7 @@ limitations under the License.
package rest package rest
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@ -33,8 +34,15 @@ type WarningHandler interface {
HandleWarningHeader(code int, agent string, text string) HandleWarningHeader(code int, agent string, text string)
} }
// WarningHandlerWithContext is an interface for handling warning headers with
// support for contextual logging.
type WarningHandlerWithContext interface {
// HandleWarningHeaderWithContext is called with the warn code, agent, and text when a warning header is countered.
HandleWarningHeaderWithContext(ctx context.Context, code int, agent string, text string)
}
var ( var (
defaultWarningHandler WarningHandler = WarningLogger{} defaultWarningHandler WarningHandlerWithContext = WarningLogger{}
defaultWarningHandlerLock sync.RWMutex defaultWarningHandlerLock sync.RWMutex
) )
@ -43,33 +51,68 @@ var (
// - NoWarnings suppresses warnings. // - NoWarnings suppresses warnings.
// - WarningLogger logs warnings. // - WarningLogger logs warnings.
// - NewWarningWriter() outputs warnings to the provided writer. // - NewWarningWriter() outputs warnings to the provided writer.
//
// logcheck:context // SetDefaultWarningHandlerWithContext should be used instead of SetDefaultWarningHandler in code which supports contextual logging.
func SetDefaultWarningHandler(l WarningHandler) { func SetDefaultWarningHandler(l WarningHandler) {
if l == nil {
SetDefaultWarningHandlerWithContext(nil)
return
}
SetDefaultWarningHandlerWithContext(warningLoggerNopContext{l: l})
}
// SetDefaultWarningHandlerWithContext is a variant of [SetDefaultWarningHandler] which supports contextual logging.
func SetDefaultWarningHandlerWithContext(l WarningHandlerWithContext) {
defaultWarningHandlerLock.Lock() defaultWarningHandlerLock.Lock()
defer defaultWarningHandlerLock.Unlock() defer defaultWarningHandlerLock.Unlock()
defaultWarningHandler = l defaultWarningHandler = l
} }
func getDefaultWarningHandler() WarningHandler {
func getDefaultWarningHandler() WarningHandlerWithContext {
defaultWarningHandlerLock.RLock() defaultWarningHandlerLock.RLock()
defer defaultWarningHandlerLock.RUnlock() defer defaultWarningHandlerLock.RUnlock()
l := defaultWarningHandler l := defaultWarningHandler
return l return l
} }
// NoWarnings is an implementation of WarningHandler that suppresses warnings. type warningLoggerNopContext struct {
l WarningHandler
}
func (w warningLoggerNopContext) HandleWarningHeaderWithContext(_ context.Context, code int, agent string, message string) {
w.l.HandleWarningHeader(code, agent, message)
}
// NoWarnings is an implementation of [WarningHandler] and [WarningHandlerWithContext] that suppresses warnings.
type NoWarnings struct{} type NoWarnings struct{}
func (NoWarnings) HandleWarningHeader(code int, agent string, message string) {} func (NoWarnings) HandleWarningHeader(code int, agent string, message string) {}
func (NoWarnings) HandleWarningHeaderWithContext(ctx context.Context, code int, agent string, message string) {
}
// WarningLogger is an implementation of WarningHandler that logs code 299 warnings var _ WarningHandler = NoWarnings{}
var _ WarningHandlerWithContext = NoWarnings{}
// WarningLogger is an implementation of [WarningHandler] and [WarningHandlerWithContext] that logs code 299 warnings
type WarningLogger struct{} type WarningLogger struct{}
func (WarningLogger) HandleWarningHeader(code int, agent string, message string) { func (WarningLogger) HandleWarningHeader(code int, agent string, message string) {
if code != 299 || len(message) == 0 { if code != 299 || len(message) == 0 {
return return
} }
klog.Warning(message) klog.Background().Info("Warning: " + message)
} }
func (WarningLogger) HandleWarningHeaderWithContext(ctx context.Context, code int, agent string, message string) {
if code != 299 || len(message) == 0 {
return
}
klog.FromContext(ctx).Info("Warning: " + message)
}
var _ WarningHandler = WarningLogger{}
var _ WarningHandlerWithContext = WarningLogger{}
type warningWriter struct { type warningWriter struct {
// out is the writer to output warnings to // out is the writer to output warnings to
out io.Writer out io.Writer
@ -134,14 +177,14 @@ func (w *warningWriter) WarningCount() int {
return w.writtenCount return w.writtenCount
} }
func handleWarnings(headers http.Header, handler WarningHandler) []net.WarningHeader { func handleWarnings(ctx context.Context, headers http.Header, handler WarningHandlerWithContext) []net.WarningHeader {
if handler == nil { if handler == nil {
handler = getDefaultWarningHandler() handler = getDefaultWarningHandler()
} }
warnings, _ := net.ParseWarningHeaders(headers["Warning"]) warnings, _ := net.ParseWarningHeaders(headers["Warning"])
for _, warning := range warnings { for _, warning := range warnings {
handler.HandleWarningHeader(warning.Code, warning.Agent, warning.Text) handler.HandleWarningHeaderWithContext(ctx, warning.Code, warning.Agent, warning.Text)
} }
return warnings return warnings
} }

View File

@ -0,0 +1,57 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package rest
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDefaultWarningHandler(t *testing.T) {
t.Run("default", func(t *testing.T) {
assert.IsType(t, WarningHandlerWithContext(WarningLogger{}), getDefaultWarningHandler())
})
deferRestore := func(t *testing.T) {
handler := getDefaultWarningHandler()
t.Cleanup(func() {
SetDefaultWarningHandlerWithContext(handler)
})
}
t.Run("no-context", func(t *testing.T) {
deferRestore(t)
handler := &fakeWarningHandlerWithLogging{}
//nolint:logcheck
SetDefaultWarningHandler(handler)
getDefaultWarningHandler().HandleWarningHeaderWithContext(context.Background(), 0, "", "message")
assert.Equal(t, []string{"message"}, handler.messages)
SetDefaultWarningHandler(nil)
assert.Nil(t, getDefaultWarningHandler())
})
t.Run("with-context", func(t *testing.T) {
deferRestore(t)
handler := &fakeWarningHandlerWithContext{}
SetDefaultWarningHandlerWithContext(handler)
assert.Equal(t, handler, getDefaultWarningHandler())
SetDefaultWarningHandlerWithContext(nil)
assert.Nil(t, getDefaultWarningHandler())
})
}

View File

@ -209,18 +209,18 @@ func (r *withRetry) Before(ctx context.Context, request *Request) error {
// we do a backoff sleep before the first attempt is made, // we do a backoff sleep before the first attempt is made,
// (preserving current behavior). // (preserving current behavior).
if request.backoff != nil { if request.backoff != nil {
request.backoff.Sleep(request.backoff.CalculateBackoff(url)) request.backoff.SleepWithContext(ctx, request.backoff.CalculateBackoffWithContext(ctx, url))
} }
return nil return nil
} }
// if we are here, we have made attempt(s) at least once before. // if we are here, we have made attempt(s) at least once before.
if request.backoff != nil { if request.backoff != nil {
delay := request.backoff.CalculateBackoff(url) delay := request.backoff.CalculateBackoffWithContext(ctx, url)
if r.retryAfter.Wait > delay { if r.retryAfter.Wait > delay {
delay = r.retryAfter.Wait delay = r.retryAfter.Wait
} }
request.backoff.Sleep(delay) request.backoff.SleepWithContext(ctx, delay)
} }
// We are retrying the request that we already send to // We are retrying the request that we already send to
@ -231,7 +231,7 @@ func (r *withRetry) Before(ctx context.Context, request *Request) error {
return err return err
} }
klog.V(4).Infof("Got a Retry-After %s response for attempt %d to %v", r.retryAfter.Wait, r.retryAfter.Attempt, request.URL().String()) klog.FromContext(ctx).V(4).Info("Got a Retry-After response", "delay", r.retryAfter.Wait, "attempt", r.retryAfter.Attempt, "url", request.URL())
return nil return nil
} }
@ -258,9 +258,9 @@ func (r *withRetry) After(ctx context.Context, request *Request, resp *http.Resp
if request.c.base != nil { if request.c.base != nil {
if err != nil { if err != nil {
request.backoff.UpdateBackoff(request.URL(), err, 0) request.backoff.UpdateBackoffWithContext(ctx, request.URL(), err, 0)
} else { } else {
request.backoff.UpdateBackoff(request.URL(), err, resp.StatusCode) request.backoff.UpdateBackoffWithContext(ctx, request.URL(), err, resp.StatusCode)
} }
} }
} }

View File

@ -168,6 +168,7 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -70,6 +70,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=

View File

@ -131,6 +131,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -79,6 +79,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -164,6 +164,7 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -139,6 +139,7 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -109,6 +109,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -167,6 +167,7 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -131,6 +131,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=

View File

@ -79,6 +79,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -164,6 +164,7 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -164,6 +164,7 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -79,6 +79,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=