From 0e688dc27117b1c78a430f1c63a4d2eabbc2d407 Mon Sep 17 00:00:00 2001 From: deads2k Date: Wed, 10 Dec 2014 15:16:18 -0500 Subject: [PATCH] add kubeconfig file --- cmd/kubectl/kubectl.go | 2 +- docs/kubeconfig-file.md | 69 +++ pkg/client/clientcmd/auth_loaders.go | 21 +- pkg/client/clientcmd/client_builder.go | 207 --------- pkg/client/clientcmd/client_builder_test.go | 356 --------------- pkg/client/clientcmd/client_config.go | 183 ++++++++ pkg/client/clientcmd/client_config_test.go | 106 +++++ pkg/client/clientcmd/doc.go | 16 +- pkg/client/clientcmd/loader.go | 117 +++++ pkg/client/clientcmd/loader_test.go | 182 ++++++++ pkg/client/clientcmd/merged_client_builder.go | 70 +++ pkg/client/clientcmd/overrides.go | 135 ++++++ pkg/client/clientcmd/provided_flags.go | 100 ----- pkg/client/clientcmd/types.go | 83 ++++ pkg/client/clientcmd/types_test.go | 121 ++++++ pkg/client/clientcmd/validation.go | 182 ++++++++ pkg/client/clientcmd/validation_test.go | 404 ++++++++++++++++++ pkg/client/helper.go | 20 + pkg/clientauth/clientauth.go | 6 + pkg/kubectl/cmd/cmd.go | 151 +++++-- pkg/kubectl/cmd/log.go | 6 +- pkg/kubectl/cmd/proxy.go | 2 +- pkg/kubectl/cmd/version.go | 8 +- 23 files changed, 1828 insertions(+), 719 deletions(-) create mode 100644 docs/kubeconfig-file.md delete mode 100644 pkg/client/clientcmd/client_builder.go delete mode 100644 pkg/client/clientcmd/client_builder_test.go create mode 100644 pkg/client/clientcmd/client_config.go create mode 100644 pkg/client/clientcmd/client_config_test.go create mode 100644 pkg/client/clientcmd/loader.go create mode 100644 pkg/client/clientcmd/loader_test.go create mode 100644 pkg/client/clientcmd/merged_client_builder.go create mode 100644 pkg/client/clientcmd/overrides.go delete mode 100644 pkg/client/clientcmd/provided_flags.go create mode 100644 pkg/client/clientcmd/types.go create mode 100644 pkg/client/clientcmd/types_test.go create mode 100644 pkg/client/clientcmd/validation.go create mode 100644 pkg/client/clientcmd/validation_test.go diff --git a/cmd/kubectl/kubectl.go b/cmd/kubectl/kubectl.go index 6e5030c09c..2f96c67196 100644 --- a/cmd/kubectl/kubectl.go +++ b/cmd/kubectl/kubectl.go @@ -24,6 +24,6 @@ import ( ) func main() { - clientBuilder := clientcmd.NewBuilder(clientcmd.NewPromptingAuthLoader(os.Stdin)) + clientBuilder := clientcmd.NewInteractiveClientConfig(clientcmd.Config{}, "", &clientcmd.ConfigOverrides{}, os.Stdin) cmd.NewFactory(clientBuilder).Run(os.Stdout) } diff --git a/docs/kubeconfig-file.md b/docs/kubeconfig-file.md new file mode 100644 index 0000000000..168b76a38b --- /dev/null +++ b/docs/kubeconfig-file.md @@ -0,0 +1,69 @@ +# .kubeconfig files +In order to easily switch between multiple clusters, a .kubeconfig file was defined. This file contains a series of authentication mechanisms and cluster connection information associated with nicknames. It also introduces the concept of a tuple of authentication information (user) and cluster connection information called a context that is also associated with a nickname. + +Multiple files are .kubeconfig files are allowed. At runtime they are loaded and merged together along with override options specified from the command line (see rules below). + +## Related discussion +https://github.com/GoogleCloudPlatform/kubernetes/issues/1755 + +## Example .kubeconfig file +``` +preferences: + colors: true +clusters: + cow-cluster: + server: http://cow.org:8080 + api-version: v1beta1 + horse-cluster: + server: https://horse.org:4443 + certificate-authority: path/to/my/cafile + pig-cluster: + server: https://pig.org:443 + insecure-skip-tls-verify: true +users: + black-user: + auth-path: path/to/my/existing/.kubernetes_auth file + blue-user: + token: blue-token + green-user: + client-certificate: path/to/my/client/cert + client-key: path/to/my/client/key +contexts: + queen-anne-context: + cluster: pig-cluster + user: black-user + namespace: saw-ns + federal-context: + cluster: horse-cluster + user: green-user + namespace: chisel-ns +current-context: federal-context +``` + +## Loading and merging rules +The rules for loading and merging the .kubeconfig files are straightforward, but there are a lot of them. The final config is built in this order: + 1. Merge together the kubeconfig itself. This is done with the following hierarchy and merge rules: + + Empty filenames are ignored. Files with non-deserializable content produced errors. + The first file to set a particular value or map key wins and the value or map key is never changed. + This means that the first file to set CurrentContext will have its context preserved. It also means that if two files specify a "red-user", only values from the first file's red-user are used. Even non-conflicting entries from the second file's "red-user" are discarded. + 1. CommandLineLocation - the value of the `kubeconfig` command line option + 1. EnvVarLocation - the value of $KUBECONFIG + 1. CurrentDirectoryLocation - ``pwd``/.kubeconfig + 1. HomeDirectoryLocation = ~/.kube/.kubeconfig + 1. Determine the context to use based on the first hit in this chain + 1. command line argument - the value of the `context` command line option + 1. current-context from the merged kubeconfig file + 1. Empty is allowed at this stage + 1. Determine the cluster info and user to use. At this point, we may or may not have a context. They are built based on the first hit in this chain. (run it twice, once for user, once for cluster) + 1. command line argument - `user` for user name and `cluster` for cluster name + 1. If context is present, then use the context's value + 1. Empty is allowed + 1. Determine the actual cluster info to use. At this point, we may or may not have a cluster info. Build each piece of the cluster info based on the chain (first hit wins): + 1. command line arguments - `server`, `api-version`, `certificate-authority`, and `insecure-skip-tls-verify` + 1. If cluster info is present and a value for the attribute is present, use it. + 1. If you don't have a server location, error. + 1. User is build using the same rules as cluster info, EXCEPT that you can only have one authentication technique per user. + + The command line flags are: `auth-path`, `client-certificate`, `client-key`, and `token`. If there are two conflicting techniques, fail. + 1. For any information still missing, use default values and potentially prompt for authentication information diff --git a/pkg/client/clientcmd/auth_loaders.go b/pkg/client/clientcmd/auth_loaders.go index 35fb1a6d4b..e3c63962e8 100644 --- a/pkg/client/clientcmd/auth_loaders.go +++ b/pkg/client/clientcmd/auth_loaders.go @@ -40,17 +40,16 @@ func (*defaultAuthLoader) LoadAuth(path string) (*clientauth.Info, error) { return clientauth.LoadFromFile(path) } -type promptingAuthLoader struct { +type PromptingAuthLoader struct { reader io.Reader } // LoadAuth parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. -func (a *promptingAuthLoader) LoadAuth(path string) (*clientauth.Info, error) { +func (a *PromptingAuthLoader) LoadAuth(path string) (*clientauth.Info, error) { var auth clientauth.Info // Prompt for user/pass and write a file if none exists. if _, err := os.Stat(path); os.IsNotExist(err) { - auth.User = promptForString("Username", a.reader) - auth.Password = promptForString("Password", a.reader) + auth = *a.Prompt() data, err := json.Marshal(auth) if err != nil { return &auth, err @@ -64,6 +63,16 @@ func (a *promptingAuthLoader) LoadAuth(path string) (*clientauth.Info, error) { } return authPtr, nil } + +// Prompt pulls the user and password from a reader +func (a *PromptingAuthLoader) Prompt() *clientauth.Info { + auth := &clientauth.Info{} + auth.User = promptForString("Username", a.reader) + auth.Password = promptForString("Password", a.reader) + + return auth +} + func promptForString(field string, r io.Reader) string { fmt.Printf("Please enter %s: ", field) var result string @@ -72,8 +81,8 @@ func promptForString(field string, r io.Reader) string { } // NewDefaultAuthLoader is an AuthLoader that parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. -func NewPromptingAuthLoader(reader io.Reader) AuthLoader { - return &promptingAuthLoader{reader} +func NewPromptingAuthLoader(reader io.Reader) *PromptingAuthLoader { + return &PromptingAuthLoader{reader} } // NewDefaultAuthLoader returns a default implementation of an AuthLoader that only reads from a config file diff --git a/pkg/client/clientcmd/client_builder.go b/pkg/client/clientcmd/client_builder.go deleted file mode 100644 index 201c8a83be..0000000000 --- a/pkg/client/clientcmd/client_builder.go +++ /dev/null @@ -1,207 +0,0 @@ -/* -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 ( - "fmt" - "os" - "reflect" - - "github.com/spf13/pflag" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" - "github.com/GoogleCloudPlatform/kubernetes/pkg/version" -) - -// Builder are used to bind and interpret command line flags to make it easy to get an api server client -type Builder interface { - // BindFlags must bind and keep track of all the flags required to build a client config object - BindFlags(flags *pflag.FlagSet) - // Client calls BuildConfig under the covers and uses that config to return a client - Client() (*client.Client, error) - - // Config uses the values of the bound flags and builds a complete client config - Config() (*client.Config, error) - // Override invokes Config(), then passes that to the provided function, and returns a new - // builder that will use that config as its default. If Config() returns an error for the default - // values the function will not be invoked, and the error will be available when Client() is called. - Override(func(*client.Config)) Builder -} - -// cmdAuthInfo is used to track whether flags have been set -type cmdAuthInfo struct { - User StringFlag - Password StringFlag - CAFile StringFlag - CertFile StringFlag - KeyFile StringFlag - BearerToken StringFlag - Insecure BoolFlag -} - -// builder is a default implementation of a Builder -type builder struct { - authLoader AuthLoader - cmdAuthInfo cmdAuthInfo - authPath string - apiserver string - apiVersion string - matchApiVersion bool - - config *client.Config -} - -// NewBuilder returns a valid Builder that uses the passed authLoader. If authLoader is nil, the NewDefaultAuthLoader is used. -func NewBuilder(authLoader AuthLoader) Builder { - if authLoader == nil { - authLoader = NewDefaultAuthLoader() - } - - return &builder{ - authLoader: authLoader, - } -} - -const ( - FlagApiServer = "server" - FlagMatchApiVersion = "match-server-version" - FlagApiVersion = "api-version" - FlagAuthPath = "auth-path" - FlagInsecure = "insecure-skip-tls-verify" - FlagCertFile = "client-certificate" - FlagKeyFile = "client-key" - FlagCAFile = "certificate-authority" - FlagBearerToken = "token" -) - -// BindFlags implements Builder -func (builder *builder) BindFlags(flags *pflag.FlagSet) { - flags.StringVarP(&builder.apiserver, FlagApiServer, "s", builder.apiserver, "The address of the Kubernetes API server") - flags.BoolVar(&builder.matchApiVersion, FlagMatchApiVersion, false, "Require server version to match client version") - flags.StringVar(&builder.apiVersion, FlagApiVersion, latest.Version, "The API version to use when talking to the server") - flags.StringVarP(&builder.authPath, FlagAuthPath, "a", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user. Only used if using https.") - flags.Var(&builder.cmdAuthInfo.Insecure, FlagInsecure, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.") - flags.Var(&builder.cmdAuthInfo.CertFile, FlagCertFile, "Path to a client key file for TLS.") - flags.Var(&builder.cmdAuthInfo.KeyFile, FlagKeyFile, "Path to a client key file for TLS.") - flags.Var(&builder.cmdAuthInfo.CAFile, FlagCAFile, "Path to a cert. file for the certificate authority.") - flags.Var(&builder.cmdAuthInfo.BearerToken, FlagBearerToken, "Bearer token for authentication to the API server.") -} - -// Client implements Builder -func (builder *builder) Client() (*client.Client, error) { - clientConfig, err := builder.Config() - if err != nil { - return nil, err - } - - c, err := client.New(clientConfig) - if err != nil { - return nil, err - } - - if builder.matchApiVersion { - clientVersion := version.Get() - serverVersion, err := c.ServerVersion() - if err != nil { - return nil, fmt.Errorf("couldn't read version from server: %v\n", err) - } - if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) { - return nil, fmt.Errorf("server version (%#v) differs from client version (%#v)!\n", s, clientVersion) - } - } - - return c, nil -} - -// Config implements Builder -func (builder *builder) Config() (*client.Config, error) { - if builder.config != nil { - return builder.config, nil - } - return builder.newConfig() -} - -// Override implements Builder -func (builder *builder) Override(fn func(*client.Config)) Builder { - config, err := builder.newConfig() - if err != nil { - return builder - } - fn(config) - b := *builder - b.config = config - return &b -} - -// newConfig creates a new config object for this builder -func (builder *builder) newConfig() (*client.Config, error) { - clientConfig := client.Config{} - if len(builder.apiserver) > 0 { - clientConfig.Host = builder.apiserver - } else if len(os.Getenv("KUBERNETES_MASTER")) > 0 { - clientConfig.Host = os.Getenv("KUBERNETES_MASTER") - } else { - // TODO: eventually apiserver should start on 443 and be secure by default - clientConfig.Host = "http://localhost:8080" - } - clientConfig.Version = builder.apiVersion - - // only try to read the auth information if we are secure - if client.IsConfigTransportTLS(&clientConfig) { - authInfoFileFound := true - authInfo, err := builder.authLoader.LoadAuth(builder.authPath) - if authInfo == nil && err != nil { // only consider failing if we don't have any auth info - if !os.IsNotExist(err) { // if it's just a case of a missing file, simply flag the auth as not found and use the command line arguments - return nil, err - } - authInfoFileFound = false - authInfo = &clientauth.Info{} - } - - // If provided, the command line options override options from the auth file - if !authInfoFileFound || builder.cmdAuthInfo.User.Provided() { - authInfo.User = builder.cmdAuthInfo.User.Value - } - if !authInfoFileFound || builder.cmdAuthInfo.Password.Provided() { - authInfo.Password = builder.cmdAuthInfo.Password.Value - } - if !authInfoFileFound || builder.cmdAuthInfo.CAFile.Provided() { - authInfo.CAFile = builder.cmdAuthInfo.CAFile.Value - } - if !authInfoFileFound || builder.cmdAuthInfo.CertFile.Provided() { - authInfo.CertFile = builder.cmdAuthInfo.CertFile.Value - } - if !authInfoFileFound || builder.cmdAuthInfo.KeyFile.Provided() { - authInfo.KeyFile = builder.cmdAuthInfo.KeyFile.Value - } - if !authInfoFileFound || builder.cmdAuthInfo.BearerToken.Provided() { - authInfo.BearerToken = builder.cmdAuthInfo.BearerToken.Value - } - if !authInfoFileFound || builder.cmdAuthInfo.Insecure.Provided() { - authInfo.Insecure = &builder.cmdAuthInfo.Insecure.Value - } - - clientConfig, err = authInfo.MergeWithConfig(clientConfig) - if err != nil { - return nil, err - } - } - - return &clientConfig, nil -} diff --git a/pkg/client/clientcmd/client_builder_test.go b/pkg/client/clientcmd/client_builder_test.go deleted file mode 100644 index 6262fe83ca..0000000000 --- a/pkg/client/clientcmd/client_builder_test.go +++ /dev/null @@ -1,356 +0,0 @@ -/* -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 ( - "bytes" - "fmt" - "io" - "io/ioutil" - "os" - "reflect" - "strings" - "testing" - - "github.com/spf13/pflag" - - "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" -) - -func TestSetAllArgumentsOnly(t *testing.T) { - flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError) - clientBuilder := NewBuilder(nil) - clientBuilder.BindFlags(flags) - - args := argValues{"https://localhost:8080", "v1beta1", "/auth-path", "cert-file", "key-file", "ca-file", "bearer-token", true, true} - flags.Parse(strings.Split(args.toArguments(), " ")) - - castBuilder, ok := clientBuilder.(*builder) - if !ok { - t.Errorf("Got unexpected cast result: %#v", castBuilder) - } - - matchStringArg(args.server, castBuilder.apiserver, t) - matchStringArg(args.apiVersion, castBuilder.apiVersion, t) - matchStringArg(args.authPath, castBuilder.authPath, t) - matchStringArg(args.certFile, castBuilder.cmdAuthInfo.CertFile.Value, t) - matchStringArg(args.keyFile, castBuilder.cmdAuthInfo.KeyFile.Value, t) - matchStringArg(args.caFile, castBuilder.cmdAuthInfo.CAFile.Value, t) - matchStringArg(args.bearerToken, castBuilder.cmdAuthInfo.BearerToken.Value, t) - matchBoolArg(args.insecure, castBuilder.cmdAuthInfo.Insecure.Value, t) - matchBoolArg(args.matchApiVersion, castBuilder.matchApiVersion, t) - - clientConfig, err := clientBuilder.Config() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - matchStringArg(args.server, clientConfig.Host, t) - matchStringArg(args.apiVersion, clientConfig.Version, t) - matchStringArg(args.certFile, clientConfig.CertFile, t) - matchStringArg(args.keyFile, clientConfig.KeyFile, t) - matchStringArg(args.caFile, clientConfig.CAFile, t) - matchStringArg(args.bearerToken, clientConfig.BearerToken, t) - matchBoolArg(args.insecure, clientConfig.Insecure, t) -} - -func TestSetInsecureArgumentsOnly(t *testing.T) { - flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError) - clientBuilder := NewBuilder(nil) - clientBuilder.BindFlags(flags) - - args := argValues{"http://localhost:8080", "v1beta1", "/auth-path", "cert-file", "key-file", "ca-file", "bearer-token", true, true} - flags.Parse(strings.Split(args.toArguments(), " ")) - - clientConfig, err := clientBuilder.Config() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - matchStringArg(args.server, clientConfig.Host, t) - matchStringArg(args.apiVersion, clientConfig.Version, t) - - // all security related params should be empty in the resulting config even though we set them because we're using http transport - matchStringArg("", clientConfig.CertFile, t) - matchStringArg("", clientConfig.KeyFile, t) - matchStringArg("", clientConfig.CAFile, t) - matchStringArg("", clientConfig.BearerToken, t) - matchBoolArg(false, clientConfig.Insecure, t) -} - -func TestReadAuthFile(t *testing.T) { - flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError) - clientBuilder := NewBuilder(nil) - clientBuilder.BindFlags(flags) - authFileContents := fmt.Sprintf(`{"user": "alfa-user", "password": "bravo-password", "cAFile": "charlie", "certFile": "delta", "keyFile": "echo", "bearerToken": "foxtrot"}`) - authFile := writeTempAuthFile(authFileContents, t) - - args := argValues{"https://localhost:8080", "v1beta1", authFile, "", "", "", "", true, true} - flags.Parse(strings.Split(args.toArguments(), " ")) - - castBuilder, ok := clientBuilder.(*builder) - if !ok { - t.Errorf("Got unexpected cast result: %#v", castBuilder) - } - - matchStringArg(args.server, castBuilder.apiserver, t) - matchStringArg(args.apiVersion, castBuilder.apiVersion, t) - matchStringArg(args.authPath, castBuilder.authPath, t) - matchStringArg(args.certFile, castBuilder.cmdAuthInfo.CertFile.Value, t) - matchStringArg(args.keyFile, castBuilder.cmdAuthInfo.KeyFile.Value, t) - matchStringArg(args.caFile, castBuilder.cmdAuthInfo.CAFile.Value, t) - matchStringArg(args.bearerToken, castBuilder.cmdAuthInfo.BearerToken.Value, t) - matchBoolArg(args.insecure, castBuilder.cmdAuthInfo.Insecure.Value, t) - matchBoolArg(args.matchApiVersion, castBuilder.matchApiVersion, t) - - clientConfig, err := clientBuilder.Config() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - matchStringArg(args.server, clientConfig.Host, t) - matchStringArg(args.apiVersion, clientConfig.Version, t) - matchStringArg("delta", clientConfig.CertFile, t) - matchStringArg("echo", clientConfig.KeyFile, t) - matchStringArg("charlie", clientConfig.CAFile, t) - matchStringArg("foxtrot", clientConfig.BearerToken, t) - matchStringArg("alfa-user", clientConfig.Username, t) - matchStringArg("bravo-password", clientConfig.Password, t) - matchBoolArg(args.insecure, clientConfig.Insecure, t) -} - -func TestAuthFileOverridden(t *testing.T) { - flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError) - clientBuilder := NewBuilder(nil) - clientBuilder.BindFlags(flags) - authFileContents := fmt.Sprintf(`{"user": "alfa-user", "password": "bravo-password", "cAFile": "charlie", "certFile": "delta", "keyFile": "echo", "bearerToken": "foxtrot"}`) - authFile := writeTempAuthFile(authFileContents, t) - - args := argValues{"https://localhost:8080", "v1beta1", authFile, "cert-file", "key-file", "ca-file", "bearer-token", true, true} - flags.Parse(strings.Split(args.toArguments(), " ")) - - castBuilder, ok := clientBuilder.(*builder) - if !ok { - t.Errorf("Got unexpected cast result: %#v", castBuilder) - } - - matchStringArg(args.server, castBuilder.apiserver, t) - matchStringArg(args.apiVersion, castBuilder.apiVersion, t) - matchStringArg(args.authPath, castBuilder.authPath, t) - matchStringArg(args.certFile, castBuilder.cmdAuthInfo.CertFile.Value, t) - matchStringArg(args.keyFile, castBuilder.cmdAuthInfo.KeyFile.Value, t) - matchStringArg(args.caFile, castBuilder.cmdAuthInfo.CAFile.Value, t) - matchStringArg(args.bearerToken, castBuilder.cmdAuthInfo.BearerToken.Value, t) - matchBoolArg(args.insecure, castBuilder.cmdAuthInfo.Insecure.Value, t) - matchBoolArg(args.matchApiVersion, castBuilder.matchApiVersion, t) - - clientConfig, err := clientBuilder.Config() - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - - matchStringArg(args.server, clientConfig.Host, t) - matchStringArg(args.apiVersion, clientConfig.Version, t) - matchStringArg(args.certFile, clientConfig.CertFile, t) - matchStringArg(args.keyFile, clientConfig.KeyFile, t) - matchStringArg(args.caFile, clientConfig.CAFile, t) - matchStringArg(args.bearerToken, clientConfig.BearerToken, t) - matchStringArg("alfa-user", clientConfig.Username, t) - matchStringArg("bravo-password", clientConfig.Password, t) - matchBoolArg(args.insecure, clientConfig.Insecure, t) -} - -func TestUseDefaultArgumentsOnly(t *testing.T) { - flags := pflag.NewFlagSet("test-flags", pflag.ContinueOnError) - clientBuilder := NewBuilder(nil) - clientBuilder.BindFlags(flags) - - flags.Parse(strings.Split("", " ")) - - castBuilder, ok := clientBuilder.(*builder) - if !ok { - t.Errorf("Got unexpected cast result: %#v", castBuilder) - } - - matchStringArg("", castBuilder.apiserver, t) - matchStringArg(latest.Version, castBuilder.apiVersion, t) - matchStringArg(os.Getenv("HOME")+"/.kubernetes_auth", castBuilder.authPath, t) - matchStringArg("", castBuilder.cmdAuthInfo.CertFile.Value, t) - matchStringArg("", castBuilder.cmdAuthInfo.KeyFile.Value, t) - matchStringArg("", castBuilder.cmdAuthInfo.CAFile.Value, t) - matchStringArg("", castBuilder.cmdAuthInfo.BearerToken.Value, t) - matchBoolArg(false, castBuilder.matchApiVersion, t) -} - -func TestLoadClientAuthInfoOrPrompt(t *testing.T) { - loadAuthInfoTests := []struct { - authData string - authInfo *clientauth.Info - r io.Reader - }{ - { - `{"user": "user", "password": "pass"}`, - &clientauth.Info{User: "user", Password: "pass"}, - nil, - }, - { - "", nil, nil, - }, - { - "missing", - &clientauth.Info{User: "user", Password: "pass"}, - bytes.NewBufferString("user\npass"), - }, - } - for _, loadAuthInfoTest := range loadAuthInfoTests { - tt := loadAuthInfoTest - aifile, err := ioutil.TempFile("", "testAuthInfo") - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - if tt.authData != "missing" { - defer os.Remove(aifile.Name()) - defer aifile.Close() - _, err = aifile.WriteString(tt.authData) - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - } else { - aifile.Close() - os.Remove(aifile.Name()) - } - prompter := NewPromptingAuthLoader(tt.r) - authInfo, err := prompter.LoadAuth(aifile.Name()) - if len(tt.authData) == 0 && tt.authData != "missing" { - if err == nil { - t.Error("LoadAuth didn't fail on empty file") - } - continue - } - if err != nil { - t.Errorf("Unexpected error: %v", err) - } - if !reflect.DeepEqual(authInfo, tt.authInfo) { - t.Errorf("Expected %#v, got %#v", tt.authInfo, authInfo) - } - } -} - -func TestOverride(t *testing.T) { - b := NewBuilder(nil) - cfg, err := b.Config() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if cfg.Version != "" { - t.Errorf("unexpected default config version") - } - - newCfg, err := b.Override(func(cfg *client.Config) { - if cfg.Version != "" { - t.Errorf("unexpected default config version") - } - cfg.Version = "test" - }).Config() - - if newCfg.Version != "test" { - t.Errorf("unexpected override config version") - } - - if cfg.Version != "" { - t.Errorf("original object should not change") - } - - cfg, err = b.Config() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if cfg.Version != "" { - t.Errorf("override should not be persistent") - } -} - -func matchStringArg(expected, got string, t *testing.T) { - if expected != got { - t.Errorf("Expected %v, got %v", expected, got) - } -} - -func matchBoolArg(expected, got bool, t *testing.T) { - if expected != got { - t.Errorf("Expected %v, got %v", expected, got) - } -} - -func writeTempAuthFile(contents string, t *testing.T) string { - file, err := ioutil.TempFile("", "testAuthInfo") - if err != nil { - t.Errorf("Failed to write config file. Test cannot continue due to: %v", err) - return "" - } - _, err = file.WriteString(contents) - if err != nil { - t.Errorf("Unexpected error: %v", err) - return "" - } - file.Close() - - return file.Name() -} - -type argValues struct { - server string - apiVersion string - authPath string - certFile string - keyFile string - caFile string - bearerToken string - insecure bool - matchApiVersion bool -} - -func (a *argValues) toArguments() string { - args := "" - if len(a.server) > 0 { - args += "--" + FlagApiServer + "=" + a.server + " " - } - if len(a.apiVersion) > 0 { - args += "--" + FlagApiVersion + "=" + a.apiVersion + " " - } - if len(a.authPath) > 0 { - args += "--" + FlagAuthPath + "=" + a.authPath + " " - } - if len(a.certFile) > 0 { - args += "--" + FlagCertFile + "=" + a.certFile + " " - } - if len(a.keyFile) > 0 { - args += "--" + FlagKeyFile + "=" + a.keyFile + " " - } - if len(a.caFile) > 0 { - args += "--" + FlagCAFile + "=" + a.caFile + " " - } - if len(a.bearerToken) > 0 { - args += "--" + FlagBearerToken + "=" + a.bearerToken + " " - } - args += "--" + FlagInsecure + "=" + fmt.Sprintf("%v", a.insecure) + " " - args += "--" + FlagMatchApiVersion + "=" + fmt.Sprintf("%v", a.matchApiVersion) + " " - - return args -} diff --git a/pkg/client/clientcmd/client_config.go b/pkg/client/clientcmd/client_config.go new file mode 100644 index 0000000000..beef3815b6 --- /dev/null +++ b/pkg/client/clientcmd/client_config.go @@ -0,0 +1,183 @@ +/* +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 ( + "io" + "os" + + "github.com/imdario/mergo" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +var ( + // TODO: eventually apiserver should start on 443 and be secure by default + defaultCluster = Cluster{Server: "http://localhost:8080"} + envVarCluster = Cluster{Server: os.Getenv("KUBERNETES_MASTER")} +) + +// ClientConfig is used to make it easy to get an api server client +type ClientConfig interface { + // ClientConfig returns a complete client config + ClientConfig() (*client.Config, error) +} + +// DirectClientConfig is a ClientConfig interface that is backed by a Config, options overrides, and an optional fallbackReader for auth information +type DirectClientConfig struct { + config Config + contextName string + overrides *ConfigOverrides + fallbackReader io.Reader +} + +// NewDefaultClientConfig creates a DirectClientConfig using the config.CurrentContext as the context name +func NewDefaultClientConfig(config Config, overrides *ConfigOverrides) ClientConfig { + return DirectClientConfig{config, config.CurrentContext, overrides, nil} +} + +// NewNonInteractiveClientConfig creates a DirectClientConfig using the passed context name and does not have a fallback reader for auth information +func NewNonInteractiveClientConfig(config Config, contextName string, overrides *ConfigOverrides) ClientConfig { + return DirectClientConfig{config, contextName, overrides, nil} +} + +// 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 Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig { + return DirectClientConfig{config, contextName, overrides, fallbackReader} +} + +// ClientConfig implements ClientConfig +func (config DirectClientConfig) ClientConfig() (*client.Config, error) { + if err := config.ConfirmUsable(); err != nil { + return nil, err + } + + configAuthInfo := config.getAuthInfo() + configClusterInfo := config.getCluster() + + 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 + 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) + if err != nil { + return nil, err + } + } + + return &clientConfig, nil +} + +// 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 { + validationErrors := make([]error, 0) + validationErrors = append(validationErrors, validateAuthInfo(config.getAuthInfoName(), config.getAuthInfo())...) + validationErrors = append(validationErrors, validateClusterInfo(config.getClusterName(), config.getCluster())...) + + return util.SliceToError(validationErrors) +} + +func (config DirectClientConfig) getContextName() string { + if len(config.overrides.CurrentContext) != 0 { + return config.overrides.CurrentContext + } + if len(config.contextName) != 0 { + return config.contextName + } + + return config.config.CurrentContext +} + +func (config DirectClientConfig) getAuthInfoName() string { + if len(config.overrides.AuthInfoName) != 0 { + return config.overrides.AuthInfoName + } + return config.getContext().AuthInfo +} + +func (config DirectClientConfig) getClusterName() string { + if len(config.overrides.ClusterName) != 0 { + return config.overrides.ClusterName + } + return config.getContext().Cluster +} + +func (config DirectClientConfig) getContext() Context { + return config.config.Contexts[config.getContextName()] +} + +func (config DirectClientConfig) getAuthInfo() AuthInfo { + authInfos := config.config.AuthInfos + authInfoName := config.getAuthInfoName() + + var mergedAuthInfo AuthInfo + if configAuthInfo, exists := authInfos[authInfoName]; exists { + mergo.Merge(&mergedAuthInfo, configAuthInfo) + } + mergo.Merge(&mergedAuthInfo, config.overrides.AuthInfo) + + return mergedAuthInfo +} + +func (config DirectClientConfig) getCluster() Cluster { + clusterInfos := config.config.Clusters + clusterInfoName := config.getClusterName() + + var mergedClusterInfo Cluster + mergo.Merge(&mergedClusterInfo, defaultCluster) + mergo.Merge(&mergedClusterInfo, envVarCluster) + if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists { + mergo.Merge(&mergedClusterInfo, configClusterInfo) + } + mergo.Merge(&mergedClusterInfo, config.overrides.ClusterInfo) + + return mergedClusterInfo +} diff --git a/pkg/client/clientcmd/client_config_test.go b/pkg/client/clientcmd/client_config_test.go new file mode 100644 index 0000000000..39d8aeb3b4 --- /dev/null +++ b/pkg/client/clientcmd/client_config_test.go @@ -0,0 +1,106 @@ +/* +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 ( + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func createValidTestConfig() *Config { + const ( + server = "https://anything.com:8080" + token = "the-token" + ) + + config := NewConfig() + config.Clusters["clean"] = Cluster{ + Server: server, + APIVersion: latest.Version, + } + config.AuthInfos["clean"] = AuthInfo{ + Token: token, + } + config.Contexts["clean"] = Context{ + Cluster: "clean", + AuthInfo: "clean", + } + config.CurrentContext = "clean" + + return config +} + +func TestCreateClean(t *testing.T) { + config := createValidTestConfig() + clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}) + + clientConfig, err := clientBuilder.ClientConfig() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + matchStringArg(config.Clusters["clean"].Server, clientConfig.Host, t) + matchStringArg(config.Clusters["clean"].APIVersion, clientConfig.Version, t) + matchBoolArg(config.Clusters["clean"].InsecureSkipTLSVerify, clientConfig.Insecure, t) + matchStringArg(config.AuthInfos["clean"].Token, clientConfig.BearerToken, t) +} + +func TestCreateCleanDefault(t *testing.T) { + config := createValidTestConfig() + clientBuilder := NewDefaultClientConfig(*config, &ConfigOverrides{}) + + clientConfig, err := clientBuilder.ClientConfig() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + matchStringArg(config.Clusters["clean"].Server, clientConfig.Host, t) + matchStringArg(config.Clusters["clean"].APIVersion, clientConfig.Version, t) + matchBoolArg(config.Clusters["clean"].InsecureSkipTLSVerify, clientConfig.Insecure, t) + matchStringArg(config.AuthInfos["clean"].Token, clientConfig.BearerToken, t) +} + +func TestCreateMissingContext(t *testing.T) { + const expectedErrorContains = "Context was not found for specified context" + config := createValidTestConfig() + clientBuilder := NewNonInteractiveClientConfig(*config, "not-present", &ConfigOverrides{}) + expectedConfig := &client.Config{Host: "http://localhost:8080"} + + clientConfig, err := clientBuilder.ClientConfig() + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !reflect.DeepEqual(expectedConfig, clientConfig) { + t.Errorf("Expected %#v, got %#v", expectedConfig, clientConfig) + } + +} + +func matchBoolArg(expected, got bool, t *testing.T) { + if expected != got { + t.Errorf("Expected %v, got %v", expected, got) + } +} + +func matchStringArg(expected, got string, t *testing.T) { + if expected != got { + t.Errorf("Expected %v, got %v", expected, got) + } +} diff --git a/pkg/client/clientcmd/doc.go b/pkg/client/clientcmd/doc.go index 79ea31ea83..7658b93180 100644 --- a/pkg/client/clientcmd/doc.go +++ b/pkg/client/clientcmd/doc.go @@ -15,11 +15,17 @@ limitations under the License. */ /* -Package cmd provides one stop shopping for a command line executable to bind the correct flags, -build the client config, and create a working client. The code for usage looks like this: +Package clientcmd provides one stop shopping for building a working client from a fixed config, +from a .kubeconfig file, from command line flags, or from any merged combination. - clientBuilder := clientcmd.NewBuilder(clientcmd.NewDefaultAuthLoader()) - clientBuilder.BindFlags(cmds.PersistentFlags()) - apiClient, err := clientBuilder.Client() +Sample usage from merged .kubeconfig files (local directory, home directory) + loadingRules := clientcmd.NewKubeConfigLoadingRules() + // if you want to change the loading rules (which files in which order), you can do so here + + configOverrides := &clientcmd.ConfigOverrides{} + // if you want to change override values or bind them to flags, there are methods to help you + + kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingKubeConfig(loadingRules, configOverrides) + kubeConfig.Client() */ package clientcmd diff --git a/pkg/client/clientcmd/loader.go b/pkg/client/clientcmd/loader.go new file mode 100644 index 0000000000..a79e3b1920 --- /dev/null +++ b/pkg/client/clientcmd/loader.go @@ -0,0 +1,117 @@ +/* +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 ( + "io/ioutil" + "os" + + "github.com/imdario/mergo" + "gopkg.in/v2/yaml" +) + +const ( + RecommendedConfigPathFlag = "kubeconfig" + RecommendedConfigPathEnvVar = "KUBECONFIG" +) + +// ClientConfigLoadingRules is a struct that calls our specific locations that are used for merging together a Config +type ClientConfigLoadingRules struct { + CommandLinePath string + EnvVarPath string + CurrentDirectoryPath string + HomeDirectoryPath string +} + +// NewClientConfigLoadingRules returns a ClientConfigLoadingRules object with default fields filled in. You are not required to +// use this constructor +func NewClientConfigLoadingRules() *ClientConfigLoadingRules { + return &ClientConfigLoadingRules{ + CurrentDirectoryPath: ".kubeconfig", + HomeDirectoryPath: os.Getenv("HOME") + "/.kube/.kubeconfig", + } +} + +// Load takes the loading rules and merges together a Config object based on following order. +// 1. CommandLinePath +// 2. EnvVarPath +// 3. CurrentDirectoryPath +// 4. HomeDirectoryPath +// Empty filenames are ignored. Files with non-deserializable content produced errors. +// The first file to set a particular value or map key wins and the value or map key is never changed. +// This means that the first file to set CurrentContext will have its context preserved. It also means +// that if two files specify a "red-user", only values from the first file's red-user are used. Even +// non-conflicting entries from the second file's "red-user" are discarded. +func (rules *ClientConfigLoadingRules) Load() (*Config, error) { + config := NewConfig() + + mergeConfigWithFile(config, rules.CommandLinePath) + mergeConfigWithFile(config, rules.EnvVarPath) + mergeConfigWithFile(config, rules.CurrentDirectoryPath) + mergeConfigWithFile(config, rules.HomeDirectoryPath) + + return config, nil +} + +func mergeConfigWithFile(startingConfig *Config, filename string) error { + if len(filename) == 0 { + // no work to do + return nil + } + + config, err := LoadFromFile(filename) + if err != nil { + return err + } + + mergo.Merge(startingConfig, config) + + return nil +} + +// LoadFromFile takes a filename and deserializes the contents into Config object +func LoadFromFile(filename string) (*Config, error) { + config := &Config{} + + kubeconfigBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(kubeconfigBytes, &config) + if err != nil { + return nil, err + } + + return config, nil +} + +// WriteToFile serializes the config to yaml and writes it out to a file. If no present, it creates the file with 0644. If it is present +// it stomps the contents +func WriteToFile(config Config, filename string) error { + content, err := yaml.Marshal(config) + if err != nil { + return err + } + + err = ioutil.WriteFile(filename, content, 0644) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/client/clientcmd/loader_test.go b/pkg/client/clientcmd/loader_test.go new file mode 100644 index 0000000000..a1063abdca --- /dev/null +++ b/pkg/client/clientcmd/loader_test.go @@ -0,0 +1,182 @@ +/* +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 ( + "fmt" + "io/ioutil" + "os" + + "gopkg.in/v2/yaml" +) + +var ( + testConfigAlfa = Config{ + AuthInfos: map[string]AuthInfo{ + "red-user": {Token: "red-token"}}, + Clusters: map[string]Cluster{ + "cow-cluster": {Server: "http://cow.org:8080"}}, + Contexts: map[string]Context{ + "federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"}}, + } + testConfigBravo = Config{ + AuthInfos: map[string]AuthInfo{ + "black-user": {Token: "black-token"}}, + Clusters: map[string]Cluster{ + "pig-cluster": {Server: "http://pig.org:8080"}}, + Contexts: map[string]Context{ + "queen-anne-context": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"}}, + } + testConfigCharlie = Config{ + AuthInfos: map[string]AuthInfo{ + "green-user": {Token: "green-token"}}, + Clusters: map[string]Cluster{ + "horse-cluster": {Server: "http://horse.org:8080"}}, + Contexts: map[string]Context{ + "shaker-context": {AuthInfo: "green-user", Cluster: "horse-cluster", Namespace: "chisel-ns"}}, + } + testConfigDelta = Config{ + AuthInfos: map[string]AuthInfo{ + "blue-user": {Token: "blue-token"}}, + Clusters: map[string]Cluster{ + "chicken-cluster": {Server: "http://chicken.org:8080"}}, + Contexts: map[string]Context{ + "gothic-context": {AuthInfo: "blue-user", Cluster: "chicken-cluster", Namespace: "plane-ns"}}, + } + testConfigConflictAlfa = Config{ + AuthInfos: map[string]AuthInfo{ + "red-user": {Token: "a-different-red-token"}, + "yellow-user": {Token: "yellow-token"}}, + Clusters: map[string]Cluster{ + "cow-cluster": {Server: "http://a-different-cow.org:8080", InsecureSkipTLSVerify: true}, + "donkey-cluster": {Server: "http://donkey.org:8080", InsecureSkipTLSVerify: true}}, + CurrentContext: "federal-context", + } +) + +func ExampleMergingSomeWithConflict() { + commandLineFile, _ := ioutil.TempFile("", "") + defer os.Remove(commandLineFile.Name()) + envVarFile, _ := ioutil.TempFile("", "") + defer os.Remove(envVarFile.Name()) + + WriteToFile(testConfigAlfa, commandLineFile.Name()) + WriteToFile(testConfigConflictAlfa, envVarFile.Name()) + + loadingRules := ClientConfigLoadingRules{ + CommandLinePath: commandLineFile.Name(), + EnvVarPath: envVarFile.Name(), + } + + mergedConfig, err := loadingRules.Load() + + output, err := yaml.Marshal(mergedConfig) + if err != nil { + fmt.Printf("Unexpected error: %v", err) + } + + fmt.Printf("%v", string(output)) + // Output: + // preferences: {} + // clusters: + // cow-cluster: + // server: http://cow.org:8080 + // donkey-cluster: + // server: http://donkey.org:8080 + // insecure-skip-tls-verify: true + // users: + // red-user: + // token: red-token + // yellow-user: + // token: yellow-token + // contexts: + // federal-context: + // cluster: cow-cluster + // user: red-user + // namespace: hammer-ns + // current-context: federal-context +} + +func ExampleMergingEverythingNoConflicts() { + commandLineFile, _ := ioutil.TempFile("", "") + defer os.Remove(commandLineFile.Name()) + envVarFile, _ := ioutil.TempFile("", "") + defer os.Remove(envVarFile.Name()) + currentDirFile, _ := ioutil.TempFile("", "") + defer os.Remove(currentDirFile.Name()) + homeDirFile, _ := ioutil.TempFile("", "") + defer os.Remove(homeDirFile.Name()) + + WriteToFile(testConfigAlfa, commandLineFile.Name()) + WriteToFile(testConfigBravo, envVarFile.Name()) + WriteToFile(testConfigCharlie, currentDirFile.Name()) + WriteToFile(testConfigDelta, homeDirFile.Name()) + + loadingRules := ClientConfigLoadingRules{ + CommandLinePath: commandLineFile.Name(), + EnvVarPath: envVarFile.Name(), + CurrentDirectoryPath: currentDirFile.Name(), + HomeDirectoryPath: homeDirFile.Name(), + } + + mergedConfig, err := loadingRules.Load() + + output, err := yaml.Marshal(mergedConfig) + if err != nil { + fmt.Printf("Unexpected error: %v", err) + } + + fmt.Printf("%v", string(output)) + // Output: + // preferences: {} + // clusters: + // chicken-cluster: + // server: http://chicken.org:8080 + // cow-cluster: + // server: http://cow.org:8080 + // horse-cluster: + // server: http://horse.org:8080 + // pig-cluster: + // server: http://pig.org:8080 + // users: + // black-user: + // token: black-token + // blue-user: + // token: blue-token + // green-user: + // token: green-token + // red-user: + // token: red-token + // contexts: + // federal-context: + // cluster: cow-cluster + // user: red-user + // namespace: hammer-ns + // gothic-context: + // cluster: chicken-cluster + // user: blue-user + // namespace: plane-ns + // queen-anne-context: + // cluster: pig-cluster + // user: black-user + // namespace: saw-ns + // shaker-context: + // cluster: horse-cluster + // user: green-user + // namespace: chisel-ns + // current-context: "" +} diff --git a/pkg/client/clientcmd/merged_client_builder.go b/pkg/client/clientcmd/merged_client_builder.go new file mode 100644 index 0000000000..a0fa856bb2 --- /dev/null +++ b/pkg/client/clientcmd/merged_client_builder.go @@ -0,0 +1,70 @@ +/* +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 ( + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +// DeferredLoadingClientConfig is a ClientConfig interface that is backed by a set of loading rules +// It is used in cases where the loading rules may change after you've instantiated them and you want to be sure that +// the most recent rules are used. This is useful in cases where you bind flags to loading rule parameters before +// the parse happens and you want your calling code to be ignorant of how the values are being mutated to avoid +// passing extraneous information down a call stack +type DeferredLoadingClientConfig struct { + loadingRules *ClientConfigLoadingRules + overrides *ConfigOverrides + fallbackReader io.Reader +} + +// NewNonInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name +func NewNonInteractiveDeferredLoadingClientConfig(loadingRules *ClientConfigLoadingRules, overrides *ConfigOverrides) ClientConfig { + return DeferredLoadingClientConfig{loadingRules, overrides, nil} +} + +// NewInteractiveDeferredLoadingClientConfig creates a ConfigClientClientConfig using the passed context name and the fallback auth reader +func NewInteractiveDeferredLoadingClientConfig(loadingRules *ClientConfigLoadingRules, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig { + return DeferredLoadingClientConfig{loadingRules, overrides, fallbackReader} +} + +func (config DeferredLoadingClientConfig) createClientConfig() (ClientConfig, error) { + mergedConfig, err := config.loadingRules.Load() + if err != nil { + return nil, err + } + + var mergedClientConfig ClientConfig + if config.fallbackReader != nil { + mergedClientConfig = NewInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides, config.fallbackReader) + } else { + mergedClientConfig = NewNonInteractiveClientConfig(*mergedConfig, config.overrides.CurrentContext, config.overrides) + } + + return mergedClientConfig, nil +} + +// ClientConfig implements ClientConfig +func (config DeferredLoadingClientConfig) ClientConfig() (*client.Config, error) { + mergedClientConfig, err := config.createClientConfig() + if err != nil { + return nil, err + } + + return mergedClientConfig.ClientConfig() +} diff --git a/pkg/client/clientcmd/overrides.go b/pkg/client/clientcmd/overrides.go new file mode 100644 index 0000000000..6ccacafb06 --- /dev/null +++ b/pkg/client/clientcmd/overrides.go @@ -0,0 +1,135 @@ +/* +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 ( + "github.com/spf13/pflag" +) + +// ConfigOverrides holds values that should override whatever information is pulled from the actual Config object. You can't +// simply use an actual Config object, because Configs hold maps, but overrides are restricted to "at most one" +type ConfigOverrides struct { + AuthInfo AuthInfo + ClusterInfo Cluster + Namespace string + CurrentContext string + ClusterName string + AuthInfoName string +} + +// ConfigOverrideFlags holds the flag names to be used for binding command line flags. Notice that this structure tightly +// corresponds to ConfigOverrides +type ConfigOverrideFlags struct { + AuthOverrideFlags AuthOverrideFlags + ClusterOverrideFlags ClusterOverrideFlags + Namespace string + CurrentContext string + ClusterName string + AuthInfoName string +} + +// AuthOverrideFlags holds the flag names to be used for binding command line flags for AuthInfo objects +type AuthOverrideFlags struct { + AuthPath string + ClientCertificate string + ClientKey string + Token string +} + +// ClusterOverride holds the flag names to be used for binding command line flags for Cluster objects +type ClusterOverrideFlags struct { + APIServer string + APIVersion string + CertificateAuthority string + InsecureSkipTLSVerify string +} + +const ( + FlagClusterName = "cluster" + FlagAuthInfoName = "user" + FlagContext = "context" + FlagNamespace = "namespace" + FlagAPIServer = "server" + FlagAPIVersion = "api-version" + FlagAuthPath = "auth-path" + FlagInsecure = "insecure-skip-tls-verify" + FlagCertFile = "client-certificate" + FlagKeyFile = "client-key" + FlagCAFile = "certificate-authority" + FlagBearerToken = "token" +) + +// RecommendedAuthOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing +func RecommendedAuthOverrideFlags(prefix string) AuthOverrideFlags { + return AuthOverrideFlags{ + AuthPath: prefix + FlagAuthPath, + ClientCertificate: prefix + FlagCertFile, + ClientKey: prefix + FlagKeyFile, + Token: prefix + FlagBearerToken, + } +} + +// RecommendedClusterOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing +func RecommendedClusterOverrideFlags(prefix string) ClusterOverrideFlags { + return ClusterOverrideFlags{ + APIServer: prefix + FlagAPIServer, + APIVersion: prefix + FlagAPIVersion, + CertificateAuthority: prefix + FlagCAFile, + InsecureSkipTLSVerify: prefix + FlagInsecure, + } +} + +// RecommendedConfigOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing +func RecommendedConfigOverrideFlags(prefix string) ConfigOverrideFlags { + return ConfigOverrideFlags{ + AuthOverrideFlags: RecommendedAuthOverrideFlags(prefix), + ClusterOverrideFlags: RecommendedClusterOverrideFlags(prefix), + Namespace: prefix + FlagNamespace, + CurrentContext: prefix + FlagContext, + ClusterName: prefix + FlagClusterName, + AuthInfoName: prefix + FlagAuthInfoName, + } +} + +// BindFlags is a convenience method to bind the specified flags to their associated variables +func (authInfo *AuthInfo) BindFlags(flags *pflag.FlagSet, flagNames AuthOverrideFlags) { + // TODO short flag names are impossible to prefix, decide whether to keep them or not + flags.StringVarP(&authInfo.AuthPath, flagNames.AuthPath, "a", "", "Path to the auth info file. If missing, prompt the user. Only used if using https.") + flags.StringVar(&authInfo.ClientCertificate, flagNames.ClientCertificate, "", "Path to a client key file for TLS.") + flags.StringVar(&authInfo.ClientKey, flagNames.ClientKey, "", "Path to a client key file for TLS.") + flags.StringVar(&authInfo.Token, flagNames.Token, "", "Bearer token for authentication to the API server.") +} + +// BindFlags is a convenience method to bind the specified flags to their associated variables +func (clusterInfo *Cluster) BindFlags(flags *pflag.FlagSet, flagNames ClusterOverrideFlags) { + // TODO short flag names are impossible to prefix, decide whether to keep them or not + flags.StringVarP(&clusterInfo.Server, flagNames.APIServer, "s", "", "The address of the Kubernetes API server") + flags.StringVar(&clusterInfo.APIVersion, flagNames.APIVersion, "", "The API version to use when talking to the server") + flags.StringVar(&clusterInfo.CertificateAuthority, flagNames.CertificateAuthority, "", "Path to a cert. file for the certificate authority.") + flags.BoolVar(&clusterInfo.InsecureSkipTLSVerify, flagNames.InsecureSkipTLSVerify, false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.") +} + +// BindFlags is a convenience method to bind the specified flags to their associated variables +func (overrides *ConfigOverrides) BindFlags(flags *pflag.FlagSet, flagNames ConfigOverrideFlags) { + (&overrides.AuthInfo).BindFlags(flags, flagNames.AuthOverrideFlags) + (&overrides.ClusterInfo).BindFlags(flags, flagNames.ClusterOverrideFlags) + // TODO not integrated yet + // flags.StringVar(&overrides.Namespace, flagNames.Namespace, "", "If present, the namespace scope for this CLI request.") + flags.StringVar(&overrides.CurrentContext, flagNames.CurrentContext, "", "The name of the kubeconfig context to use") + flags.StringVar(&overrides.ClusterName, flagNames.ClusterName, "", "The name of the kubeconfig cluster to use") + flags.StringVar(&overrides.AuthInfoName, flagNames.AuthInfoName, "", "The name of the kubeconfig user to use") +} diff --git a/pkg/client/clientcmd/provided_flags.go b/pkg/client/clientcmd/provided_flags.go deleted file mode 100644 index e90ed4b913..0000000000 --- a/pkg/client/clientcmd/provided_flags.go +++ /dev/null @@ -1,100 +0,0 @@ -/* -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 ( - "fmt" - "strconv" - - "github.com/spf13/pflag" -) - -// FlagProvider adds a check for whether .Set was called on this flag variable -type FlagProvider interface { - // Provided returns true iff .Set was called on this flag - Provided() bool - pflag.Value -} - -// StringFlag implements FlagProvider -type StringFlag struct { - Default string - Value string - WasProvided bool -} - -// SetDefault sets a default value for a flag while keeping Provided() false -func (flag *StringFlag) SetDefault(value string) { - flag.Value = value - flag.WasProvided = false -} - -func (flag *StringFlag) Set(value string) error { - flag.Value = value - flag.WasProvided = true - - return nil -} - -func (flag *StringFlag) Type() string { - return "string" -} - -func (flag *StringFlag) Provided() bool { - return flag.WasProvided -} - -func (flag *StringFlag) String() string { - return flag.Value -} - -// BoolFlag implements FlagProvider -type BoolFlag struct { - Default bool - Value bool - WasProvided bool -} - -// SetDefault sets a default value for a flag while keeping Provided() false -func (flag *BoolFlag) SetDefault(value bool) { - flag.Value = value - flag.WasProvided = false -} - -func (flag *BoolFlag) Set(value string) error { - boolValue, err := strconv.ParseBool(value) - if err != nil { - return err - } - - flag.Value = boolValue - flag.WasProvided = true - - return nil -} - -func (flag *BoolFlag) Type() string { - return "bool" -} - -func (flag *BoolFlag) Provided() bool { - return flag.WasProvided -} - -func (flag *BoolFlag) String() string { - return fmt.Sprintf("%t", flag.Value) -} diff --git a/pkg/client/clientcmd/types.go b/pkg/client/clientcmd/types.go new file mode 100644 index 0000000000..3384c2f455 --- /dev/null +++ b/pkg/client/clientcmd/types.go @@ -0,0 +1,83 @@ +/* +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 () + +// Where possible, yaml tags match the cli argument names. +// Top level config objects and all values required for proper functioning are not "omitempty". Any truly optional piece of config is allowed to be omitted. + +// Config holds the information needed to build connect to remote kubernetes clusters as a given user +type Config struct { + // Preferences holds general information to be use for cli interactions + Preferences Preferences `yaml:"preferences"` + // Clusters is a map of referencable names to cluster configs + Clusters map[string]Cluster `yaml:"clusters"` + // AuthInfos is a map of referencable names to user configs + AuthInfos map[string]AuthInfo `yaml:"users"` + // Contexts is a map of referencable names to context configs + Contexts map[string]Context `yaml:"contexts"` + // CurrentContext is the name of the context that you would like to use by default + CurrentContext string `yaml:"current-context"` +} + +type Preferences struct { + Colors bool `yaml:"colors,omitempty"` +} + +// Cluster contains information about how to communicate with a kubernetes cluster +type Cluster struct { + // Server is the address of the kubernetes cluster (https://hostname:port). + Server string `yaml:"server"` + // APIVersion is the preferred api version for communicating with the kubernetes cluster (v1beta1, v1beta2, v1beta3, etc). + APIVersion string `yaml:"api-version,omitempty"` + // InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure. + InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify,omitempty"` + // CertificateAuthority is the path to a cert file for the certificate authority. + CertificateAuthority string `yaml:"certificate-authority,omitempty"` +} + +// AuthInfo contains information that describes identity information. This is use to tell the kubernetes cluster who you are. +type AuthInfo struct { + // AuthPath is the path to a kubernetes auth file (~/.kubernetes_auth). If you provide an AuthPath, the other options specified are ignored + AuthPath string `yaml:"auth-path,omitempty"` + // ClientCertificate is the path to a client cert file for TLS. + ClientCertificate string `yaml:"client-certificate,omitempty"` + // ClientKey is the path to a client key file for TLS. + ClientKey string `yaml:"client-key,omitempty"` + // Token is the bearer token for authentication to the kubernetes cluster. + Token string `yaml:"token,omitempty"` +} + +// Context is a tuple of references to a cluster (how do I communicate with a kubernetes cluster), a user (how do I identify myself), and a namespace (what subset of resources do I want to work with) +type Context struct { + // Cluster is the name of the cluster for this context + Cluster string `yaml:"cluster"` + // AuthInfo is the name of the authInfo for this context + AuthInfo string `yaml:"user"` + // Namespace is the default namespace to use on unspecified requests + Namespace string `yaml:"namespace,omitempty"` +} + +// NewConfig is a convenience function that returns a new Config object with non-nil maps +func NewConfig() *Config { + return &Config{ + Clusters: make(map[string]Cluster), + AuthInfos: make(map[string]AuthInfo), + Contexts: make(map[string]Context), + } +} diff --git a/pkg/client/clientcmd/types_test.go b/pkg/client/clientcmd/types_test.go new file mode 100644 index 0000000000..89f0a41da7 --- /dev/null +++ b/pkg/client/clientcmd/types_test.go @@ -0,0 +1,121 @@ +/* +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 ( + "fmt" + + "gopkg.in/v2/yaml" +) + +func ExampleEmptyConfig() { + defaultConfig := NewConfig() + + output, err := yaml.Marshal(defaultConfig) + if err != nil { + fmt.Printf("Unexpected error: %v", err) + } + + fmt.Printf("%v", string(output)) + // Output: + // preferences: {} + // clusters: {} + // users: {} + // contexts: {} + // current-context: "" +} + +func ExampleOfOptionsConfig() { + defaultConfig := NewConfig() + defaultConfig.Preferences.Colors = true + defaultConfig.Clusters["alfa"] = Cluster{ + Server: "https://alfa.org:8080", + APIVersion: "v1beta2", + InsecureSkipTLSVerify: true, + CertificateAuthority: "path/to/my/cert-ca-filename", + } + defaultConfig.Clusters["bravo"] = Cluster{ + Server: "https://bravo.org:8080", + APIVersion: "v1beta1", + InsecureSkipTLSVerify: false, + } + defaultConfig.AuthInfos["black-mage-via-file"] = AuthInfo{ + AuthPath: "path/to/my/.kubernetes_auth", + } + defaultConfig.AuthInfos["white-mage-via-cert"] = AuthInfo{ + ClientCertificate: "path/to/my/client-cert-filename", + ClientKey: "path/to/my/client-key-filename", + } + defaultConfig.AuthInfos["red-mage-via-token"] = AuthInfo{ + Token: "my-secret-token", + } + defaultConfig.Contexts["bravo-as-black-mage"] = Context{ + Cluster: "bravo", + AuthInfo: "black-mage-via-file", + Namespace: "yankee", + } + defaultConfig.Contexts["alfa-as-black-mage"] = Context{ + Cluster: "alfa", + AuthInfo: "black-mage-via-file", + Namespace: "zulu", + } + defaultConfig.Contexts["alfa-as-white-mage"] = Context{ + Cluster: "alfa", + AuthInfo: "white-mage-via-cert", + } + defaultConfig.CurrentContext = "alfa-as-white-mage" + + output, err := yaml.Marshal(defaultConfig) + if err != nil { + fmt.Printf("Unexpected error: %v", err) + } + + fmt.Printf("%v", string(output)) + // Output: + // preferences: + // colors: true + // clusters: + // alfa: + // server: https://alfa.org:8080 + // api-version: v1beta2 + // insecure-skip-tls-verify: true + // certificate-authority: path/to/my/cert-ca-filename + // bravo: + // server: https://bravo.org:8080 + // api-version: v1beta1 + // users: + // black-mage-via-file: + // auth-path: path/to/my/.kubernetes_auth + // red-mage-via-token: + // token: my-secret-token + // white-mage-via-cert: + // client-certificate: path/to/my/client-cert-filename + // client-key: path/to/my/client-key-filename + // contexts: + // alfa-as-black-mage: + // cluster: alfa + // user: black-mage-via-file + // namespace: zulu + // alfa-as-white-mage: + // cluster: alfa + // user: white-mage-via-cert + // bravo-as-black-mage: + // cluster: bravo + // user: black-mage-via-file + // namespace: yankee + // current-context: alfa-as-white-mage +} diff --git a/pkg/client/clientcmd/validation.go b/pkg/client/clientcmd/validation.go new file mode 100644 index 0000000000..a9b2682922 --- /dev/null +++ b/pkg/client/clientcmd/validation.go @@ -0,0 +1,182 @@ +/* +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 ( + "errors" + "fmt" + "os" + "strings" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +var ErrNoContext = errors.New("no context chosen") + +type errContextNotFound struct { + ContextName string +} + +func (e *errContextNotFound) Error() string { + return fmt.Sprintf("context was not found for specified context: %v", e.ContextName) +} + +// IsContextNotFound returns a boolean indicating whether the error is known to +// report that a context was not found +func IsContextNotFound(err error) bool { + if err == nil { + return false + } + + return strings.Contains(err.Error(), "context was not found for specified context") +} + +// Validate checks for errors in the Config. It does not return early so that it can find as many errors as possible. +func Validate(config Config) error { + validationErrors := make([]error, 0) + + if len(config.CurrentContext) != 0 { + if _, exists := config.Contexts[config.CurrentContext]; !exists { + validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext}) + } + } + + for contextName, context := range config.Contexts { + validationErrors = append(validationErrors, validateContext(contextName, context, config)...) + } + + for authInfoName, authInfo := range config.AuthInfos { + validationErrors = append(validationErrors, validateAuthInfo(authInfoName, authInfo)...) + } + + for clusterName, clusterInfo := range config.Clusters { + validationErrors = append(validationErrors, validateClusterInfo(clusterName, clusterInfo)...) + } + + return util.SliceToError(validationErrors) +} + +// 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 ConfirmUsable(config Config, passedContextName string) error { + validationErrors := make([]error, 0) + + var contextName string + if len(passedContextName) != 0 { + contextName = passedContextName + } else { + contextName = config.CurrentContext + } + + if len(contextName) == 0 { + return ErrNoContext + } + + context, exists := config.Contexts[contextName] + if !exists { + validationErrors = append(validationErrors, &errContextNotFound{contextName}) + } + + if exists { + validationErrors = append(validationErrors, validateContext(contextName, context, config)...) + validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, config.AuthInfos[context.AuthInfo])...) + validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, config.Clusters[context.Cluster])...) + } + + return util.SliceToError(validationErrors) +} + +// validateClusterInfo looks for conflicts and errors in the cluster info +func validateClusterInfo(clusterName string, clusterInfo Cluster) []error { + validationErrors := make([]error, 0) + + if len(clusterInfo.Server) == 0 { + validationErrors = append(validationErrors, fmt.Errorf("no server found for %v", clusterName)) + } + if len(clusterInfo.CertificateAuthority) != 0 { + clientCertCA, err := os.Open(clusterInfo.CertificateAuthority) + defer clientCertCA.Close() + if err != nil { + validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err)) + } + } + + return validationErrors +} + +// validateAuthInfo looks for conflicts and errors in the auth info +func validateAuthInfo(authInfoName string, authInfo AuthInfo) []error { + validationErrors := make([]error, 0) + + methods := make([]string, 0, 3) + if len(authInfo.Token) != 0 { + methods = append(methods, "token") + } + if len(authInfo.AuthPath) != 0 { + methods = append(methods, "authFile") + + file, err := os.Open(authInfo.AuthPath) + os.IsNotExist(err) + defer file.Close() + if err != nil { + validationErrors = append(validationErrors, fmt.Errorf("unable to read auth-path %v for %v due to %v", authInfo.AuthPath, authInfoName, err)) + } + } + if len(authInfo.ClientCertificate) != 0 { + methods = append(methods, "clientCert") + + clientCertFile, err := os.Open(authInfo.ClientCertificate) + defer clientCertFile.Close() + if err != nil { + validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err)) + } + clientKeyFile, err := os.Open(authInfo.ClientKey) + defer clientKeyFile.Close() + if err != nil { + validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err)) + } + } + + if (len(methods)) > 1 { + validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v. Found %v, only one is allowed", authInfoName, methods)) + } + + return validationErrors +} + +// validateContext looks for errors in the context. It is not transitive, so errors in the reference authInfo or cluster configs are not included in this return +func validateContext(contextName string, context Context, config Config) []error { + validationErrors := make([]error, 0) + + if len(context.AuthInfo) == 0 { + validationErrors = append(validationErrors, fmt.Errorf("user was not specified for Context %v", contextName)) + } else if _, exists := config.AuthInfos[context.AuthInfo]; !exists { + validationErrors = append(validationErrors, fmt.Errorf("user, %v, was not found for Context %v", context.AuthInfo, contextName)) + } + + if len(context.Cluster) == 0 { + validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for Context %v", contextName)) + } else if _, exists := config.Clusters[context.Cluster]; !exists { + validationErrors = append(validationErrors, fmt.Errorf("cluster, %v, was not found for Context %v", context.Cluster, contextName)) + } + + if (len(context.Namespace) != 0) && !util.IsDNS952Label(context.Namespace) { + validationErrors = append(validationErrors, fmt.Errorf("namespace, %v, for context %v, does not conform to the kubernetest DNS952 rules", context.Namespace, contextName)) + } + + return validationErrors +} diff --git a/pkg/client/clientcmd/validation_test.go b/pkg/client/clientcmd/validation_test.go new file mode 100644 index 0000000000..ceb0cbe10d --- /dev/null +++ b/pkg/client/clientcmd/validation_test.go @@ -0,0 +1,404 @@ +/* +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 ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +func TestConfirmUsableBadInfoButOkConfig(t *testing.T) { + config := NewConfig() + config.Clusters["missing ca"] = Cluster{ + Server: "anything", + CertificateAuthority: "missing", + } + config.AuthInfos["error"] = AuthInfo{ + AuthPath: "anything", + Token: "here", + } + config.Contexts["dirty"] = Context{ + Cluster: "missing ca", + AuthInfo: "error", + } + config.Clusters["clean"] = Cluster{ + Server: "anything", + } + config.AuthInfos["clean"] = AuthInfo{ + Token: "here", + } + config.Contexts["clean"] = Context{ + Cluster: "clean", + AuthInfo: "clean", + } + + badValidation := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"unable to read auth-path", "more than one authentication method", "unable to read certificate-authority"}, + } + okTest := configValidationTest{ + config: config, + } + + okTest.testConfirmUsable("clean", t) + badValidation.testConfig(t) +} +func TestConfirmUsableBadInfoConfig(t *testing.T) { + config := NewConfig() + config.Clusters["missing ca"] = Cluster{ + Server: "anything", + CertificateAuthority: "missing", + } + config.AuthInfos["error"] = AuthInfo{ + AuthPath: "anything", + Token: "here", + } + config.Contexts["first"] = Context{ + Cluster: "missing ca", + AuthInfo: "error", + } + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"unable to read auth-path", "more than one authentication method", "unable to read certificate-authority"}, + } + + test.testConfirmUsable("first", t) +} +func TestConfirmUsableEmptyConfig(t *testing.T) { + config := NewConfig() + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"no context chosen"}, + } + + test.testConfirmUsable("", t) +} +func TestConfirmUsableMissingConfig(t *testing.T) { + config := NewConfig() + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"context was not found for"}, + } + + test.testConfirmUsable("not-here", t) +} +func TestValidateEmptyConfig(t *testing.T) { + config := NewConfig() + test := configValidationTest{ + config: config, + } + + test.testConfig(t) +} +func TestValidateMissingCurrentContextConfig(t *testing.T) { + config := NewConfig() + config.CurrentContext = "anything" + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"context was not found for specified "}, + } + + test.testConfig(t) +} +func TestIsContextNotFound(t *testing.T) { + config := NewConfig() + config.CurrentContext = "anything" + + err := Validate(*config) + if !IsContextNotFound(err) { + t.Errorf("Expected context not found, but got %v", err) + } +} +func TestValidateMissingReferencesConfig(t *testing.T) { + config := NewConfig() + config.CurrentContext = "anything" + config.Contexts["anything"] = Context{Cluster: "missing", AuthInfo: "missing"} + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"user, missing, was not found for Context anything", "cluster, missing, was not found for Context anything"}, + } + + test.testContext("anything", t) + test.testConfig(t) +} +func TestValidateEmptyContext(t *testing.T) { + config := NewConfig() + config.CurrentContext = "anything" + config.Contexts["anything"] = Context{} + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"user was not specified for Context anything", "cluster was not specified for Context anything"}, + } + + test.testContext("anything", t) + test.testConfig(t) +} + +func TestValidateEmptyClusterInfo(t *testing.T) { + config := NewConfig() + config.Clusters["empty"] = Cluster{} + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"no server found for"}, + } + + test.testCluster("empty", t) + test.testConfig(t) +} +func TestValidateMissingCAFileClusterInfo(t *testing.T) { + config := NewConfig() + config.Clusters["missing ca"] = Cluster{ + Server: "anything", + CertificateAuthority: "missing", + } + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"unable to read certificate-authority"}, + } + + test.testCluster("missing ca", t) + test.testConfig(t) +} +func TestValidateCleanClusterInfo(t *testing.T) { + config := NewConfig() + config.Clusters["clean"] = Cluster{ + Server: "anything", + } + test := configValidationTest{ + config: config, + } + + test.testCluster("clean", t) + test.testConfig(t) +} +func TestValidateCleanWithCAClusterInfo(t *testing.T) { + tempFile, _ := ioutil.TempFile("", "") + defer os.Remove(tempFile.Name()) + + config := NewConfig() + config.Clusters["clean"] = Cluster{ + Server: "anything", + CertificateAuthority: tempFile.Name(), + } + test := configValidationTest{ + config: config, + } + + test.testCluster("clean", t) + test.testConfig(t) +} + +func TestValidateEmptyAuthInfo(t *testing.T) { + config := NewConfig() + config.AuthInfos["error"] = AuthInfo{} + test := configValidationTest{ + config: config, + } + + 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{ + AuthPath: "missing", + } + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"unable to read auth-path"}, + } + + test.testAuthInfo("error", t) + test.testConfig(t) +} +func TestValidateCertFilesNotFoundAuthInfo(t *testing.T) { + config := NewConfig() + config.AuthInfos["error"] = AuthInfo{ + ClientCertificate: "missing", + ClientKey: "missing", + } + test := configValidationTest{ + config: config, + expectedErrorSubstring: []string{"unable to read client-cert", "unable to read client-key"}, + } + + test.testAuthInfo("error", t) + test.testConfig(t) +} +func TestValidateCleanCertFilesAuthInfo(t *testing.T) { + tempFile, _ := ioutil.TempFile("", "") + defer os.Remove(tempFile.Name()) + + config := NewConfig() + config.AuthInfos["clean"] = AuthInfo{ + ClientCertificate: tempFile.Name(), + ClientKey: tempFile.Name(), + } + test := configValidationTest{ + config: config, + } + + test.testAuthInfo("clean", t) + test.testConfig(t) +} +func TestValidateCleanPathAuthInfo(t *testing.T) { + tempFile, _ := ioutil.TempFile("", "") + defer os.Remove(tempFile.Name()) + + config := NewConfig() + config.AuthInfos["clean"] = AuthInfo{ + AuthPath: tempFile.Name(), + } + test := configValidationTest{ + config: config, + } + + test.testAuthInfo("clean", t) + test.testConfig(t) +} +func TestValidateCleanTokenAuthInfo(t *testing.T) { + config := NewConfig() + config.AuthInfos["clean"] = AuthInfo{ + Token: "any-value", + } + test := configValidationTest{ + config: config, + } + + test.testAuthInfo("clean", t) + test.testConfig(t) +} + +type configValidationTest struct { + config *Config + expectedErrorSubstring []string +} + +func (c configValidationTest) testContext(contextName string, t *testing.T) { + errs := validateContext(contextName, c.config.Contexts[contextName], *c.config) + + if len(c.expectedErrorSubstring) != 0 { + if len(errs) == 0 { + t.Errorf("Expected error containing: %v", c.expectedErrorSubstring) + } + for _, curr := range c.expectedErrorSubstring { + if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) { + t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs)) + } + } + + } else { + if len(errs) != 0 { + t.Errorf("Unexpected error: %v", util.SliceToError(errs)) + } + } +} +func (c configValidationTest) testConfirmUsable(contextName string, t *testing.T) { + err := ConfirmUsable(*c.config, contextName) + + if len(c.expectedErrorSubstring) != 0 { + if err == nil { + t.Errorf("Expected error containing: %v", c.expectedErrorSubstring) + } else { + for _, curr := range c.expectedErrorSubstring { + if err != nil && !strings.Contains(err.Error(), curr) { + t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err) + } + } + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } +} +func (c configValidationTest) testConfig(t *testing.T) { + err := Validate(*c.config) + + if len(c.expectedErrorSubstring) != 0 { + if err == nil { + t.Errorf("Expected error containing: %v", c.expectedErrorSubstring) + } else { + for _, curr := range c.expectedErrorSubstring { + if err != nil && !strings.Contains(err.Error(), curr) { + t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err) + } + } + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } +} +func (c configValidationTest) testCluster(clusterName string, t *testing.T) { + errs := validateClusterInfo(clusterName, c.config.Clusters[clusterName]) + + if len(c.expectedErrorSubstring) != 0 { + if len(errs) == 0 { + t.Errorf("Expected error containing: %v", c.expectedErrorSubstring) + } + for _, curr := range c.expectedErrorSubstring { + if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) { + t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs)) + } + } + + } else { + if len(errs) != 0 { + t.Errorf("Unexpected error: %v", util.SliceToError(errs)) + } + } +} + +func (c configValidationTest) testAuthInfo(authInfoName string, t *testing.T) { + errs := validateAuthInfo(authInfoName, c.config.AuthInfos[authInfoName]) + + if len(c.expectedErrorSubstring) != 0 { + if len(errs) == 0 { + t.Errorf("Expected error containing: %v", c.expectedErrorSubstring) + } + for _, curr := range c.expectedErrorSubstring { + if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) { + t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs)) + } + } + + } else { + if len(errs) != 0 { + t.Errorf("Unexpected error: %v", util.SliceToError(errs)) + } + } +} diff --git a/pkg/client/helper.go b/pkg/client/helper.go index ac8c66780a..8ead0fb75f 100644 --- a/pkg/client/helper.go +++ b/pkg/client/helper.go @@ -21,9 +21,11 @@ import ( "net/http" "net/url" "path" + "reflect" "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" + "github.com/GoogleCloudPlatform/kubernetes/pkg/version" ) // Config holds the common attributes that can be passed to a Kubernetes client on @@ -92,6 +94,24 @@ func New(c *Config) (*Client, error) { return &Client{client, isPreV1Beta3}, nil } +func MatchesServerVersion(c *Config) error { + client, err := New(c) + if err != nil { + return err + } + + clientVersion := version.Get() + serverVersion, err := client.ServerVersion() + if err != nil { + return fmt.Errorf("couldn't read version from server: %v\n", err) + } + if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) { + return fmt.Errorf("server version (%#v) differs from client version (%#v)!\n", s, clientVersion) + } + + return nil +} + // NewOrDie creates a Kubernetes client and panics if the provided API version is not recognized. func NewOrDie(c *Config) *Client { client, err := New(c) diff --git a/pkg/clientauth/clientauth.go b/pkg/clientauth/clientauth.go index d5cdb0c717..5e2fbaf1f9 100644 --- a/pkg/clientauth/clientauth.go +++ b/pkg/clientauth/clientauth.go @@ -117,3 +117,9 @@ func (info Info) MergeWithConfig(c client.Config) (client.Config, error) { } return config, nil } + +func (info Info) Complete() bool { + return len(info.User) > 0 || + len(info.CertFile) > 0 || + len(info.BearerToken) > 0 +} diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 6547ae56e6..8de6d55073 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -34,55 +34,59 @@ import ( "github.com/spf13/cobra" ) +const ( + FlagMatchBinaryVersion = "match-server-version" +) + // Factory provides abstractions that allow the Kubectl command to be extended across multiple types // of resources and different API sets. type Factory struct { - ClientBuilder clientcmd.Builder - Mapper meta.RESTMapper - Typer runtime.ObjectTyper - Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) - Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) - Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) - Validator func(*cobra.Command) (validation.Schema, error) + ClientConfig clientcmd.ClientConfig + Mapper meta.RESTMapper + Typer runtime.ObjectTyper + Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) + Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) + Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) + Validator func(*cobra.Command) (validation.Schema, error) } // NewFactory creates a factory with the default Kubernetes resources defined -func NewFactory(clientBuilder clientcmd.Builder) *Factory { - return &Factory{ - ClientBuilder: clientBuilder, - Mapper: latest.RESTMapper, - Typer: api.Scheme, - Validator: func(cmd *cobra.Command) (validation.Schema, error) { - if GetFlagBool(cmd, "validate") { - client, err := clientBuilder.Client() - if err != nil { - return nil, err - } - return &clientSwaggerSchema{client, api.Scheme}, nil - } else { - return validation.NullSchema{}, nil - } - }, - Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { - return clientBuilder.Override(func(c *client.Config) { - c.Version = mapping.APIVersion - }).Client() - }, - Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { - client, err := clientBuilder.Client() - if err != nil { - return nil, err - } - describer, ok := kubectl.DescriberFor(mapping.Kind, client) - if !ok { - return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) - } - return describer, nil - }, +func NewFactory(clientConfig clientcmd.ClientConfig) *Factory { + ret := &Factory{ + ClientConfig: clientConfig, + Mapper: latest.RESTMapper, + Typer: api.Scheme, Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) { return kubectl.NewHumanReadablePrinter(noHeaders), nil }, } + + ret.Validator = func(cmd *cobra.Command) (validation.Schema, error) { + if GetFlagBool(cmd, "validate") { + client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) + if err != nil { + return nil, err + } + return &clientSwaggerSchema{client, api.Scheme}, nil + } else { + return validation.NullSchema{}, nil + } + } + ret.Client = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { + return getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) + } + ret.Describer = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { + client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion)) + if err != nil { + return nil, err + } + describer, ok := kubectl.DescriberFor(mapping.Kind, client) + if !ok { + return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind) + } + return describer, nil + } + return ret } func (f *Factory) Run(out io.Writer) { @@ -96,12 +100,13 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, Run: runHelp, } - f.ClientBuilder.BindFlags(cmds.PersistentFlags()) + f.ClientConfig = getClientConfig(cmds) // Globally persistent flags across all subcommands. // TODO Change flag names to consts to allow safer lookup from subcommands. // TODO Add a verbose flag that turns on glog logging. Probably need a way // to do that automatically for every subcommand. + cmds.PersistentFlags().Bool(FlagMatchBinaryVersion, false, "Require server version to match client version") cmds.PersistentFlags().String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.") cmds.PersistentFlags().StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.") cmds.PersistentFlags().Bool("validate", false, "If true, use a schema to validate the input before sending it") @@ -124,6 +129,50 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, } } +// getClientBuilder creates a clientcmd.ClientConfig that has a hierarchy like this: +// 1. Use the kubeconfig builder. The number of merges and overrides here gets a little crazy. Stay with me. +// 1. Merge together the kubeconfig itself. This is done with the following hierarchy and merge rules: +// 1. CommandLineLocation - this parsed from the command line, so it must be late bound +// 2. EnvVarLocation +// 3. CurrentDirectoryLocation +// 4. HomeDirectoryLocation +// Empty filenames are ignored. Files with non-deserializable content produced errors. +// The first file to set a particular value or map key wins and the value or map key is never changed. +// This means that the first file to set CurrentContext will have its context preserved. It also means +// that if two files specify a "red-user", only values from the first file's red-user are used. Even +// non-conflicting entries from the second file's "red-user" are discarded. +// 2. Determine the context to use based on the first hit in this chain +// 1. command line argument - again, parsed from the command line, so it must be late bound +// 2. CurrentContext from the merged kubeconfig file +// 3. Empty is allowed at this stage +// 3. Determine the cluster info and auth info to use. At this point, we may or may not have a context. They +// are built based on the first hit in this chain. (run it twice, once for auth, once for cluster) +// 1. command line argument +// 2. If context is present, then use the context value +// 3. Empty is allowed +// 4. Determine the actual cluster info to use. At this point, we may or may not have a cluster info. Build +// each piece of the cluster info based on the chain: +// 1. command line argument +// 2. If cluster info is present and a value for the attribute is present, use it. +// 3. If you don't have a server location, bail. +// 5. Auth info is build using the same rules as cluster info, EXCEPT that you can only have one authentication +// technique per auth info. The following conditions result in an error: +// 1. If there are two conflicting techniques specified from the command line, fail. +// 2. If the command line does not specify one, and the auth info has conflicting techniques, fail. +// 3. If the command line specifies one and the auth info specifies another, honor the command line technique. +// 2. Use default values and potentially prompt for auth information +func getClientConfig(cmd *cobra.Command) clientcmd.ClientConfig { + loadingRules := clientcmd.NewClientConfigLoadingRules() + loadingRules.EnvVarPath = os.Getenv(clientcmd.RecommendedConfigPathEnvVar) + cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") + + overrides := &clientcmd.ConfigOverrides{} + overrides.BindFlags(cmd.PersistentFlags(), clientcmd.RecommendedConfigOverrideFlags("")) + clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin) + + return clientConfig +} + func checkErr(err error) { if err != nil { glog.FatalDepth(1, err) @@ -195,3 +244,25 @@ func (c *clientSwaggerSchema) ValidateBytes(data []byte) error { } return schema.ValidateBytes(data) } + +// TODO Need to only run server version match once per client host creation +func getClient(clientConfig clientcmd.ClientConfig, matchServerVersion bool) (*client.Client, error) { + config, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + + if matchServerVersion { + err := client.MatchesServerVersion(config) + if err != nil { + return nil, err + } + } + + client, err := client.New(config) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/kubectl/cmd/log.go b/pkg/kubectl/cmd/log.go index 91e5b79f48..4b4eeea031 100644 --- a/pkg/kubectl/cmd/log.go +++ b/pkg/kubectl/cmd/log.go @@ -21,6 +21,8 @@ import ( "strconv" "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" ) func (f *Factory) NewCmdLog(out io.Writer) *cobra.Command { @@ -44,7 +46,9 @@ Examples: } namespace := GetKubeNamespace(cmd) - client, err := f.ClientBuilder.Client() + config, err := f.ClientConfig.ClientConfig() + checkErr(err) + client, err := client.New(config) checkErr(err) podID := args[0] diff --git a/pkg/kubectl/cmd/proxy.go b/pkg/kubectl/cmd/proxy.go index 263022234b..85f2e85d75 100644 --- a/pkg/kubectl/cmd/proxy.go +++ b/pkg/kubectl/cmd/proxy.go @@ -33,7 +33,7 @@ func (f *Factory) NewCmdProxy(out io.Writer) *cobra.Command { port := GetFlagInt(cmd, "port") glog.Infof("Starting to serve on localhost:%d", port) - clientConfig, err := f.ClientBuilder.Config() + clientConfig, err := f.ClientConfig.ClientConfig() checkErr(err) server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), clientConfig, port) diff --git a/pkg/kubectl/cmd/version.go b/pkg/kubectl/cmd/version.go index a0ee4cdc2b..9dbedc4b0d 100644 --- a/pkg/kubectl/cmd/version.go +++ b/pkg/kubectl/cmd/version.go @@ -19,8 +19,10 @@ package cmd import ( "io" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" ) func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command { @@ -31,7 +33,9 @@ func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command { if GetFlagBool(cmd, "client") { kubectl.GetClientVersion(out) } else { - client, err := f.ClientBuilder.Client() + config, err := f.ClientConfig.ClientConfig() + checkErr(err) + client, err := client.New(config) checkErr(err) kubectl.GetVersion(out, client)