Merge pull request #2340 from erictune/refactor_kube_auth

Refactor kube auth
pull/6/head
Clayton Coleman 2014-11-14 14:10:53 -05:00
commit c95b8694d6
9 changed files with 228 additions and 63 deletions

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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.

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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)
}
}
}