Merge pull request #3316 from deads2k/deads-add-kubeconfig-file-properly

add kubeconfig file properly
This commit is contained in:
Jeff Lowdermilk
2015-01-08 13:50:52 -08:00
38 changed files with 2805 additions and 724 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,288 @@
/*
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 err error
// mergo is a first write wins for map value and a last writing wins for interface values
userAuthPartialConfig, err := getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader)
if err != nil {
return nil, err
}
mergo.Merge(clientConfig, userAuthPartialConfig)
serverAuthPartialConfig, err := getServerIdentificationPartialConfig(configAuthInfo, configClusterInfo)
if err != nil {
return nil, err
}
mergo.Merge(clientConfig, serverAuthPartialConfig)
}
return clientConfig, nil
}
// clientauth.Info object contain both user identification and server identification. We want different precedence orders for
// both, so we have to split the objects and merge them separately
// we want this order of precedence for the server identification
// 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files)
// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority)
// 3. load the ~/.kubernetes_auth file as a default
func getServerIdentificationPartialConfig(configAuthInfo AuthInfo, configClusterInfo Cluster) (*client.Config, error) {
mergedConfig := &client.Config{}
defaultAuthPathInfo, err := NewDefaultAuthLoader().LoadAuth(os.Getenv("HOME") + "/.kubernetes_auth")
// if the error is anything besides a does not exist, then fail. Not existing is ok
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if defaultAuthPathInfo != nil {
defaultAuthPathConfig := makeServerIdentificationConfig(*defaultAuthPathInfo)
mergo.Merge(mergedConfig, defaultAuthPathConfig)
}
if len(configAuthInfo.AuthPath) > 0 {
authPathInfo, err := NewDefaultAuthLoader().LoadAuth(configAuthInfo.AuthPath)
if err != nil {
return nil, err
}
authPathConfig := makeServerIdentificationConfig(*authPathInfo)
mergo.Merge(mergedConfig, authPathConfig)
}
// configClusterInfo holds the information identify the server provided by .kubeconfig
configClientConfig := &client.Config{}
configClientConfig.CAFile = configClusterInfo.CertificateAuthority
configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify
mergo.Merge(mergedConfig, configClientConfig)
return mergedConfig, nil
}
// clientauth.Info object contain both user identification and server identification. We want different precedence orders for
// both, so we have to split the objects and merge them separately
// we want this order of precedence for user identifcation
// 1. configAuthInfo minus auth-path (the final result of command line flags and merged .kubeconfig files)
// 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority)
// 3. if there is not enough information to idenfity the user, load try the ~/.kubernetes_auth file
// 4. if there is not enough information to identify the user, prompt if possible
func getUserIdentificationPartialConfig(configAuthInfo AuthInfo, fallbackReader io.Reader) (*client.Config, error) {
mergedConfig := &client.Config{}
if len(configAuthInfo.AuthPath) > 0 {
authPathInfo, err := NewDefaultAuthLoader().LoadAuth(configAuthInfo.AuthPath)
if err != nil {
return nil, err
}
authPathConfig := makeUserIdentificationConfig(*authPathInfo)
mergo.Merge(mergedConfig, authPathConfig)
}
// blindly overwrite existing values based on precedence
if len(configAuthInfo.Token) > 0 {
mergedConfig.BearerToken = configAuthInfo.Token
}
if len(configAuthInfo.ClientCertificate) > 0 {
mergedConfig.CertFile = configAuthInfo.ClientCertificate
mergedConfig.KeyFile = configAuthInfo.ClientKey
}
// if there isn't sufficient information to authenticate the user to the server, merge in ~/.kubernetes_auth.
if !canIdentifyUser(*mergedConfig) {
defaultAuthPathInfo, err := NewDefaultAuthLoader().LoadAuth(os.Getenv("HOME") + "/.kubernetes_auth")
// if the error is anything besides a does not exist, then fail. Not existing is ok
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if defaultAuthPathInfo != nil {
defaultAuthPathConfig := makeUserIdentificationConfig(*defaultAuthPathInfo)
previouslyMergedConfig := mergedConfig
mergedConfig = &client.Config{}
mergo.Merge(mergedConfig, defaultAuthPathConfig)
mergo.Merge(mergedConfig, previouslyMergedConfig)
}
}
// if there still isn't enough information to authenticate the user, try prompting
if !canIdentifyUser(*mergedConfig) && (fallbackReader != nil) {
prompter := NewPromptingAuthLoader(fallbackReader)
promptedAuthInfo := prompter.Prompt()
promptedConfig := makeUserIdentificationConfig(*promptedAuthInfo)
previouslyMergedConfig := mergedConfig
mergedConfig = &client.Config{}
mergo.Merge(mergedConfig, promptedConfig)
mergo.Merge(mergedConfig, previouslyMergedConfig)
}
return mergedConfig, nil
}
// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only user identification information
func makeUserIdentificationConfig(info clientauth.Info) *client.Config {
config := &client.Config{}
config.Username = info.User
config.Password = info.Password
config.CertFile = info.CertFile
config.KeyFile = info.KeyFile
config.BearerToken = info.BearerToken
return config
}
// makeUserIdentificationFieldsConfig returns a client.Config capable of being merged using mergo for only server identification information
func makeServerIdentificationConfig(info clientauth.Info) client.Config {
config := client.Config{}
config.CAFile = info.CAFile
if info.Insecure != nil {
config.Insecure = *info.Insecure
}
return config
}
func canIdentifyUser(config client.Config) bool {
return len(config.Username) > 0 ||
len(config.CertFile) > 0 ||
len(config.BearerToken) > 0
}
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
// 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
"github.com/spf13/cobra"
"github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth"
)
// Verifies that referencing an old .kubernetes_auth file respects all fields
func TestAuthPathUpdatesBothClusterAndUser(t *testing.T) {
authFile, _ := ioutil.TempFile("", "")
defer os.Remove(authFile.Name())
insecure := true
auth := &clientauth.Info{
User: "user",
Password: "password",
CAFile: "ca-file",
CertFile: "cert-file",
KeyFile: "key-file",
BearerToken: "bearer-token",
Insecure: &insecure,
}
err := testWriteAuthInfoFile(*auth, authFile.Name())
if err != nil {
t.Errorf("Unexpected error %v", err)
}
cmd := &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
},
}
clientConfig := testBindClientConfig(cmd)
cmd.ParseFlags([]string{"--server=https://localhost", "--auth-path=" + authFile.Name()})
config, err := clientConfig.ClientConfig()
if err != nil {
t.Errorf("Unexpected error %v", err)
}
matchStringArg(auth.User, config.Username, t)
matchStringArg(auth.Password, config.Password, t)
matchStringArg(auth.CAFile, config.CAFile, t)
matchStringArg(auth.CertFile, config.CertFile, t)
matchStringArg(auth.KeyFile, config.KeyFile, t)
matchStringArg(auth.BearerToken, config.BearerToken, t)
matchBoolArg(*auth.Insecure, config.Insecure, t)
}
func testWriteAuthInfoFile(auth clientauth.Info, filename string) error {
data, err := json.Marshal(auth)
if err != nil {
return err
}
err = ioutil.WriteFile(filename, data, 0600)
return err
}
func testBindClientConfig(cmd *cobra.Command) ClientConfig {
loadingRules := NewClientConfigLoadingRules()
loadingRules.EnvVarPath = ""
loadingRules.HomeDirectoryPath = ""
loadingRules.CurrentDirectoryPath = ""
cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.")
overrides := &ConfigOverrides{}
overrides.BindFlags(cmd.PersistentFlags(), RecommendedConfigOverrideFlags(""))
clientConfig := NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin)
return clientConfig
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,185 @@
/*
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)
usingAuthPath := false
methods := make([]string, 0, 3)
if len(authInfo.Token) != 0 {
methods = append(methods, "token")
}
if len(authInfo.AuthPath) != 0 {
usingAuthPath = true
methods = append(methods, "authFile")
file, err := os.Open(authInfo.AuthPath)
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))
}
}
// authPath also provides information for the client to identify the server, so allow multiple auth methods in that case
if (len(methods) > 1) && (!usingAuthPath) {
validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v. Found %v, only one is allowed", authInfoName, methods))
}
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
}

View File

@@ -0,0 +1,390 @@
/*
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", "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", "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 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))
}
}
}

View File

@@ -21,10 +21,12 @@ import (
"net/http"
"net/url"
"path"
"reflect"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
)
// Config holds the common attributes that can be passed to a Kubernetes client on
@@ -101,6 +103,24 @@ func New(c *Config) (*Client, error) {
return &Client{client}, 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)
@@ -252,8 +272,12 @@ func DefaultServerURL(host, prefix, version string, defaultTLS bool) (*url.URL,
//
// Note: the Insecure flag is ignored when testing for this value, so MITM attacks are
// still possible.
func IsConfigTransportTLS(config *Config) bool {
baseURL, err := defaultServerUrlFor(config)
func IsConfigTransportTLS(config Config) bool {
// determination of TLS transport does not logically require a version to be specified
// modify the copy of the config we got to satisfy preconditions for defaultServerUrlFor
config.Version = defaultVersionFor(&config)
baseURL, err := defaultServerUrlFor(&config)
if err != nil {
return false
}

View File

@@ -89,7 +89,7 @@ func TestIsConfigTransportTLS(t *testing.T) {
t.Errorf("setting defaults failed for %#v: %v", testCase.Config, err)
continue
}
useTLS := IsConfigTransportTLS(testCase.Config)
useTLS := IsConfigTransportTLS(*testCase.Config)
if testCase.TransportTLS != useTLS {
t.Errorf("expected %v for %#v", testCase.TransportTLS, testCase.Config)
}