blob: 9c6ac3b5d2dd3598c6ad659af1d24f4e32bfcef1 [file] [log] [blame]
Zack Williamse940c7a2019-08-21 14:25:39 -07001/*
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
David Bainbridge86971522019-09-26 22:09:39 +0000231 mergedConfig.BearerTokenFile = configAuthInfo.TokenFile
Zack Williamse940c7a2019-08-21 14:25:39 -0700232 } 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
Zack Williamse940c7a2019-08-21 14:25:39 -0700300func 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 }
David Bainbridge86971522019-09-26 22:09:39 +0000493 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
Zack Williamse940c7a2019-08-21 14:25:39 -0700496 }
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}