add kubeconfig file

pull/6/head
deads2k 2014-12-10 15:16:18 -05:00
parent 12ecd0fa49
commit 0e688dc271
23 changed files with 1828 additions and 719 deletions

View File

@ -24,6 +24,6 @@ import (
)
func main() {
clientBuilder := clientcmd.NewBuilder(clientcmd.NewPromptingAuthLoader(os.Stdin))
clientBuilder := clientcmd.NewInteractiveClientConfig(clientcmd.Config{}, "", &clientcmd.ConfigOverrides{}, os.Stdin)
cmd.NewFactory(clientBuilder).Run(os.Stdout)
}

69
docs/kubeconfig-file.md Normal file
View File

@ -0,0 +1,69 @@
# .kubeconfig files
In order to easily switch between multiple clusters, a .kubeconfig file was defined. This file contains a series of authentication mechanisms and cluster connection information associated with nicknames. It also introduces the concept of a tuple of authentication information (user) and cluster connection information called a context that is also associated with a nickname.
Multiple files are .kubeconfig files are allowed. At runtime they are loaded and merged together along with override options specified from the command line (see rules below).
## Related discussion
https://github.com/GoogleCloudPlatform/kubernetes/issues/1755
## Example .kubeconfig file
```
preferences:
colors: true
clusters:
cow-cluster:
server: http://cow.org:8080
api-version: v1beta1
horse-cluster:
server: https://horse.org:4443
certificate-authority: path/to/my/cafile
pig-cluster:
server: https://pig.org:443
insecure-skip-tls-verify: true
users:
black-user:
auth-path: path/to/my/existing/.kubernetes_auth file
blue-user:
token: blue-token
green-user:
client-certificate: path/to/my/client/cert
client-key: path/to/my/client/key
contexts:
queen-anne-context:
cluster: pig-cluster
user: black-user
namespace: saw-ns
federal-context:
cluster: horse-cluster
user: green-user
namespace: chisel-ns
current-context: federal-context
```
## Loading and merging rules
The rules for loading and merging the .kubeconfig files are straightforward, but there are a lot of them. The final config is built in this order:
1. Merge together the kubeconfig itself. This is done with the following hierarchy and merge rules:
Empty filenames are ignored. Files with non-deserializable content produced errors.
The first file to set a particular value or map key wins and the value or map key is never changed.
This means that the first file to set CurrentContext will have its context preserved. It also means that if two files specify a "red-user", only values from the first file's red-user are used. Even non-conflicting entries from the second file's "red-user" are discarded.
1. CommandLineLocation - the value of the `kubeconfig` command line option
1. EnvVarLocation - the value of $KUBECONFIG
1. CurrentDirectoryLocation - ``pwd``/.kubeconfig
1. HomeDirectoryLocation = ~/.kube/.kubeconfig
1. Determine the context to use based on the first hit in this chain
1. command line argument - the value of the `context` command line option
1. current-context from the merged kubeconfig file
1. Empty is allowed at this stage
1. Determine the cluster info and user to use. At this point, we may or may not have a context. They are built based on the first hit in this chain. (run it twice, once for user, once for cluster)
1. command line argument - `user` for user name and `cluster` for cluster name
1. If context is present, then use the context's value
1. Empty is allowed
1. Determine the actual cluster info to use. At this point, we may or may not have a cluster info. Build each piece of the cluster info based on the chain (first hit wins):
1. command line arguments - `server`, `api-version`, `certificate-authority`, and `insecure-skip-tls-verify`
1. If cluster info is present and a value for the attribute is present, use it.
1. If you don't have a server location, error.
1. User is build using the same rules as cluster info, EXCEPT that you can only have one authentication technique per user.
The command line flags are: `auth-path`, `client-certificate`, `client-key`, and `token`. If there are two conflicting techniques, fail.
1. For any information still missing, use default values and potentially prompt for authentication information

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,183 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"io"
"os"
"github.com/imdario/mergo"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
var (
// TODO: eventually apiserver should start on 443 and be secure by default
defaultCluster = Cluster{Server: "http://localhost:8080"}
envVarCluster = Cluster{Server: os.Getenv("KUBERNETES_MASTER")}
)
// ClientConfig is used to make it easy to get an api server client
type ClientConfig interface {
// ClientConfig returns a complete client config
ClientConfig() (*client.Config, error)
}
// DirectClientConfig is a ClientConfig interface that is backed by a Config, options overrides, and an optional fallbackReader for auth information
type DirectClientConfig struct {
config Config
contextName string
overrides *ConfigOverrides
fallbackReader io.Reader
}
// NewDefaultClientConfig creates a DirectClientConfig using the config.CurrentContext as the context name
func NewDefaultClientConfig(config Config, overrides *ConfigOverrides) ClientConfig {
return DirectClientConfig{config, config.CurrentContext, overrides, nil}
}
// NewNonInteractiveClientConfig creates a DirectClientConfig using the passed context name and does not have a fallback reader for auth information
func NewNonInteractiveClientConfig(config Config, contextName string, overrides *ConfigOverrides) ClientConfig {
return DirectClientConfig{config, contextName, overrides, nil}
}
// NewInteractiveClientConfig creates a DirectClientConfig using the passed context name and a reader in case auth information is not provided via files or flags
func NewInteractiveClientConfig(config Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig {
return DirectClientConfig{config, contextName, overrides, fallbackReader}
}
// ClientConfig implements ClientConfig
func (config DirectClientConfig) ClientConfig() (*client.Config, error) {
if err := config.ConfirmUsable(); err != nil {
return nil, err
}
configAuthInfo := config.getAuthInfo()
configClusterInfo := config.getCluster()
clientConfig := client.Config{}
clientConfig.Host = configClusterInfo.Server
clientConfig.Version = configClusterInfo.APIVersion
// only try to read the auth information if we are secure
if client.IsConfigTransportTLS(&clientConfig) {
var authInfo *clientauth.Info
var err error
switch {
case len(configAuthInfo.AuthPath) > 0:
authInfo, err = NewDefaultAuthLoader().LoadAuth(configAuthInfo.AuthPath)
if err != nil {
return nil, err
}
case len(configAuthInfo.Token) > 0:
authInfo = &clientauth.Info{BearerToken: configAuthInfo.Token}
case len(configAuthInfo.ClientCertificate) > 0:
authInfo = &clientauth.Info{
CertFile: configAuthInfo.ClientCertificate,
KeyFile: configAuthInfo.ClientKey,
}
default:
authInfo = &clientauth.Info{}
}
if !authInfo.Complete() && (config.fallbackReader != nil) {
prompter := NewPromptingAuthLoader(config.fallbackReader)
authInfo = prompter.Prompt()
}
authInfo.Insecure = &configClusterInfo.InsecureSkipTLSVerify
clientConfig, err = authInfo.MergeWithConfig(clientConfig)
if err != nil {
return nil, err
}
}
return &clientConfig, nil
}
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
func (config DirectClientConfig) ConfirmUsable() error {
validationErrors := make([]error, 0)
validationErrors = append(validationErrors, validateAuthInfo(config.getAuthInfoName(), config.getAuthInfo())...)
validationErrors = append(validationErrors, validateClusterInfo(config.getClusterName(), config.getCluster())...)
return util.SliceToError(validationErrors)
}
func (config DirectClientConfig) getContextName() string {
if len(config.overrides.CurrentContext) != 0 {
return config.overrides.CurrentContext
}
if len(config.contextName) != 0 {
return config.contextName
}
return config.config.CurrentContext
}
func (config DirectClientConfig) getAuthInfoName() string {
if len(config.overrides.AuthInfoName) != 0 {
return config.overrides.AuthInfoName
}
return config.getContext().AuthInfo
}
func (config DirectClientConfig) getClusterName() string {
if len(config.overrides.ClusterName) != 0 {
return config.overrides.ClusterName
}
return config.getContext().Cluster
}
func (config DirectClientConfig) getContext() Context {
return config.config.Contexts[config.getContextName()]
}
func (config DirectClientConfig) getAuthInfo() AuthInfo {
authInfos := config.config.AuthInfos
authInfoName := config.getAuthInfoName()
var mergedAuthInfo AuthInfo
if configAuthInfo, exists := authInfos[authInfoName]; exists {
mergo.Merge(&mergedAuthInfo, configAuthInfo)
}
mergo.Merge(&mergedAuthInfo, config.overrides.AuthInfo)
return mergedAuthInfo
}
func (config DirectClientConfig) getCluster() Cluster {
clusterInfos := config.config.Clusters
clusterInfoName := config.getClusterName()
var mergedClusterInfo Cluster
mergo.Merge(&mergedClusterInfo, defaultCluster)
mergo.Merge(&mergedClusterInfo, envVarCluster)
if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists {
mergo.Merge(&mergedClusterInfo, configClusterInfo)
}
mergo.Merge(&mergedClusterInfo, config.overrides.ClusterInfo)
return mergedClusterInfo
}

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,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,182 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"errors"
"fmt"
"os"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
var ErrNoContext = errors.New("no context chosen")
type errContextNotFound struct {
ContextName string
}
func (e *errContextNotFound) Error() string {
return fmt.Sprintf("context was not found for specified context: %v", e.ContextName)
}
// IsContextNotFound returns a boolean indicating whether the error is known to
// report that a context was not found
func IsContextNotFound(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "context was not found for specified context")
}
// Validate checks for errors in the Config. It does not return early so that it can find as many errors as possible.
func Validate(config Config) error {
validationErrors := make([]error, 0)
if len(config.CurrentContext) != 0 {
if _, exists := config.Contexts[config.CurrentContext]; !exists {
validationErrors = append(validationErrors, &errContextNotFound{config.CurrentContext})
}
}
for contextName, context := range config.Contexts {
validationErrors = append(validationErrors, validateContext(contextName, context, config)...)
}
for authInfoName, authInfo := range config.AuthInfos {
validationErrors = append(validationErrors, validateAuthInfo(authInfoName, authInfo)...)
}
for clusterName, clusterInfo := range config.Clusters {
validationErrors = append(validationErrors, validateClusterInfo(clusterName, clusterInfo)...)
}
return util.SliceToError(validationErrors)
}
// ConfirmUsable looks a particular context and determines if that particular part of the config is useable. There might still be errors in the config,
// but no errors in the sections requested or referenced. It does not return early so that it can find as many errors as possible.
func ConfirmUsable(config Config, passedContextName string) error {
validationErrors := make([]error, 0)
var contextName string
if len(passedContextName) != 0 {
contextName = passedContextName
} else {
contextName = config.CurrentContext
}
if len(contextName) == 0 {
return ErrNoContext
}
context, exists := config.Contexts[contextName]
if !exists {
validationErrors = append(validationErrors, &errContextNotFound{contextName})
}
if exists {
validationErrors = append(validationErrors, validateContext(contextName, context, config)...)
validationErrors = append(validationErrors, validateAuthInfo(context.AuthInfo, config.AuthInfos[context.AuthInfo])...)
validationErrors = append(validationErrors, validateClusterInfo(context.Cluster, config.Clusters[context.Cluster])...)
}
return util.SliceToError(validationErrors)
}
// validateClusterInfo looks for conflicts and errors in the cluster info
func validateClusterInfo(clusterName string, clusterInfo Cluster) []error {
validationErrors := make([]error, 0)
if len(clusterInfo.Server) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("no server found for %v", clusterName))
}
if len(clusterInfo.CertificateAuthority) != 0 {
clientCertCA, err := os.Open(clusterInfo.CertificateAuthority)
defer clientCertCA.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read certificate-authority %v for %v due to %v", clusterInfo.CertificateAuthority, clusterName, err))
}
}
return validationErrors
}
// validateAuthInfo looks for conflicts and errors in the auth info
func validateAuthInfo(authInfoName string, authInfo AuthInfo) []error {
validationErrors := make([]error, 0)
methods := make([]string, 0, 3)
if len(authInfo.Token) != 0 {
methods = append(methods, "token")
}
if len(authInfo.AuthPath) != 0 {
methods = append(methods, "authFile")
file, err := os.Open(authInfo.AuthPath)
os.IsNotExist(err)
defer file.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read auth-path %v for %v due to %v", authInfo.AuthPath, authInfoName, err))
}
}
if len(authInfo.ClientCertificate) != 0 {
methods = append(methods, "clientCert")
clientCertFile, err := os.Open(authInfo.ClientCertificate)
defer clientCertFile.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
}
clientKeyFile, err := os.Open(authInfo.ClientKey)
defer clientKeyFile.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
}
}
if (len(methods)) > 1 {
validationErrors = append(validationErrors, fmt.Errorf("more than one authentication method found for %v. Found %v, only one is allowed", authInfoName, methods))
}
return validationErrors
}
// validateContext looks for errors in the context. It is not transitive, so errors in the reference authInfo or cluster configs are not included in this return
func validateContext(contextName string, context Context, config Config) []error {
validationErrors := make([]error, 0)
if len(context.AuthInfo) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("user was not specified for Context %v", contextName))
} else if _, exists := config.AuthInfos[context.AuthInfo]; !exists {
validationErrors = append(validationErrors, fmt.Errorf("user, %v, was not found for Context %v", context.AuthInfo, contextName))
}
if len(context.Cluster) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("cluster was not specified for Context %v", contextName))
} else if _, exists := config.Clusters[context.Cluster]; !exists {
validationErrors = append(validationErrors, fmt.Errorf("cluster, %v, was not found for Context %v", context.Cluster, contextName))
}
if (len(context.Namespace) != 0) && !util.IsDNS952Label(context.Namespace) {
validationErrors = append(validationErrors, fmt.Errorf("namespace, %v, for context %v, does not conform to the kubernetest DNS952 rules", context.Namespace, contextName))
}
return validationErrors
}

