From 13a7d92d0f50e877edeb4225f73ae106b33bf152 Mon Sep 17 00:00:00 2001 From: CJ Cullen Date: Thu, 14 Apr 2016 11:55:34 -0700 Subject: [PATCH] Add a ConfigPersister for AuthProvider plugins in kubectl/clients. --- pkg/client/restclient/config.go | 3 + pkg/client/restclient/plugin.go | 19 ++- .../{transport_test.go => plugin_test.go} | 148 +++++++++++++++++- pkg/client/restclient/transport.go | 2 +- pkg/client/unversioned/clientcmd/api/types.go | 3 +- .../unversioned/clientcmd/api/types_test.go | 11 +- .../unversioned/clientcmd/api/v1/types.go | 3 +- .../unversioned/clientcmd/client_config.go | 38 +++-- .../clientcmd/client_config_test.go | 16 +- pkg/client/unversioned/clientcmd/config.go | 23 +++ pkg/client/unversioned/clientcmd/loader.go | 48 ++++++ .../clientcmd/merged_client_builder.go | 11 +- plugin/pkg/client/auth/gcp/gcp.go | 59 ++++++- test/integration/kubectl_test.go | 2 +- 14 files changed, 350 insertions(+), 36 deletions(-) rename pkg/client/restclient/{transport_test.go => plugin_test.go} (50%) diff --git a/pkg/client/restclient/config.go b/pkg/client/restclient/config.go index 6e7494b1230..0741e3c2d8f 100644 --- a/pkg/client/restclient/config.go +++ b/pkg/client/restclient/config.go @@ -70,6 +70,9 @@ type Config struct { // Server requires plugin-specified authentication. AuthProvider *clientcmdapi.AuthProviderConfig + // Callback to persist config for AuthProvider. + AuthConfigPersister AuthProviderConfigPersister + // TLSClientConfig contains settings to enable transport layer security TLSClientConfig diff --git a/pkg/client/restclient/plugin.go b/pkg/client/restclient/plugin.go index 80d13547b74..7136e3c0e05 100644 --- a/pkg/client/restclient/plugin.go +++ b/pkg/client/restclient/plugin.go @@ -30,9 +30,22 @@ type AuthProvider interface { // WrapTransport allows the plugin to create a modified RoundTripper that // attaches authorization headers (or other info) to requests. WrapTransport(http.RoundTripper) http.RoundTripper + // Login allows the plugin to initialize its configuration. It must not + // require direct user interaction. + Login() error } -type Factory func() (AuthProvider, error) +// Factory generates an AuthProvider plugin. +// clusterAddress is the address of the current cluster. +// config is the inital configuration for this plugin. +// persister allows the plugin to save updated configuration. +type Factory func(clusterAddress string, config map[string]string, persister AuthProviderConfigPersister) (AuthProvider, error) + +// AuthProviderConfigPersister allows a plugin to persist configuration info +// for just itself. +type AuthProviderConfigPersister interface { + Persist(map[string]string) error +} // All registered auth provider plugins. var pluginsLock sync.Mutex @@ -49,12 +62,12 @@ func RegisterAuthProviderPlugin(name string, plugin Factory) error { return nil } -func GetAuthProvider(apc *clientcmdapi.AuthProviderConfig) (AuthProvider, error) { +func GetAuthProvider(clusterAddress string, apc *clientcmdapi.AuthProviderConfig, persister AuthProviderConfigPersister) (AuthProvider, error) { pluginsLock.Lock() defer pluginsLock.Unlock() p, ok := plugins[apc.Name] if !ok { return nil, fmt.Errorf("No Auth Provider found for name %q", apc.Name) } - return p() + return p(clusterAddress, apc.Config, persister) } diff --git a/pkg/client/restclient/transport_test.go b/pkg/client/restclient/plugin_test.go similarity index 50% rename from pkg/client/restclient/transport_test.go rename to pkg/client/restclient/plugin_test.go index d94f0a99d75..3419ecb8acb 100644 --- a/pkg/client/restclient/transport_test.go +++ b/pkg/client/restclient/plugin_test.go @@ -19,12 +19,14 @@ package restclient import ( "fmt" "net/http" + "reflect" + "strconv" "testing" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" ) -func TestTransportConfigAuthPlugins(t *testing.T) { +func TestAuthPluginWrapTransport(t *testing.T) { if err := RegisterAuthProviderPlugin("pluginA", pluginAProvider); err != nil { t.Errorf("Unexpected error: failed to register pluginA: %v", err) } @@ -92,6 +94,83 @@ func TestTransportConfigAuthPlugins(t *testing.T) { } } +func TestAuthPluginPersist(t *testing.T) { + // register pluginA by a different name so we don't collide across tests. + if err := RegisterAuthProviderPlugin("pluginA2", pluginAProvider); err != nil { + t.Errorf("Unexpected error: failed to register pluginA: %v", err) + } + if err := RegisterAuthProviderPlugin("pluginPersist", pluginPersistProvider); err != nil { + t.Errorf("Unexpected error: failed to register pluginPersist: %v", err) + } + fooBarConfig := map[string]string{"foo": "bar"} + testCases := []struct { + plugin string + startingConfig map[string]string + expectedConfigAfterLogin map[string]string + expectedConfigAfterRoundTrip map[string]string + }{ + // non-persisting plugins should work fine without modifying config. + {"pluginA2", map[string]string{}, map[string]string{}, map[string]string{}}, + {"pluginA2", fooBarConfig, fooBarConfig, fooBarConfig}, + // plugins that persist config should be able to persist when they want. + { + "pluginPersist", + map[string]string{}, + map[string]string{ + "login": "Y", + }, + map[string]string{ + "login": "Y", + "roundTrips": "1", + }, + }, + { + "pluginPersist", + map[string]string{ + "login": "Y", + "roundTrips": "123", + }, + map[string]string{ + "login": "Y", + "roundTrips": "123", + }, + map[string]string{ + "login": "Y", + "roundTrips": "124", + }, + }, + } + for i, tc := range testCases { + cfg := &clientcmdapi.AuthProviderConfig{ + Name: tc.plugin, + Config: tc.startingConfig, + } + persister := &inMemoryPersister{make(map[string]string)} + persister.Persist(tc.startingConfig) + plugin, err := GetAuthProvider("127.0.0.1", cfg, persister) + if err != nil { + t.Errorf("%d. Unexpected error: failed to get plugin %q: %v", i, tc.plugin, err) + } + if err := plugin.Login(); err != nil { + t.Errorf("%d. Unexpected error calling Login() w/ plugin %q: %v", i, tc.plugin, err) + } + // Make sure the plugin persisted what we expect after Login(). + if !reflect.DeepEqual(persister.savedConfig, tc.expectedConfigAfterLogin) { + t.Errorf("%d. Unexpected persisted config after calling %s.Login(): \nGot:\n%v\nExpected:\n%v", + i, tc.plugin, persister.savedConfig, tc.expectedConfigAfterLogin) + } + if _, err := plugin.WrapTransport(&emptyTransport{}).RoundTrip(&http.Request{}); err != nil { + t.Errorf("%d. Unexpected error round-tripping w/ plugin %q: %v", i, tc.plugin, err) + } + // Make sure the plugin persisted what we expect after RoundTrip(). + if !reflect.DeepEqual(persister.savedConfig, tc.expectedConfigAfterRoundTrip) { + t.Errorf("%d. Unexpected persisted config after calling %s.WrapTransport.RoundTrip(): \nGot:\n%v\nExpected:\n%v", + i, tc.plugin, persister.savedConfig, tc.expectedConfigAfterLogin) + } + } + +} + // emptyTransport provides an empty http.Response with an initialized header // to allow wrapping RoundTrippers to set header values. type emptyTransport struct{} @@ -137,7 +216,9 @@ func (*pluginA) WrapTransport(rt http.RoundTripper) http.RoundTripper { return &wrapTransportA{rt} } -func pluginAProvider() (AuthProvider, error) { +func (*pluginA) Login() error { return nil } + +func pluginAProvider(string, map[string]string, AuthProviderConfigPersister) (AuthProvider, error) { return &pluginA{}, nil } @@ -161,11 +242,70 @@ func (*pluginB) WrapTransport(rt http.RoundTripper) http.RoundTripper { return &wrapTransportB{rt} } -func pluginBProvider() (AuthProvider, error) { +func (*pluginB) Login() error { return nil } + +func pluginBProvider(string, map[string]string, AuthProviderConfigPersister) (AuthProvider, error) { return &pluginB{}, nil } // pluginFailProvider simulates a registered AuthPlugin that fails to load. -func pluginFailProvider() (AuthProvider, error) { +func pluginFailProvider(string, map[string]string, AuthProviderConfigPersister) (AuthProvider, error) { return nil, fmt.Errorf("Failed to load AuthProvider") } + +type inMemoryPersister struct { + savedConfig map[string]string +} + +func (i *inMemoryPersister) Persist(config map[string]string) error { + i.savedConfig = make(map[string]string) + for k, v := range config { + i.savedConfig[k] = v + } + return nil +} + +// wrapTransportPersist increments the "roundTrips" entry from the config when +// roundTrip is called. +type wrapTransportPersist struct { + rt http.RoundTripper + config map[string]string + persister AuthProviderConfigPersister +} + +func (w *wrapTransportPersist) RoundTrip(req *http.Request) (*http.Response, error) { + roundTrips := 0 + if rtVal, ok := w.config["roundTrips"]; ok { + var err error + roundTrips, err = strconv.Atoi(rtVal) + if err != nil { + return nil, err + } + } + roundTrips++ + w.config["roundTrips"] = fmt.Sprintf("%d", roundTrips) + if err := w.persister.Persist(w.config); err != nil { + return nil, err + } + return w.rt.RoundTrip(req) +} + +type pluginPersist struct { + config map[string]string + persister AuthProviderConfigPersister +} + +func (p *pluginPersist) WrapTransport(rt http.RoundTripper) http.RoundTripper { + return &wrapTransportPersist{rt, p.config, p.persister} +} + +// Login sets the config entry "login" to "Y". +func (p *pluginPersist) Login() error { + p.config["login"] = "Y" + p.persister.Persist(p.config) + return nil +} + +func pluginPersistProvider(_ string, config map[string]string, persister AuthProviderConfigPersister) (AuthProvider, error) { + return &pluginPersist{config, persister}, nil +} diff --git a/pkg/client/restclient/transport.go b/pkg/client/restclient/transport.go index 8c8a4464cb8..0bfa2ea2720 100644 --- a/pkg/client/restclient/transport.go +++ b/pkg/client/restclient/transport.go @@ -60,7 +60,7 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip func (c *Config) transportConfig() (*transport.Config, error) { wt := c.WrapTransport if c.AuthProvider != nil { - provider, err := GetAuthProvider(c.AuthProvider) + provider, err := GetAuthProvider(c.Host, c.AuthProvider, c.AuthConfigPersister) if err != nil { return nil, err } diff --git a/pkg/client/unversioned/clientcmd/api/types.go b/pkg/client/unversioned/clientcmd/api/types.go index 1b193f74627..56b44e8f42a 100644 --- a/pkg/client/unversioned/clientcmd/api/types.go +++ b/pkg/client/unversioned/clientcmd/api/types.go @@ -116,7 +116,8 @@ type Context struct { // AuthProviderConfig holds the configuration for a specified auth provider. type AuthProviderConfig struct { - Name string `json:"name"` + Name string `json:"name"` + Config map[string]string `json:"config,omitempty"` } // NewConfig is a convenience function that returns a new Config object with non-nil maps diff --git a/pkg/client/unversioned/clientcmd/api/types_test.go b/pkg/client/unversioned/clientcmd/api/types_test.go index 552b6cab849..6c79728f4c3 100644 --- a/pkg/client/unversioned/clientcmd/api/types_test.go +++ b/pkg/client/unversioned/clientcmd/api/types_test.go @@ -59,7 +59,13 @@ func Example_ofOptionsConfig() { Token: "my-secret-token", } defaultConfig.AuthInfos["black-mage-via-auth-provider"] = &AuthInfo{ - AuthProvider: &AuthProviderConfig{Name: "gcp"}, + AuthProvider: &AuthProviderConfig{ + Name: "gcp", + Config: map[string]string{ + "foo": "bar", + "token": "s3cr3t-t0k3n", + }, + }, } defaultConfig.Contexts["bravo-as-black-mage"] = &Context{ Cluster: "bravo", @@ -115,6 +121,9 @@ func Example_ofOptionsConfig() { // black-mage-via-auth-provider: // LocationOfOrigin: "" // auth-provider: + // config: + // foo: bar + // token: s3cr3t-t0k3n // name: gcp // red-mage-via-token: // LocationOfOrigin: "" diff --git a/pkg/client/unversioned/clientcmd/api/v1/types.go b/pkg/client/unversioned/clientcmd/api/v1/types.go index 54f5a80b0be..46b5dbaa72d 100644 --- a/pkg/client/unversioned/clientcmd/api/v1/types.go +++ b/pkg/client/unversioned/clientcmd/api/v1/types.go @@ -140,5 +140,6 @@ type NamedExtension struct { // AuthProviderConfig holds the configuration for a specified auth provider. type AuthProviderConfig struct { - Name string `json:"name"` + Name string `json:"name"` + Config map[string]string `json:"config"` } diff --git a/pkg/client/unversioned/clientcmd/client_config.go b/pkg/client/unversioned/clientcmd/client_config.go index 64d305bd875..c030ec1bd66 100644 --- a/pkg/client/unversioned/clientcmd/client_config.go +++ b/pkg/client/unversioned/clientcmd/client_config.go @@ -41,7 +41,7 @@ var ( // EnvVarCluster allows overriding the DefaultCluster using an envvar for the server name EnvVarCluster = clientcmdapi.Cluster{Server: os.Getenv("KUBERNETES_MASTER")} - DefaultClientConfig = DirectClientConfig{*clientcmdapi.NewConfig(), "", &ConfigOverrides{}, nil} + DefaultClientConfig = DirectClientConfig{*clientcmdapi.NewConfig(), "", &ConfigOverrides{}, nil, NewDefaultClientConfigLoadingRules()} ) // ClientConfig is used to make it easy to get an api server client @@ -54,29 +54,34 @@ type ClientConfig interface { // result of all overrides and a boolean indicating if it was // overridden Namespace() (string, bool, error) + // ConfigAccess returns the rules for loading/persisting the config. + ConfigAccess() ConfigAccess } +type PersistAuthProviderConfigForUser func(user string) restclient.AuthProviderConfigPersister + // DirectClientConfig is a ClientConfig interface that is backed by a clientcmdapi.Config, options overrides, and an optional fallbackReader for auth information type DirectClientConfig struct { config clientcmdapi.Config contextName string overrides *ConfigOverrides fallbackReader io.Reader + configAccess ConfigAccess } // NewDefaultClientConfig creates a DirectClientConfig using the config.CurrentContext as the context name func NewDefaultClientConfig(config clientcmdapi.Config, overrides *ConfigOverrides) ClientConfig { - return &DirectClientConfig{config, config.CurrentContext, overrides, nil} + return &DirectClientConfig{config, config.CurrentContext, overrides, nil, NewDefaultClientConfigLoadingRules()} } // NewNonInteractiveClientConfig creates a DirectClientConfig using the passed context name and does not have a fallback reader for auth information -func NewNonInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides) ClientConfig { - return &DirectClientConfig{config, contextName, overrides, nil} +func NewNonInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, configAccess ConfigAccess) ClientConfig { + return &DirectClientConfig{config, contextName, overrides, nil, configAccess} } // NewInteractiveClientConfig creates a DirectClientConfig using the passed context name and a reader in case auth information is not provided via files or flags -func NewInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig { - return &DirectClientConfig{config, contextName, overrides, fallbackReader} +func NewInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader, configAccess ConfigAccess) ClientConfig { + return &DirectClientConfig{config, contextName, overrides, fallbackReader, configAccess} } func (config *DirectClientConfig) RawConfig() (clientcmdapi.Config, error) { @@ -110,7 +115,11 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) { // mergo is a first write wins for map value and a last writing wins for interface values // NOTE: This behavior changed with https://github.com/imdario/mergo/commit/d304790b2ed594794496464fadd89d2bb266600a. // Our mergo.Merge version is older than this change. - userAuthPartialConfig, err := getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader) + var persister restclient.AuthProviderConfigPersister + if config.configAccess != nil { + persister = PersisterForUser(config.configAccess, config.getAuthInfoName()) + } + userAuthPartialConfig, err := getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister) if err != nil { return nil, err } @@ -152,7 +161,7 @@ func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 3. if there is not enough information to idenfity the user, load try the ~/.kubernetes_auth file // 4. if there is not enough information to identify the user, prompt if possible -func getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader) (*restclient.Config, error) { +func getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister) (*restclient.Config, error) { mergedConfig := &restclient.Config{} // blindly overwrite existing values based on precedence @@ -174,6 +183,7 @@ func getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fa } if configAuthInfo.AuthProvider != nil { mergedConfig.AuthProvider = configAuthInfo.AuthProvider + mergedConfig.AuthConfigPersister = persistAuthConfig } // if there still isn't enough information to authenticate the user, try prompting @@ -219,7 +229,7 @@ func canIdentifyUser(config restclient.Config) bool { config.AuthProvider != nil } -// Namespace implements KubeConfig +// Namespace implements ClientConfig func (config *DirectClientConfig) Namespace() (string, bool, error) { if err := config.ConfirmUsable(); err != nil { return "", false, err @@ -238,6 +248,11 @@ func (config *DirectClientConfig) Namespace() (string, bool, error) { return configContext.Namespace, overridden, nil } +// ConfigAccess implements ClientConfig +func (config *DirectClientConfig) ConfigAccess() ConfigAccess { + return config.configAccess +} + // ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config, // but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible. func (config *DirectClientConfig) ConfirmUsable() error { @@ -354,6 +369,10 @@ func (inClusterClientConfig) Namespace() (string, error) { return "default", nil } +func (inClusterClientConfig) ConfigAccess() ConfigAccess { + return NewDefaultClientConfigLoadingRules() +} + // Possible returns true if loading an inside-kubernetes-cluster is possible. func (inClusterClientConfig) Possible() bool { fi, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token") @@ -376,7 +395,6 @@ func BuildConfigFromFlags(masterUrl, kubeconfigPath string) (*restclient.Config, } glog.Warning("error creating inClusterConfig, falling back to default config: ", err) } - return NewNonInteractiveDeferredLoadingClientConfig( &ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}, &ConfigOverrides{ClusterInfo: clientcmdapi.Cluster{Server: masterUrl}}).ClientConfig() diff --git a/pkg/client/unversioned/clientcmd/client_config_test.go b/pkg/client/unversioned/clientcmd/client_config_test.go index 8e68ff7a13c..1e9b5b8b39a 100644 --- a/pkg/client/unversioned/clientcmd/client_config_test.go +++ b/pkg/client/unversioned/clientcmd/client_config_test.go @@ -78,7 +78,7 @@ func TestInsecureOverridesCA(t *testing.T) { ClusterInfo: clientcmdapi.Cluster{ InsecureSkipTLSVerify: true, }, - }) + }, nil) actualCfg, err := clientBuilder.ClientConfig() if err != nil { @@ -94,7 +94,7 @@ func TestMergeContext(t *testing.T) { const namespace = "overriden-namespace" config := createValidTestConfig() - clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}) + clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil) _, overridden, err := clientBuilder.Namespace() if err != nil { @@ -109,7 +109,7 @@ func TestMergeContext(t *testing.T) { Context: clientcmdapi.Context{ Namespace: namespace, }, - }) + }, nil) actual, overridden, err := clientBuilder.Namespace() if err != nil { @@ -143,7 +143,7 @@ func TestCertificateData(t *testing.T) { } config.CurrentContext = "clean" - clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}) + clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil) clientConfig, err := clientBuilder.ClientConfig() if err != nil { @@ -174,7 +174,7 @@ func TestBasicAuthData(t *testing.T) { } config.CurrentContext = "clean" - clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}) + clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil) clientConfig, err := clientBuilder.ClientConfig() if err != nil { @@ -188,7 +188,7 @@ func TestBasicAuthData(t *testing.T) { func TestCreateClean(t *testing.T) { config := createValidTestConfig() - clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}) + clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil) clientConfig, err := clientBuilder.ClientConfig() if err != nil { @@ -228,7 +228,7 @@ func TestCreateCleanWithPrefix(t *testing.T) { cleanConfig.Server = tc.server config.Clusters["clean"] = cleanConfig - clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}) + clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil) clientConfig, err := clientBuilder.ClientConfig() if err != nil { @@ -256,7 +256,7 @@ func TestCreateCleanDefault(t *testing.T) { func TestCreateMissingContext(t *testing.T) { const expectedErrorContains = "Context was not found for specified context" config := createValidTestConfig() - clientBuilder := NewNonInteractiveClientConfig(*config, "not-present", &ConfigOverrides{}) + clientBuilder := NewNonInteractiveClientConfig(*config, "not-present", &ConfigOverrides{}, nil) clientConfig, err := clientBuilder.ClientConfig() if err != nil { diff --git a/pkg/client/unversioned/clientcmd/config.go b/pkg/client/unversioned/clientcmd/config.go index 045055cd77f..049fc39213c 100644 --- a/pkg/client/unversioned/clientcmd/config.go +++ b/pkg/client/unversioned/clientcmd/config.go @@ -25,6 +25,7 @@ import ( "github.com/golang/glog" + "k8s.io/kubernetes/pkg/client/restclient" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" ) @@ -294,6 +295,28 @@ func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, rela return nil } +func PersisterForUser(configAccess ConfigAccess, user string) restclient.AuthProviderConfigPersister { + return &persister{configAccess, user} +} + +type persister struct { + configAccess ConfigAccess + user string +} + +func (p *persister) Persist(config map[string]string) error { + newConfig, err := p.configAccess.GetStartingConfig() + if err != nil { + return err + } + authInfo, ok := newConfig.AuthInfos[p.user] + if ok && authInfo.AuthProvider != nil { + authInfo.AuthProvider.Config = config + ModifyConfig(p.configAccess, *newConfig, false) + } + return nil +} + // writeCurrentContext takes three possible paths. // If newCurrentContext is the same as the startingConfig's current context, then we exit. // If newCurrentContext has a value, then that value is written into the default destination file. diff --git a/pkg/client/unversioned/clientcmd/loader.go b/pkg/client/unversioned/clientcmd/loader.go index 7650dd24f42..9707144701a 100644 --- a/pkg/client/unversioned/clientcmd/loader.go +++ b/pkg/client/unversioned/clientcmd/loader.go @@ -229,6 +229,54 @@ func (rules *ClientConfigLoadingRules) Migrate() error { return nil } +// GetLoadingPrecedence implements ConfigAccess +func (rules *ClientConfigLoadingRules) GetLoadingPrecedence() []string { + return rules.Precedence +} + +// GetStartingConfig implements ConfigAccess +func (rules *ClientConfigLoadingRules) GetStartingConfig() (*clientcmdapi.Config, error) { + clientConfig := NewNonInteractiveDeferredLoadingClientConfig(rules, &ConfigOverrides{}) + rawConfig, err := clientConfig.RawConfig() + if os.IsNotExist(err) { + return clientcmdapi.NewConfig(), nil + } + if err != nil { + return nil, err + } + + return &rawConfig, nil +} + +// GetDefaultFilename implements ConfigAccess +func (rules *ClientConfigLoadingRules) GetDefaultFilename() string { + // Explicit file if we have one. + if rules.IsExplicitFile() { + return rules.GetExplicitFile() + } + // Otherwise, first existing file from precedence. + for _, filename := range rules.GetLoadingPrecedence() { + if _, err := os.Stat(filename); err == nil { + return filename + } + } + // If none exists, use the first from precendence. + if len(rules.Precedence) > 0 { + return rules.Precedence[0] + } + return "" +} + +// IsExplicitFile implements ConfigAccess +func (rules *ClientConfigLoadingRules) IsExplicitFile() bool { + return len(rules.ExplicitPath) > 0 +} + +// GetExplicitFile implements ConfigAccess +func (rules *ClientConfigLoadingRules) GetExplicitFile() string { + return rules.ExplicitPath +} + // LoadFromFile takes a filename and deserializes the contents into Config object func LoadFromFile(filename string) (*clientcmdapi.Config, error) { kubeconfigBytes, err := ioutil.ReadFile(filename) diff --git a/pkg/client/unversioned/clientcmd/merged_client_builder.go b/pkg/client/unversioned/clientcmd/merged_client_builder.go index 321eae9e87c..57fb2e20f39 100644 --- a/pkg/client/unversioned/clientcmd/merged_client_builder.go +++ b/pkg/client/unversioned/clientcmd/merged_client_builder.go @@ -64,9 +64,9 @@ func (config *DeferredLoadingClientConfig) createClientConfig() (ClientConfig, e var mergedClientConfig ClientConfig if config.fallbackReader != nil { - mergedClientConfig = NewInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.fallbackReader) + mergedClientConfig = NewInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.fallbackReader, config.loadingRules) } else { - mergedClientConfig = NewNonInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides) + mergedClientConfig = NewNonInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.loadingRules) } config.clientConfig = mergedClientConfig @@ -91,6 +91,7 @@ func (config *DeferredLoadingClientConfig) ClientConfig() (*restclient.Config, e if err != nil { return nil, err } + mergedConfig, err := mergedClientConfig.ClientConfig() if err != nil { return nil, err @@ -102,7 +103,6 @@ func (config *DeferredLoadingClientConfig) ClientConfig() (*restclient.Config, e glog.V(2).Info("No kubeconfig could be created, falling back to service account.") return icc.ClientConfig() } - return mergedConfig, nil } @@ -115,3 +115,8 @@ func (config *DeferredLoadingClientConfig) Namespace() (string, bool, error) { return mergedKubeConfig.Namespace() } + +// ConfigAccess implements ClientConfig +func (config *DeferredLoadingClientConfig) ConfigAccess() ConfigAccess { + return config.loadingRules +} diff --git a/plugin/pkg/client/auth/gcp/gcp.go b/plugin/pkg/client/auth/gcp/gcp.go index 4e4b894ceab..1efbb20f11a 100644 --- a/plugin/pkg/client/auth/gcp/gcp.go +++ b/plugin/pkg/client/auth/gcp/gcp.go @@ -18,6 +18,7 @@ package gcp import ( "net/http" + "time" "github.com/golang/glog" "golang.org/x/net/context" @@ -35,14 +36,15 @@ func init() { type gcpAuthProvider struct { tokenSource oauth2.TokenSource + persister restclient.AuthProviderConfigPersister } -func newGCPAuthProvider() (restclient.AuthProvider, error) { - ts, err := google.DefaultTokenSource(context.TODO(), "https://www.googleapis.com/auth/cloud-platform") +func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) { + ts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister) if err != nil { return nil, err } - return &gcpAuthProvider{ts}, nil + return &gcpAuthProvider{ts, persister}, nil } func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper { @@ -51,3 +53,54 @@ func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper Base: rt, } } + +func (g *gcpAuthProvider) Login() error { return nil } + +type cachedTokenSource struct { + source oauth2.TokenSource + accessToken string + expiry time.Time + persister restclient.AuthProviderConfigPersister +} + +func newCachedTokenSource(accessToken, expiry string, persister restclient.AuthProviderConfigPersister) (*cachedTokenSource, error) { + var expiryTime time.Time + if parsedTime, err := time.Parse(time.RFC3339Nano, expiry); err == nil { + expiryTime = parsedTime + } + ts, err := google.DefaultTokenSource(context.Background(), "https://www.googleapis.com/auth/cloud-platform") + if err != nil { + return nil, err + } + return &cachedTokenSource{ + source: ts, + accessToken: accessToken, + expiry: expiryTime, + persister: persister, + }, nil +} + +func (t *cachedTokenSource) Token() (*oauth2.Token, error) { + tok := &oauth2.Token{ + AccessToken: t.accessToken, + TokenType: "Bearer", + Expiry: t.expiry, + } + if tok.Valid() && !tok.Expiry.IsZero() { + return tok, nil + } + tok, err := t.source.Token() + if err != nil { + return nil, err + } + if t.persister != nil { + cached := map[string]string{ + "access-token": tok.AccessToken, + "expiry": tok.Expiry.Format(time.RFC3339Nano), + } + if err := t.persister.Persist(cached); err != nil { + glog.V(4).Infof("Failed to persist token: %v", err) + } + } + return tok, nil +} diff --git a/test/integration/kubectl_test.go b/test/integration/kubectl_test.go index 10b3e0f404f..b17e97feda0 100644 --- a/test/integration/kubectl_test.go +++ b/test/integration/kubectl_test.go @@ -56,7 +56,7 @@ func TestKubectlValidation(t *testing.T) { Context: *ctx, CurrentContext: "test", } - cmdConfig := clientcmd.NewNonInteractiveClientConfig(*cfg, "test", &overrides) + cmdConfig := clientcmd.NewNonInteractiveClientConfig(*cfg, "test", &overrides, nil) factory := util.NewFactory(cmdConfig) schema, err := factory.Validator(true, "") if err != nil {