From 0727219c832373f06d6b9fd8f900285724a822c4 Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Tue, 11 Nov 2014 15:23:09 -0800 Subject: [PATCH] New package defines .kubernetes_auth format. Refactored common code to that package. Subsequent PRs will load and emit these files. --- cmd/e2e/e2e.go | 15 ++-- cmd/kubecfg/kubecfg.go | 3 +- pkg/clientauth/clientauth.go | 119 ++++++++++++++++++++++++++++++ pkg/clientauth/clientauth_test.go | 69 +++++++++++++++++ pkg/kubecfg/kubecfg.go | 27 ++----- pkg/kubecfg/kubecfg_test.go | 13 ++-- pkg/kubectl/cmd/cmd.go | 4 +- pkg/kubectl/kubectl.go | 26 ++----- pkg/kubectl/kubectl_test.go | 15 ++-- 9 files changed, 228 insertions(+), 63 deletions(-) create mode 100644 pkg/clientauth/clientauth.go create mode 100644 pkg/clientauth/clientauth_test.go diff --git a/cmd/e2e/e2e.go b/cmd/e2e/e2e.go index a7583ba8436..031997f77f1 100644 --- a/cmd/e2e/e2e.go +++ b/cmd/e2e/e2e.go @@ -27,7 +27,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" - "github.com/GoogleCloudPlatform/kubernetes/pkg/kubecfg" + "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" @@ -78,18 +78,13 @@ func loadClientOrDie() *client.Client { config := client.Config{ Host: *host, } - auth, err := kubecfg.LoadAuthInfo(*authConfig, os.Stdin) + auth, err := clientauth.LoadFromFile(*authConfig) if err != nil { glog.Fatalf("Error loading auth: %v", err) } - config.Username = auth.User - config.Password = auth.Password - config.CAFile = auth.CAFile - config.CertFile = auth.CertFile - config.KeyFile = auth.KeyFile - config.BearerToken = auth.BearerToken - if auth.Insecure != nil { - config.Insecure = *auth.Insecure + config, err = auth.MergeWithConfig(config) + if err != nil { + glog.Fatalf("Error creating client") } c, err := client.New(&config) if err != nil { diff --git a/cmd/kubecfg/kubecfg.go b/cmd/kubecfg/kubecfg.go index b93c2ba94a1..14f5a5c99b4 100644 --- a/cmd/kubecfg/kubecfg.go +++ b/cmd/kubecfg/kubecfg.go @@ -199,10 +199,11 @@ func main() { if clientConfig.Host == "" { // TODO: eventually apiserver should start on 443 and be secure by default + // TODO: don't specify http or https in Host, and infer that from auth options. clientConfig.Host = "http://localhost:8080" } if client.IsConfigTransportTLS(clientConfig) { - auth, err := kubecfg.LoadAuthInfo(*authConfig, os.Stdin) + auth, err := kubecfg.LoadClientAuthInfoOrPrompt(*authConfig, os.Stdin) if err != nil { glog.Fatalf("Error loading auth: %v", err) } diff --git a/pkg/clientauth/clientauth.go b/pkg/clientauth/clientauth.go new file mode 100644 index 00000000000..d5cdb0c717b --- /dev/null +++ b/pkg/clientauth/clientauth.go @@ -0,0 +1,119 @@ +/* +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 authcfg defines a file format for holding authentication +information needed by clients of Kubernetes. Typically, +a Kubernetes cluster will put auth info for the admin in a known +location when it is created, and will (soon) put it in a known +location within a Container's file tree for Containers that +need access to the Kubernetes API. + +Having a defined format allows: + - clients to be implmented in multiple languages + - applications which link clients to be portable across + clusters with different authentication styles (e.g. + some may use SSL Client certs, others may not, etc) + - when the format changes, applications only + need to update this code. + +The file format is json, marshalled from a struct authcfg.Info. + +Clinet libraries in other languages should use the same format. + +It is not intended to store general preferences, such as default +namespace, output options, etc. CLIs (such as kubectl) and UIs should +develop their own format and may wish to inline the authcfg.Info type. + +The authcfg.Info is just a file format. It is distinct from +client.Config which holds options for creating a client.Client. +Helper functions are provided in this package to fill in a +client.Client from an authcfg.Info. + +Example: + + import ( + "pkg/client" + "pkg/clientauth" + ) + + info, err := clientauth.LoadFromFile(filename) + if err != nil { + // handle error + } + clientConfig = client.Config{} + clientConfig.Host = "example.com:4901" + clientConfig = info.MergeWithConfig() + client := client.New(clientConfig) + client.ListPods() +*/ +package clientauth + +// TODO: need a way to rotate Tokens. Therefore, need a way for client object to be reset when the authcfg is updated. +import ( + "encoding/json" + "io/ioutil" + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +// Info holds Kubernetes API authorization config. It is intended +// to be read/written from a file as a JSON object. +type Info struct { + User string + Password string + CAFile string + CertFile string + KeyFile string + BearerToken string + Insecure *bool +} + +// LoadFromFile parses an Info object from a file path. +// If the file does not exist, then os.IsNotExist(err) == true +func LoadFromFile(path string) (*Info, error) { + var info Info + if _, err := os.Stat(path); os.IsNotExist(err) { + return nil, err + } + data, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + err = json.Unmarshal(data, &info) + if err != nil { + return nil, err + } + return &info, err +} + +// MergeWithConfig returns a copy of a client.Config with values from the Info. +// The fields of client.Config with a corresponding field in the Info are set +// with the value from the Info. +func (info Info) MergeWithConfig(c client.Config) (client.Config, error) { + var config client.Config = c + config.Username = info.User + config.Password = info.Password + config.CAFile = info.CAFile + config.CertFile = info.CertFile + config.KeyFile = info.KeyFile + config.BearerToken = info.BearerToken + if info.Insecure != nil { + config.Insecure = *info.Insecure + } + return config, nil +} diff --git a/pkg/clientauth/clientauth_test.go b/pkg/clientauth/clientauth_test.go new file mode 100644 index 00000000000..da680eaf97d --- /dev/null +++ b/pkg/clientauth/clientauth_test.go @@ -0,0 +1,69 @@ +/* +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 clientauth_test + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" +) + +func TestLoadFromFile(t *testing.T) { + loadAuthInfoTests := []struct { + authData string + authInfo *clientauth.Info + expectErr bool + }{ + { + `{"user": "user", "password": "pass"}`, + &clientauth.Info{User: "user", Password: "pass"}, + false, + }, + { + "", nil, true, + }, + } + 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 := clientauth.LoadFromFile(aifile.Name()) + gotErr := err != nil + if gotErr != tt.expectErr { + t.Errorf("expected errorness: %v, actual errorness: %v", tt.expectErr, gotErr) + } + if !reflect.DeepEqual(authInfo, tt.authInfo) { + t.Errorf("Expected %v, got %v", tt.authInfo, authInfo) + } + } +} diff --git a/pkg/kubecfg/kubecfg.go b/pkg/kubecfg/kubecfg.go index 687f4939e53..26ee248d578 100644 --- a/pkg/kubecfg/kubecfg.go +++ b/pkg/kubecfg/kubecfg.go @@ -28,6 +28,7 @@ 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/util/wait" @@ -51,23 +52,15 @@ func promptForString(field string, r io.Reader) string { return result } -type AuthInfo struct { - User string - Password string - CAFile string - CertFile string - KeyFile string - BearerToken string - Insecure *bool -} - type NamespaceInfo struct { Namespace string } -// LoadAuthInfo parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. -func LoadAuthInfo(path string, r io.Reader) (*AuthInfo, error) { - var auth AuthInfo +// LoadClientAuthInfoOrPrompt parses a clientauth.Info object from a file path. It prompts user and creates file if it doesn't exist. +// Oddly, it returns a clientauth.Info even if there is an error. +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) @@ -78,15 +71,11 @@ func LoadAuthInfo(path string, r io.Reader) (*AuthInfo, error) { err = ioutil.WriteFile(path, data, 0600) return &auth, err } - data, err := ioutil.ReadFile(path) + authPtr, err := clientauth.LoadFromFile(path) if err != nil { return nil, err } - err = json.Unmarshal(data, &auth) - if err != nil { - return nil, err - } - return &auth, err + return authPtr, nil } // LoadNamespaceInfo parses a NamespaceInfo object from a file path. It creates a file at the specified path if it doesn't exist with the default namespace. diff --git a/pkg/kubecfg/kubecfg_test.go b/pkg/kubecfg/kubecfg_test.go index 238c2f57840..196ec427094 100644 --- a/pkg/kubecfg/kubecfg_test.go +++ b/pkg/kubecfg/kubecfg_test.go @@ -26,6 +26,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" ) func validateAction(expectedAction, actualAction client.FakeAction, t *testing.T) { @@ -290,15 +291,15 @@ func TestLoadNamespaceInfo(t *testing.T) { } } -func TestLoadAuthInfo(t *testing.T) { +func TestLoadClientAuthInfoOrPrompt(t *testing.T) { loadAuthInfoTests := []struct { authData string - authInfo *AuthInfo + authInfo *clientauth.Info r io.Reader }{ { `{"user": "user", "password": "pass"}`, - &AuthInfo{User: "user", Password: "pass"}, + &clientauth.Info{User: "user", Password: "pass"}, nil, }, { @@ -306,7 +307,7 @@ func TestLoadAuthInfo(t *testing.T) { }, { "missing", - &AuthInfo{User: "user", Password: "pass"}, + &clientauth.Info{User: "user", Password: "pass"}, bytes.NewBufferString("user\npass"), }, } @@ -327,10 +328,10 @@ func TestLoadAuthInfo(t *testing.T) { aifile.Close() os.Remove(aifile.Name()) } - authInfo, err := LoadAuthInfo(aifile.Name(), tt.r) + authInfo, err := LoadClientAuthInfoOrPrompt(aifile.Name(), tt.r) if len(tt.authData) == 0 && tt.authData != "missing" { if err == nil { - t.Error("LoadAuthInfo didn't fail on empty file") + t.Error("LoadClientAuthInfoOrPrompt didn't fail on empty file") } continue } diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 3c01eec4ffd..67d5e779181 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -175,7 +175,9 @@ func GetKubeConfig(cmd *cobra.Command) *client.Config { // command line). Override them with the command line parameters, if // provided. authPath := GetFlagString(cmd, "auth-path") - authInfo, err := kubectl.LoadAuthInfo(authPath, os.Stdin) + 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) } diff --git a/pkg/kubectl/kubectl.go b/pkg/kubectl/kubectl.go index 7bd216e905d..ee3db803443 100644 --- a/pkg/kubectl/kubectl.go +++ b/pkg/kubectl/kubectl.go @@ -28,6 +28,7 @@ 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" @@ -56,16 +57,6 @@ func GetKubeClient(config *client.Config, matchVersion bool) (*client.Client, er return c, nil } -type AuthInfo struct { - User string - Password string - CAFile string - CertFile string - KeyFile string - BearerToken string - Insecure *bool -} - type NamespaceInfo struct { Namespace string } @@ -99,9 +90,10 @@ func SaveNamespaceInfo(path string, ns *NamespaceInfo) error { return err } -// LoadAuthInfo parses an AuthInfo object from a file path. It prompts user and creates file if it doesn't exist. -func LoadAuthInfo(path string, r io.Reader) (*AuthInfo, error) { - var auth AuthInfo +// 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) @@ -112,15 +104,11 @@ func LoadAuthInfo(path string, r io.Reader) (*AuthInfo, error) { err = ioutil.WriteFile(path, data, 0600) return &auth, err } - data, err := ioutil.ReadFile(path) + authPtr, err := clientauth.LoadFromFile(path) if err != nil { return nil, err } - err = json.Unmarshal(data, &auth) - if err != nil { - return nil, err - } - return &auth, err + return authPtr, nil } func promptForString(field string, r io.Reader) string { diff --git a/pkg/kubectl/kubectl_test.go b/pkg/kubectl/kubectl_test.go index f2ee55f31f6..a5211119c90 100644 --- a/pkg/kubectl/kubectl_test.go +++ b/pkg/kubectl/kubectl_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" ) func validateAction(expectedAction, actualAction client.FakeAction, t *testing.T) { @@ -85,15 +86,15 @@ func TestLoadNamespaceInfo(t *testing.T) { } } -func TestLoadAuthInfo(t *testing.T) { +func TestLoadClientAuthInfoOrPrompt(t *testing.T) { loadAuthInfoTests := []struct { authData string - authInfo *AuthInfo + authInfo *clientauth.Info r io.Reader }{ { `{"user": "user", "password": "pass"}`, - &AuthInfo{User: "user", Password: "pass"}, + &clientauth.Info{User: "user", Password: "pass"}, nil, }, { @@ -101,7 +102,7 @@ func TestLoadAuthInfo(t *testing.T) { }, { "missing", - &AuthInfo{User: "user", Password: "pass"}, + &clientauth.Info{User: "user", Password: "pass"}, bytes.NewBufferString("user\npass"), }, } @@ -122,10 +123,10 @@ func TestLoadAuthInfo(t *testing.T) { aifile.Close() os.Remove(aifile.Name()) } - authInfo, err := LoadAuthInfo(aifile.Name(), tt.r) + authInfo, err := LoadClientAuthInfoOrPrompt(aifile.Name(), tt.r) if len(tt.authData) == 0 && tt.authData != "missing" { if err == nil { - t.Error("LoadAuthInfo didn't fail on empty file") + t.Error("LoadClientAuthInfoOrPrompt didn't fail on empty file") } continue } @@ -133,7 +134,7 @@ func TestLoadAuthInfo(t *testing.T) { t.Errorf("Unexpected error: %v", err) } if !reflect.DeepEqual(authInfo, tt.authInfo) { - t.Errorf("Expected %v, got %v", tt.authInfo, authInfo) + t.Errorf("Expected %#v, got %#v", tt.authInfo, authInfo) } } }