diff --git a/pkg/client/clientcmd/client_config.go b/pkg/client/clientcmd/client_config.go index 5ccafd6c114..47a4d9ac76b 100644 --- a/pkg/client/clientcmd/client_config.go +++ b/pkg/client/clientcmd/client_config.go @@ -71,48 +71,153 @@ func (config DirectClientConfig) ClientConfig() (*client.Config, error) { configAuthInfo := config.getAuthInfo() configClusterInfo := config.getCluster() - clientConfig := client.Config{} + clientConfig := &client.Config{} clientConfig.Host = configClusterInfo.Server clientConfig.Version = configClusterInfo.APIVersion // only try to read the auth information if we are secure - if client.IsConfigTransportTLS(clientConfig) { - var authInfo *clientauth.Info + if client.IsConfigTransportTLS(*clientConfig) { var err error - switch { - case len(configAuthInfo.AuthPath) > 0: - authInfo, err = NewDefaultAuthLoader().LoadAuth(configAuthInfo.AuthPath) - if err != nil { - return nil, err - } - case len(configAuthInfo.Token) > 0: - authInfo = &clientauth.Info{BearerToken: configAuthInfo.Token} - - case len(configAuthInfo.ClientCertificate) > 0: - authInfo = &clientauth.Info{ - CertFile: configAuthInfo.ClientCertificate, - KeyFile: configAuthInfo.ClientKey, - } - - default: - authInfo = &clientauth.Info{} - } - - if !authInfo.Complete() && (config.fallbackReader != nil) { - prompter := NewPromptingAuthLoader(config.fallbackReader) - authInfo = prompter.Prompt() - } - - authInfo.Insecure = &configClusterInfo.InsecureSkipTLSVerify - - clientConfig, err = authInfo.MergeWithConfig(clientConfig) + // mergo is a first write wins for map value and a last writing wins for interface values + userAuthPartialConfig, err := getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader) if err != nil { return nil, err } + mergo.Merge(clientConfig, userAuthPartialConfig) + + serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo) + if err != nil { + return nil, err + } + mergo.Merge(clientConfig, serverAuthPartialConfig) } - return &clientConfig, nil + return clientConfig, nil +} + +// clientauth.Info object contain both user identification and server identification. We want different precedence orders for +// both, so we have to split the objects and merge them separately +// we want this order of precedence for the server identification +// 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files) +// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) +// 3. load the ~/.kubernetes_auth file as a default +func getServerIdentificationPartialConfig(configAuthInfo AuthInfo, configClusterInfo Cluster) (*client.Config, error) { + mergedConfig := &client.Config{} + + defaultAuthPathInfo, err := NewDefaultAuthLoader().LoadAuth(os.Getenv("HOME") + "/.kubernetes_auth") + // if the error is anything besides a does not exist, then fail. Not existing is ok + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if defaultAuthPathInfo != nil { + defaultAuthPathConfig := makeServerIdentificationConfig(*defaultAuthPathInfo) + mergo.Merge(mergedConfig, defaultAuthPathConfig) + } + + if len(configAuthInfo.AuthPath) > 0 { + authPathInfo, err := NewDefaultAuthLoader().LoadAuth(configAuthInfo.AuthPath) + if err != nil { + return nil, err + } + authPathConfig := makeServerIdentificationConfig(*authPathInfo) + mergo.Merge(mergedConfig, authPathConfig) + } + + // configClusterInfo holds the information identify the server provided by .kubeconfig + configClientConfig := &client.Config{} + configClientConfig.CAFile = configClusterInfo.CertificateAuthority + configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify + mergo.Merge(mergedConfig, configClientConfig) + + return mergedConfig, nil +} + +// clientauth.Info object contain both user identification and server identification. We want different precedence orders for +// both, so we have to split the objects and merge them separately +// we want this order of precedence for user identifcation +// 1. configAuthInfo minus auth-path (the final result of command line flags and merged .kubeconfig files) +// 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 AuthInfo, fallbackReader io.Reader) (*client.Config, error) { + mergedConfig := &client.Config{} + + if len(configAuthInfo.AuthPath) > 0 { + authPathInfo, err := NewDefaultAuthLoader().LoadAuth(configAuthInfo.AuthPath) + if err != nil { + return nil, err + } + authPathConfig := makeUserIdentificationConfig(*authPathInfo) + mergo.Merge(mergedConfig, authPathConfig) + } + + // blindly overwrite existing values based on precedence + if len(configAuthInfo.Token) > 0 { + mergedConfig.BearerToken = configAuthInfo.Token + } + if len(configAuthInfo.ClientCertificate) > 0 { + mergedConfig.CertFile = configAuthInfo.ClientCertificate + mergedConfig.KeyFile = configAuthInfo.ClientKey + } + + // if there isn't sufficient information to authenticate the user to the server, merge in ~/.kubernetes_auth. + if !canIdentifyUser(*mergedConfig) { + defaultAuthPathInfo, err := NewDefaultAuthLoader().LoadAuth(os.Getenv("HOME") + "/.kubernetes_auth") + // if the error is anything besides a does not exist, then fail. Not existing is ok + if err != nil && !os.IsNotExist(err) { + return nil, err + } + if defaultAuthPathInfo != nil { + defaultAuthPathConfig := makeUserIdentificationConfig(*defaultAuthPathInfo) + previouslyMergedConfig := mergedConfig + mergedConfig = &client.Config{} + mergo.Merge(mergedConfig, defaultAuthPathConfig) + mergo.Merge(mergedConfig, previouslyMergedConfig) + } + } + + // if there still isn't enough information to authenticate the user, try prompting + if !canIdentifyUser(*mergedConfig) && (fallbackReader != nil) { + prompter := NewPromptingAuthLoader(fallbackReader) + promptedAuthInfo := prompter.Prompt() + + promptedConfig := makeUserIdentificationConfig(*promptedAuthInfo) + previouslyMergedConfig := mergedConfig + mergedConfig = &client.Config{} + mergo.Merge(mergedConfig, promptedConfig) + mergo.Merge(mergedConfig, previouslyMergedConfig) + } + + return mergedConfig, nil +} + +// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only user identification information +func makeUserIdentificationConfig(info clientauth.Info) *client.Config { + config := &client.Config{} + config.Username = info.User + config.Password = info.Password + config.CertFile = info.CertFile + config.KeyFile = info.KeyFile + config.BearerToken = info.BearerToken + return config +} + +// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only server identification information +func makeServerIdentificationConfig(info clientauth.Info) client.Config { + config := client.Config{} + config.CAFile = info.CAFile + if info.Insecure != nil { + config.Insecure = *info.Insecure + } + return config +} + +func canIdentifyUser(config client.Config) bool { + return len(config.Username) > 0 || + len(config.CertFile) > 0 || + len(config.BearerToken) > 0 + } // ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config, diff --git a/pkg/client/clientcmd/merged_client_builder_test.go b/pkg/client/clientcmd/merged_client_builder_test.go new file mode 100644 index 00000000000..8a048908e4d --- /dev/null +++ b/pkg/client/clientcmd/merged_client_builder_test.go @@ -0,0 +1,92 @@ +/* +Copyright 2014 Google Inc. 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 ( + "encoding/json" + "io/ioutil" + "os" + "testing" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" +) + +// Verifies that referencing an old .kubernetes_auth file respects all fields +func TestAuthPathUpdatesBothClusterAndUser(t *testing.T) { + authFile, _ := ioutil.TempFile("", "") + defer os.Remove(authFile.Name()) + + insecure := true + auth := &clientauth.Info{ + User: "user", + Password: "password", + CAFile: "ca-file", + CertFile: "cert-file", + KeyFile: "key-file", + BearerToken: "bearer-token", + Insecure: &insecure, + } + err := testWriteAuthInfoFile(*auth, authFile.Name()) + if err != nil { + t.Errorf("Unexpected error %v", err) + } + + cmd := &cobra.Command{ + Run: func(cmd *cobra.Command, args []string) { + }, + } + clientConfig := testBindClientConfig(cmd) + cmd.ParseFlags([]string{"--server=https://localhost", "--auth-path=" + authFile.Name()}) + + config, err := clientConfig.ClientConfig() + if err != nil { + t.Errorf("Unexpected error %v", err) + } + + matchStringArg(auth.User, config.Username, t) + matchStringArg(auth.Password, config.Password, t) + matchStringArg(auth.CAFile, config.CAFile, t) + matchStringArg(auth.CertFile, config.CertFile, t) + matchStringArg(auth.KeyFile, config.KeyFile, t) + matchStringArg(auth.BearerToken, config.BearerToken, t) + matchBoolArg(*auth.Insecure, config.Insecure, t) +} + +func testWriteAuthInfoFile(auth clientauth.Info, filename string) error { + data, err := json.Marshal(auth) + if err != nil { + return err + } + err = ioutil.WriteFile(filename, data, 0600) + return err +} + +func testBindClientConfig(cmd *cobra.Command) ClientConfig { + loadingRules := NewClientConfigLoadingRules() + loadingRules.EnvVarPath = "" + loadingRules.HomeDirectoryPath = "" + loadingRules.CurrentDirectoryPath = "" + cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + + overrides := &ConfigOverrides{} + overrides.BindFlags(cmd.PersistentFlags(), RecommendedConfigOverrideFlags("")) + clientConfig := NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin) + + return clientConfig +} diff --git a/pkg/client/clientcmd/validation.go b/pkg/client/clientcmd/validation.go index a9b26829222..4d1a52abd92 100644 --- a/pkg/client/clientcmd/validation.go +++ b/pkg/client/clientcmd/validation.go @@ -122,11 +122,13 @@ func validateClusterInfo(clusterName string, clusterInfo Cluster) []error { func validateAuthInfo(authInfoName string, authInfo AuthInfo) []error { validationErrors := make([]error, 0) + usingAuthPath := false methods := make([]string, 0, 3) if len(authInfo.Token) != 0 { methods = append(methods, "token") } if len(authInfo.AuthPath) != 0 { + usingAuthPath = true methods = append(methods, "authFile") file, err := os.Open(authInfo.AuthPath) @@ -151,7 +153,8 @@ func validateAuthInfo(authInfoName string, authInfo AuthInfo) []error { } } - if (len(methods)) > 1 { + // authPath also provides information for the client to identify the server, so allow multiple auth methods in that case + if (len(methods) > 1) && (!usingAuthPath) { validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v. Found %v, only one is allowed", authInfoName, methods)) } diff --git a/pkg/client/clientcmd/validation_test.go b/pkg/client/clientcmd/validation_test.go index ceb0cbe10df..7408cbe134c 100644 --- a/pkg/client/clientcmd/validation_test.go +++ b/pkg/client/clientcmd/validation_test.go @@ -52,7 +52,7 @@ func TestConfirmUsableBadInfoButOkConfig(t *testing.T) { badValidation := configValidationTest{ config: config, - expectedErrorSubstring: []string{"unable to read auth-path", "more than one authentication method", "unable to read certificate-authority"}, + expectedErrorSubstring: []string{"unable to read auth-path", "unable to read certificate-authority"}, } okTest := configValidationTest{ config: config, @@ -77,7 +77,7 @@ func TestConfirmUsableBadInfoConfig(t *testing.T) { } test := configValidationTest{ config: config, - expectedErrorSubstring: []string{"unable to read auth-path", "more than one authentication method", "unable to read certificate-authority"}, + expectedErrorSubstring: []string{"unable to read auth-path", "unable to read certificate-authority"}, } test.testConfirmUsable("first", t) @@ -216,20 +216,6 @@ func TestValidateEmptyAuthInfo(t *testing.T) { test.testAuthInfo("error", t) test.testConfig(t) } -func TestValidateTooMayTechniquesAuthInfo(t *testing.T) { - config := NewConfig() - config.AuthInfos["error"] = AuthInfo{ - AuthPath: "anything", - Token: "here", - } - test := configValidationTest{ - config: config, - expectedErrorSubstring: []string{"more than one authentication method found"}, - } - - test.testAuthInfo("error", t) - test.testConfig(t) -} func TestValidatePathNotFoundAuthInfo(t *testing.T) { config := NewConfig() config.AuthInfos["error"] = AuthInfo{ diff --git a/pkg/kubectl/cmd/rollingupdate.go b/pkg/kubectl/cmd/rollingupdate.go index 839c09c874e..8d1e8b88b2a 100644 --- a/pkg/kubectl/cmd/rollingupdate.go +++ b/pkg/kubectl/cmd/rollingupdate.go @@ -21,6 +21,7 @@ import ( "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/spf13/cobra" ) @@ -68,8 +69,11 @@ $ cat frontend-v2.json | kubectl rollingupdate frontend-v1 -f - err = CompareNamespaceFromFile(cmd, namespace) checkErr(err) - client, err := f.ClientBuilder.Client() + config, err := f.ClientConfig.ClientConfig() checkErr(err) + client, err := client.New(config) + checkErr(err) + obj, err := mapping.Codec.Decode(data) checkErr(err) newRc := obj.(*api.ReplicationController)