Update to kube v1.17

Signed-off-by: Humble Chirammal <hchiramm@redhat.com>
This commit is contained in:
Humble Chirammal
2020-01-14 16:08:55 +05:30
committed by mergify[bot]
parent 327fcd1b1b
commit 3af1e26d7c
1710 changed files with 289562 additions and 168638 deletions

View File

@ -0,0 +1,234 @@
/*
Copyright 2017 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 webhook
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"strconv"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)
// AuthenticationInfoResolverWrapper can be used to inject Dial function to the
// rest.Config generated by the resolver.
type AuthenticationInfoResolverWrapper func(AuthenticationInfoResolver) AuthenticationInfoResolver
// NewDefaultAuthenticationInfoResolverWrapper builds a default authn resolver wrapper
func NewDefaultAuthenticationInfoResolverWrapper(
proxyTransport *http.Transport,
kubeapiserverClientConfig *rest.Config) AuthenticationInfoResolverWrapper {
webhookAuthResolverWrapper := func(delegate AuthenticationInfoResolver) AuthenticationInfoResolver {
return &AuthenticationInfoResolverDelegator{
ClientConfigForFunc: func(hostPort string) (*rest.Config, error) {
if hostPort == "kubernetes.default.svc:443" {
return kubeapiserverClientConfig, nil
}
return delegate.ClientConfigFor(hostPort)
},
ClientConfigForServiceFunc: func(serviceName, serviceNamespace string, servicePort int) (*rest.Config, error) {
if serviceName == "kubernetes" && serviceNamespace == corev1.NamespaceDefault && servicePort == 443 {
return kubeapiserverClientConfig, nil
}
ret, err := delegate.ClientConfigForService(serviceName, serviceNamespace, servicePort)
if err != nil {
return nil, err
}
if proxyTransport != nil && proxyTransport.DialContext != nil {
ret.Dial = proxyTransport.DialContext
}
return ret, err
},
}
}
return webhookAuthResolverWrapper
}
// AuthenticationInfoResolver builds rest.Config base on the server or service
// name and service namespace.
type AuthenticationInfoResolver interface {
// ClientConfigFor builds rest.Config based on the hostPort.
ClientConfigFor(hostPort string) (*rest.Config, error)
// ClientConfigForService builds rest.Config based on the serviceName and
// serviceNamespace.
ClientConfigForService(serviceName, serviceNamespace string, servicePort int) (*rest.Config, error)
}
// AuthenticationInfoResolverDelegator implements AuthenticationInfoResolver.
type AuthenticationInfoResolverDelegator struct {
ClientConfigForFunc func(hostPort string) (*rest.Config, error)
ClientConfigForServiceFunc func(serviceName, serviceNamespace string, servicePort int) (*rest.Config, error)
}
// ClientConfigFor returns client config for given hostPort.
func (a *AuthenticationInfoResolverDelegator) ClientConfigFor(hostPort string) (*rest.Config, error) {
return a.ClientConfigForFunc(hostPort)
}
// ClientConfigForService returns client config for given service.
func (a *AuthenticationInfoResolverDelegator) ClientConfigForService(serviceName, serviceNamespace string, servicePort int) (*rest.Config, error) {
return a.ClientConfigForServiceFunc(serviceName, serviceNamespace, servicePort)
}
type defaultAuthenticationInfoResolver struct {
kubeconfig clientcmdapi.Config
}
// NewDefaultAuthenticationInfoResolver generates an AuthenticationInfoResolver
// that builds rest.Config based on the kubeconfig file. kubeconfigFile is the
// path to the kubeconfig.
func NewDefaultAuthenticationInfoResolver(kubeconfigFile string) (AuthenticationInfoResolver, error) {
if len(kubeconfigFile) == 0 {
return &defaultAuthenticationInfoResolver{}, nil
}
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeconfigFile
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
clientConfig, err := loader.RawConfig()
if err != nil {
return nil, err
}
return &defaultAuthenticationInfoResolver{kubeconfig: clientConfig}, nil
}
func (c *defaultAuthenticationInfoResolver) ClientConfigFor(hostPort string) (*rest.Config, error) {
return c.clientConfig(hostPort)
}
func (c *defaultAuthenticationInfoResolver) ClientConfigForService(serviceName, serviceNamespace string, servicePort int) (*rest.Config, error) {
return c.clientConfig(net.JoinHostPort(serviceName+"."+serviceNamespace+".svc", strconv.Itoa(servicePort)))
}
func (c *defaultAuthenticationInfoResolver) clientConfig(target string) (*rest.Config, error) {
// exact match
if authConfig, ok := c.kubeconfig.AuthInfos[target]; ok {
return restConfigFromKubeconfig(authConfig)
}
// star prefixed match
serverSteps := strings.Split(target, ".")
for i := 1; i < len(serverSteps); i++ {
nickName := "*." + strings.Join(serverSteps[i:], ".")
if authConfig, ok := c.kubeconfig.AuthInfos[nickName]; ok {
return restConfigFromKubeconfig(authConfig)
}
}
// If target included the default https port (443), search again without the port
if target, port, err := net.SplitHostPort(target); err == nil && port == "443" {
// exact match without port
if authConfig, ok := c.kubeconfig.AuthInfos[target]; ok {
return restConfigFromKubeconfig(authConfig)
}
// star prefixed match without port
serverSteps := strings.Split(target, ".")
for i := 1; i < len(serverSteps); i++ {
nickName := "*." + strings.Join(serverSteps[i:], ".")
if authConfig, ok := c.kubeconfig.AuthInfos[nickName]; ok {
return restConfigFromKubeconfig(authConfig)
}
}
}
// if we're trying to hit the kube-apiserver and there wasn't an explicit config, use the in-cluster config
if target == "kubernetes.default.svc:443" {
// if we can find an in-cluster-config use that. If we can't, fall through.
inClusterConfig, err := rest.InClusterConfig()
if err == nil {
return setGlobalDefaults(inClusterConfig), nil
}
}
// star (default) match
if authConfig, ok := c.kubeconfig.AuthInfos["*"]; ok {
return restConfigFromKubeconfig(authConfig)
}
// use the current context from the kubeconfig if possible
if len(c.kubeconfig.CurrentContext) > 0 {
if currContext, ok := c.kubeconfig.Contexts[c.kubeconfig.CurrentContext]; ok {
if len(currContext.AuthInfo) > 0 {
if currAuth, ok := c.kubeconfig.AuthInfos[currContext.AuthInfo]; ok {
return restConfigFromKubeconfig(currAuth)
}
}
}
}
// anonymous
return setGlobalDefaults(&rest.Config{}), nil
}
func restConfigFromKubeconfig(configAuthInfo *clientcmdapi.AuthInfo) (*rest.Config, error) {
config := &rest.Config{}
// blindly overwrite existing values based on precedence
if len(configAuthInfo.Token) > 0 {
config.BearerToken = configAuthInfo.Token
config.BearerTokenFile = configAuthInfo.TokenFile
} else if len(configAuthInfo.TokenFile) > 0 {
tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile)
if err != nil {
return nil, err
}
config.BearerToken = string(tokenBytes)
config.BearerTokenFile = configAuthInfo.TokenFile
}
if len(configAuthInfo.Impersonate) > 0 {
config.Impersonate = rest.ImpersonationConfig{
UserName: configAuthInfo.Impersonate,
Groups: configAuthInfo.ImpersonateGroups,
Extra: configAuthInfo.ImpersonateUserExtra,
}
}
if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 {
config.CertFile = configAuthInfo.ClientCertificate
config.CertData = configAuthInfo.ClientCertificateData
config.KeyFile = configAuthInfo.ClientKey
config.KeyData = configAuthInfo.ClientKeyData
}
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
config.Username = configAuthInfo.Username
config.Password = configAuthInfo.Password
}
if configAuthInfo.Exec != nil {
config.ExecProvider = configAuthInfo.Exec.DeepCopy()
}
if configAuthInfo.AuthProvider != nil {
return nil, fmt.Errorf("auth provider not supported")
}
return setGlobalDefaults(config), nil
}
func setGlobalDefaults(config *rest.Config) *rest.Config {
config.UserAgent = "kube-apiserver-admission"
config.Timeout = 30 * time.Second
return config
}

224
vendor/k8s.io/apiserver/pkg/util/webhook/client.go generated vendored Normal file
View File

@ -0,0 +1,224 @@
/*
Copyright 2017 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 webhook
import (
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"github.com/hashicorp/golang-lru"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/rest"
)
const (
defaultCacheSize = 200
)
// ClientConfig defines parameters required for creating a hook client.
type ClientConfig struct {
Name string
URL string
CABundle []byte
Service *ClientConfigService
}
// ClientConfigService defines service discovery parameters of the webhook.
type ClientConfigService struct {
Name string
Namespace string
Path string
Port int32
}
// ClientManager builds REST clients to talk to webhooks. It caches the clients
// to avoid duplicate creation.
type ClientManager struct {
authInfoResolver AuthenticationInfoResolver
serviceResolver ServiceResolver
negotiatedSerializer runtime.NegotiatedSerializer
cache *lru.Cache
}
// NewClientManager creates a clientManager.
func NewClientManager(gvs []schema.GroupVersion, addToSchemaFuncs ...func(s *runtime.Scheme) error) (ClientManager, error) {
cache, err := lru.New(defaultCacheSize)
if err != nil {
return ClientManager{}, err
}
hookScheme := runtime.NewScheme()
for _, addToSchemaFunc := range addToSchemaFuncs {
if err := addToSchemaFunc(hookScheme); err != nil {
return ClientManager{}, err
}
}
return ClientManager{
cache: cache,
negotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
Serializer: serializer.NewCodecFactory(hookScheme).LegacyCodec(gvs...),
}),
}, nil
}
// SetAuthenticationInfoResolverWrapper sets the
// AuthenticationInfoResolverWrapper.
func (cm *ClientManager) SetAuthenticationInfoResolverWrapper(wrapper AuthenticationInfoResolverWrapper) {
if wrapper != nil {
cm.authInfoResolver = wrapper(cm.authInfoResolver)
}
}
// SetAuthenticationInfoResolver sets the AuthenticationInfoResolver.
func (cm *ClientManager) SetAuthenticationInfoResolver(resolver AuthenticationInfoResolver) {
cm.authInfoResolver = resolver
}
// SetServiceResolver sets the ServiceResolver.
func (cm *ClientManager) SetServiceResolver(sr ServiceResolver) {
if sr != nil {
cm.serviceResolver = sr
}
}
// Validate checks if ClientManager is properly set up.
func (cm *ClientManager) Validate() error {
var errs []error
if cm.negotiatedSerializer == nil {
errs = append(errs, fmt.Errorf("the clientManager requires a negotiatedSerializer"))
}
if cm.serviceResolver == nil {
errs = append(errs, fmt.Errorf("the clientManager requires a serviceResolver"))
}
if cm.authInfoResolver == nil {
errs = append(errs, fmt.Errorf("the clientManager requires an authInfoResolver"))
}
return utilerrors.NewAggregate(errs)
}
// HookClient get a RESTClient from the cache, or constructs one based on the
// webhook configuration.
func (cm *ClientManager) HookClient(cc ClientConfig) (*rest.RESTClient, error) {
ccWithNoName := cc
ccWithNoName.Name = ""
cacheKey, err := json.Marshal(ccWithNoName)
if err != nil {
return nil, err
}
if client, ok := cm.cache.Get(string(cacheKey)); ok {
return client.(*rest.RESTClient), nil
}
complete := func(cfg *rest.Config) (*rest.RESTClient, error) {
// Avoid client-side rate limiting talking to the webhook backend.
// Rate limiting should happen when deciding how many requests to serve.
cfg.QPS = -1
// Combine CAData from the config with any existing CA bundle provided
if len(cfg.TLSClientConfig.CAData) > 0 {
cfg.TLSClientConfig.CAData = append(cfg.TLSClientConfig.CAData, '\n')
}
cfg.TLSClientConfig.CAData = append(cfg.TLSClientConfig.CAData, cc.CABundle...)
// Use http/1.1 instead of http/2.
// This is a workaround for http/2-enabled clients not load-balancing concurrent requests to multiple backends.
// See http://issue.k8s.io/75791 for details.
cfg.NextProtos = []string{"http/1.1"}
cfg.ContentConfig.NegotiatedSerializer = cm.negotiatedSerializer
cfg.ContentConfig.ContentType = runtime.ContentTypeJSON
client, err := rest.UnversionedRESTClientFor(cfg)
if err == nil {
cm.cache.Add(string(cacheKey), client)
}
return client, err
}
if cc.Service != nil {
port := cc.Service.Port
if port == 0 {
// Default to port 443 if no service port is specified
port = 443
}
restConfig, err := cm.authInfoResolver.ClientConfigForService(cc.Service.Name, cc.Service.Namespace, int(port))
if err != nil {
return nil, err
}
cfg := rest.CopyConfig(restConfig)
serverName := cc.Service.Name + "." + cc.Service.Namespace + ".svc"
host := net.JoinHostPort(serverName, strconv.Itoa(int(port)))
cfg.Host = "https://" + host
cfg.APIPath = cc.Service.Path
// Set the server name if not already set
if len(cfg.TLSClientConfig.ServerName) == 0 {
cfg.TLSClientConfig.ServerName = serverName
}
delegateDialer := cfg.Dial
if delegateDialer == nil {
var d net.Dialer
delegateDialer = d.DialContext
}
cfg.Dial = func(ctx context.Context, network, addr string) (net.Conn, error) {
if addr == host {
u, err := cm.serviceResolver.ResolveEndpoint(cc.Service.Namespace, cc.Service.Name, port)
if err != nil {
return nil, err
}
addr = u.Host
}
return delegateDialer(ctx, network, addr)
}
return complete(cfg)
}
if cc.URL == "" {
return nil, &ErrCallingWebhook{WebhookName: cc.Name, Reason: errors.New("webhook configuration must have either service or URL")}
}
u, err := url.Parse(cc.URL)
if err != nil {
return nil, &ErrCallingWebhook{WebhookName: cc.Name, Reason: fmt.Errorf("Unparsable URL: %v", err)}
}
hostPort := u.Host
if len(u.Port()) == 0 {
// Default to port 443 if no port is specified
hostPort = net.JoinHostPort(hostPort, "443")
}
restConfig, err := cm.authInfoResolver.ClientConfigFor(hostPort)
if err != nil {
return nil, err
}
cfg := rest.CopyConfig(restConfig)
cfg.Host = u.Scheme + "://" + u.Host
cfg.APIPath = u.Path
return complete(cfg)
}

47
vendor/k8s.io/apiserver/pkg/util/webhook/error.go generated vendored Normal file
View File

@ -0,0 +1,47 @@
/*
Copyright 2017 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 webhook
import (
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
// ErrCallingWebhook is returned for transport-layer errors calling webhooks. It
// represents a failure to talk to the webhook, not the webhook rejecting a
// request.
type ErrCallingWebhook struct {
WebhookName string
Reason error
}
func (e *ErrCallingWebhook) Error() string {
if e.Reason != nil {
return fmt.Sprintf("failed calling webhook %q: %v", e.WebhookName, e.Reason)
}
return fmt.Sprintf("failed calling webhook %q; no further details available", e.WebhookName)
}
// ErrWebhookRejection represents a webhook properly rejecting a request.
type ErrWebhookRejection struct {
Status *apierrors.StatusError
}
func (e *ErrWebhookRejection) Error() string {
return e.Status.Error()
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2017 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 webhook
import (
"errors"
"fmt"
"net/url"
)
// ServiceResolver knows how to convert a service reference into an actual location.
type ServiceResolver interface {
ResolveEndpoint(namespace, name string, port int32) (*url.URL, error)
}
type defaultServiceResolver struct{}
// NewDefaultServiceResolver creates a new default server resolver.
func NewDefaultServiceResolver() ServiceResolver {
return &defaultServiceResolver{}
}
// ResolveEndpoint constructs a service URL from a given namespace and name
// note that the name, namespace, and port are required and by default all
// created addresses use HTTPS scheme.
// for example:
// name=ross namespace=andromeda resolves to https://ross.andromeda.svc:443
func (sr defaultServiceResolver) ResolveEndpoint(namespace, name string, port int32) (*url.URL, error) {
if len(name) == 0 || len(namespace) == 0 || port == 0 {
return nil, errors.New("cannot resolve an empty service name or namespace or port")
}
return &url.URL{Scheme: "https", Host: fmt.Sprintf("%s.%s.svc:%d", name, namespace, port)}, nil
}

105
vendor/k8s.io/apiserver/pkg/util/webhook/validation.go generated vendored Normal file
View File

@ -0,0 +1,105 @@
/*
Copyright 2018 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 webhook
import (
"fmt"
"net/url"
"strings"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
)
// ValidateWebhookURL validates webhook's URL.
func ValidateWebhookURL(fldPath *field.Path, URL string, forceHttps bool) field.ErrorList {
var allErrors field.ErrorList
const form = "; desired format: https://host[/path]"
if u, err := url.Parse(URL); err != nil {
allErrors = append(allErrors, field.Required(fldPath, "url must be a valid URL: "+err.Error()+form))
} else {
if forceHttps && u.Scheme != "https" {
allErrors = append(allErrors, field.Invalid(fldPath, u.Scheme, "'https' is the only allowed URL scheme"+form))
}
if len(u.Host) == 0 {
allErrors = append(allErrors, field.Invalid(fldPath, u.Host, "host must be provided"+form))
}
if u.User != nil {
allErrors = append(allErrors, field.Invalid(fldPath, u.User.String(), "user information is not permitted in the URL"))
}
if len(u.Fragment) != 0 {
allErrors = append(allErrors, field.Invalid(fldPath, u.Fragment, "fragments are not permitted in the URL"))
}
if len(u.RawQuery) != 0 {
allErrors = append(allErrors, field.Invalid(fldPath, u.RawQuery, "query parameters are not permitted in the URL"))
}
}
return allErrors
}
func ValidateWebhookService(fldPath *field.Path, namespace, name string, path *string, port int32) field.ErrorList {
var allErrors field.ErrorList
if len(name) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("name"), "service name is required"))
}
if len(namespace) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("namespace"), "service namespace is required"))
}
if errs := validation.IsValidPortNum(int(port)); errs != nil {
allErrors = append(allErrors, field.Invalid(fldPath.Child("port"), port, "port is not valid: "+strings.Join(errs, ", ")))
}
if path == nil {
return allErrors
}
// TODO: replace below with url.Parse + verifying that host is empty?
urlPath := *path
if urlPath == "/" || len(urlPath) == 0 {
return allErrors
}
if urlPath == "//" {
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "segment[0] may not be empty"))
return allErrors
}
if !strings.HasPrefix(urlPath, "/") {
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, "must start with a '/'"))
}
urlPathToCheck := urlPath[1:]
if strings.HasSuffix(urlPathToCheck, "/") {
urlPathToCheck = urlPathToCheck[:len(urlPathToCheck)-1]
}
steps := strings.Split(urlPathToCheck, "/")
for i, step := range steps {
if len(step) == 0 {
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d] may not be empty", i)))
continue
}
failures := validation.IsDNS1123Subdomain(step)
for _, failure := range failures {
allErrors = append(allErrors, field.Invalid(fldPath.Child("path"), urlPath, fmt.Sprintf("segment[%d]: %v", i, failure)))
}
}
return allErrors
}

148
vendor/k8s.io/apiserver/pkg/util/webhook/webhook.go generated vendored Normal file
View File

@ -0,0 +1,148 @@
/*
Copyright 2016 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 webhook implements a generic HTTP webhook plugin.
package webhook
import (
"context"
"fmt"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)
// defaultRequestTimeout is set for all webhook request. This is the absolute
// timeout of the HTTP request, including reading the response body.
const defaultRequestTimeout = 30 * time.Second
// GenericWebhook defines a generic client for webhooks with commonly used capabilities,
// such as retry requests.
type GenericWebhook struct {
RestClient *rest.RESTClient
InitialBackoff time.Duration
ShouldRetry func(error) bool
}
// DefaultShouldRetry is a default implementation for the GenericWebhook ShouldRetry function property.
// If the error reason is one of: networking (connection reset) or http (InternalServerError (500), GatewayTimeout (504), TooManyRequests (429)),
// or apierrors.SuggestsClientDelay() returns true, then the function advises a retry.
// Otherwise it returns false for an immediate fail.
func DefaultShouldRetry(err error) bool {
// these errors indicate a transient error that should be retried.
if net.IsConnectionReset(err) || apierrors.IsInternalError(err) || apierrors.IsTimeout(err) || apierrors.IsTooManyRequests(err) {
return true
}
// if the error sends the Retry-After header, we respect it as an explicit confirmation we should retry.
if _, shouldRetry := apierrors.SuggestsClientDelay(err); shouldRetry {
return true
}
return false
}
// NewGenericWebhook creates a new GenericWebhook from the provided kubeconfig file.
func NewGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, kubeConfigFile string, groupVersions []schema.GroupVersion, initialBackoff time.Duration) (*GenericWebhook, error) {
return newGenericWebhook(scheme, codecFactory, kubeConfigFile, groupVersions, initialBackoff, defaultRequestTimeout)
}
func newGenericWebhook(scheme *runtime.Scheme, codecFactory serializer.CodecFactory, kubeConfigFile string, groupVersions []schema.GroupVersion, initialBackoff, requestTimeout time.Duration) (*GenericWebhook, error) {
for _, groupVersion := range groupVersions {
if !scheme.IsVersionRegistered(groupVersion) {
return nil, fmt.Errorf("webhook plugin requires enabling extension resource: %s", groupVersion)
}
}
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeConfigFile
loader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{})
clientConfig, err := loader.ClientConfig()
if err != nil {
return nil, err
}
// Kubeconfigs can't set a timeout, this can only be set through a command line flag.
//
// https://github.com/kubernetes/client-go/blob/master/tools/clientcmd/overrides.go
//
// Set this to something reasonable so request to webhooks don't hang forever.
clientConfig.Timeout = requestTimeout
// Avoid client-side rate limiting talking to the webhook backend.
// Rate limiting should happen when deciding how many requests to serve.
clientConfig.QPS = -1
codec := codecFactory.LegacyCodec(groupVersions...)
clientConfig.ContentConfig.NegotiatedSerializer = serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{Serializer: codec})
restClient, err := rest.UnversionedRESTClientFor(clientConfig)
if err != nil {
return nil, err
}
return &GenericWebhook{restClient, initialBackoff, DefaultShouldRetry}, nil
}
// WithExponentialBackoff will retry webhookFn() up to 5 times with exponentially increasing backoff when
// it returns an error for which this GenericWebhook's ShouldRetry function returns true, confirming it to
// be retriable. If no ShouldRetry has been defined for the webhook, then the default one is used (DefaultShouldRetry).
func (g *GenericWebhook) WithExponentialBackoff(ctx context.Context, webhookFn func() rest.Result) rest.Result {
var result rest.Result
shouldRetry := g.ShouldRetry
if shouldRetry == nil {
shouldRetry = DefaultShouldRetry
}
WithExponentialBackoff(ctx, g.InitialBackoff, func() error {
result = webhookFn()
return result.Error()
}, shouldRetry)
return result
}
// WithExponentialBackoff will retry webhookFn up to 5 times with exponentially increasing backoff when
// it returns an error for which shouldRetry returns true, confirming it to be retriable.
func WithExponentialBackoff(ctx context.Context, initialBackoff time.Duration, webhookFn func() error, shouldRetry func(error) bool) error {
backoff := wait.Backoff{
Duration: initialBackoff,
Factor: 1.5,
Jitter: 0.2,
Steps: 5,
}
var err error
wait.ExponentialBackoff(backoff, func() (bool, error) {
err = webhookFn()
if ctx.Err() != nil {
// we timed out or were cancelled, we should not retry
return true, err
}
if shouldRetry(err) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
})
return err
}