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 }