k8s.io/client-go@v0.31.1/rest/config.go (about) 1 /* 2 Copyright 2016 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 rest 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "net" 24 "net/http" 25 "net/url" 26 "os" 27 "path/filepath" 28 gruntime "runtime" 29 "strings" 30 "time" 31 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/runtime" 34 "k8s.io/apimachinery/pkg/runtime/schema" 35 "k8s.io/client-go/pkg/version" 36 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 37 "k8s.io/client-go/transport" 38 certutil "k8s.io/client-go/util/cert" 39 "k8s.io/client-go/util/flowcontrol" 40 "k8s.io/klog/v2" 41 ) 42 43 const ( 44 DefaultQPS float32 = 5.0 45 DefaultBurst int = 10 46 ) 47 48 var ErrNotInCluster = errors.New("unable to load in-cluster configuration, KUBERNETES_SERVICE_HOST and KUBERNETES_SERVICE_PORT must be defined") 49 50 // Config holds the common attributes that can be passed to a Kubernetes client on 51 // initialization. 52 type Config struct { 53 // Host must be a host string, a host:port pair, or a URL to the base of the apiserver. 54 // If a URL is given then the (optional) Path of that URL represents a prefix that must 55 // be appended to all request URIs used to access the apiserver. This allows a frontend 56 // proxy to easily relocate all of the apiserver endpoints. 57 Host string 58 // APIPath is a sub-path that points to an API root. 59 APIPath string 60 61 // ContentConfig contains settings that affect how objects are transformed when 62 // sent to the server. 63 ContentConfig 64 65 // Server requires Basic authentication 66 Username string 67 Password string `datapolicy:"password"` 68 69 // Server requires Bearer authentication. This client will not attempt to use 70 // refresh tokens for an OAuth2 flow. 71 // TODO: demonstrate an OAuth2 compatible client. 72 BearerToken string `datapolicy:"token"` 73 74 // Path to a file containing a BearerToken. 75 // If set, the contents are periodically read. 76 // The last successfully read value takes precedence over BearerToken. 77 BearerTokenFile string 78 79 // Impersonate is the configuration that RESTClient will use for impersonation. 80 Impersonate ImpersonationConfig 81 82 // Server requires plugin-specified authentication. 83 AuthProvider *clientcmdapi.AuthProviderConfig 84 85 // Callback to persist config for AuthProvider. 86 AuthConfigPersister AuthProviderConfigPersister 87 88 // Exec-based authentication provider. 89 ExecProvider *clientcmdapi.ExecConfig 90 91 // TLSClientConfig contains settings to enable transport layer security 92 TLSClientConfig 93 94 // UserAgent is an optional field that specifies the caller of this request. 95 UserAgent string 96 97 // DisableCompression bypasses automatic GZip compression requests to the 98 // server. 99 DisableCompression bool 100 101 // Transport may be used for custom HTTP behavior. This attribute may not 102 // be specified with the TLS client certificate options. Use WrapTransport 103 // to provide additional per-server middleware behavior. 104 Transport http.RoundTripper 105 // WrapTransport will be invoked for custom HTTP behavior after the underlying 106 // transport is initialized (either the transport created from TLSClientConfig, 107 // Transport, or http.DefaultTransport). The config may layer other RoundTrippers 108 // on top of the returned RoundTripper. 109 // 110 // A future release will change this field to an array. Use config.Wrap() 111 // instead of setting this value directly. 112 WrapTransport transport.WrapperFunc 113 114 // QPS indicates the maximum QPS to the master from this client. 115 // If it's zero, the created RESTClient will use DefaultQPS: 5 116 QPS float32 117 118 // Maximum burst for throttle. 119 // If it's zero, the created RESTClient will use DefaultBurst: 10. 120 Burst int 121 122 // Rate limiter for limiting connections to the master from this client. If present overwrites QPS/Burst 123 RateLimiter flowcontrol.RateLimiter 124 125 // WarningHandler handles warnings in server responses. 126 // If not set, the default warning handler is used. 127 // See documentation for SetDefaultWarningHandler() for details. 128 WarningHandler WarningHandler 129 130 // The maximum length of time to wait before giving up on a server request. A value of zero means no timeout. 131 Timeout time.Duration 132 133 // Dial specifies the dial function for creating unencrypted TCP connections. 134 Dial func(ctx context.Context, network, address string) (net.Conn, error) 135 136 // Proxy is the proxy func to be used for all requests made by this 137 // transport. If Proxy is nil, http.ProxyFromEnvironment is used. If Proxy 138 // returns a nil *URL, no proxy is used. 139 // 140 // socks5 proxying does not currently support spdy streaming endpoints. 141 Proxy func(*http.Request) (*url.URL, error) 142 143 // Version forces a specific version to be used (if registered) 144 // Do we need this? 145 // Version string 146 } 147 148 var _ fmt.Stringer = new(Config) 149 var _ fmt.GoStringer = new(Config) 150 151 type sanitizedConfig *Config 152 153 type sanitizedAuthConfigPersister struct{ AuthProviderConfigPersister } 154 155 func (sanitizedAuthConfigPersister) GoString() string { 156 return "rest.AuthProviderConfigPersister(--- REDACTED ---)" 157 } 158 func (sanitizedAuthConfigPersister) String() string { 159 return "rest.AuthProviderConfigPersister(--- REDACTED ---)" 160 } 161 162 type sanitizedObject struct{ runtime.Object } 163 164 func (sanitizedObject) GoString() string { 165 return "runtime.Object(--- REDACTED ---)" 166 } 167 func (sanitizedObject) String() string { 168 return "runtime.Object(--- REDACTED ---)" 169 } 170 171 // GoString implements fmt.GoStringer and sanitizes sensitive fields of Config 172 // to prevent accidental leaking via logs. 173 func (c *Config) GoString() string { 174 return c.String() 175 } 176 177 // String implements fmt.Stringer and sanitizes sensitive fields of Config to 178 // prevent accidental leaking via logs. 179 func (c *Config) String() string { 180 if c == nil { 181 return "<nil>" 182 } 183 cc := sanitizedConfig(CopyConfig(c)) 184 // Explicitly mark non-empty credential fields as redacted. 185 if cc.Password != "" { 186 cc.Password = "--- REDACTED ---" 187 } 188 if cc.BearerToken != "" { 189 cc.BearerToken = "--- REDACTED ---" 190 } 191 if cc.AuthConfigPersister != nil { 192 cc.AuthConfigPersister = sanitizedAuthConfigPersister{cc.AuthConfigPersister} 193 } 194 if cc.ExecProvider != nil && cc.ExecProvider.Config != nil { 195 cc.ExecProvider.Config = sanitizedObject{Object: cc.ExecProvider.Config} 196 } 197 return fmt.Sprintf("%#v", cc) 198 } 199 200 // ImpersonationConfig has all the available impersonation options 201 type ImpersonationConfig struct { 202 // UserName is the username to impersonate on each request. 203 UserName string 204 // UID is a unique value that identifies the user. 205 UID string 206 // Groups are the groups to impersonate on each request. 207 Groups []string 208 // Extra is a free-form field which can be used to link some authentication information 209 // to authorization information. This field allows you to impersonate it. 210 Extra map[string][]string 211 } 212 213 // +k8s:deepcopy-gen=true 214 // TLSClientConfig contains settings to enable transport layer security 215 type TLSClientConfig struct { 216 // Server should be accessed without verifying the TLS certificate. For testing only. 217 Insecure bool 218 // ServerName is passed to the server for SNI and is used in the client to check server 219 // certificates against. If ServerName is empty, the hostname used to contact the 220 // server is used. 221 ServerName string 222 223 // Server requires TLS client certificate authentication 224 CertFile string 225 // Server requires TLS client certificate authentication 226 KeyFile string 227 // Trusted root certificates for server 228 CAFile string 229 230 // CertData holds PEM-encoded bytes (typically read from a client certificate file). 231 // CertData takes precedence over CertFile 232 CertData []byte 233 // KeyData holds PEM-encoded bytes (typically read from a client certificate key file). 234 // KeyData takes precedence over KeyFile 235 KeyData []byte `datapolicy:"security-key"` 236 // CAData holds PEM-encoded bytes (typically read from a root certificates bundle). 237 // CAData takes precedence over CAFile 238 CAData []byte 239 240 // NextProtos is a list of supported application level protocols, in order of preference. 241 // Used to populate tls.Config.NextProtos. 242 // To indicate to the server http/1.1 is preferred over http/2, set to ["http/1.1", "h2"] (though the server is free to ignore that preference). 243 // To use only http/1.1, set to ["http/1.1"]. 244 NextProtos []string 245 } 246 247 var _ fmt.Stringer = TLSClientConfig{} 248 var _ fmt.GoStringer = TLSClientConfig{} 249 250 type sanitizedTLSClientConfig TLSClientConfig 251 252 // GoString implements fmt.GoStringer and sanitizes sensitive fields of 253 // TLSClientConfig to prevent accidental leaking via logs. 254 func (c TLSClientConfig) GoString() string { 255 return c.String() 256 } 257 258 // String implements fmt.Stringer and sanitizes sensitive fields of 259 // TLSClientConfig to prevent accidental leaking via logs. 260 func (c TLSClientConfig) String() string { 261 cc := sanitizedTLSClientConfig{ 262 Insecure: c.Insecure, 263 ServerName: c.ServerName, 264 CertFile: c.CertFile, 265 KeyFile: c.KeyFile, 266 CAFile: c.CAFile, 267 CertData: c.CertData, 268 KeyData: c.KeyData, 269 CAData: c.CAData, 270 NextProtos: c.NextProtos, 271 } 272 // Explicitly mark non-empty credential fields as redacted. 273 if len(cc.CertData) != 0 { 274 cc.CertData = []byte("--- TRUNCATED ---") 275 } 276 if len(cc.KeyData) != 0 { 277 cc.KeyData = []byte("--- REDACTED ---") 278 } 279 return fmt.Sprintf("%#v", cc) 280 } 281 282 type ContentConfig struct { 283 // AcceptContentTypes specifies the types the client will accept and is optional. 284 // If not set, ContentType will be used to define the Accept header 285 AcceptContentTypes string 286 // ContentType specifies the wire format used to communicate with the server. 287 // This value will be set as the Accept header on requests made to the server, and 288 // as the default content type on any object sent to the server. If not set, 289 // "application/json" is used. 290 ContentType string 291 // GroupVersion is the API version to talk to. Must be provided when initializing 292 // a RESTClient directly. When initializing a Client, will be set with the default 293 // code version. 294 GroupVersion *schema.GroupVersion 295 // NegotiatedSerializer is used for obtaining encoders and decoders for multiple 296 // supported media types. 297 // 298 // TODO: NegotiatedSerializer will be phased out as internal clients are removed 299 // from Kubernetes. 300 NegotiatedSerializer runtime.NegotiatedSerializer 301 } 302 303 // RESTClientFor returns a RESTClient that satisfies the requested attributes on a client Config 304 // object. Note that a RESTClient may require fields that are optional when initializing a Client. 305 // A RESTClient created by this method is generic - it expects to operate on an API that follows 306 // the Kubernetes conventions, but may not be the Kubernetes API. 307 // RESTClientFor is equivalent to calling RESTClientForConfigAndClient(config, httpClient), 308 // where httpClient was generated with HTTPClientFor(config). 309 func RESTClientFor(config *Config) (*RESTClient, error) { 310 if config.GroupVersion == nil { 311 return nil, fmt.Errorf("GroupVersion is required when initializing a RESTClient") 312 } 313 if config.NegotiatedSerializer == nil { 314 return nil, fmt.Errorf("NegotiatedSerializer is required when initializing a RESTClient") 315 } 316 317 // Validate config.Host before constructing the transport/client so we can fail fast. 318 // ServerURL will be obtained later in RESTClientForConfigAndClient() 319 _, _, err := DefaultServerUrlFor(config) 320 if err != nil { 321 return nil, err 322 } 323 324 httpClient, err := HTTPClientFor(config) 325 if err != nil { 326 return nil, err 327 } 328 329 return RESTClientForConfigAndClient(config, httpClient) 330 } 331 332 // RESTClientForConfigAndClient returns a RESTClient that satisfies the requested attributes on a 333 // client Config object. 334 // Unlike RESTClientFor, RESTClientForConfigAndClient allows to pass an http.Client that is shared 335 // between all the API Groups and Versions. 336 // Note that the http client takes precedence over the transport values configured. 337 // The http client defaults to the `http.DefaultClient` if nil. 338 func RESTClientForConfigAndClient(config *Config, httpClient *http.Client) (*RESTClient, error) { 339 if config.GroupVersion == nil { 340 return nil, fmt.Errorf("GroupVersion is required when initializing a RESTClient") 341 } 342 if config.NegotiatedSerializer == nil { 343 return nil, fmt.Errorf("NegotiatedSerializer is required when initializing a RESTClient") 344 } 345 346 baseURL, versionedAPIPath, err := DefaultServerUrlFor(config) 347 if err != nil { 348 return nil, err 349 } 350 351 rateLimiter := config.RateLimiter 352 if rateLimiter == nil { 353 qps := config.QPS 354 if config.QPS == 0.0 { 355 qps = DefaultQPS 356 } 357 burst := config.Burst 358 if config.Burst == 0 { 359 burst = DefaultBurst 360 } 361 if qps > 0 { 362 rateLimiter = flowcontrol.NewTokenBucketRateLimiter(qps, burst) 363 } 364 } 365 366 var gv schema.GroupVersion 367 if config.GroupVersion != nil { 368 gv = *config.GroupVersion 369 } 370 clientContent := ClientContentConfig{ 371 AcceptContentTypes: config.AcceptContentTypes, 372 ContentType: config.ContentType, 373 GroupVersion: gv, 374 Negotiator: runtime.NewClientNegotiator(config.NegotiatedSerializer, gv), 375 } 376 377 restClient, err := NewRESTClient(baseURL, versionedAPIPath, clientContent, rateLimiter, httpClient) 378 if err == nil && config.WarningHandler != nil { 379 restClient.warningHandler = config.WarningHandler 380 } 381 return restClient, err 382 } 383 384 // UnversionedRESTClientFor is the same as RESTClientFor, except that it allows 385 // the config.Version to be empty. 386 func UnversionedRESTClientFor(config *Config) (*RESTClient, error) { 387 if config.NegotiatedSerializer == nil { 388 return nil, fmt.Errorf("NegotiatedSerializer is required when initializing a RESTClient") 389 } 390 391 // Validate config.Host before constructing the transport/client so we can fail fast. 392 // ServerURL will be obtained later in UnversionedRESTClientForConfigAndClient() 393 _, _, err := DefaultServerUrlFor(config) 394 if err != nil { 395 return nil, err 396 } 397 398 httpClient, err := HTTPClientFor(config) 399 if err != nil { 400 return nil, err 401 } 402 403 return UnversionedRESTClientForConfigAndClient(config, httpClient) 404 } 405 406 // UnversionedRESTClientForConfigAndClient is the same as RESTClientForConfigAndClient, 407 // except that it allows the config.Version to be empty. 408 func UnversionedRESTClientForConfigAndClient(config *Config, httpClient *http.Client) (*RESTClient, error) { 409 if config.NegotiatedSerializer == nil { 410 return nil, fmt.Errorf("NegotiatedSerializer is required when initializing a RESTClient") 411 } 412 413 baseURL, versionedAPIPath, err := DefaultServerUrlFor(config) 414 if err != nil { 415 return nil, err 416 } 417 418 rateLimiter := config.RateLimiter 419 if rateLimiter == nil { 420 qps := config.QPS 421 if config.QPS == 0.0 { 422 qps = DefaultQPS 423 } 424 burst := config.Burst 425 if config.Burst == 0 { 426 burst = DefaultBurst 427 } 428 if qps > 0 { 429 rateLimiter = flowcontrol.NewTokenBucketRateLimiter(qps, burst) 430 } 431 } 432 433 gv := metav1.SchemeGroupVersion 434 if config.GroupVersion != nil { 435 gv = *config.GroupVersion 436 } 437 clientContent := ClientContentConfig{ 438 AcceptContentTypes: config.AcceptContentTypes, 439 ContentType: config.ContentType, 440 GroupVersion: gv, 441 Negotiator: runtime.NewClientNegotiator(config.NegotiatedSerializer, gv), 442 } 443 444 restClient, err := NewRESTClient(baseURL, versionedAPIPath, clientContent, rateLimiter, httpClient) 445 if err == nil && config.WarningHandler != nil { 446 restClient.warningHandler = config.WarningHandler 447 } 448 return restClient, err 449 } 450 451 // SetKubernetesDefaults sets default values on the provided client config for accessing the 452 // Kubernetes API or returns an error if any of the defaults are impossible or invalid. 453 func SetKubernetesDefaults(config *Config) error { 454 if len(config.UserAgent) == 0 { 455 config.UserAgent = DefaultKubernetesUserAgent() 456 } 457 return nil 458 } 459 460 // adjustCommit returns sufficient significant figures of the commit's git hash. 461 func adjustCommit(c string) string { 462 if len(c) == 0 { 463 return "unknown" 464 } 465 if len(c) > 7 { 466 return c[:7] 467 } 468 return c 469 } 470 471 // adjustVersion strips "alpha", "beta", etc. from version in form 472 // major.minor.patch-[alpha|beta|etc]. 473 func adjustVersion(v string) string { 474 if len(v) == 0 { 475 return "unknown" 476 } 477 seg := strings.SplitN(v, "-", 2) 478 return seg[0] 479 } 480 481 // adjustCommand returns the last component of the 482 // OS-specific command path for use in User-Agent. 483 func adjustCommand(p string) string { 484 // Unlikely, but better than returning "". 485 if len(p) == 0 { 486 return "unknown" 487 } 488 return filepath.Base(p) 489 } 490 491 // buildUserAgent builds a User-Agent string from given args. 492 func buildUserAgent(command, version, os, arch, commit string) string { 493 return fmt.Sprintf( 494 "%s/%s (%s/%s) kubernetes/%s", command, version, os, arch, commit) 495 } 496 497 // DefaultKubernetesUserAgent returns a User-Agent string built from static global vars. 498 func DefaultKubernetesUserAgent() string { 499 return buildUserAgent( 500 adjustCommand(os.Args[0]), 501 adjustVersion(version.Get().GitVersion), 502 gruntime.GOOS, 503 gruntime.GOARCH, 504 adjustCommit(version.Get().GitCommit)) 505 } 506 507 // InClusterConfig returns a config object which uses the service account 508 // kubernetes gives to pods. It's intended for clients that expect to be 509 // running inside a pod running on kubernetes. It will return ErrNotInCluster 510 // if called from a process not running in a kubernetes environment. 511 func InClusterConfig() (*Config, error) { 512 const ( 513 tokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" 514 rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" 515 ) 516 host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") 517 if len(host) == 0 || len(port) == 0 { 518 return nil, ErrNotInCluster 519 } 520 521 token, err := os.ReadFile(tokenFile) 522 if err != nil { 523 return nil, err 524 } 525 526 tlsClientConfig := TLSClientConfig{} 527 528 if _, err := certutil.NewPool(rootCAFile); err != nil { 529 klog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err) 530 } else { 531 tlsClientConfig.CAFile = rootCAFile 532 } 533 534 return &Config{ 535 // TODO: switch to using cluster DNS. 536 Host: "https://" + net.JoinHostPort(host, port), 537 TLSClientConfig: tlsClientConfig, 538 BearerToken: string(token), 539 BearerTokenFile: tokenFile, 540 }, nil 541 } 542 543 // IsConfigTransportTLS returns true if and only if the provided 544 // config will result in a protected connection to the server when it 545 // is passed to restclient.RESTClientFor(). Use to determine when to 546 // send credentials over the wire. 547 // 548 // Note: the Insecure flag is ignored when testing for this value, so MITM attacks are 549 // still possible. 550 func IsConfigTransportTLS(config Config) bool { 551 baseURL, _, err := DefaultServerUrlFor(&config) 552 if err != nil { 553 return false 554 } 555 return baseURL.Scheme == "https" 556 } 557 558 // LoadTLSFiles copies the data from the CertFile, KeyFile, and CAFile fields into the CertData, 559 // KeyData, and CAFile fields, or returns an error. If no error is returned, all three fields are 560 // either populated or were empty to start. 561 func LoadTLSFiles(c *Config) error { 562 var err error 563 c.CAData, err = dataFromSliceOrFile(c.CAData, c.CAFile) 564 if err != nil { 565 return err 566 } 567 568 c.CertData, err = dataFromSliceOrFile(c.CertData, c.CertFile) 569 if err != nil { 570 return err 571 } 572 573 c.KeyData, err = dataFromSliceOrFile(c.KeyData, c.KeyFile) 574 return err 575 } 576 577 // dataFromSliceOrFile returns data from the slice (if non-empty), or from the file, 578 // or an error if an error occurred reading the file 579 func dataFromSliceOrFile(data []byte, file string) ([]byte, error) { 580 if len(data) > 0 { 581 return data, nil 582 } 583 if len(file) > 0 { 584 fileData, err := os.ReadFile(file) 585 if err != nil { 586 return []byte{}, err 587 } 588 return fileData, nil 589 } 590 return nil, nil 591 } 592 593 func AddUserAgent(config *Config, userAgent string) *Config { 594 fullUserAgent := DefaultKubernetesUserAgent() + "/" + userAgent 595 config.UserAgent = fullUserAgent 596 return config 597 } 598 599 // AnonymousClientConfig returns a copy of the given config with all user credentials (cert/key, bearer token, and username/password) and custom transports (WrapTransport, Transport) removed 600 func AnonymousClientConfig(config *Config) *Config { 601 // copy only known safe fields 602 return &Config{ 603 Host: config.Host, 604 APIPath: config.APIPath, 605 ContentConfig: config.ContentConfig, 606 TLSClientConfig: TLSClientConfig{ 607 Insecure: config.Insecure, 608 ServerName: config.ServerName, 609 CAFile: config.TLSClientConfig.CAFile, 610 CAData: config.TLSClientConfig.CAData, 611 NextProtos: config.TLSClientConfig.NextProtos, 612 }, 613 RateLimiter: config.RateLimiter, 614 WarningHandler: config.WarningHandler, 615 UserAgent: config.UserAgent, 616 DisableCompression: config.DisableCompression, 617 QPS: config.QPS, 618 Burst: config.Burst, 619 Timeout: config.Timeout, 620 Dial: config.Dial, 621 Proxy: config.Proxy, 622 } 623 } 624 625 // CopyConfig returns a copy of the given config 626 func CopyConfig(config *Config) *Config { 627 c := &Config{ 628 Host: config.Host, 629 APIPath: config.APIPath, 630 ContentConfig: config.ContentConfig, 631 Username: config.Username, 632 Password: config.Password, 633 BearerToken: config.BearerToken, 634 BearerTokenFile: config.BearerTokenFile, 635 Impersonate: ImpersonationConfig{ 636 UserName: config.Impersonate.UserName, 637 UID: config.Impersonate.UID, 638 Groups: config.Impersonate.Groups, 639 Extra: config.Impersonate.Extra, 640 }, 641 AuthProvider: config.AuthProvider, 642 AuthConfigPersister: config.AuthConfigPersister, 643 ExecProvider: config.ExecProvider, 644 TLSClientConfig: TLSClientConfig{ 645 Insecure: config.TLSClientConfig.Insecure, 646 ServerName: config.TLSClientConfig.ServerName, 647 CertFile: config.TLSClientConfig.CertFile, 648 KeyFile: config.TLSClientConfig.KeyFile, 649 CAFile: config.TLSClientConfig.CAFile, 650 CertData: config.TLSClientConfig.CertData, 651 KeyData: config.TLSClientConfig.KeyData, 652 CAData: config.TLSClientConfig.CAData, 653 NextProtos: config.TLSClientConfig.NextProtos, 654 }, 655 UserAgent: config.UserAgent, 656 DisableCompression: config.DisableCompression, 657 Transport: config.Transport, 658 WrapTransport: config.WrapTransport, 659 QPS: config.QPS, 660 Burst: config.Burst, 661 RateLimiter: config.RateLimiter, 662 WarningHandler: config.WarningHandler, 663 Timeout: config.Timeout, 664 Dial: config.Dial, 665 Proxy: config.Proxy, 666 } 667 if config.ExecProvider != nil && config.ExecProvider.Config != nil { 668 c.ExecProvider.Config = config.ExecProvider.Config.DeepCopyObject() 669 } 670 return c 671 }