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 new file mode 100644 index 00000000000..049fc39213c --- /dev/null +++ b/pkg/client/unversioned/clientcmd/config.go @@ -0,0 +1,419 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clientcmd + +import ( + "errors" + "os" + "path" + "path/filepath" + "reflect" + + "github.com/golang/glog" + + "k8s.io/kubernetes/pkg/client/restclient" + clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" +) + +// ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files +type ConfigAccess interface { + // GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config + GetLoadingPrecedence() []string + // GetStartingConfig returns the config that subcommands should being operating against. It may or may not be merged depending on loading rules + GetStartingConfig() (*clientcmdapi.Config, error) + // GetDefaultFilename returns the name of the file you should write into (create if necessary), if you're trying to create a new stanza as opposed to updating an existing one. + GetDefaultFilename() string + // IsExplicitFile indicates whether or not this command is interested in exactly one file. This implementation only ever does that via a flag, but implementations that handle local, global, and flags may have more + IsExplicitFile() bool + // GetExplicitFile returns the particular file this command is operating against. This implementation only ever has one, but implementations that handle local, global, and flags may have more + GetExplicitFile() string +} + +type PathOptions struct { + // GlobalFile is the full path to the file to load as the global (final) option + GlobalFile string + // EnvVar is the env var name that points to the list of kubeconfig files to load + EnvVar string + // ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file + ExplicitFileFlag string + + // GlobalFileSubpath is an optional value used for displaying help + GlobalFileSubpath string + + LoadingRules *ClientConfigLoadingRules +} + +func (o *PathOptions) GetEnvVarFiles() []string { + if len(o.EnvVar) == 0 { + return []string{} + } + + envVarValue := os.Getenv(o.EnvVar) + if len(envVarValue) == 0 { + return []string{} + } + + return filepath.SplitList(envVarValue) +} + +func (o *PathOptions) GetLoadingPrecedence() []string { + if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 { + return envVarFiles + } + + return []string{o.GlobalFile} +} + +func (o *PathOptions) GetStartingConfig() (*clientcmdapi.Config, error) { + // don't mutate the original + loadingRules := *o.LoadingRules + loadingRules.Precedence = o.GetLoadingPrecedence() + + clientConfig := NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &ConfigOverrides{}) + rawConfig, err := clientConfig.RawConfig() + if os.IsNotExist(err) { + return clientcmdapi.NewConfig(), nil + } + if err != nil { + return nil, err + } + + return &rawConfig, nil +} + +func (o *PathOptions) GetDefaultFilename() string { + if o.IsExplicitFile() { + return o.GetExplicitFile() + } + + if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 { + if len(envVarFiles) == 1 { + return envVarFiles[0] + } + + // if any of the envvar files already exists, return it + for _, envVarFile := range envVarFiles { + if _, err := os.Stat(envVarFile); err == nil { + return envVarFile + } + } + + // otherwise, return the last one in the list + return envVarFiles[len(envVarFiles)-1] + } + + return o.GlobalFile +} + +func (o *PathOptions) IsExplicitFile() bool { + if len(o.LoadingRules.ExplicitPath) > 0 { + return true + } + + return false +} + +func (o *PathOptions) GetExplicitFile() string { + return o.LoadingRules.ExplicitPath +} + +func NewDefaultPathOptions() *PathOptions { + ret := &PathOptions{ + GlobalFile: RecommendedHomeFile, + EnvVar: RecommendedConfigPathEnvVar, + ExplicitFileFlag: RecommendedConfigPathFlag, + + GlobalFileSubpath: path.Join(RecommendedHomeDir, RecommendedFileName), + + LoadingRules: NewDefaultClientConfigLoadingRules(), + } + ret.LoadingRules.DoNotResolvePaths = true + + return ret +} + +// ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or +// uses the default destination file to write the results into. This results in multiple file reads, but it's very easy to follow. +// Preferences and CurrentContext should always be set in the default destination file. Since we can't distinguish between empty and missing values +// (no nil strings), we're forced have separate handling for them. In the kubeconfig cases, newConfig should have at most one difference, +// that means that this code will only write into a single file. If you want to relativizePaths, you must provide a fully qualified path in any +// modified element. +func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error { + startingConfig, err := configAccess.GetStartingConfig() + if err != nil { + return err + } + + // We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file. + // Special case the test for current context and preferences since those always write to the default file. + if reflect.DeepEqual(*startingConfig, newConfig) { + // nothing to do + return nil + } + + if startingConfig.CurrentContext != newConfig.CurrentContext { + if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil { + return err + } + } + + if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) { + if err := writePreferences(configAccess, newConfig.Preferences); err != nil { + return err + } + } + + // Search every cluster, authInfo, and context. First from new to old for differences, then from old to new for deletions + for key, cluster := range newConfig.Clusters { + startingCluster, exists := startingConfig.Clusters[key] + if !reflect.DeepEqual(cluster, startingCluster) || !exists { + destinationFile := cluster.LocationOfOrigin + if len(destinationFile) == 0 { + destinationFile = configAccess.GetDefaultFilename() + } + + configToWrite := GetConfigFromFileOrDie(destinationFile) + t := *cluster + + configToWrite.Clusters[key] = &t + configToWrite.Clusters[key].LocationOfOrigin = destinationFile + if relativizePaths { + if err := RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil { + return err + } + } + + if err := WriteToFile(*configToWrite, destinationFile); err != nil { + return err + } + } + } + + for key, context := range newConfig.Contexts { + startingContext, exists := startingConfig.Contexts[key] + if !reflect.DeepEqual(context, startingContext) || !exists { + destinationFile := context.LocationOfOrigin + if len(destinationFile) == 0 { + destinationFile = configAccess.GetDefaultFilename() + } + + configToWrite := GetConfigFromFileOrDie(destinationFile) + configToWrite.Contexts[key] = context + + if err := WriteToFile(*configToWrite, destinationFile); err != nil { + return err + } + } + } + + for key, authInfo := range newConfig.AuthInfos { + startingAuthInfo, exists := startingConfig.AuthInfos[key] + if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists { + destinationFile := authInfo.LocationOfOrigin + if len(destinationFile) == 0 { + destinationFile = configAccess.GetDefaultFilename() + } + + configToWrite := GetConfigFromFileOrDie(destinationFile) + t := *authInfo + configToWrite.AuthInfos[key] = &t + configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile + if relativizePaths { + if err := RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil { + return err + } + } + + if err := WriteToFile(*configToWrite, destinationFile); err != nil { + return err + } + } + } + + for key, cluster := range startingConfig.Clusters { + if _, exists := newConfig.Clusters[key]; !exists { + destinationFile := cluster.LocationOfOrigin + if len(destinationFile) == 0 { + destinationFile = configAccess.GetDefaultFilename() + } + + configToWrite := GetConfigFromFileOrDie(destinationFile) + delete(configToWrite.Clusters, key) + + if err := WriteToFile(*configToWrite, destinationFile); err != nil { + return err + } + } + } + + for key, context := range startingConfig.Contexts { + if _, exists := newConfig.Contexts[key]; !exists { + destinationFile := context.LocationOfOrigin + if len(destinationFile) == 0 { + destinationFile = configAccess.GetDefaultFilename() + } + + configToWrite := GetConfigFromFileOrDie(destinationFile) + delete(configToWrite.Contexts, key) + + if err := WriteToFile(*configToWrite, destinationFile); err != nil { + return err + } + } + } + + for key, authInfo := range startingConfig.AuthInfos { + if _, exists := newConfig.AuthInfos[key]; !exists { + destinationFile := authInfo.LocationOfOrigin + if len(destinationFile) == 0 { + destinationFile = configAccess.GetDefaultFilename() + } + + configToWrite := GetConfigFromFileOrDie(destinationFile) + delete(configToWrite.AuthInfos, key) + + if err := WriteToFile(*configToWrite, destinationFile); err != nil { + return err + } + } + } + + 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. +// If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file +func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error { + if startingConfig, err := configAccess.GetStartingConfig(); err != nil { + return err + } else if startingConfig.CurrentContext == newCurrentContext { + return nil + } + + if configAccess.IsExplicitFile() { + file := configAccess.GetExplicitFile() + currConfig := GetConfigFromFileOrDie(file) + currConfig.CurrentContext = newCurrentContext + if err := WriteToFile(*currConfig, file); err != nil { + return err + } + + return nil + } + + if len(newCurrentContext) > 0 { + destinationFile := configAccess.GetDefaultFilename() + config := GetConfigFromFileOrDie(destinationFile) + config.CurrentContext = newCurrentContext + + if err := WriteToFile(*config, destinationFile); err != nil { + return err + } + + return nil + } + + // we're supposed to be clearing the current context. We need to find the first spot in the chain that is setting it and clear it + for _, file := range configAccess.GetLoadingPrecedence() { + if _, err := os.Stat(file); err == nil { + currConfig := GetConfigFromFileOrDie(file) + + if len(currConfig.CurrentContext) > 0 { + currConfig.CurrentContext = newCurrentContext + if err := WriteToFile(*currConfig, file); err != nil { + return err + } + + return nil + } + } + } + + return errors.New("no config found to write context") +} + +func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error { + if startingConfig, err := configAccess.GetStartingConfig(); err != nil { + return err + } else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) { + return nil + } + + if configAccess.IsExplicitFile() { + file := configAccess.GetExplicitFile() + currConfig := GetConfigFromFileOrDie(file) + currConfig.Preferences = newPrefs + if err := WriteToFile(*currConfig, file); err != nil { + return err + } + + return nil + } + + for _, file := range configAccess.GetLoadingPrecedence() { + currConfig := GetConfigFromFileOrDie(file) + + if !reflect.DeepEqual(currConfig.Preferences, newPrefs) { + currConfig.Preferences = newPrefs + if err := WriteToFile(*currConfig, file); err != nil { + return err + } + + return nil + } + } + + return errors.New("no config found to write preferences") +} + +// GetConfigFromFileOrDie tries to read a kubeconfig file and if it can't, it calls exit. One exception, missing files result in empty configs, not an exit +func GetConfigFromFileOrDie(filename string) *clientcmdapi.Config { + config, err := LoadFromFile(filename) + if err != nil && !os.IsNotExist(err) { + glog.FatalDepth(1, err) + } + + if config == nil { + return clientcmdapi.NewConfig() + } + + return config +} 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/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 5ff4a1a59e4..6225a457315 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -20,6 +20,7 @@ import ( "io" "github.com/golang/glog" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" cmdconfig "k8s.io/kubernetes/pkg/kubectl/cmd/config" "k8s.io/kubernetes/pkg/kubectl/cmd/rollout" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" @@ -225,7 +226,7 @@ Find more information at https://github.com/kubernetes/kubernetes.`, cmds.AddCommand(NewCmdLabel(f, out)) cmds.AddCommand(NewCmdAnnotate(f, out)) - cmds.AddCommand(cmdconfig.NewCmdConfig(cmdconfig.NewDefaultPathOptions(), out)) + cmds.AddCommand(cmdconfig.NewCmdConfig(clientcmd.NewDefaultPathOptions(), out)) cmds.AddCommand(NewCmdClusterInfo(f, out)) cmds.AddCommand(NewCmdApiVersions(f, out)) cmds.AddCommand(NewCmdVersion(f, out)) diff --git a/pkg/kubectl/cmd/config/config.go b/pkg/kubectl/cmd/config/config.go index 47eeecf5c07..a2856346838 100644 --- a/pkg/kubectl/cmd/config/config.go +++ b/pkg/kubectl/cmd/config/config.go @@ -17,50 +17,16 @@ limitations under the License. package config import ( - "errors" "io" - "os" "path" - "path/filepath" - "reflect" "strconv" - "github.com/golang/glog" "github.com/spf13/cobra" "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" - clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" ) -type PathOptions struct { - // GlobalFile is the full path to the file to load as the global (final) option - GlobalFile string - // EnvVar is the env var name that points to the list of kubeconfig files to load - EnvVar string - // ExplicitFileFlag is the name of the flag to use for prompting for the kubeconfig file - ExplicitFileFlag string - - // GlobalFileSubpath is an optional value used for displaying help - GlobalFileSubpath string - - LoadingRules *clientcmd.ClientConfigLoadingRules -} - -// ConfigAccess is used by subcommands and methods in this package to load and modify the appropriate config files -type ConfigAccess interface { - // GetLoadingPrecedence returns the slice of files that should be used for loading and inspecting the config - GetLoadingPrecedence() []string - // GetStartingConfig returns the config that subcommands should being operating against. It may or may not be merged depending on loading rules - GetStartingConfig() (*clientcmdapi.Config, error) - // GetDefaultFilename returns the name of the file you should write into (create if necessary), if you're trying to create a new stanza as opposed to updating an existing one. - GetDefaultFilename() string - // IsExplicitFile indicates whether or not this command is interested in exactly one file. This implementation only ever does that via a flag, but implementations that handle local, global, and flags may have more - IsExplicitFile() bool - // GetExplicitFile returns the particular file this command is operating against. This implementation only ever has one, but implementations that handle local, global, and flags may have more - GetExplicitFile() string -} - -func NewCmdConfig(pathOptions *PathOptions, out io.Writer) *cobra.Command { +func NewCmdConfig(pathOptions *clientcmd.PathOptions, out io.Writer) *cobra.Command { if len(pathOptions.ExplicitFileFlag) == 0 { pathOptions.ExplicitFileFlag = clientcmd.RecommendedConfigPathFlag } @@ -95,345 +61,6 @@ The loading order follows these rules: return cmd } -func NewDefaultPathOptions() *PathOptions { - ret := &PathOptions{ - GlobalFile: clientcmd.RecommendedHomeFile, - EnvVar: clientcmd.RecommendedConfigPathEnvVar, - ExplicitFileFlag: clientcmd.RecommendedConfigPathFlag, - - GlobalFileSubpath: path.Join(clientcmd.RecommendedHomeDir, clientcmd.RecommendedFileName), - - LoadingRules: clientcmd.NewDefaultClientConfigLoadingRules(), - } - ret.LoadingRules.DoNotResolvePaths = true - - return ret -} - -func (o *PathOptions) GetEnvVarFiles() []string { - if len(o.EnvVar) == 0 { - return []string{} - } - - envVarValue := os.Getenv(o.EnvVar) - if len(envVarValue) == 0 { - return []string{} - } - - return filepath.SplitList(envVarValue) -} - -func (o *PathOptions) GetLoadingPrecedence() []string { - if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 { - return envVarFiles - } - - return []string{o.GlobalFile} -} - -func (o *PathOptions) GetStartingConfig() (*clientcmdapi.Config, error) { - // don't mutate the original - loadingRules := *o.LoadingRules - loadingRules.Precedence = o.GetLoadingPrecedence() - - clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(&loadingRules, &clientcmd.ConfigOverrides{}) - rawConfig, err := clientConfig.RawConfig() - if os.IsNotExist(err) { - return clientcmdapi.NewConfig(), nil - } - if err != nil { - return nil, err - } - - return &rawConfig, nil -} - -func (o *PathOptions) GetDefaultFilename() string { - if o.IsExplicitFile() { - return o.GetExplicitFile() - } - - if envVarFiles := o.GetEnvVarFiles(); len(envVarFiles) > 0 { - if len(envVarFiles) == 1 { - return envVarFiles[0] - } - - // if any of the envvar files already exists, return it - for _, envVarFile := range envVarFiles { - if _, err := os.Stat(envVarFile); err == nil { - return envVarFile - } - } - - // otherwise, return the last one in the list - return envVarFiles[len(envVarFiles)-1] - } - - return o.GlobalFile -} - -func (o *PathOptions) IsExplicitFile() bool { - if len(o.LoadingRules.ExplicitPath) > 0 { - return true - } - - return false -} - -func (o *PathOptions) GetExplicitFile() string { - return o.LoadingRules.ExplicitPath -} - -// ModifyConfig takes a Config object, iterates through Clusters, AuthInfos, and Contexts, uses the LocationOfOrigin if specified or -// uses the default destination file to write the results into. This results in multiple file reads, but it's very easy to follow. -// Preferences and CurrentContext should always be set in the default destination file. Since we can't distinguish between empty and missing values -// (no nil strings), we're forced have separate handling for them. In the kubeconfig cases, newConfig should have at most one difference, -// that means that this code will only write into a single file. If you want to relativizePaths, you must provide a fully qualified path in any -// modified element. -func ModifyConfig(configAccess ConfigAccess, newConfig clientcmdapi.Config, relativizePaths bool) error { - startingConfig, err := configAccess.GetStartingConfig() - if err != nil { - return err - } - - // We need to find all differences, locate their original files, read a partial config to modify only that stanza and write out the file. - // Special case the test for current context and preferences since those always write to the default file. - if reflect.DeepEqual(*startingConfig, newConfig) { - // nothing to do - return nil - } - - if startingConfig.CurrentContext != newConfig.CurrentContext { - if err := writeCurrentContext(configAccess, newConfig.CurrentContext); err != nil { - return err - } - } - - if !reflect.DeepEqual(startingConfig.Preferences, newConfig.Preferences) { - if err := writePreferences(configAccess, newConfig.Preferences); err != nil { - return err - } - } - - // Search every cluster, authInfo, and context. First from new to old for differences, then from old to new for deletions - for key, cluster := range newConfig.Clusters { - startingCluster, exists := startingConfig.Clusters[key] - if !reflect.DeepEqual(cluster, startingCluster) || !exists { - destinationFile := cluster.LocationOfOrigin - if len(destinationFile) == 0 { - destinationFile = configAccess.GetDefaultFilename() - } - - configToWrite := getConfigFromFileOrDie(destinationFile) - t := *cluster - - configToWrite.Clusters[key] = &t - configToWrite.Clusters[key].LocationOfOrigin = destinationFile - if relativizePaths { - if err := clientcmd.RelativizeClusterLocalPaths(configToWrite.Clusters[key]); err != nil { - return err - } - } - - if err := clientcmd.WriteToFile(*configToWrite, destinationFile); err != nil { - return err - } - } - } - - for key, context := range newConfig.Contexts { - startingContext, exists := startingConfig.Contexts[key] - if !reflect.DeepEqual(context, startingContext) || !exists { - destinationFile := context.LocationOfOrigin - if len(destinationFile) == 0 { - destinationFile = configAccess.GetDefaultFilename() - } - - configToWrite := getConfigFromFileOrDie(destinationFile) - configToWrite.Contexts[key] = context - - if err := clientcmd.WriteToFile(*configToWrite, destinationFile); err != nil { - return err - } - } - } - - for key, authInfo := range newConfig.AuthInfos { - startingAuthInfo, exists := startingConfig.AuthInfos[key] - if !reflect.DeepEqual(authInfo, startingAuthInfo) || !exists { - destinationFile := authInfo.LocationOfOrigin - if len(destinationFile) == 0 { - destinationFile = configAccess.GetDefaultFilename() - } - - configToWrite := getConfigFromFileOrDie(destinationFile) - t := *authInfo - configToWrite.AuthInfos[key] = &t - configToWrite.AuthInfos[key].LocationOfOrigin = destinationFile - if relativizePaths { - if err := clientcmd.RelativizeAuthInfoLocalPaths(configToWrite.AuthInfos[key]); err != nil { - return err - } - } - - if err := clientcmd.WriteToFile(*configToWrite, destinationFile); err != nil { - return err - } - } - } - - for key, cluster := range startingConfig.Clusters { - if _, exists := newConfig.Clusters[key]; !exists { - destinationFile := cluster.LocationOfOrigin - if len(destinationFile) == 0 { - destinationFile = configAccess.GetDefaultFilename() - } - - configToWrite := getConfigFromFileOrDie(destinationFile) - delete(configToWrite.Clusters, key) - - if err := clientcmd.WriteToFile(*configToWrite, destinationFile); err != nil { - return err - } - } - } - - for key, context := range startingConfig.Contexts { - if _, exists := newConfig.Contexts[key]; !exists { - destinationFile := context.LocationOfOrigin - if len(destinationFile) == 0 { - destinationFile = configAccess.GetDefaultFilename() - } - - configToWrite := getConfigFromFileOrDie(destinationFile) - delete(configToWrite.Contexts, key) - - if err := clientcmd.WriteToFile(*configToWrite, destinationFile); err != nil { - return err - } - } - } - - for key, authInfo := range startingConfig.AuthInfos { - if _, exists := newConfig.AuthInfos[key]; !exists { - destinationFile := authInfo.LocationOfOrigin - if len(destinationFile) == 0 { - destinationFile = configAccess.GetDefaultFilename() - } - - configToWrite := getConfigFromFileOrDie(destinationFile) - delete(configToWrite.AuthInfos, key) - - if err := clientcmd.WriteToFile(*configToWrite, destinationFile); err != nil { - return err - } - } - } - - 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. -// If newCurrentContext is empty, then we find the config file that is setting the CurrentContext and clear the value from that file -func writeCurrentContext(configAccess ConfigAccess, newCurrentContext string) error { - if startingConfig, err := configAccess.GetStartingConfig(); err != nil { - return err - } else if startingConfig.CurrentContext == newCurrentContext { - return nil - } - - if configAccess.IsExplicitFile() { - file := configAccess.GetExplicitFile() - currConfig := getConfigFromFileOrDie(file) - currConfig.CurrentContext = newCurrentContext - if err := clientcmd.WriteToFile(*currConfig, file); err != nil { - return err - } - - return nil - } - - if len(newCurrentContext) > 0 { - destinationFile := configAccess.GetDefaultFilename() - config := getConfigFromFileOrDie(destinationFile) - config.CurrentContext = newCurrentContext - - if err := clientcmd.WriteToFile(*config, destinationFile); err != nil { - return err - } - - return nil - } - - // we're supposed to be clearing the current context. We need to find the first spot in the chain that is setting it and clear it - for _, file := range configAccess.GetLoadingPrecedence() { - if _, err := os.Stat(file); err == nil { - currConfig := getConfigFromFileOrDie(file) - - if len(currConfig.CurrentContext) > 0 { - currConfig.CurrentContext = newCurrentContext - if err := clientcmd.WriteToFile(*currConfig, file); err != nil { - return err - } - - return nil - } - } - } - - return errors.New("no config found to write context") -} - -func writePreferences(configAccess ConfigAccess, newPrefs clientcmdapi.Preferences) error { - if startingConfig, err := configAccess.GetStartingConfig(); err != nil { - return err - } else if reflect.DeepEqual(startingConfig.Preferences, newPrefs) { - return nil - } - - if configAccess.IsExplicitFile() { - file := configAccess.GetExplicitFile() - currConfig := getConfigFromFileOrDie(file) - currConfig.Preferences = newPrefs - if err := clientcmd.WriteToFile(*currConfig, file); err != nil { - return err - } - - return nil - } - - for _, file := range configAccess.GetLoadingPrecedence() { - currConfig := getConfigFromFileOrDie(file) - - if !reflect.DeepEqual(currConfig.Preferences, newPrefs) { - currConfig.Preferences = newPrefs - if err := clientcmd.WriteToFile(*currConfig, file); err != nil { - return err - } - - return nil - } - } - - return errors.New("no config found to write preferences") -} - -// getConfigFromFileOrDie tries to read a kubeconfig file and if it can't, it calls exit. One exception, missing files result in empty configs, not an exit -func getConfigFromFileOrDie(filename string) *clientcmdapi.Config { - config, err := clientcmd.LoadFromFile(filename) - if err != nil && !os.IsNotExist(err) { - glog.FatalDepth(1, err) - } - - if config == nil { - return clientcmdapi.NewConfig() - } - - return config -} - func toBool(propertyValue string) (bool, error) { boolValue := false if len(propertyValue) != 0 { diff --git a/pkg/kubectl/cmd/config/config_test.go b/pkg/kubectl/cmd/config/config_test.go index 3eca3d53c68..33ba5f4759f 100644 --- a/pkg/kubectl/cmd/config/config_test.go +++ b/pkg/kubectl/cmd/config/config_test.go @@ -770,12 +770,12 @@ func testConfigCommand(args []string, startingConfig clientcmdapi.Config, t *tes buf := bytes.NewBuffer([]byte{}) - cmd := NewCmdConfig(NewDefaultPathOptions(), buf) + cmd := NewCmdConfig(clientcmd.NewDefaultPathOptions(), buf) cmd.SetArgs(argsToUse) cmd.Execute() // outBytes, _ := ioutil.ReadFile(fakeKubeFile.Name()) - config := getConfigFromFileOrDie(fakeKubeFile.Name()) + config := clientcmd.GetConfigFromFileOrDie(fakeKubeFile.Name()) return buf.String(), *config } diff --git a/pkg/kubectl/cmd/config/create_authinfo.go b/pkg/kubectl/cmd/config/create_authinfo.go index 468a390a143..2fd8cf2cb18 100644 --- a/pkg/kubectl/cmd/config/create_authinfo.go +++ b/pkg/kubectl/cmd/config/create_authinfo.go @@ -33,7 +33,7 @@ import ( ) type createAuthInfoOptions struct { - configAccess ConfigAccess + configAccess clientcmd.ConfigAccess name string authPath util.StringFlag clientCertificate util.StringFlag @@ -69,7 +69,7 @@ kubectl config set-credentials cluster-admin --username=admin --password=uXFGweU # Embed client certificate data in the "cluster-admin" entry kubectl config set-credentials cluster-admin --client-certificate=~/.kube/admin.crt --embed-certs=true` -func NewCmdConfigSetAuthInfo(out io.Writer, configAccess ConfigAccess) *cobra.Command { +func NewCmdConfigSetAuthInfo(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &createAuthInfoOptions{configAccess: configAccess} cmd := &cobra.Command{ @@ -122,7 +122,7 @@ func (o createAuthInfoOptions) run() error { authInfo := o.modifyAuthInfo(*startingStanza) config.AuthInfos[o.name] = &authInfo - if err := ModifyConfig(o.configAccess, *config, true); err != nil { + if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil { return err } diff --git a/pkg/kubectl/cmd/config/create_cluster.go b/pkg/kubectl/cmd/config/create_cluster.go index 80daa98d7e9..dc9de40a09c 100644 --- a/pkg/kubectl/cmd/config/create_cluster.go +++ b/pkg/kubectl/cmd/config/create_cluster.go @@ -32,7 +32,7 @@ import ( ) type createClusterOptions struct { - configAccess ConfigAccess + configAccess clientcmd.ConfigAccess name string server util.StringFlag apiVersion util.StringFlag @@ -54,7 +54,7 @@ kubectl config set-cluster e2e --certificate-authority=~/.kube/e2e/kubernetes.ca kubectl config set-cluster e2e --insecure-skip-tls-verify=true` ) -func NewCmdConfigSetCluster(out io.Writer, configAccess ConfigAccess) *cobra.Command { +func NewCmdConfigSetCluster(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &createClusterOptions{configAccess: configAccess} cmd := &cobra.Command{ @@ -108,7 +108,7 @@ func (o createClusterOptions) run() error { cluster := o.modifyCluster(*startingStanza) config.Clusters[o.name] = &cluster - if err := ModifyConfig(o.configAccess, *config, true); err != nil { + if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil { return err } diff --git a/pkg/kubectl/cmd/config/create_context.go b/pkg/kubectl/cmd/config/create_context.go index e3d165e07c2..7f0ca2170db 100644 --- a/pkg/kubectl/cmd/config/create_context.go +++ b/pkg/kubectl/cmd/config/create_context.go @@ -29,7 +29,7 @@ import ( ) type createContextOptions struct { - configAccess ConfigAccess + configAccess clientcmd.ConfigAccess name string cluster util.StringFlag authInfo util.StringFlag @@ -43,7 +43,7 @@ Specifying a name that already exists will merge new fields on top of existing v kubectl config set-context gce --user=cluster-admin` ) -func NewCmdConfigSetContext(out io.Writer, configAccess ConfigAccess) *cobra.Command { +func NewCmdConfigSetContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &createContextOptions{configAccess: configAccess} cmd := &cobra.Command{ @@ -90,7 +90,7 @@ func (o createContextOptions) run() error { context := o.modifyContext(*startingStanza) config.Contexts[o.name] = &context - if err := ModifyConfig(o.configAccess, *config, true); err != nil { + if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil { return err } diff --git a/pkg/kubectl/cmd/config/current_context.go b/pkg/kubectl/cmd/config/current_context.go index fe5bcff69ab..f2941c6dbdd 100644 --- a/pkg/kubectl/cmd/config/current_context.go +++ b/pkg/kubectl/cmd/config/current_context.go @@ -19,13 +19,15 @@ package config import ( "fmt" "io" - cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" ) type CurrentContextOptions struct { - ConfigAccess ConfigAccess + ConfigAccess clientcmd.ConfigAccess } const ( @@ -34,7 +36,7 @@ const ( kubectl config current-context` ) -func NewCmdConfigCurrentContext(out io.Writer, configAccess ConfigAccess) *cobra.Command { +func NewCmdConfigCurrentContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &CurrentContextOptions{ConfigAccess: configAccess} cmd := &cobra.Command{ diff --git a/pkg/kubectl/cmd/config/current_context_test.go b/pkg/kubectl/cmd/config/current_context_test.go index 169c654435a..7a68415f6a6 100644 --- a/pkg/kubectl/cmd/config/current_context_test.go +++ b/pkg/kubectl/cmd/config/current_context_test.go @@ -64,7 +64,7 @@ func (test currentContextTest) run(t *testing.T) { t.Fatalf("unexpected error: %v", err) } - pathOptions := NewDefaultPathOptions() + pathOptions := clientcmd.NewDefaultPathOptions() pathOptions.GlobalFile = fakeKubeFile.Name() pathOptions.EnvVar = "" options := CurrentContextOptions{ diff --git a/pkg/kubectl/cmd/config/set.go b/pkg/kubectl/cmd/config/set.go index bc7fcae7e10..c1c078bcb86 100644 --- a/pkg/kubectl/cmd/config/set.go +++ b/pkg/kubectl/cmd/config/set.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/cobra" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" "k8s.io/kubernetes/pkg/util/flag" ) @@ -35,7 +36,7 @@ const ( ) type setOptions struct { - configAccess ConfigAccess + configAccess clientcmd.ConfigAccess propertyName string propertyValue string setRawBytes flag.Tristate @@ -45,7 +46,7 @@ const set_long = `Sets an individual value in a kubeconfig file PROPERTY_NAME is a dot delimited name where each token represents either a attribute name or a map key. Map keys may not contain dots. PROPERTY_VALUE is the new value you wish to set. Binary fields such as 'certificate-authority-data' expect a base64 encoded string unless the --set-raw-bytes flag is used.` -func NewCmdConfigSet(out io.Writer, configAccess ConfigAccess) *cobra.Command { +func NewCmdConfigSet(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &setOptions{configAccess: configAccess} cmd := &cobra.Command{ @@ -96,7 +97,7 @@ func (o setOptions) run() error { return err } - if err := ModifyConfig(o.configAccess, *config, false); err != nil { + if err := clientcmd.ModifyConfig(o.configAccess, *config, false); err != nil { return err } diff --git a/pkg/kubectl/cmd/config/unset.go b/pkg/kubectl/cmd/config/unset.go index 9e77c447544..f9446df5163 100644 --- a/pkg/kubectl/cmd/config/unset.go +++ b/pkg/kubectl/cmd/config/unset.go @@ -23,17 +23,19 @@ import ( "reflect" "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" ) type unsetOptions struct { - configAccess ConfigAccess + configAccess clientcmd.ConfigAccess propertyName string } const unset_long = `Unsets an individual value in a kubeconfig file PROPERTY_NAME is a dot delimited name where each token represents either a attribute name or a map key. Map keys may not contain dots.` -func NewCmdConfigUnset(out io.Writer, configAccess ConfigAccess) *cobra.Command { +func NewCmdConfigUnset(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &unsetOptions{configAccess: configAccess} cmd := &cobra.Command{ @@ -77,7 +79,7 @@ func (o unsetOptions) run() error { return err } - if err := ModifyConfig(o.configAccess, *config, false); err != nil { + if err := clientcmd.ModifyConfig(o.configAccess, *config, false); err != nil { return err } diff --git a/pkg/kubectl/cmd/config/use_context.go b/pkg/kubectl/cmd/config/use_context.go index a6a5c26d557..abfe8bbf91f 100644 --- a/pkg/kubectl/cmd/config/use_context.go +++ b/pkg/kubectl/cmd/config/use_context.go @@ -23,15 +23,16 @@ import ( "github.com/spf13/cobra" + "k8s.io/kubernetes/pkg/client/unversioned/clientcmd" clientcmdapi "k8s.io/kubernetes/pkg/client/unversioned/clientcmd/api" ) type useContextOptions struct { - configAccess ConfigAccess + configAccess clientcmd.ConfigAccess contextName string } -func NewCmdConfigUseContext(out io.Writer, configAccess ConfigAccess) *cobra.Command { +func NewCmdConfigUseContext(out io.Writer, configAccess clientcmd.ConfigAccess) *cobra.Command { options := &useContextOptions{configAccess: configAccess} cmd := &cobra.Command{ @@ -68,7 +69,7 @@ func (o useContextOptions) run() error { config.CurrentContext = o.contextName - if err := ModifyConfig(o.configAccess, *config, true); err != nil { + if err := clientcmd.ModifyConfig(o.configAccess, *config, true); err != nil { return err } diff --git a/pkg/kubectl/cmd/config/view.go b/pkg/kubectl/cmd/config/view.go index 135e2c03b89..1c1ae5df3a0 100644 --- a/pkg/kubectl/cmd/config/view.go +++ b/pkg/kubectl/cmd/config/view.go @@ -32,7 +32,7 @@ import ( ) type ViewOptions struct { - ConfigAccess ConfigAccess + ConfigAccess clientcmd.ConfigAccess Merge flag.Tristate Flatten bool Minify bool @@ -50,7 +50,7 @@ kubectl config view kubectl config view -o jsonpath='{.users[?(@.name == "e2e")].user.password}'` ) -func NewCmdConfigView(out io.Writer, ConfigAccess ConfigAccess) *cobra.Command { +func NewCmdConfigView(out io.Writer, ConfigAccess clientcmd.ConfigAccess) *cobra.Command { options := &ViewOptions{ConfigAccess: ConfigAccess} // Default to yaml defaultOutputFormat := "yaml" 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 {