View File

@ -0,0 +1,404 @@
/*
Copyright 2014 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package clientcmd
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)
func TestConfirmUsableBadInfoButOkConfig(t *testing.T) {
config := NewConfig()
config.Clusters["missing ca"] = Cluster{
Server: "anything",
CertificateAuthority: "missing",
}
config.AuthInfos["error"] = AuthInfo{
AuthPath: "anything",
Token: "here",
}
config.Contexts["dirty"] = Context{
Cluster: "missing ca",
AuthInfo: "error",
}
config.Clusters["clean"] = Cluster{
Server: "anything",
}
config.AuthInfos["clean"] = AuthInfo{
Token: "here",
}
config.Contexts["clean"] = Context{
Cluster: "clean",
AuthInfo: "clean",
}
badValidation := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read auth-path", "more than one authentication method", "unable to read certificate-authority"},
}
okTest := configValidationTest{
config: config,
}
okTest.testConfirmUsable("clean", t)
badValidation.testConfig(t)
}
func TestConfirmUsableBadInfoConfig(t *testing.T) {
config := NewConfig()
config.Clusters["missing ca"] = Cluster{
Server: "anything",
CertificateAuthority: "missing",
}
config.AuthInfos["error"] = AuthInfo{
AuthPath: "anything",
Token: "here",
}
config.Contexts["first"] = Context{
Cluster: "missing ca",
AuthInfo: "error",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read auth-path", "more than one authentication method", "unable to read certificate-authority"},
}
test.testConfirmUsable("first", t)
}
func TestConfirmUsableEmptyConfig(t *testing.T) {
config := NewConfig()
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"no context chosen"},
}
test.testConfirmUsable("", t)
}
func TestConfirmUsableMissingConfig(t *testing.T) {
config := NewConfig()
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"context was not found for"},
}
test.testConfirmUsable("not-here", t)
}
func TestValidateEmptyConfig(t *testing.T) {
config := NewConfig()
test := configValidationTest{
config: config,
}
test.testConfig(t)
}
func TestValidateMissingCurrentContextConfig(t *testing.T) {
config := NewConfig()
config.CurrentContext = "anything"
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"context was not found for specified "},
}
test.testConfig(t)
}
func TestIsContextNotFound(t *testing.T) {
config := NewConfig()
config.CurrentContext = "anything"
err := Validate(*config)
if !IsContextNotFound(err) {
t.Errorf("Expected context not found, but got %v", err)
}
}
func TestValidateMissingReferencesConfig(t *testing.T) {
config := NewConfig()
config.CurrentContext = "anything"
config.Contexts["anything"] = Context{Cluster: "missing", AuthInfo: "missing"}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"user, missing, was not found for Context anything", "cluster, missing, was not found for Context anything"},
}
test.testContext("anything", t)
test.testConfig(t)
}
func TestValidateEmptyContext(t *testing.T) {
config := NewConfig()
config.CurrentContext = "anything"
config.Contexts["anything"] = Context{}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"user was not specified for Context anything", "cluster was not specified for Context anything"},
}
test.testContext("anything", t)
test.testConfig(t)
}
func TestValidateEmptyClusterInfo(t *testing.T) {
config := NewConfig()
config.Clusters["empty"] = Cluster{}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"no server found for"},
}
test.testCluster("empty", t)
test.testConfig(t)
}
func TestValidateMissingCAFileClusterInfo(t *testing.T) {
config := NewConfig()
config.Clusters["missing ca"] = Cluster{
Server: "anything",
CertificateAuthority: "missing",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read certificate-authority"},
}
test.testCluster("missing ca", t)
test.testConfig(t)
}
func TestValidateCleanClusterInfo(t *testing.T) {
config := NewConfig()
config.Clusters["clean"] = Cluster{
Server: "anything",
}
test := configValidationTest{
config: config,
}
test.testCluster("clean", t)
test.testConfig(t)
}
func TestValidateCleanWithCAClusterInfo(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "")
defer os.Remove(tempFile.Name())
config := NewConfig()
config.Clusters["clean"] = Cluster{
Server: "anything",
CertificateAuthority: tempFile.Name(),
}
test := configValidationTest{
config: config,
}
test.testCluster("clean", t)
test.testConfig(t)
}
func TestValidateEmptyAuthInfo(t *testing.T) {
config := NewConfig()
config.AuthInfos["error"] = AuthInfo{}
test := configValidationTest{
config: config,
}
test.testAuthInfo("error", t)
test.testConfig(t)
}
func TestValidateTooMayTechniquesAuthInfo(t *testing.T) {
config := NewConfig()
config.AuthInfos["error"] = AuthInfo{
AuthPath: "anything",
Token: "here",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"more than one authentication method found"},
}
test.testAuthInfo("error", t)
test.testConfig(t)
}
func TestValidatePathNotFoundAuthInfo(t *testing.T) {
config := NewConfig()
config.AuthInfos["error"] = AuthInfo{
AuthPath: "missing",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read auth-path"},
}
test.testAuthInfo("error", t)
test.testConfig(t)
}
func TestValidateCertFilesNotFoundAuthInfo(t *testing.T) {
config := NewConfig()
config.AuthInfos["error"] = AuthInfo{
ClientCertificate: "missing",
ClientKey: "missing",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"unable to read client-cert", "unable to read client-key"},
}
test.testAuthInfo("error", t)
test.testConfig(t)
}
func TestValidateCleanCertFilesAuthInfo(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "")
defer os.Remove(tempFile.Name())
config := NewConfig()
config.AuthInfos["clean"] = AuthInfo{
ClientCertificate: tempFile.Name(),
ClientKey: tempFile.Name(),
}
test := configValidationTest{
config: config,
}
test.testAuthInfo("clean", t)
test.testConfig(t)
}
func TestValidateCleanPathAuthInfo(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "")
defer os.Remove(tempFile.Name())
config := NewConfig()
config.AuthInfos["clean"] = AuthInfo{
AuthPath: tempFile.Name(),
}
test := configValidationTest{
config: config,
}
test.testAuthInfo("clean", t)
test.testConfig(t)
}
func TestValidateCleanTokenAuthInfo(t *testing.T) {
config := NewConfig()
config.AuthInfos["clean"] = AuthInfo{
Token: "any-value",
}
test := configValidationTest{
config: config,
}
test.testAuthInfo("clean", t)
test.testConfig(t)
}
type configValidationTest struct {
config *Config
expectedErrorSubstring []string
}
func (c configValidationTest) testContext(contextName string, t *testing.T) {
errs := validateContext(contextName, c.config.Contexts[contextName], *c.config)
if len(c.expectedErrorSubstring) != 0 {
if len(errs) == 0 {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
}
for _, curr := range c.expectedErrorSubstring {
if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs))
}
}
} else {
if len(errs) != 0 {
t.Errorf("Unexpected error: %v", util.SliceToError(errs))
}
}
}
func (c configValidationTest) testConfirmUsable(contextName string, t *testing.T) {
err := ConfirmUsable(*c.config, contextName)
if len(c.expectedErrorSubstring) != 0 {
if err == nil {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
} else {
for _, curr := range c.expectedErrorSubstring {
if err != nil && !strings.Contains(err.Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
}
}
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
}
func (c configValidationTest) testConfig(t *testing.T) {
err := Validate(*c.config)
if len(c.expectedErrorSubstring) != 0 {
if err == nil {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
} else {
for _, curr := range c.expectedErrorSubstring {
if err != nil && !strings.Contains(err.Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, err)
}
}
}
} else {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
}
}
func (c configValidationTest) testCluster(clusterName string, t *testing.T) {
errs := validateClusterInfo(clusterName, c.config.Clusters[clusterName])
if len(c.expectedErrorSubstring) != 0 {
if len(errs) == 0 {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
}
for _, curr := range c.expectedErrorSubstring {
if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs))
}
}
} else {
if len(errs) != 0 {
t.Errorf("Unexpected error: %v", util.SliceToError(errs))
}
}
}
func (c configValidationTest) testAuthInfo(authInfoName string, t *testing.T) {
errs := validateAuthInfo(authInfoName, c.config.AuthInfos[authInfoName])
if len(c.expectedErrorSubstring) != 0 {
if len(errs) == 0 {
t.Errorf("Expected error containing: %v", c.expectedErrorSubstring)
}
for _, curr := range c.expectedErrorSubstring {
if len(errs) != 0 && !strings.Contains(util.SliceToError(errs).Error(), curr) {
t.Errorf("Expected error containing: %v, but got %v", c.expectedErrorSubstring, util.SliceToError(errs))
}
}
} else {
if len(errs) != 0 {
t.Errorf("Unexpected error: %v", util.SliceToError(errs))
}
}
}

View File

@ -21,9 +21,11 @@ import (
"net/http"
"net/url"
"path"
"reflect"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest"
"github.com/GoogleCloudPlatform/kubernetes/pkg/version"
)
// Config holds the common attributes that can be passed to a Kubernetes client on
@ -92,6 +94,24 @@ func New(c *Config) (*Client, error) {
return &Client{client, isPreV1Beta3}, nil
}
func MatchesServerVersion(c *Config) error {
client, err := New(c)
if err != nil {
return err
}
clientVersion := version.Get()
serverVersion, err := client.ServerVersion()
if err != nil {
return fmt.Errorf("couldn't read version from server: %v\n", err)
}
if s := *serverVersion; !reflect.DeepEqual(clientVersion, s) {
return fmt.Errorf("server version (%#v) differs from client version (%#v)!\n", s, clientVersion)
}
return nil
}
// NewOrDie creates a Kubernetes client and panics if the provided API version is not recognized.
func NewOrDie(c *Config) *Client {
client, err := New(c)

View File

@ -117,3 +117,9 @@ func (info Info) MergeWithConfig(c client.Config) (client.Config, error) {
}
return config, nil
}
func (info Info) Complete() bool {
return len(info.User) > 0 ||
len(info.CertFile) > 0 ||
len(info.BearerToken) > 0
}

View File

@ -34,55 +34,59 @@ import (
"github.com/spf13/cobra"
)
const (
FlagMatchBinaryVersion = "match-server-version"
)
// Factory provides abstractions that allow the Kubectl command to be extended across multiple types
// of resources and different API sets.
type Factory struct {
ClientBuilder clientcmd.Builder
Mapper meta.RESTMapper
Typer runtime.ObjectTyper
Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error)
Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error)
Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error)
Validator func(*cobra.Command) (validation.Schema, error)
ClientConfig clientcmd.ClientConfig
Mapper meta.RESTMapper
Typer runtime.ObjectTyper
Client func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error)
Describer func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error)
Printer func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error)
Validator func(*cobra.Command) (validation.Schema, error)
}
// NewFactory creates a factory with the default Kubernetes resources defined
func NewFactory(clientBuilder clientcmd.Builder) *Factory {
return &Factory{
ClientBuilder: clientBuilder,
Mapper: latest.RESTMapper,
Typer: api.Scheme,
Validator: func(cmd *cobra.Command) (validation.Schema, error) {
if GetFlagBool(cmd, "validate") {
client, err := clientBuilder.Client()
if err != nil {
return nil, err
}
return &clientSwaggerSchema{client, api.Scheme}, nil
} else {
return validation.NullSchema{}, nil
}
},
Client: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) {
return clientBuilder.Override(func(c *client.Config) {
c.Version = mapping.APIVersion
}).Client()
},
Describer: func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) {
client, err := clientBuilder.Client()
if err != nil {
return nil, err
}
describer, ok := kubectl.DescriberFor(mapping.Kind, client)
if !ok {
return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind)
}
return describer, nil
},
func NewFactory(clientConfig clientcmd.ClientConfig) *Factory {
ret := &Factory{
ClientConfig: clientConfig,
Mapper: latest.RESTMapper,
Typer: api.Scheme,
Printer: func(cmd *cobra.Command, mapping *meta.RESTMapping, noHeaders bool) (kubectl.ResourcePrinter, error) {
return kubectl.NewHumanReadablePrinter(noHeaders), nil
},
}
ret.Validator = func(cmd *cobra.Command) (validation.Schema, error) {
if GetFlagBool(cmd, "validate") {
client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion))
if err != nil {
return nil, err
}
return &clientSwaggerSchema{client, api.Scheme}, nil
} else {
return validation.NullSchema{}, nil
}
}
ret.Client = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.RESTClient, error) {
return getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion))
}
ret.Describer = func(cmd *cobra.Command, mapping *meta.RESTMapping) (kubectl.Describer, error) {
client, err := getClient(ret.ClientConfig, GetFlagBool(cmd, FlagMatchBinaryVersion))
if err != nil {
return nil, err
}
describer, ok := kubectl.DescriberFor(mapping.Kind, client)
if !ok {
return nil, fmt.Errorf("no description has been implemented for %q", mapping.Kind)
}
return describer, nil
}
return ret
}
func (f *Factory) Run(out io.Writer) {
@ -96,12 +100,13 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
Run: runHelp,
}
f.ClientBuilder.BindFlags(cmds.PersistentFlags())
f.ClientConfig = getClientConfig(cmds)
// Globally persistent flags across all subcommands.
// TODO Change flag names to consts to allow safer lookup from subcommands.
// TODO Add a verbose flag that turns on glog logging. Probably need a way
// to do that automatically for every subcommand.
cmds.PersistentFlags().Bool(FlagMatchBinaryVersion, false, "Require server version to match client version")
cmds.PersistentFlags().String("ns-path", os.Getenv("HOME")+"/.kubernetes_ns", "Path to the namespace info file that holds the namespace context to use for CLI requests.")
cmds.PersistentFlags().StringP("namespace", "n", "", "If present, the namespace scope for this CLI request.")
cmds.PersistentFlags().Bool("validate", false, "If true, use a schema to validate the input before sending it")
@ -124,6 +129,50 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`,
}
}
// getClientBuilder creates a clientcmd.ClientConfig that has a hierarchy like this:
// 1. Use the kubeconfig builder. The number of merges and overrides here gets a little crazy. Stay with me.
// 1. Merge together the kubeconfig itself. This is done with the following hierarchy and merge rules:
// 1. CommandLineLocation - this parsed from the command line, so it must be late bound
// 2. EnvVarLocation
// 3. CurrentDirectoryLocation
// 4. HomeDirectoryLocation
// Empty filenames are ignored. Files with non-deserializable content produced errors.
// The first file to set a particular value or map key wins and the value or map key is never changed.
// This means that the first file to set CurrentContext will have its context preserved. It also means
// that if two files specify a "red-user", only values from the first file's red-user are used. Even
// non-conflicting entries from the second file's "red-user" are discarded.
// 2. Determine the context to use based on the first hit in this chain
// 1. command line argument - again, parsed from the command line, so it must be late bound
// 2. CurrentContext from the merged kubeconfig file
// 3. Empty is allowed at this stage
// 3. Determine the cluster info and auth info to use. At this point, we may or may not have a context. They
// are built based on the first hit in this chain. (run it twice, once for auth, once for cluster)
// 1. command line argument
// 2. If context is present, then use the context value
// 3. Empty is allowed
// 4. Determine the actual cluster info to use. At this point, we may or may not have a cluster info. Build
// each piece of the cluster info based on the chain:
// 1. command line argument
// 2. If cluster info is present and a value for the attribute is present, use it.
// 3. If you don't have a server location, bail.
// 5. Auth info is build using the same rules as cluster info, EXCEPT that you can only have one authentication
// technique per auth info. The following conditions result in an error:
// 1. If there are two conflicting techniques specified from the command line, fail.
// 2. If the command line does not specify one, and the auth info has conflicting techniques, fail.
// 3. If the command line specifies one and the auth info specifies another, honor the command line technique.
// 2. Use default values and potentially prompt for auth information
func getClientConfig(cmd *cobra.Command) clientcmd.ClientConfig {
loadingRules := clientcmd.NewClientConfigLoadingRules()
loadingRules.EnvVarPath = os.Getenv(clientcmd.RecommendedConfigPathEnvVar)
cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.")
overrides := &clientcmd.ConfigOverrides{}
overrides.BindFlags(cmd.PersistentFlags(), clientcmd.RecommendedConfigOverrideFlags(""))
clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin)
return clientConfig
}
func checkErr(err error) {
if err != nil {
glog.FatalDepth(1, err)
@ -195,3 +244,25 @@ func (c *clientSwaggerSchema) ValidateBytes(data []byte) error {
}
return schema.ValidateBytes(data)
}
// TODO Need to only run server version match once per client host creation
func getClient(clientConfig clientcmd.ClientConfig, matchServerVersion bool) (*client.Client, error) {
config, err := clientConfig.ClientConfig()
if err != nil {
return nil, err
}
if matchServerVersion {
err := client.MatchesServerVersion(config)
if err != nil {
return nil, err
}
}
client, err := client.New(config)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -21,6 +21,8 @@ import (
"strconv"
"github.com/spf13/cobra"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
)
func (f *Factory) NewCmdLog(out io.Writer) *cobra.Command {
@ -44,7 +46,9 @@ Examples:
}
namespace := GetKubeNamespace(cmd)
client, err := f.ClientBuilder.Client()
config, err := f.ClientConfig.ClientConfig()
checkErr(err)
client, err := client.New(config)
checkErr(err)
podID := args[0]

View File

@ -33,7 +33,7 @@ func (f *Factory) NewCmdProxy(out io.Writer) *cobra.Command {
port := GetFlagInt(cmd, "port")
glog.Infof("Starting to serve on localhost:%d", port)
clientConfig, err := f.ClientBuilder.Config()
clientConfig, err := f.ClientConfig.ClientConfig()
checkErr(err)
server, err := kubectl.NewProxyServer(GetFlagString(cmd, "www"), clientConfig, port)

View File

@ -19,8 +19,10 @@ package cmd
import (
"io"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
"github.com/spf13/cobra"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl"
)
func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command {
@ -31,7 +33,9 @@ func (f *Factory) NewCmdVersion(out io.Writer) *cobra.Command {
if GetFlagBool(cmd, "client") {
kubectl.GetClientVersion(out)
} else {
client, err := f.ClientBuilder.Client()
config, err := f.ClientConfig.ClientConfig()
checkErr(err)
client, err := client.New(config)
checkErr(err)
kubectl.GetVersion(out, client)