1/* 2Copyright 2014 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package clientcmd 18 19import ( 20 "fmt" 21 "io" 22 "io/ioutil" 23 "net/url" 24 "os" 25 "strings" 26 27 "github.com/golang/glog" 28 "github.com/imdario/mergo" 29 30 restclient "k8s.io/client-go/rest" 31 clientauth "k8s.io/client-go/tools/auth" 32 clientcmdapi "k8s.io/client-go/tools/clientcmd/api" 33) 34 35var ( 36 // ClusterDefaults has the same behavior as the old EnvVar and DefaultCluster fields 37 // DEPRECATED will be replaced 38 ClusterDefaults = clientcmdapi.Cluster{Server: getDefaultServer()} 39 // DefaultClientConfig represents the legacy behavior of this package for defaulting 40 // DEPRECATED will be replace 41 DefaultClientConfig = DirectClientConfig{*clientcmdapi.NewConfig(), "", &ConfigOverrides{ 42 ClusterDefaults: ClusterDefaults, 43 }, nil, NewDefaultClientConfigLoadingRules(), promptedCredentials{}} 44) 45 46// getDefaultServer returns a default setting for DefaultClientConfig 47// DEPRECATED 48func getDefaultServer() string { 49 if server := os.Getenv("KUBERNETES_MASTER"); len(server) > 0 { 50 return server 51 } 52 return "http://localhost:8080" 53} 54 55// ClientConfig is used to make it easy to get an api server client 56type ClientConfig interface { 57 // RawConfig returns the merged result of all overrides 58 RawConfig() (clientcmdapi.Config, error) 59 // ClientConfig returns a complete client config 60 ClientConfig() (*restclient.Config, error) 61 // Namespace returns the namespace resulting from the merged 62 // result of all overrides and a boolean indicating if it was 63 // overridden 64 Namespace() (string, bool, error) 65 // ConfigAccess returns the rules for loading/persisting the config. 66 ConfigAccess() ConfigAccess 67} 68 69type PersistAuthProviderConfigForUser func(user string) restclient.AuthProviderConfigPersister 70 71type promptedCredentials struct { 72 username string 73 password string 74} 75 76// DirectClientConfig is a ClientConfig interface that is backed by a clientcmdapi.Config, options overrides, and an optional fallbackReader for auth information 77type DirectClientConfig struct { 78 config clientcmdapi.Config 79 contextName string 80 overrides *ConfigOverrides 81 fallbackReader io.Reader 82 configAccess ConfigAccess 83 // promptedCredentials store the credentials input by the user 84 promptedCredentials promptedCredentials 85} 86 87// NewDefaultClientConfig creates a DirectClientConfig using the config.CurrentContext as the context name 88func NewDefaultClientConfig(config clientcmdapi.Config, overrides *ConfigOverrides) ClientConfig { 89 return &DirectClientConfig{config, config.CurrentContext, overrides, nil, NewDefaultClientConfigLoadingRules(), promptedCredentials{}} 90} 91 92// NewNonInteractiveClientConfig creates a DirectClientConfig using the passed context name and does not have a fallback reader for auth information 93func NewNonInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, configAccess ConfigAccess) ClientConfig { 94 return &DirectClientConfig{config, contextName, overrides, nil, configAccess, promptedCredentials{}} 95} 96 97// NewInteractiveClientConfig creates a DirectClientConfig using the passed context name and a reader in case auth information is not provided via files or flags 98func NewInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader, configAccess ConfigAccess) ClientConfig { 99 return &DirectClientConfig{config, contextName, overrides, fallbackReader, configAccess, promptedCredentials{}} 100} 101 102// NewClientConfigFromBytes takes your kubeconfig and gives you back a ClientConfig 103func NewClientConfigFromBytes(configBytes []byte) (ClientConfig, error) { 104 config, err := Load(configBytes) 105 if err != nil { 106 return nil, err 107 } 108 109 return &DirectClientConfig{*config, "", &ConfigOverrides{}, nil, nil, promptedCredentials{}}, nil 110} 111 112// RESTConfigFromKubeConfig is a convenience method to give back a restconfig from your kubeconfig bytes. 113// For programmatic access, this is what you want 80% of the time 114func RESTConfigFromKubeConfig(configBytes []byte) (*restclient.Config, error) { 115 clientConfig, err := NewClientConfigFromBytes(configBytes) 116 if err != nil { 117 return nil, err 118 } 119 return clientConfig.ClientConfig() 120} 121 122func (config *DirectClientConfig) RawConfig() (clientcmdapi.Config, error) { 123 return config.config, nil 124} 125 126// ClientConfig implements ClientConfig 127func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) { 128 // check that getAuthInfo, getContext, and getCluster do not return an error. 129 // Do this before checking if the current config is usable in the event that an 130 // AuthInfo, Context, or Cluster config with user-defined names are not found. 131 // This provides a user with the immediate cause for error if one is found 132 configAuthInfo, err := config.getAuthInfo() 133 if err != nil { 134 return nil, err 135 } 136 137 _, err = config.getContext() 138 if err != nil { 139 return nil, err 140 } 141 142 configClusterInfo, err := config.getCluster() 143 if err != nil { 144 return nil, err 145 } 146 147 if err := config.ConfirmUsable(); err != nil { 148 return nil, err 149 } 150 151 clientConfig := &restclient.Config{} 152 clientConfig.Host = configClusterInfo.Server 153 154 if len(config.overrides.Timeout) > 0 { 155 timeout, err := ParseTimeout(config.overrides.Timeout) 156 if err != nil { 157 return nil, err 158 } 159 clientConfig.Timeout = timeout 160 } 161 162 if u, err := url.ParseRequestURI(clientConfig.Host); err == nil && u.Opaque == "" && len(u.Path) > 1 { 163 u.RawQuery = "" 164 u.Fragment = "" 165 clientConfig.Host = u.String() 166 } 167 if len(configAuthInfo.Impersonate) > 0 { 168 clientConfig.Impersonate = restclient.ImpersonationConfig{ 169 UserName: configAuthInfo.Impersonate, 170 Groups: configAuthInfo.ImpersonateGroups, 171 Extra: configAuthInfo.ImpersonateUserExtra, 172 } 173 } 174 175 // only try to read the auth information if we are secure 176 if restclient.IsConfigTransportTLS(*clientConfig) { 177 var err error 178 179 // mergo is a first write wins for map value and a last writing wins for interface values 180 // NOTE: This behavior changed with https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a. 181 // Our mergo.Merge version is older than this change. 182 var persister restclient.AuthProviderConfigPersister 183 if config.configAccess != nil { 184 authInfoName, _ := config.getAuthInfoName() 185 persister = PersisterForUser(config.configAccess, authInfoName) 186 } 187 userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister) 188 if err != nil { 189 return nil, err 190 } 191 mergo.Merge(clientConfig, userAuthPartialConfig) 192 193 serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo) 194 if err != nil { 195 return nil, err 196 } 197 mergo.Merge(clientConfig, serverAuthPartialConfig) 198 } 199 200 return clientConfig, nil 201} 202 203// clientauth.Info object contain both user identification and server identification. We want different precedence orders for 204// both, so we have to split the objects and merge them separately 205// we want this order of precedence for the server identification 206// 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files) 207// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) 208// 3. load the ~/.kubernetes_auth file as a default 209func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, configClusterInfo clientcmdapi.Cluster) (*restclient.Config, error) { 210 mergedConfig := &restclient.Config{} 211 212 // configClusterInfo holds the information identify the server provided by .kubeconfig 213 configClientConfig := &restclient.Config{} 214 configClientConfig.CAFile = configClusterInfo.CertificateAuthority 215 configClientConfig.CAData = configClusterInfo.CertificateAuthorityData 216 configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify 217 mergo.Merge(mergedConfig, configClientConfig) 218 219 return mergedConfig, nil 220} 221 222// clientauth.Info object contain both user identification and server identification. We want different precedence orders for 223// both, so we have to split the objects and merge them separately 224// we want this order of precedence for user identification 225// 1. configAuthInfo minus auth-path (the final result of command line flags and merged .kubeconfig files) 226// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) 227// 3. if there is not enough information to identify the user, load try the ~/.kubernetes_auth file 228// 4. if there is not enough information to identify the user, prompt if possible 229func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister) (*restclient.Config, error) { 230 mergedConfig := &restclient.Config{} 231 232 // blindly overwrite existing values based on precedence 233 if len(configAuthInfo.Token) > 0 { 234 mergedConfig.BearerToken = configAuthInfo.Token 235 } else if len(configAuthInfo.TokenFile) > 0 { 236 tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile) 237 if err != nil { 238 return nil, err 239 } 240 mergedConfig.BearerToken = string(tokenBytes) 241 } 242 if len(configAuthInfo.Impersonate) > 0 { 243 mergedConfig.Impersonate = restclient.ImpersonationConfig{ 244 UserName: configAuthInfo.Impersonate, 245 Groups: configAuthInfo.ImpersonateGroups, 246 Extra: configAuthInfo.ImpersonateUserExtra, 247 } 248 } 249 if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 { 250 mergedConfig.CertFile = configAuthInfo.ClientCertificate 251 mergedConfig.CertData = configAuthInfo.ClientCertificateData 252 mergedConfig.KeyFile = configAuthInfo.ClientKey 253 mergedConfig.KeyData = configAuthInfo.ClientKeyData 254 } 255 if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 { 256 mergedConfig.Username = configAuthInfo.Username 257 mergedConfig.Password = configAuthInfo.Password 258 } 259 if configAuthInfo.AuthProvider != nil { 260 mergedConfig.AuthProvider = configAuthInfo.AuthProvider 261 mergedConfig.AuthConfigPersister = persistAuthConfig 262 } 263 if configAuthInfo.Exec != nil { 264 mergedConfig.ExecProvider = configAuthInfo.Exec 265 } 266 267 // if there still isn't enough information to authenticate the user, try prompting 268 if !canIdentifyUser(*mergedConfig) && (fallbackReader != nil) { 269 if len(config.promptedCredentials.username) > 0 && len(config.promptedCredentials.password) > 0 { 270 mergedConfig.Username = config.promptedCredentials.username 271 mergedConfig.Password = config.promptedCredentials.password 272 return mergedConfig, nil 273 } 274 prompter := NewPromptingAuthLoader(fallbackReader) 275 promptedAuthInfo, err := prompter.Prompt() 276 if err != nil { 277 return nil, err 278 } 279 promptedConfig := makeUserIdentificationConfig(*promptedAuthInfo) 280 previouslyMergedConfig := mergedConfig 281 mergedConfig = &restclient.Config{} 282 mergo.Merge(mergedConfig, promptedConfig) 283 mergo.Merge(mergedConfig, previouslyMergedConfig) 284 config.promptedCredentials.username = mergedConfig.Username 285 config.promptedCredentials.password = mergedConfig.Password 286 } 287 288 return mergedConfig, nil 289} 290 291// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only user identification information 292func makeUserIdentificationConfig(info clientauth.Info) *restclient.Config { 293 config := &restclient.Config{} 294 config.Username = info.User 295 config.Password = info.Password 296 config.CertFile = info.CertFile 297 config.KeyFile = info.KeyFile 298 config.BearerToken = info.BearerToken 299 return config 300} 301 302// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only server identification information 303func makeServerIdentificationConfig(info clientauth.Info) restclient.Config { 304 config := restclient.Config{} 305 config.CAFile = info.CAFile 306 if info.Insecure != nil { 307 config.Insecure = *info.Insecure 308 } 309 return config 310} 311 312func canIdentifyUser(config restclient.Config) bool { 313 return len(config.Username) > 0 || 314 (len(config.CertFile) > 0 || len(config.CertData) > 0) || 315 len(config.BearerToken) > 0 || 316 config.AuthProvider != nil || 317 config.ExecProvider != nil 318} 319 320// Namespace implements ClientConfig 321func (config *DirectClientConfig) Namespace() (string, bool, error) { 322 if config.overrides != nil && config.overrides.Context.Namespace != "" { 323 // In the event we have an empty config but we do have a namespace override, we should return 324 // the namespace override instead of having config.ConfirmUsable() return an error. This allows 325 // things like in-cluster clients to execute `kubectl get pods --namespace=foo` and have the 326 // --namespace flag honored instead of being ignored. 327 return config.overrides.Context.Namespace, true, nil 328 } 329 330 if err := config.ConfirmUsable(); err != nil { 331 return "", false, err 332 } 333 334 configContext, err := config.getContext() 335 if err != nil { 336 return "", false, err 337 } 338 339 if len(configContext.Namespace) == 0 { 340 return "default", false, nil 341 } 342 343 return configContext.Namespace, false, nil 344} 345 346// ConfigAccess implements ClientConfig 347func (config *DirectClientConfig) ConfigAccess() ConfigAccess { 348 return config.configAccess 349} 350 351// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config, 352// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible. 353func (config *DirectClientConfig) ConfirmUsable() error { 354 validationErrors := make([]error, 0) 355 356 var contextName string 357 if len(config.contextName) != 0 { 358 contextName = config.contextName 359 } else { 360 contextName = config.config.CurrentContext 361 } 362 363 if len(contextName) > 0 { 364 _, exists := config.config.Contexts[contextName] 365 if !exists { 366 validationErrors = append(validationErrors, &errContextNotFound{contextName}) 367 } 368 } 369 370 authInfoName, _ := config.getAuthInfoName() 371 authInfo, _ := config.getAuthInfo() 372 validationErrors = append(validationErrors, validateAuthInfo(authInfoName, authInfo)...) 373 clusterName, _ := config.getClusterName() 374 cluster, _ := config.getCluster() 375 validationErrors = append(validationErrors, validateClusterInfo(clusterName, cluster)...) 376 // when direct client config is specified, and our only error is that no server is defined, we should 377 // return a standard "no config" error 378 if len(validationErrors) == 1 && validationErrors[0] == ErrEmptyCluster { 379 return newErrConfigurationInvalid([]error{ErrEmptyConfig}) 380 } 381 return newErrConfigurationInvalid(validationErrors) 382} 383 384// getContextName returns the default, or user-set context name, and a boolean that indicates 385// whether the default context name has been overwritten by a user-set flag, or left as its default value 386func (config *DirectClientConfig) getContextName() (string, bool) { 387 if len(config.overrides.CurrentContext) != 0 { 388 return config.overrides.CurrentContext, true 389 } 390 if len(config.contextName) != 0 { 391 return config.contextName, false 392 } 393 394 return config.config.CurrentContext, false 395} 396 397// getAuthInfoName returns a string containing the current authinfo name for the current context, 398// and a boolean indicating whether the default authInfo name is overwritten by a user-set flag, or 399// left as its default value 400func (config *DirectClientConfig) getAuthInfoName() (string, bool) { 401 if len(config.overrides.Context.AuthInfo) != 0 { 402 return config.overrides.Context.AuthInfo, true 403 } 404 context, _ := config.getContext() 405 return context.AuthInfo, false 406} 407 408// getClusterName returns a string containing the default, or user-set cluster name, and a boolean 409// indicating whether the default clusterName has been overwritten by a user-set flag, or left as 410// its default value 411func (config *DirectClientConfig) getClusterName() (string, bool) { 412 if len(config.overrides.Context.Cluster) != 0 { 413 return config.overrides.Context.Cluster, true 414 } 415 context, _ := config.getContext() 416 return context.Cluster, false 417} 418 419// getContext returns the clientcmdapi.Context, or an error if a required context is not found. 420func (config *DirectClientConfig) getContext() (clientcmdapi.Context, error) { 421 contexts := config.config.Contexts 422 contextName, required := config.getContextName() 423 424 mergedContext := clientcmdapi.NewContext() 425 if configContext, exists := contexts[contextName]; exists { 426 mergo.Merge(mergedContext, configContext) 427 } else if required { 428 return clientcmdapi.Context{}, fmt.Errorf("context %q does not exist", contextName) 429 } 430 mergo.Merge(mergedContext, config.overrides.Context) 431 432 return *mergedContext, nil 433} 434 435// getAuthInfo returns the clientcmdapi.AuthInfo, or an error if a required auth info is not found. 436func (config *DirectClientConfig) getAuthInfo() (clientcmdapi.AuthInfo, error) { 437 authInfos := config.config.AuthInfos 438 authInfoName, required := config.getAuthInfoName() 439 440 mergedAuthInfo := clientcmdapi.NewAuthInfo() 441 if configAuthInfo, exists := authInfos[authInfoName]; exists { 442 mergo.Merge(mergedAuthInfo, configAuthInfo) 443 } else if required { 444 return clientcmdapi.AuthInfo{}, fmt.Errorf("auth info %q does not exist", authInfoName) 445 } 446 mergo.Merge(mergedAuthInfo, config.overrides.AuthInfo) 447 448 return *mergedAuthInfo, nil 449} 450 451// getCluster returns the clientcmdapi.Cluster, or an error if a required cluster is not found. 452func (config *DirectClientConfig) getCluster() (clientcmdapi.Cluster, error) { 453 clusterInfos := config.config.Clusters 454 clusterInfoName, required := config.getClusterName() 455 456 mergedClusterInfo := clientcmdapi.NewCluster() 457 mergo.Merge(mergedClusterInfo, config.overrides.ClusterDefaults) 458 if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists { 459 mergo.Merge(mergedClusterInfo, configClusterInfo) 460 } else if required { 461 return clientcmdapi.Cluster{}, fmt.Errorf("cluster %q does not exist", clusterInfoName) 462 } 463 mergo.Merge(mergedClusterInfo, config.overrides.ClusterInfo) 464 // An override of --insecure-skip-tls-verify=true and no accompanying CA/CA data should clear already-set CA/CA data 465 // otherwise, a kubeconfig containing a CA reference would return an error that "CA and insecure-skip-tls-verify couldn't both be set" 466 caLen := len(config.overrides.ClusterInfo.CertificateAuthority) 467 caDataLen := len(config.overrides.ClusterInfo.CertificateAuthorityData) 468 if config.overrides.ClusterInfo.InsecureSkipTLSVerify && caLen == 0 && caDataLen == 0 { 469 mergedClusterInfo.CertificateAuthority = "" 470 mergedClusterInfo.CertificateAuthorityData = nil 471 } 472 473 return *mergedClusterInfo, nil 474} 475 476// inClusterClientConfig makes a config that will work from within a kubernetes cluster container environment. 477// Can take options overrides for flags explicitly provided to the command inside the cluster container. 478type inClusterClientConfig struct { 479 overrides *ConfigOverrides 480 inClusterConfigProvider func() (*restclient.Config, error) 481} 482 483var _ ClientConfig = &inClusterClientConfig{} 484 485func (config *inClusterClientConfig) RawConfig() (clientcmdapi.Config, error) { 486 return clientcmdapi.Config{}, fmt.Errorf("inCluster environment config doesn't support multiple clusters") 487} 488 489func (config *inClusterClientConfig) ClientConfig() (*restclient.Config, error) { 490 if config.inClusterConfigProvider == nil { 491 config.inClusterConfigProvider = restclient.InClusterConfig 492 } 493 494 icc, err := config.inClusterConfigProvider() 495 if err != nil { 496 return nil, err 497 } 498 499 // in-cluster configs only takes a host, token, or CA file 500 // if any of them were individually provided, overwrite anything else 501 if config.overrides != nil { 502 if server := config.overrides.ClusterInfo.Server; len(server) > 0 { 503 icc.Host = server 504 } 505 if token := config.overrides.AuthInfo.Token; len(token) > 0 { 506 icc.BearerToken = token 507 } 508 if certificateAuthorityFile := config.overrides.ClusterInfo.CertificateAuthority; len(certificateAuthorityFile) > 0 { 509 icc.TLSClientConfig.CAFile = certificateAuthorityFile 510 } 511 } 512 513 return icc, err 514} 515 516func (config *inClusterClientConfig) Namespace() (string, bool, error) { 517 // This way assumes you've set the POD_NAMESPACE environment variable using the downward API. 518 // This check has to be done first for backwards compatibility with the way InClusterConfig was originally set up 519 if ns := os.Getenv("POD_NAMESPACE"); ns != "" { 520 return ns, false, nil 521 } 522 523 // Fall back to the namespace associated with the service account token, if available 524 if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { 525 if ns := strings.TrimSpace(string(data)); len(ns) > 0 { 526 return ns, false, nil 527 } 528 } 529 530 return "default", false, nil 531} 532 533func (config *inClusterClientConfig) ConfigAccess() ConfigAccess { 534 return NewDefaultClientConfigLoadingRules() 535} 536 537// Possible returns true if loading an inside-kubernetes-cluster is possible. 538func (config *inClusterClientConfig) Possible() bool { 539 fi, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token") 540 return os.Getenv("KUBERNETES_SERVICE_HOST") != "" && 541 os.Getenv("KUBERNETES_SERVICE_PORT") != "" && 542 err == nil && !fi.IsDir() 543} 544 545// BuildConfigFromFlags is a helper function that builds configs from a master 546// url or a kubeconfig filepath. These are passed in as command line flags for cluster 547// components. Warnings should reflect this usage. If neither masterUrl or kubeconfigPath 548// are passed in we fallback to inClusterConfig. If inClusterConfig fails, we fallback 549// to the default config. 550func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) { 551 if kubeconfigPath == "" && masterUrl == "" { 552 glog.Warningf("Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.") 553 kubeconfig, err := restclient.InClusterConfig() 554 if err == nil { 555 return kubeconfig, nil 556 } 557 glog.Warning("error creating inClusterConfig, falling back to default config: ", err) 558 } 559 return NewNonInteractiveDeferredLoadingClientConfig( 560 &ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, 561 &ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig() 562} 563 564// BuildConfigFromKubeconfigGetter is a helper function that builds configs from a master 565// url and a kubeconfigGetter. 566func BuildConfigFromKubeconfigGetter(masterUrl string, kubeconfigGetter KubeconfigGetter) (*restclient.Config, error) { 567 // TODO: We do not need a DeferredLoader here. Refactor code and see if we can use DirectClientConfig here. 568 cc := NewNonInteractiveDeferredLoadingClientConfig( 569 &ClientConfigGetter{kubeconfigGetter: kubeconfigGetter}, 570 &ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}) 571 return cc.ClientConfig() 572} 573