From 2dbfb803497017b074422886dbb6bcb02af9e9e9 Mon Sep 17 00:00:00 2001 From: deads2k Date: Mon, 17 Nov 2014 13:29:45 -0500 Subject: [PATCH] add utility for binding flags and building api server clients --- cmd/kubectl/kubectl.go | 4 +- pkg/client/clientcmd/auth_loaders.go | 82 +++++ pkg/client/clientcmd/client_builder.go | 181 +++++++++++ pkg/client/clientcmd/client_builder_test.go | 321 ++++++++++++++++++++ pkg/client/clientcmd/doc.go | 25 ++ pkg/client/clientcmd/provided_flags.go | 100 ++++++ pkg/client/flags.go | 1 + pkg/config/config.go | 10 +- pkg/config/config_test.go | 6 +- pkg/kubectl/cmd/cmd.go | 111 ++----- pkg/kubectl/cmd/createall.go | 7 +- pkg/kubectl/cmd/log.go | 5 +- pkg/kubectl/cmd/proxy.go | 8 +- pkg/kubectl/cmd/version.go | 7 +- pkg/kubectl/kubectl.go | 30 -- pkg/kubectl/kubectl_test.go | 56 ---- 16 files changed, 764 insertions(+), 190 deletions(-) create mode 100644 pkg/client/clientcmd/auth_loaders.go create mode 100644 pkg/client/clientcmd/client_builder.go create mode 100644 pkg/client/clientcmd/client_builder_test.go create mode 100644 pkg/client/clientcmd/doc.go create mode 100644 pkg/client/clientcmd/provided_flags.go diff --git a/cmd/kubectl/kubectl.go b/cmd/kubectl/kubectl.go index ccb2359863f..6e5030c09ca 100644 --- a/cmd/kubectl/kubectl.go +++ b/cmd/kubectl/kubectl.go @@ -19,9 +19,11 @@ package main import ( "os" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd" ) func main() { - cmd.NewFactory().Run(os.Stdout) + clientBuilder := clientcmd.NewBuilder(clientcmd.NewPromptingAuthLoader(os.Stdin)) + cmd.NewFactory(clientBuilder).Run(os.Stdout) } diff --git a/pkg/client/clientcmd/auth_loaders.go b/pkg/client/clientcmd/auth_loaders.go new file mode 100644 index 00000000000..35fb1a6d4b2 --- /dev/null +++ b/pkg/client/clientcmd/auth_loaders.go @@ -0,0 +1,82 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clientcmd + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" +) + +// AuthLoaders are used to build clientauth.Info objects. +type AuthLoader interface { + // LoadAuth takes a path to a config file and can then do anything it needs in order to return a valid clientauth.Info + LoadAuth(path string) (*clientauth.Info, error) +} + +// default implementation of an AuthLoader +type defaultAuthLoader struct{} + +// LoadAuth for defaultAuthLoader simply delegates to clientauth.LoadFromFile +func (*defaultAuthLoader) LoadAuth(path string) (*clientauth.Info, error) { + return clientauth.LoadFromFile(path) +} + +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) { + 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) + data, err := json.Marshal(auth) + if err != nil { + return &auth, err + } + err = ioutil.WriteFile(path, data, 0600) + return &auth, err + } + authPtr, err := clientauth.LoadFromFile(path) + if err != nil { + return nil, err + } + return authPtr, nil +} +func promptForString(field string, r io.Reader) string { + fmt.Printf("Please enter %s: ", field) + var result string + fmt.Fscan(r, &result) + return result +} + +// 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} +} + +// NewDefaultAuthLoader returns a default implementation of an AuthLoader that only reads from a config file +func NewDefaultAuthLoader() AuthLoader { + return &defaultAuthLoader{} +} diff --git a/pkg/client/clientcmd/client_builder.go b/pkg/client/clientcmd/client_builder.go new file mode 100644 index 00000000000..80ef5b704fe --- /dev/null +++ b/pkg/client/clientcmd/client_builder.go @@ -0,0 +1,181 @@ +/* +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) + // Config uses the values of the bound flags and builds a complete client config + Config() (*client.Config, error) + // Client calls BuildConfig under the covers and uses that config to return a client + Client() (*client.Client, error) +} + +// 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 +} + +// 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) { + 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 + authInfoFileFound = false + authInfo = &clientauth.Info{} + } else { + return nil, err + } + } + + // 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 new file mode 100644 index 00000000000..cb2a7b45e68 --- /dev/null +++ b/pkg/client/clientcmd/client_builder_test.go @@ -0,0 +1,321 @@ +/* +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/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 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/doc.go b/pkg/client/clientcmd/doc.go new file mode 100644 index 00000000000..79ea31ea831 --- /dev/null +++ b/pkg/client/clientcmd/doc.go @@ -0,0 +1,25 @@ +/* +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 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: + + clientBuilder := clientcmd.NewBuilder(clientcmd.NewDefaultAuthLoader()) + clientBuilder.BindFlags(cmds.PersistentFlags()) + apiClient, err := clientBuilder.Client() +*/ +package clientcmd diff --git a/pkg/client/clientcmd/provided_flags.go b/pkg/client/clientcmd/provided_flags.go new file mode 100644 index 00000000000..a37256c78ba --- /dev/null +++ b/pkg/client/clientcmd/provided_flags.go @@ -0,0 +1,100 @@ +/* +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("%s", flag.Value) +} diff --git a/pkg/client/flags.go b/pkg/client/flags.go index 074e038ec96..cd2faa2657e 100644 --- a/pkg/client/flags.go +++ b/pkg/client/flags.go @@ -25,6 +25,7 @@ type FlagSet interface { } // BindClientConfigFlags registers a standard set of CLI flags for connecting to a Kubernetes API server. +// TODO this method is superceded by pkg/client/clientcmd/client_builder.go func BindClientConfigFlags(flags FlagSet, config *Config) { flags.StringVar(&config.Host, "master", config.Host, "The address of the Kubernetes API server") flags.StringVar(&config.Version, "api_version", config.Version, "The API version to use when talking to the server") diff --git a/pkg/config/config.go b/pkg/config/config.go index 930aa0f408c..49ac1599a39 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -26,14 +26,18 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) +type RESTClientPoster interface { + Post() *client.Request +} + // ClientFunc returns the RESTClient defined for given resource -type ClientFunc func(mapping *meta.RESTMapping) (*client.RESTClient, error) +type ClientPosterFunc func(mapping *meta.RESTMapping) (RESTClientPoster, error) // CreateObjects creates bulk of resources provided by items list. Each item must // be valid API type. It requires ObjectTyper to parse the Version and Kind and // RESTMapper to get the resource URI and REST client that knows how to create // given type -func CreateObjects(typer runtime.ObjectTyper, mapper meta.RESTMapper, clientFor ClientFunc, objects []runtime.Object) util.ErrorList { +func CreateObjects(typer runtime.ObjectTyper, mapper meta.RESTMapper, clientFor ClientPosterFunc, objects []runtime.Object) util.ErrorList { allErrors := util.ErrorList{} for i, obj := range objects { version, kind, err := typer.ObjectVersionAndKind(obj) @@ -65,7 +69,7 @@ func CreateObjects(typer runtime.ObjectTyper, mapper meta.RESTMapper, clientFor // CreateObject creates the obj using the provided clients and the resource URI // mapping. It reports ValidationError when the object is missing the Metadata // or the Name and it will report any error occured during create REST call -func CreateObject(client *client.RESTClient, mapping *meta.RESTMapping, obj runtime.Object) *errs.ValidationError { +func CreateObject(client RESTClientPoster, mapping *meta.RESTMapping, obj runtime.Object) *errs.ValidationError { name, err := mapping.MetadataAccessor.Name(obj) if err != nil || name == "" { return errs.NewFieldRequired("name", err) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 799ae67fec3..5150c46de87 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -35,7 +35,7 @@ func getTyperAndMapper() (runtime.ObjectTyper, meta.RESTMapper) { return api.Scheme, latest.RESTMapper } -func getFakeClient(t *testing.T, validURLs []string) (ClientFunc, *httptest.Server) { +func getFakeClient(t *testing.T, validURLs []string) (ClientPosterFunc, *httptest.Server) { handlerFunc := func(w http.ResponseWriter, r *http.Request) { for _, u := range validURLs { if u == r.RequestURI { @@ -45,7 +45,7 @@ func getFakeClient(t *testing.T, validURLs []string) (ClientFunc, *httptest.Serv t.Errorf("Unexpected HTTP request: %s, expected %v", r.RequestURI, validURLs) } server := httptest.NewServer(http.HandlerFunc(handlerFunc)) - return func(mapping *meta.RESTMapping) (*client.RESTClient, error) { + return func(mapping *meta.RESTMapping) (RESTClientPoster, error) { fakeCodec := runtime.CodecFor(api.Scheme, "v1beta1") fakeUri, _ := url.Parse(server.URL + "/api/v1beta1") return client.NewRESTClient(fakeUri, fakeCodec), nil @@ -134,7 +134,7 @@ func TestCreateNoClientItems(t *testing.T) { typer, mapper := getTyperAndMapper() _, s := getFakeClient(t, []string{"/api/v1beta1/pods", "/api/v1beta1/services"}) - noClientFunc := func(mapping *meta.RESTMapping) (*client.RESTClient, error) { + noClientFunc := func(mapping *meta.RESTMapping) (RESTClientPoster, error) { return nil, fmt.Errorf("no client") } diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index fe1bc246346..cbff011685e 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -24,9 +24,10 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/golang/glog" "github.com/spf13/cobra" ) @@ -34,23 +35,29 @@ import ( // Factory provides abstractions that allow the Kubectl command to be extended across multiple types // of resources and different API sets. type Factory struct { - Mapper meta.RESTMapper - Typer runtime.ObjectTyper - Client func(*cobra.Command, *meta.RESTMapping) (kubectl.RESTClient, error) - Describer func(*cobra.Command, *meta.RESTMapping) (kubectl.Describer, error) - Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) + 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) } // NewFactory creates a factory with the default Kubernetes resources defined -func NewFactory() *Factory { +func NewFactory(clientBuilder clientcmd.Builder) *Factory { return &Factory{ - Mapper: latest.RESTMapper, - Typer: api.Scheme, + ClientBuilder: clientBuilder, + Mapper: latest.RESTMapper, + Typer: api.Scheme, Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) { - return getKubeClient(cmd), nil + return clientBuilder.Client() }, Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) { - describer, ok := kubectl.DescriberFor(mapping.Kind, getKubeClient(cmd)) + 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) } @@ -73,23 +80,17 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, Run: runHelp, } + f.ClientBuilder.BindFlags(cmds.PersistentFlags()) + // 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().StringP("server", "s", "", "Kubernetes apiserver to connect to") - cmds.PersistentFlags().StringP("auth-path", "a", os.Getenv("HOME")+"/.kubernetes_auth", "Path to the auth info file. If missing, prompt the user. Only used if using https.") - cmds.PersistentFlags().Bool("match-server-version", false, "Require server version to match client version") - cmds.PersistentFlags().String("api-version", latest.Version, "The version of the API to use against the server") - cmds.PersistentFlags().String("certificate-authority", "", "Path to a certificate file for the certificate authority") - cmds.PersistentFlags().String("client-certificate", "", "Path to a client certificate for TLS.") - cmds.PersistentFlags().String("client-key", "", "Path to a client key file for TLS.") - cmds.PersistentFlags().Bool("insecure-skip-tls-verify", false, "If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure.") 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.AddCommand(NewCmdVersion(out)) - cmds.AddCommand(NewCmdProxy(out)) + cmds.AddCommand(f.NewCmdVersion(out)) + cmds.AddCommand(f.NewCmdProxy(out)) cmds.AddCommand(f.NewCmdGet(out)) cmds.AddCommand(f.NewCmdDescribe(out)) @@ -99,7 +100,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(f.NewCmdDelete(out)) cmds.AddCommand(NewCmdNamespace(out)) - cmds.AddCommand(NewCmdLog(out)) + cmds.AddCommand(f.NewCmdLog(out)) if err := cmds.Execute(); err != nil { os.Exit(1) @@ -150,69 +151,3 @@ func getExplicitKubeNamespace(cmd *cobra.Command) (string, bool) { // value and return its value and true. return "", false } - -// GetKubeConfig returns a config used for the Kubernetes client with CLI -// options parsed. -func GetKubeConfig(cmd *cobra.Command) *client.Config { - config := &client.Config{} - - var host string - if hostFlag := GetFlagString(cmd, "server"); len(hostFlag) > 0 { - host = hostFlag - glog.V(2).Infof("Using server from -s flag: %s", host) - } else if len(os.Getenv("KUBERNETES_MASTER")) > 0 { - host = os.Getenv("KUBERNETES_MASTER") - glog.V(2).Infof("Using server from env var KUBERNETES_MASTER: %s", host) - } else { - // TODO: eventually apiserver should start on 443 and be secure by default - host = "http://localhost:8080" - glog.V(2).Infof("No server found in flag or env var, using default: %s", host) - } - config.Host = host - - if client.IsConfigTransportTLS(config) { - // Get the values from the file on disk (or from the user at the - // command line). Override them with the command line parameters, if - // provided. - authPath := GetFlagString(cmd, "auth-path") - authInfo, err := kubectl.LoadClientAuthInfoOrPrompt(authPath, os.Stdin) - // TODO: handle the case where the file could not be written but - // we still got a user/pass from prompting. - if err != nil { - glog.Fatalf("Error loading auth: %v", err) - } - - config.Username = authInfo.User - config.Password = authInfo.Password - // First priority is flag, then file. - config.CAFile = FirstNonEmptyString(GetFlagString(cmd, "certificate-authority"), authInfo.CAFile) - config.CertFile = FirstNonEmptyString(GetFlagString(cmd, "client-certificate"), authInfo.CertFile) - config.KeyFile = FirstNonEmptyString(GetFlagString(cmd, "client-key"), authInfo.KeyFile) - config.BearerToken = authInfo.BearerToken - // For config.Insecure, the command line ALWAYS overrides the authInfo - // file, regardless of its setting. - if insecureFlag := GetFlagBoolPtr(cmd, "insecure-skip-tls-verify"); insecureFlag != nil { - config.Insecure = *insecureFlag - } else if authInfo.Insecure != nil { - config.Insecure = *authInfo.Insecure - } - } - - // The API version (e.g. v1beta1), not the binary version. - config.Version = GetFlagString(cmd, "api-version") - - return config -} - -func getKubeClient(cmd *cobra.Command) *client.Client { - config := GetKubeConfig(cmd) - - // The binary version. - matchVersion := GetFlagBool(cmd, "match-server-version") - - c, err := kubectl.GetKubeClient(config, matchVersion) - if err != nil { - glog.Fatalf("Error creating kubernetes client: %v", err) - } - return c -} diff --git a/pkg/kubectl/cmd/createall.go b/pkg/kubectl/cmd/createall.go index 80e145a641b..b8e912db1d6 100644 --- a/pkg/kubectl/cmd/createall.go +++ b/pkg/kubectl/cmd/createall.go @@ -21,7 +21,6 @@ import ( "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/meta" - "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/config" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -80,8 +79,10 @@ Examples: $ cat config.json | kubectl apply -f - `, Run: func(cmd *cobra.Command, args []string) { - clientFunc := func(*meta.RESTMapping) (*client.RESTClient, error) { - return getKubeClient(cmd).RESTClient, nil + clientFunc := func(mapper *meta.RESTMapping) (config.RESTClientPoster, error) { + client, err := f.Client(cmd, mapper) + checkErr(err) + return client, nil } filename := GetFlagString(cmd, "filename") diff --git a/pkg/kubectl/cmd/log.go b/pkg/kubectl/cmd/log.go index a505503a9e0..ecf219b6745 100644 --- a/pkg/kubectl/cmd/log.go +++ b/pkg/kubectl/cmd/log.go @@ -21,7 +21,7 @@ import ( "io" ) -func NewCmdLog(out io.Writer) *cobra.Command { +func (f *Factory) NewCmdLog(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "log ", Short: "Print the logs for a container in a pod", @@ -32,7 +32,8 @@ func NewCmdLog(out io.Writer) *cobra.Command { namespace := getKubeNamespace(cmd) - client := getKubeClient(cmd) + client, err := f.ClientBuilder.Client() + checkErr(err) pod, err := client.Pods(namespace).Get(args[0]) checkErr(err) diff --git a/pkg/kubectl/cmd/proxy.go b/pkg/kubectl/cmd/proxy.go index f0ab531e518..263022234bd 100644 --- a/pkg/kubectl/cmd/proxy.go +++ b/pkg/kubectl/cmd/proxy.go @@ -24,7 +24,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmdProxy(out io.Writer) *cobra.Command { +func (f *Factory) NewCmdProxy(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "proxy", Short: "Run a proxy to the Kubernetes API server", @@ -32,7 +32,11 @@ func NewCmdProxy(out io.Writer) *cobra.Command { Run: func(cmd *cobra.Command, args []string) { port := GetFlagInt(cmd, "port") glog.Infof("Starting to serve on localhost:%d", port) - server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), GetKubeConfig(cmd), port) + + clientConfig, err := f.ClientBuilder.Config() + checkErr(err) + + server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), clientConfig, port) checkErr(err) glog.Fatal(server.Serve()) }, diff --git a/pkg/kubectl/cmd/version.go b/pkg/kubectl/cmd/version.go index 0dc43da5abd..a0ee4cdc2b8 100644 --- a/pkg/kubectl/cmd/version.go +++ b/pkg/kubectl/cmd/version.go @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" ) -func NewCmdVersion(out io.Writer) *cobra.Command { +func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "version", Short: "Print version of client and server", @@ -31,7 +31,10 @@ func NewCmdVersion(out io.Writer) *cobra.Command { if GetFlagBool(cmd, "client") { kubectl.GetClientVersion(out) } else { - kubectl.GetVersion(out, getKubeClient(cmd)) + client, err := f.ClientBuilder.Client() + checkErr(err) + + kubectl.GetVersion(out, client) } }, } diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 19d24ba86c2..c32254cbd2f 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -20,7 +20,6 @@ package kubectl import ( "encoding/json" "fmt" - "io" "io/ioutil" "os" "reflect" @@ -28,7 +27,6 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/version" @@ -90,34 +88,6 @@ func SaveNamespaceInfo(path string, ns *NamespaceInfo) error { return err } -// LoadClientAuthInfoOrPrompt parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. -func LoadClientAuthInfoOrPrompt(path string, r io.Reader) (*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", r) - auth.Password = promptForString("Password", r) - data, err := json.Marshal(auth) - if err != nil { - return &auth, err - } - err = ioutil.WriteFile(path, data, 0600) - return &auth, err - } - authPtr, err := clientauth.LoadFromFile(path) - if err != nil { - return nil, err - } - return authPtr, nil -} - -func promptForString(field string, r io.Reader) string { - fmt.Printf("Please enter %s: ", field) - var result string - fmt.Fscan(r, &result) - return result -} - // TODO Move to labels package. func formatLabels(labelMap map[string]string) string { l := labels.Set(labelMap).String() diff --git a/pkg/kubectl/kubectl_test.go b/pkg/kubectl/kubectl_test.go index a5211119c90..11ec81f572c 100644 --- a/pkg/kubectl/kubectl_test.go +++ b/pkg/kubectl/kubectl_test.go @@ -17,15 +17,12 @@ limitations under the License. package kubectl import ( - "bytes" - "io" "io/ioutil" "os" "reflect" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" ) func validateAction(expectedAction, actualAction client.FakeAction, t *testing.T) { @@ -85,56 +82,3 @@ func TestLoadNamespaceInfo(t *testing.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()) - } - authInfo, err := LoadClientAuthInfoOrPrompt(aifile.Name(), tt.r) - if len(tt.authData) == 0 && tt.authData != "missing" { - if err == nil { - t.Error("LoadClientAuthInfoOrPrompt 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) - } - } -}