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/imdario/mergo" 28 "k8s.io/klog" 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 var persister restclient.AuthProviderConfigPersister 179 if config.configAccess != nil { 180 authInfoName, _ := config.getAuthInfoName() 181 persister = PersisterForUser(config.configAccess, authInfoName) 182 } 183 userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister) 184 if err != nil { 185 return nil, err 186 } 187 mergo.MergeWithOverwrite(clientConfig, userAuthPartialConfig) 188 189 serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo) 190 if err != nil { 191 return nil, err 192 } 193 mergo.MergeWithOverwrite(clientConfig, serverAuthPartialConfig) 194 } 195 196 return clientConfig, nil 197} 198 199// clientauth.Info object contain both user identification and server identification. We want different precedence orders for 200// both, so we have to split the objects and merge them separately 201// we want this order of precedence for the server identification 202// 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files) 203// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) 204// 3. load the ~/.kubernetes_auth file as a default 205func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, configClusterInfo clientcmdapi.Cluster) (*restclient.Config, error) { 206 mergedConfig := &restclient.Config{} 207 208 // configClusterInfo holds the information identify the server provided by .kubeconfig 209 configClientConfig := &restclient.Config{} 210 configClientConfig.CAFile = configClusterInfo.CertificateAuthority 211 configClientConfig.CAData = configClusterInfo.CertificateAuthorityData 212 configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify 213 mergo.MergeWithOverwrite(mergedConfig, configClientConfig) 214 215 return mergedConfig, nil 216} 217 218// clientauth.Info object contain both user identification and server identification. We want different precedence orders for 219// both, so we have to split the objects and merge them separately 220// we want this order of precedence for user identification 221// 1. configAuthInfo minus auth-path (the final result of command line flags and merged .kubeconfig files) 222// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) 223// 3. if there is not enough information to identify the user, load try the ~/.kubernetes_auth file 224// 4. if there is not enough information to identify the user, prompt if possible 225func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister) (*restclient.Config, error) { 226 mergedConfig := &restclient.Config{} 227 228 // blindly overwrite existing values based on precedence 229 if len(configAuthInfo.Token) > 0 { 230 mergedConfig.BearerToken = configAuthInfo.Token 231 mergedConfig.BearerTokenFile = configAuthInfo.TokenFile 232 } else if len(configAuthInfo.TokenFile) > 0 { 233 tokenBytes, err := ioutil.ReadFile(configAuthInfo.TokenFile) 234 if err != nil { 235 return nil, err 236 } 237 mergedConfig.BearerToken = string(tokenBytes) 238 mergedConfig.BearerTokenFile = configAuthInfo.TokenFile 239 } 240 if len(configAuthInfo.Impersonate) > 0 { 241 mergedConfig.Impersonate = restclient.ImpersonationConfig{ 242 UserName: configAuthInfo.Impersonate, 243 Groups: configAuthInfo.ImpersonateGroups, 244 Extra: configAuthInfo.ImpersonateUserExtra, 245 } 246 } 247 if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 { 248 mergedConfig.CertFile = configAuthInfo.ClientCertificate 249 mergedConfig.CertData = configAuthInfo.ClientCertificateData 250 mergedConfig.KeyFile = configAuthInfo.ClientKey 251 mergedConfig.KeyData = configAuthInfo.ClientKeyData 252 } 253 if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 { 254 mergedConfig.Username = configAuthInfo.Username 255 mergedConfig.Password = configAuthInfo.Password 256 } 257 if configAuthInfo.AuthProvider != nil { 258 mergedConfig.AuthProvider = configAuthInfo.AuthProvider 259 mergedConfig.AuthConfigPersister = persistAuthConfig 260 } 261 if configAuthInfo.Exec != nil { 262 mergedConfig.ExecProvider = configAuthInfo.Exec 263 } 264 265 // if there still isn't enough information to authenticate the user, try prompting 266 if !canIdentifyUser(*mergedConfig) && (fallbackReader != nil) { 267 if len(config.promptedCredentials.username) > 0 && len(config.promptedCredentials.password) > 0 { 268 mergedConfig.Username = config.promptedCredentials.username 269 mergedConfig.Password = config.promptedCredentials.password 270 return mergedConfig, nil 271 } 272 prompter := NewPromptingAuthLoader(fallbackReader) 273 promptedAuthInfo, err := prompter.Prompt() 274 if err != nil { 275 return nil, err 276 } 277 promptedConfig := makeUserIdentificationConfig(*promptedAuthInfo) 278 previouslyMergedConfig := mergedConfig 279 mergedConfig = &restclient.Config{} 280 mergo.MergeWithOverwrite(mergedConfig, promptedConfig) 281 mergo.MergeWithOverwrite(mergedConfig, previouslyMergedConfig) 282 config.promptedCredentials.username = mergedConfig.Username 283 config.promptedCredentials.password = mergedConfig.Password 284 } 285 286 return mergedConfig, nil 287} 288 289// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only user identification information 290func makeUserIdentificationConfig(info clientauth.Info) *restclient.Config { 291 config := &restclient.Config{} 292 config.Username = info.User 293 config.Password = info.Password 294 config.CertFile = info.CertFile 295 config.KeyFile = info.KeyFile 296 config.BearerToken = info.BearerToken 297 return config 298} 299 300func canIdentifyUser(config restclient.Config) bool { 301 return len(config.Username) > 0 || 302 (len(config.CertFile) > 0 || len(config.CertData) > 0) || 303 len(config.BearerToken) > 0 || 304 config.AuthProvider != nil || 305 config.ExecProvider != nil 306} 307 308// Namespace implements ClientConfig 309func (config *DirectClientConfig) Namespace() (string, bool, error) { 310 if config.overrides != nil && config.overrides.Context.Namespace != "" { 311 // In the event we have an empty config but we do have a namespace override, we should return 312 // the namespace override instead of having config.ConfirmUsable() return an error. This allows 313 // things like in-cluster clients to execute `kubectl get pods --namespace=foo` and have the 314 // --namespace flag honored instead of being ignored. 315 return config.overrides.Context.Namespace, true, nil 316 } 317 318 if err := config.ConfirmUsable(); err != nil { 319 return "", false, err 320 } 321 322 configContext, err := config.getContext() 323 if err != nil { 324 return "", false, err 325 } 326 327 if len(configContext.Namespace) == 0 { 328 return "default", false, nil 329 } 330 331 return configContext.Namespace, false, nil 332} 333 334// ConfigAccess implements ClientConfig 335func (config *DirectClientConfig) ConfigAccess() ConfigAccess { 336 return config.configAccess 337} 338 339// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config, 340// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible. 341func (config *DirectClientConfig) ConfirmUsable() error { 342 validationErrors := make([]error, 0) 343 344 var contextName string 345 if len(config.contextName) != 0 { 346 contextName = config.contextName 347 } else { 348 contextName = config.config.CurrentContext 349 } 350 351 if len(contextName) > 0 { 352 _, exists := config.config.Contexts[contextName] 353 if !exists { 354 validationErrors = append(validationErrors, &errContextNotFound{contextName}) 355 } 356 } 357 358 authInfoName, _ := config.getAuthInfoName() 359 authInfo, _ := config.getAuthInfo() 360 validationErrors = append(validationErrors, validateAuthInfo(authInfoName, authInfo)...) 361 clusterName, _ := config.getClusterName() 362 cluster, _ := config.getCluster() 363 validationErrors = append(validationErrors, validateClusterInfo(clusterName, cluster)...) 364 // when direct client config is specified, and our only error is that no server is defined, we should 365 // return a standard "no config" error 366 if len(validationErrors) == 1 && validationErrors[0] == ErrEmptyCluster { 367 return newErrConfigurationInvalid([]error{ErrEmptyConfig}) 368 } 369 return newErrConfigurationInvalid(validationErrors) 370} 371 372// getContextName returns the default, or user-set context name, and a boolean that indicates 373// whether the default context name has been overwritten by a user-set flag, or left as its default value 374func (config *DirectClientConfig) getContextName() (string, bool) { 375 if len(config.overrides.CurrentContext) != 0 { 376 return config.overrides.CurrentContext, true 377 } 378 if len(config.contextName) != 0 { 379 return config.contextName, false 380 } 381 382 return config.config.CurrentContext, false 383} 384 385// getAuthInfoName returns a string containing the current authinfo name for the current context, 386// and a boolean indicating whether the default authInfo name is overwritten by a user-set flag, or 387// left as its default value 388func (config *DirectClientConfig) getAuthInfoName() (string, bool) { 389 if len(config.overrides.Context.AuthInfo) != 0 { 390 return config.overrides.Context.AuthInfo, true 391 } 392 context, _ := config.getContext() 393 return context.AuthInfo, false 394} 395 396// getClusterName returns a string containing the default, or user-set cluster name, and a boolean 397// indicating whether the default clusterName has been overwritten by a user-set flag, or left as 398// its default value 399func (config *DirectClientConfig) getClusterName() (string, bool) { 400 if len(config.overrides.Context.Cluster) != 0 { 401 return config.overrides.Context.Cluster, true 402 } 403 context, _ := config.getContext() 404 return context.Cluster, false 405} 406 407// getContext returns the clientcmdapi.Context, or an error if a required context is not found. 408func (config *DirectClientConfig) getContext() (clientcmdapi.Context, error) { 409 contexts := config.config.Contexts 410 contextName, required := config.getContextName() 411 412 mergedContext := clientcmdapi.NewContext() 413 if configContext, exists := contexts[contextName]; exists { 414 mergo.MergeWithOverwrite(mergedContext, configContext) 415 } else if required { 416 return clientcmdapi.Context{}, fmt.Errorf("context %q does not exist", contextName) 417 } 418 mergo.MergeWithOverwrite(mergedContext, config.overrides.Context) 419 420 return *mergedContext, nil 421} 422 423// getAuthInfo returns the clientcmdapi.AuthInfo, or an error if a required auth info is not found. 424func (config *DirectClientConfig) getAuthInfo() (clientcmdapi.AuthInfo, error) { 425 authInfos := config.config.AuthInfos 426 authInfoName, required := config.getAuthInfoName() 427 428 mergedAuthInfo := clientcmdapi.NewAuthInfo() 429 if configAuthInfo, exists := authInfos[authInfoName]; exists { 430 mergo.MergeWithOverwrite(mergedAuthInfo, configAuthInfo) 431 } else if required { 432 return clientcmdapi.AuthInfo{}, fmt.Errorf("auth info %q does not exist", authInfoName) 433 } 434 mergo.MergeWithOverwrite(mergedAuthInfo, config.overrides.AuthInfo) 435 436 return *mergedAuthInfo, nil 437} 438 439// getCluster returns the clientcmdapi.Cluster, or an error if a required cluster is not found. 440func (config *DirectClientConfig) getCluster() (clientcmdapi.Cluster, error) { 441 clusterInfos := config.config.Clusters 442 clusterInfoName, required := config.getClusterName() 443 444 mergedClusterInfo := clientcmdapi.NewCluster() 445 mergo.MergeWithOverwrite(mergedClusterInfo, config.overrides.ClusterDefaults) 446 if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists { 447 mergo.MergeWithOverwrite(mergedClusterInfo, configClusterInfo) 448 } else if required { 449 return clientcmdapi.Cluster{}, fmt.Errorf("cluster %q does not exist", clusterInfoName) 450 } 451 mergo.MergeWithOverwrite(mergedClusterInfo, config.overrides.ClusterInfo) 452 // An override of --insecure-skip-tls-verify=true and no accompanying CA/CA data should clear already-set CA/CA data 453 // otherwise, a kubeconfig containing a CA reference would return an error that "CA and insecure-skip-tls-verify couldn't both be set" 454 caLen := len(config.overrides.ClusterInfo.CertificateAuthority) 455 caDataLen := len(config.overrides.ClusterInfo.CertificateAuthorityData) 456 if config.overrides.ClusterInfo.InsecureSkipTLSVerify && caLen == 0 && caDataLen == 0 { 457 mergedClusterInfo.CertificateAuthority = "" 458 mergedClusterInfo.CertificateAuthorityData = nil 459 } 460 461 return *mergedClusterInfo, nil 462} 463 464// inClusterClientConfig makes a config that will work from within a kubernetes cluster container environment. 465// Can take options overrides for flags explicitly provided to the command inside the cluster container. 466type inClusterClientConfig struct { 467 overrides *ConfigOverrides 468 inClusterConfigProvider func() (*restclient.Config, error) 469} 470 471var _ ClientConfig = &inClusterClientConfig{} 472 473func (config *inClusterClientConfig) RawConfig() (clientcmdapi.Config, error) { 474 return clientcmdapi.Config{}, fmt.Errorf("inCluster environment config doesn't support multiple clusters") 475} 476 477func (config *inClusterClientConfig) ClientConfig() (*restclient.Config, error) { 478 if config.inClusterConfigProvider == nil { 479 config.inClusterConfigProvider = restclient.InClusterConfig 480 } 481 482 icc, err := config.inClusterConfigProvider() 483 if err != nil { 484 return nil, err 485 } 486 487 // in-cluster configs only takes a host, token, or CA file 488 // if any of them were individually provided, overwrite anything else 489 if config.overrides != nil { 490 if server := config.overrides.ClusterInfo.Server; len(server) > 0 { 491 icc.Host = server 492 } 493 if len(config.overrides.AuthInfo.Token) > 0 || len(config.overrides.AuthInfo.TokenFile) > 0 { 494 icc.BearerToken = config.overrides.AuthInfo.Token 495 icc.BearerTokenFile = config.overrides.AuthInfo.TokenFile 496 } 497 if certificateAuthorityFile := config.overrides.ClusterInfo.CertificateAuthority; len(certificateAuthorityFile) > 0 { 498 icc.TLSClientConfig.CAFile = certificateAuthorityFile 499 } 500 } 501 502 return icc, err 503} 504 505func (config *inClusterClientConfig) Namespace() (string, bool, error) { 506 // This way assumes you've set the POD_NAMESPACE environment variable using the downward API. 507 // This check has to be done first for backwards compatibility with the way InClusterConfig was originally set up 508 if ns := os.Getenv("POD_NAMESPACE"); ns != "" { 509 return ns, false, nil 510 } 511 512 // Fall back to the namespace associated with the service account token, if available 513 if data, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil { 514 if ns := strings.TrimSpace(string(data)); len(ns) > 0 { 515 return ns, false, nil 516 } 517 } 518 519 return "default", false, nil 520} 521 522func (config *inClusterClientConfig) ConfigAccess() ConfigAccess { 523 return NewDefaultClientConfigLoadingRules() 524} 525 526// Possible returns true if loading an inside-kubernetes-cluster is possible. 527func (config *inClusterClientConfig) Possible() bool { 528 fi, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token") 529 return os.Getenv("KUBERNETES_SERVICE_HOST") != "" && 530 os.Getenv("KUBERNETES_SERVICE_PORT") != "" && 531 err == nil && !fi.IsDir() 532} 533 534// BuildConfigFromFlags is a helper function that builds configs from a master 535// url or a kubeconfig filepath. These are passed in as command line flags for cluster 536// components. Warnings should reflect this usage. If neither masterUrl or kubeconfigPath 537// are passed in we fallback to inClusterConfig. If inClusterConfig fails, we fallback 538// to the default config. 539func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, error) { 540 if kubeconfigPath == "" && masterUrl == "" { 541 klog.Warningf("Neither --kubeconfig nor --master was specified. Using the inClusterConfig. This might not work.") 542 kubeconfig, err := restclient.InClusterConfig() 543 if err == nil { 544 return kubeconfig, nil 545 } 546 klog.Warning("error creating inClusterConfig, falling back to default config: ", err) 547 } 548 return NewNonInteractiveDeferredLoadingClientConfig( 549 &ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, 550 &ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig() 551} 552 553// BuildConfigFromKubeconfigGetter is a helper function that builds configs from a master 554// url and a kubeconfigGetter. 555func BuildConfigFromKubeconfigGetter(masterUrl string, kubeconfigGetter KubeconfigGetter) (*restclient.Config, error) { 556 // TODO: We do not need a DeferredLoader here. Refactor code and see if we can use DirectClientConfig here. 557 cc := NewNonInteractiveDeferredLoadingClientConfig( 558 &ClientConfigGetter{kubeconfigGetter: kubeconfigGetter}, 559 &ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}) 560 return cc.ClientConfig() 561} 562