k8s.io/apiserver@v0.31.1/pkg/util/webhook/client.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package webhook
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"net"
    25  	"net/url"
    26  	"strconv"
    27  	"strings"
    28  
    29  	"k8s.io/apimachinery/pkg/runtime"
    30  	"k8s.io/apimachinery/pkg/runtime/schema"
    31  	"k8s.io/apimachinery/pkg/runtime/serializer"
    32  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    33  	"k8s.io/apiserver/pkg/util/x509metrics"
    34  	"k8s.io/client-go/rest"
    35  	"k8s.io/utils/lru"
    36  	netutils "k8s.io/utils/net"
    37  )
    38  
    39  const (
    40  	defaultCacheSize = 200
    41  )
    42  
    43  // ClientConfig defines parameters required for creating a hook client.
    44  type ClientConfig struct {
    45  	Name     string
    46  	URL      string
    47  	CABundle []byte
    48  	Service  *ClientConfigService
    49  }
    50  
    51  // ClientConfigService defines service discovery parameters of the webhook.
    52  type ClientConfigService struct {
    53  	Name      string
    54  	Namespace string
    55  	Path      string
    56  	Port      int32
    57  }
    58  
    59  // ClientManager builds REST clients to talk to webhooks. It caches the clients
    60  // to avoid duplicate creation.
    61  type ClientManager struct {
    62  	authInfoResolver     AuthenticationInfoResolver
    63  	serviceResolver      ServiceResolver
    64  	negotiatedSerializer runtime.NegotiatedSerializer
    65  	cache                *lru.Cache
    66  }
    67  
    68  // NewClientManager creates a clientManager.
    69  func NewClientManager(gvs []schema.GroupVersion, addToSchemaFuncs ...func(s *runtime.Scheme) error) (ClientManager, error) {
    70  	cache := lru.New(defaultCacheSize)
    71  	hookScheme := runtime.NewScheme()
    72  	for _, addToSchemaFunc := range addToSchemaFuncs {
    73  		if err := addToSchemaFunc(hookScheme); err != nil {
    74  			return ClientManager{}, err
    75  		}
    76  	}
    77  	return ClientManager{
    78  		cache: cache,
    79  		negotiatedSerializer: serializer.NegotiatedSerializerWrapper(runtime.SerializerInfo{
    80  			Serializer: serializer.NewCodecFactory(hookScheme).LegacyCodec(gvs...),
    81  		}),
    82  	}, nil
    83  }
    84  
    85  // SetAuthenticationInfoResolverWrapper sets the
    86  // AuthenticationInfoResolverWrapper.
    87  func (cm *ClientManager) SetAuthenticationInfoResolverWrapper(wrapper AuthenticationInfoResolverWrapper) {
    88  	if wrapper != nil {
    89  		cm.authInfoResolver = wrapper(cm.authInfoResolver)
    90  	}
    91  }
    92  
    93  // SetAuthenticationInfoResolver sets the AuthenticationInfoResolver.
    94  func (cm *ClientManager) SetAuthenticationInfoResolver(resolver AuthenticationInfoResolver) {
    95  	cm.authInfoResolver = resolver
    96  }
    97  
    98  // SetServiceResolver sets the ServiceResolver.
    99  func (cm *ClientManager) SetServiceResolver(sr ServiceResolver) {
   100  	if sr != nil {
   101  		cm.serviceResolver = sr
   102  	}
   103  }
   104  
   105  // Validate checks if ClientManager is properly set up.
   106  func (cm *ClientManager) Validate() error {
   107  	var errs []error
   108  	if cm.negotiatedSerializer == nil {
   109  		errs = append(errs, fmt.Errorf("the clientManager requires a negotiatedSerializer"))
   110  	}
   111  	if cm.serviceResolver == nil {
   112  		errs = append(errs, fmt.Errorf("the clientManager requires a serviceResolver"))
   113  	}
   114  	if cm.authInfoResolver == nil {
   115  		errs = append(errs, fmt.Errorf("the clientManager requires an authInfoResolver"))
   116  	}
   117  	return utilerrors.NewAggregate(errs)
   118  }
   119  
   120  // HookClient get a RESTClient from the cache, or constructs one based on the
   121  // webhook configuration.
   122  func (cm *ClientManager) HookClient(cc ClientConfig) (*rest.RESTClient, error) {
   123  	ccWithNoName := cc
   124  	ccWithNoName.Name = ""
   125  	cacheKey, err := json.Marshal(ccWithNoName)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  	if client, ok := cm.cache.Get(string(cacheKey)); ok {
   130  		return client.(*rest.RESTClient), nil
   131  	}
   132  
   133  	cfg, err := cm.hookClientConfig(cc)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	client, err := rest.UnversionedRESTClientFor(cfg)
   139  	if err == nil {
   140  		cm.cache.Add(string(cacheKey), client)
   141  	}
   142  	return client, err
   143  }
   144  
   145  func (cm *ClientManager) hookClientConfig(cc ClientConfig) (*rest.Config, error) {
   146  	complete := func(cfg *rest.Config) (*rest.Config, error) {
   147  		// Avoid client-side rate limiting talking to the webhook backend.
   148  		// Rate limiting should happen when deciding how many requests to serve.
   149  		cfg.QPS = -1
   150  
   151  		// Combine CAData from the config with any existing CA bundle provided
   152  		if len(cfg.TLSClientConfig.CAData) > 0 {
   153  			cfg.TLSClientConfig.CAData = append(cfg.TLSClientConfig.CAData, '\n')
   154  		}
   155  		cfg.TLSClientConfig.CAData = append(cfg.TLSClientConfig.CAData, cc.CABundle...)
   156  
   157  		cfg.ContentConfig.NegotiatedSerializer = cm.negotiatedSerializer
   158  		cfg.ContentConfig.ContentType = runtime.ContentTypeJSON
   159  
   160  		// Add a transport wrapper that allows detection of TLS connections to
   161  		// servers with serving certificates with deprecated characteristics
   162  		cfg.Wrap(x509metrics.NewDeprecatedCertificateRoundTripperWrapperConstructor(
   163  			x509MissingSANCounter,
   164  			x509InsecureSHA1Counter,
   165  		))
   166  		return cfg, nil
   167  	}
   168  
   169  	if cc.Service != nil {
   170  		port := cc.Service.Port
   171  		if port == 0 {
   172  			// Default to port 443 if no service port is specified
   173  			port = 443
   174  		}
   175  
   176  		restConfig, err := cm.authInfoResolver.ClientConfigForService(cc.Service.Name, cc.Service.Namespace, int(port))
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  		cfg := rest.CopyConfig(restConfig)
   181  
   182  		// Use http/1.1 instead of http/2.
   183  		// This is a workaround for http/2-enabled clients not load-balancing concurrent requests to multiple backends.
   184  		// See https://issue.k8s.io/75791 for details.
   185  		cfg.NextProtos = []string{"http/1.1"}
   186  
   187  		serverName := cc.Service.Name + "." + cc.Service.Namespace + ".svc"
   188  
   189  		host := net.JoinHostPort(serverName, strconv.Itoa(int(port)))
   190  		cfg.Host = "https://" + host
   191  		cfg.APIPath = cc.Service.Path
   192  		// Set the server name if not already set
   193  		if len(cfg.TLSClientConfig.ServerName) == 0 {
   194  			cfg.TLSClientConfig.ServerName = serverName
   195  		}
   196  
   197  		delegateDialer := cfg.Dial
   198  		if delegateDialer == nil {
   199  			var d net.Dialer
   200  			delegateDialer = d.DialContext
   201  		}
   202  		cfg.Dial = func(ctx context.Context, network, addr string) (net.Conn, error) {
   203  			if addr == host {
   204  				u, err := cm.serviceResolver.ResolveEndpoint(cc.Service.Namespace, cc.Service.Name, port)
   205  				if err != nil {
   206  					return nil, err
   207  				}
   208  				addr = u.Host
   209  			}
   210  			return delegateDialer(ctx, network, addr)
   211  		}
   212  
   213  		return complete(cfg)
   214  	}
   215  
   216  	if cc.URL == "" {
   217  		return nil, &ErrCallingWebhook{WebhookName: cc.Name, Reason: errors.New("webhook configuration must have either service or URL")}
   218  	}
   219  
   220  	u, err := url.Parse(cc.URL)
   221  	if err != nil {
   222  		return nil, &ErrCallingWebhook{WebhookName: cc.Name, Reason: fmt.Errorf("Unparsable URL: %v", err)}
   223  	}
   224  
   225  	hostPort := u.Host
   226  	if len(u.Port()) == 0 {
   227  		// Default to port 443 if no port is specified
   228  		hostPort = net.JoinHostPort(hostPort, "443")
   229  	}
   230  
   231  	restConfig, err := cm.authInfoResolver.ClientConfigFor(hostPort)
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	cfg := rest.CopyConfig(restConfig)
   237  	cfg.Host = u.Scheme + "://" + u.Host
   238  	cfg.APIPath = u.Path
   239  	if !isLocalHost(u) {
   240  		cfg.NextProtos = []string{"http/1.1"}
   241  	}
   242  
   243  	return complete(cfg)
   244  }
   245  
   246  func isLocalHost(u *url.URL) bool {
   247  	host := u.Hostname()
   248  	if strings.EqualFold(host, "localhost") {
   249  		return true
   250  	}
   251  
   252  	netIP := netutils.ParseIPSloppy(host)
   253  	if netIP != nil {
   254  		return netIP.IsLoopback()
   255  	}
   256  	return false
   257  }