mirror of https://github.com/k3s-io/k3s
add kubeconfig file
parent
12ecd0fa49
commit
0e688dc271
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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: ""
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue