diff --git a/docs/kubeconfig-file.md b/docs/kubeconfig-file.md index 168b76a38b4..21fba261fb2 100644 --- a/docs/kubeconfig-file.md +++ b/docs/kubeconfig-file.md @@ -8,36 +8,46 @@ https://github.com/GoogleCloudPlatform/kubernetes/issues/1755 ## Example .kubeconfig file ``` -preferences: - colors: true +apiVersion: v1 clusters: - cow-cluster: - server: http://cow.org:8080 +- cluster: api-version: v1beta1 - horse-cluster: - server: https://horse.org:4443 + server: http://cow.org:8080 + name: cow-cluster +- cluster: certificate-authority: path/to/my/cafile - pig-cluster: - server: https://pig.org:443 + server: https://horse.org:4443 + name: horse-cluster +- cluster: insecure-skip-tls-verify: true + server: https://pig.org:443 + name: pig-cluster +contexts: +- context: + cluster: horse-cluster + namespace: chisel-ns + user: green-user + name: federal-context +- context: + cluster: pig-cluster + namespace: saw-ns + user: black-user + name: queen-anne-context +current-context: federal-context +kind: Config +preferences: + colors: true users: - black-user: - auth-path: path/to/my/existing/.kubernetes_auth file - blue-user: +- name: black-user + user: + auth-path: path/to/my/existing/.kubernetes_auth_file +- name: blue-user + user: token: blue-token - green-user: +- name: green-user + 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 @@ -67,3 +77,86 @@ The rules for loading and merging the .kubeconfig files are straightforward, but 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 + +## Manipulation of .kubeconfig via `kubectl config ` +In order to more easily manipulate .kubeconfig files, there are a series of subcommands to `kubectl config` to help. +``` +kubectl config set-credentials name --auth-path=path/to/authfile --client-certificate=path/to/cert --client-key=path/to/key --token=string + Sets a user entry in .kubeconfig. If the referenced name already exists, it will be overwritten. +kubectl config set-cluster name --server=server --skip-tls=bool --certificate-authority=path/to/ca --api-version=string + Sets a cluster entry in .kubeconfig. If the referenced name already exists, it will be overwritten. +kubectl config set-context name --user=string --cluster=string --namespace=string + Sets a config entry in .kubeconfig. If the referenced name already exists, it will be overwritten. +kubectl config use-context name + Sets current-context to name +kubectl config set property-name property-value + Sets arbitrary value in .kubeconfig +kubectl config unset property-name + Unsets arbitrary value in .kubeconfig +kubectl config view --local=true --global=false --kubeconfig=specific/filename --merged + Displays the merged (or not) result of the specified .kubeconfig file + +--local, --global, and --kubeconfig are valid flags for all of these operations. +``` + +### Example +``` +$kubectl config set-credentials myself --auth-path=path/to/my/existing/auth-file +$kubectl config set-cluster local-server --server=http://localhost:8080 +$kubectl config set-context default-context --cluster=local-server --user=myself +$kubectl config use-context default-context +$kubectl config set contexts.default-context.namespace the-right-prefix +$kubectl config view +``` +produces this output +``` +clusters: + local-server: + server: http://localhost:8080 +contexts: + default-context: + cluster: local-server + namespace: the-right-prefix + user: myself +current-context: default-context +preferences: {} +users: + myself: + auth-path: path/to/my/existing/auth-file + +``` +and a .kubeconfig file that looks like this +``` +apiVersion: v1 +clusters: +- cluster: + server: http://localhost:8080 + name: local-server +contexts: +- context: + cluster: local-server + namespace: the-right-prefix + user: myself + name: default-context +current-context: default-context +kind: Config +preferences: {} +users: +- name: myself + user: + auth-path: path/to/my/existing/auth-file +``` + +#### Commands for the example file +``` +$kubectl config set preferences.colors true +$kubectl config set-cluster cow-cluster --server=http://cow.org:8080 --api-version=v1beta1 +$kubectl config set-cluster horse-cluster --server=https://horse.org:4443 --certificate-authority=path/to/my/cafile +$kubectl config set-cluster pig-cluster --server=https://pig.org:443 --insecure-skip-tls-verify=true +$kubectl config set-credentials black-user --auth-path=path/to/my/existing/.kubernetes_auth_file +$kubectl config set-credentials blue-user --token=blue-token +$kubectl config set-credentials green-user --client-certificate=path/to/my/client/cert --client-key=path/to/my/client/key +$kubectl config set-context queen-anne-context --cluster=pig-cluster --user=black-user --namespace=saw-ns +$kubectl config set-context federal-context --cluster=horse-cluster --user=green-user --namespace=chisel-ns +$kubectl config use-context federal-context +``` \ No newline at end of file diff --git a/pkg/client/clientcmd/api/latest/latest.go b/pkg/client/clientcmd/api/latest/latest.go new file mode 100644 index 00000000000..be7657ae2b5 --- /dev/null +++ b/pkg/client/clientcmd/api/latest/latest.go @@ -0,0 +1,40 @@ +/* +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 latest + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api/v1" +) + +// Version is the string that represents the current external default version. +const Version = "v1" + +// OldestVersion is the string that represents the oldest server version supported, +// for client code that wants to hardcode the lowest common denominator. +const OldestVersion = "v1" + +// Versions is the list of versions that are recognized in code. The order provided +// may be assumed to be least feature rich to most feature rich, and clients may +// choose to prefer the latter items in the list over the former items when presented +// with a set of versions to choose. +var Versions = []string{"v1"} + +// Codec is the default codec for serializing output that should use +// the latest supported version. Use this Codec when writing to +// disk, a data store that is not dynamically versioned, or in tests. +// This codec can decode any object that Kubernetes is aware of. +var Codec = v1.Codec diff --git a/pkg/client/clientcmd/api/register.go b/pkg/client/clientcmd/api/register.go new file mode 100644 index 00000000000..682447e8511 --- /dev/null +++ b/pkg/client/clientcmd/api/register.go @@ -0,0 +1,32 @@ +/* +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 api + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// Scheme is the default instance of runtime.Scheme to which types in the Kubernetes API are already registered. +var Scheme = runtime.NewScheme() + +func init() { + Scheme.AddKnownTypes("", + &Config{}, + ) +} + +func (*Config) IsAnAPIObject() {} diff --git a/pkg/client/clientcmd/api/types.go b/pkg/client/clientcmd/api/types.go new file mode 100644 index 00000000000..5b90969f5e9 --- /dev/null +++ b/pkg/client/clientcmd/api/types.go @@ -0,0 +1,119 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package api + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// Where possible, json 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 { + api.TypeMeta `json:",inline"` + // Preferences holds general information to be use for cli interactions + Preferences Preferences `json:"preferences"` + // Clusters is a map of referencable names to cluster configs + Clusters map[string]Cluster `json:"clusters"` + // AuthInfos is a map of referencable names to user configs + AuthInfos map[string]AuthInfo `json:"users"` + // Contexts is a map of referencable names to context configs + Contexts map[string]Context `json:"contexts"` + // CurrentContext is the name of the context that you would like to use by default + CurrentContext string `json:"current-context"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions map[string]runtime.EmbeddedObject `json:"extensions,omitempty"` +} + +type Preferences struct { + Colors bool `json:"colors,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions map[string]runtime.EmbeddedObject `json:"extensions,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 `json:"server"` + // APIVersion is the preferred api version for communicating with the kubernetes cluster (v1beta1, v1beta2, v1beta3, etc). + APIVersion string `json:"api-version,omitempty"` + // InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure. + InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` + // CertificateAuthority is the path to a cert file for the certificate authority. + CertificateAuthority string `json:"certificate-authority,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions map[string]runtime.EmbeddedObject `json:"extensions,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 `json:"auth-path,omitempty"` + // ClientCertificate is the path to a client cert file for TLS. + ClientCertificate string `json:"client-certificate,omitempty"` + // ClientKey is the path to a client key file for TLS. + ClientKey string `json:"client-key,omitempty"` + // Token is the bearer token for authentication to the kubernetes cluster. + Token string `json:"token,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions map[string]runtime.EmbeddedObject `json:"extensions,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 `json:"cluster"` + // AuthInfo is the name of the authInfo for this context + AuthInfo string `json:"user"` + // Namespace is the default namespace to use on unspecified requests + Namespace string `json:"namespace,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions map[string]runtime.EmbeddedObject `json:"extensions,omitempty"` +} + +// NewConfig is a convenience function that returns a new Config object with non-nil maps +func NewConfig() *Config { + return &Config{ + Preferences: *NewPreferences(), + Clusters: make(map[string]Cluster), + AuthInfos: make(map[string]AuthInfo), + Contexts: make(map[string]Context), + Extensions: make(map[string]runtime.EmbeddedObject), + } +} + +// NewConfig is a convenience function that returns a new Config object with non-nil maps +func NewContext() *Context { + return &Context{Extensions: make(map[string]runtime.EmbeddedObject)} +} + +// NewConfig is a convenience function that returns a new Config object with non-nil maps +func NewCluster() *Cluster { + return &Cluster{Extensions: make(map[string]runtime.EmbeddedObject)} +} + +// NewConfig is a convenience function that returns a new Config object with non-nil maps +func NewAuthInfo() *AuthInfo { + return &AuthInfo{Extensions: make(map[string]runtime.EmbeddedObject)} +} + +// NewConfig is a convenience function that returns a new Config object with non-nil maps +func NewPreferences() *Preferences { + return &Preferences{Extensions: make(map[string]runtime.EmbeddedObject)} +} diff --git a/pkg/client/clientcmd/types_test.go b/pkg/client/clientcmd/api/types_test.go similarity index 98% rename from pkg/client/clientcmd/types_test.go rename to pkg/client/clientcmd/api/types_test.go index 89f0a41da7f..1376d4f9666 100644 --- a/pkg/client/clientcmd/types_test.go +++ b/pkg/client/clientcmd/api/types_test.go @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package clientcmd +package api import ( "fmt" - "gopkg.in/v2/yaml" + "github.com/ghodss/yaml" ) func ExampleEmptyConfig() { @@ -32,11 +32,11 @@ func ExampleEmptyConfig() { fmt.Printf("%v", string(output)) // Output: - // preferences: {} // clusters: {} - // users: {} // contexts: {} // current-context: "" + // preferences: {} + // users: {} } func ExampleOfOptionsConfig() { @@ -86,17 +86,30 @@ func ExampleOfOptionsConfig() { 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 + // insecure-skip-tls-verify: true + // server: https://alfa.org:8080 // bravo: - // server: https://bravo.org:8080 // api-version: v1beta1 + // server: https://bravo.org:8080 + // contexts: + // alfa-as-black-mage: + // cluster: alfa + // namespace: zulu + // user: black-mage-via-file + // alfa-as-white-mage: + // cluster: alfa + // user: white-mage-via-cert + // bravo-as-black-mage: + // cluster: bravo + // namespace: yankee + // user: black-mage-via-file + // current-context: alfa-as-white-mage + // preferences: + // colors: true // users: // black-mage-via-file: // auth-path: path/to/my/.kubernetes_auth @@ -105,17 +118,4 @@ func ExampleOfOptionsConfig() { // 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 } diff --git a/pkg/client/clientcmd/api/v1/conversion.go b/pkg/client/clientcmd/api/v1/conversion.go new file mode 100644 index 00000000000..a646b673312 --- /dev/null +++ b/pkg/client/clientcmd/api/v1/conversion.go @@ -0,0 +1,206 @@ +/* +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 v1 + +import ( + "sort" + + newer "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/conversion" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +func init() { + err := newer.Scheme.AddConversionFuncs( + func(in *Config, out *newer.Config, s conversion.Scope) error { + out.CurrentContext = in.CurrentContext + if err := s.Convert(&in.Preferences, &out.Preferences, 0); err != nil { + return err + } + + out.Clusters = make(map[string]newer.Cluster) + if err := s.Convert(&in.Clusters, &out.Clusters, 0); err != nil { + return err + } + out.AuthInfos = make(map[string]newer.AuthInfo) + if err := s.Convert(&in.AuthInfos, &out.AuthInfos, 0); err != nil { + return err + } + out.Contexts = make(map[string]newer.Context) + if err := s.Convert(&in.Contexts, &out.Contexts, 0); err != nil { + return err + } + out.Extensions = make(map[string]runtime.EmbeddedObject) + if err := s.Convert(&in.Extensions, &out.Extensions, 0); err != nil { + return err + } + return nil + }, + func(in *newer.Config, out *Config, s conversion.Scope) error { + out.CurrentContext = in.CurrentContext + if err := s.Convert(&in.Preferences, &out.Preferences, 0); err != nil { + return err + } + + out.Clusters = make([]NamedCluster, 0, 0) + if err := s.Convert(&in.Clusters, &out.Clusters, 0); err != nil { + return err + } + out.AuthInfos = make([]NamedAuthInfo, 0, 0) + if err := s.Convert(&in.AuthInfos, &out.AuthInfos, 0); err != nil { + return err + } + out.Contexts = make([]NamedContext, 0, 0) + if err := s.Convert(&in.Contexts, &out.Contexts, 0); err != nil { + return err + } + out.Extensions = make([]NamedExtension, 0, 0) + if err := s.Convert(&in.Extensions, &out.Extensions, 0); err != nil { + return err + } + return nil + }, + func(in *[]NamedCluster, out *map[string]newer.Cluster, s conversion.Scope) error { + for _, curr := range *in { + newCluster := newer.NewCluster() + if err := s.Convert(&curr.Cluster, newCluster, 0); err != nil { + return err + } + (*out)[curr.Name] = *newCluster + } + + return nil + }, + func(in *map[string]newer.Cluster, out *[]NamedCluster, s conversion.Scope) error { + allKeys := make([]string, 0, len(*in)) + for key := range *in { + allKeys = append(allKeys, key) + } + sort.Strings(allKeys) + + for _, key := range allKeys { + newCluster := (*in)[key] + oldCluster := &Cluster{} + if err := s.Convert(&newCluster, oldCluster, 0); err != nil { + return err + } + + namedCluster := NamedCluster{key, *oldCluster} + *out = append(*out, namedCluster) + } + + return nil + }, + func(in *[]NamedAuthInfo, out *map[string]newer.AuthInfo, s conversion.Scope) error { + for _, curr := range *in { + newAuthInfo := newer.NewAuthInfo() + if err := s.Convert(&curr.AuthInfo, newAuthInfo, 0); err != nil { + return err + } + (*out)[curr.Name] = *newAuthInfo + } + + return nil + }, + func(in *map[string]newer.AuthInfo, out *[]NamedAuthInfo, s conversion.Scope) error { + allKeys := make([]string, 0, len(*in)) + for key := range *in { + allKeys = append(allKeys, key) + } + sort.Strings(allKeys) + + for _, key := range allKeys { + newAuthInfo := (*in)[key] + oldAuthInfo := &AuthInfo{} + if err := s.Convert(&newAuthInfo, oldAuthInfo, 0); err != nil { + return err + } + + namedAuthInfo := NamedAuthInfo{key, *oldAuthInfo} + *out = append(*out, namedAuthInfo) + } + + return nil + }, + func(in *[]NamedContext, out *map[string]newer.Context, s conversion.Scope) error { + for _, curr := range *in { + newContext := newer.NewContext() + if err := s.Convert(&curr.Context, newContext, 0); err != nil { + return err + } + (*out)[curr.Name] = *newContext + } + + return nil + }, + func(in *map[string]newer.Context, out *[]NamedContext, s conversion.Scope) error { + allKeys := make([]string, 0, len(*in)) + for key := range *in { + allKeys = append(allKeys, key) + } + sort.Strings(allKeys) + + for _, key := range allKeys { + newContext := (*in)[key] + oldContext := &Context{} + if err := s.Convert(&newContext, oldContext, 0); err != nil { + return err + } + + namedContext := NamedContext{key, *oldContext} + *out = append(*out, namedContext) + } + + return nil + }, + func(in *[]NamedExtension, out *map[string]runtime.EmbeddedObject, s conversion.Scope) error { + for _, curr := range *in { + newExtension := &runtime.EmbeddedObject{} + if err := s.Convert(&curr.Extension, newExtension, 0); err != nil { + return err + } + (*out)[curr.Name] = *newExtension + } + + return nil + }, + func(in *map[string]runtime.EmbeddedObject, out *[]NamedExtension, s conversion.Scope) error { + allKeys := make([]string, 0, len(*in)) + for key := range *in { + allKeys = append(allKeys, key) + } + sort.Strings(allKeys) + + for _, key := range allKeys { + newExtension := (*in)[key] + oldExtension := &runtime.RawExtension{} + if err := s.Convert(&newExtension, oldExtension, 0); err != nil { + return err + } + + namedExtension := NamedExtension{key, *oldExtension} + *out = append(*out, namedExtension) + } + + return nil + }, + ) + if err != nil { + // If one of the conversion functions is malformed, detect it immediately. + panic(err) + } +} diff --git a/pkg/client/clientcmd/api/v1/register.go b/pkg/client/clientcmd/api/v1/register.go new file mode 100644 index 00000000000..47f31d75fd7 --- /dev/null +++ b/pkg/client/clientcmd/api/v1/register.go @@ -0,0 +1,33 @@ +/* +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 v1 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// Codec encodes internal objects to the v1 scheme +var Codec = runtime.CodecFor(api.Scheme, "v1") + +func init() { + api.Scheme.AddKnownTypes("v1", + &Config{}, + ) +} + +func (*Config) IsAnAPIObject() {} diff --git a/pkg/client/clientcmd/api/v1/types.go b/pkg/client/clientcmd/api/v1/types.go new file mode 100644 index 00000000000..b5dcf83c54d --- /dev/null +++ b/pkg/client/clientcmd/api/v1/types.go @@ -0,0 +1,120 @@ +/* +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 v1 + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta3" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" +) + +// Where possible, json 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 { + v1beta3.TypeMeta `json:",inline"` + // Preferences holds general information to be use for cli interactions + Preferences Preferences `json:"preferences"` + // Clusters is a map of referencable names to cluster configs + Clusters []NamedCluster `json:"clusters"` + // AuthInfos is a map of referencable names to user configs + AuthInfos []NamedAuthInfo `json:"users"` + // Contexts is a map of referencable names to context configs + Contexts []NamedContext `json:"contexts"` + // CurrentContext is the name of the context that you would like to use by default + CurrentContext string `json:"current-context"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions []NamedExtension `json:"extensions,omitempty"` +} + +type Preferences struct { + Colors bool `json:"colors,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions []NamedExtension `json:"extensions,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 `json:"server"` + // APIVersion is the preferred api version for communicating with the kubernetes cluster (v1beta1, v1beta2, v1beta3, etc). + APIVersion string `json:"api-version,omitempty"` + // InsecureSkipTLSVerify skips the validity check for the server's certificate. This will make your HTTPS connections insecure. + InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` + // CertificateAuthority is the path to a cert file for the certificate authority. + CertificateAuthority string `json:"certificate-authority,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions []NamedExtension `json:"extensions,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 `json:"auth-path,omitempty"` + // ClientCertificate is the path to a client cert file for TLS. + ClientCertificate string `json:"client-certificate,omitempty"` + // ClientKey is the path to a client key file for TLS. + ClientKey string `json:"client-key,omitempty"` + // Token is the bearer token for authentication to the kubernetes cluster. + Token string `json:"token,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions []NamedExtension `json:"extensions,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 `json:"cluster"` + // AuthInfo is the name of the authInfo for this context + AuthInfo string `json:"user"` + // Namespace is the default namespace to use on unspecified requests + Namespace string `json:"namespace,omitempty"` + // Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields + Extensions []NamedExtension `json:"extensions,omitempty"` +} + +// NamedCluster relates nicknames to cluster information +type NamedCluster struct { + // Name is the nickname for this Cluster + Name string `json:"name"` + // Cluster holds the cluster information + Cluster Cluster `json:"cluster"` +} + +// NamedContext relates nicknames to context information +type NamedContext struct { + // Name is the nickname for this Context + Name string `json:"name"` + // Context holds the context information + Context Context `json:"context"` +} + +// NamedAuthInfo relates nicknames to auth information +type NamedAuthInfo struct { + // Name is the nickname for this AuthInfo + Name string `json:"name"` + // AuthInfo holds the auth information + AuthInfo AuthInfo `json:"user"` +} + +// NamedExtension relates nicknames to extension information +type NamedExtension struct { + // Name is the nickname for this Extension + Name string `json:"name"` + // Extension holds the extension information + Extension runtime.RawExtension `json:"extension"` +} diff --git a/pkg/client/clientcmd/client_config.go b/pkg/client/clientcmd/client_config.go index 6f9c2323ebf..c2854943652 100644 --- a/pkg/client/clientcmd/client_config.go +++ b/pkg/client/clientcmd/client_config.go @@ -23,45 +23,51 @@ import ( "github.com/imdario/mergo" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/clientauth" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" ) 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")} + defaultCluster = clientcmdapi.Cluster{Server: "http://localhost:8080"} + envVarCluster = clientcmdapi.Cluster{Server: os.Getenv("KUBERNETES_MASTER")} ) // ClientConfig is used to make it easy to get an api server client type ClientConfig interface { + RawConfig() (clientcmdapi.Config, error) // 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 +// DirectClientConfig is a ClientConfig interface that is backed by a clientcmdapi.Config, options overrides, and an optional fallbackReader for auth information type DirectClientConfig struct { - config Config + config clientcmdapi.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 { +func NewDefaultClientConfig(config clientcmdapi.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 { +func NewNonInteractiveClientConfig(config clientcmdapi.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 { +func NewInteractiveClientConfig(config clientcmdapi.Config, contextName string, overrides *ConfigOverrides, fallbackReader io.Reader) ClientConfig { return DirectClientConfig{config, contextName, overrides, fallbackReader} } +func (config DirectClientConfig) RawConfig() (clientcmdapi.Config, error) { + return config.config, nil +} + // ClientConfig implements ClientConfig func (config DirectClientConfig) ClientConfig() (*client.Config, error) { if err := config.ConfirmUsable(); err != nil { @@ -102,7 +108,7 @@ func (config DirectClientConfig) ClientConfig() (*client.Config, error) { // 1. configClusterInfo (the final result of command line flags and merged .kubeconfig files) // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 3. load the ~/.kubernetes_auth file as a default -func getServerIdentificationPartialConfig(configAuthInfo AuthInfo, configClusterInfo Cluster) (*client.Config, error) { +func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, configClusterInfo clientcmdapi.Cluster) (*client.Config, error) { mergedConfig := &client.Config{} defaultAuthPathInfo, err := NewDefaultAuthLoader().LoadAuth(os.Getenv("HOME") + "/.kubernetes_auth") @@ -140,7 +146,7 @@ func getServerIdentificationPartialConfig(configAuthInfo AuthInfo, configCluster // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 3. if there is not enough information to idenfity the user, load try the ~/.kubernetes_auth file // 4. if there is not enough information to identify the user, prompt if possible -func getUserIdentificationPartialConfig(configAuthInfo AuthInfo, fallbackReader io.Reader) (*client.Config, error) { +func getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader) (*client.Config, error) { mergedConfig := &client.Config{} if len(configAuthInfo.AuthPath) > 0 { @@ -255,15 +261,15 @@ func (config DirectClientConfig) getClusterName() string { return config.getContext().Cluster } -func (config DirectClientConfig) getContext() Context { +func (config DirectClientConfig) getContext() clientcmdapi.Context { return config.config.Contexts[config.getContextName()] } -func (config DirectClientConfig) getAuthInfo() AuthInfo { +func (config DirectClientConfig) getAuthInfo() clientcmdapi.AuthInfo { authInfos := config.config.AuthInfos authInfoName := config.getAuthInfoName() - var mergedAuthInfo AuthInfo + var mergedAuthInfo clientcmdapi.AuthInfo if configAuthInfo, exists := authInfos[authInfoName]; exists { mergo.Merge(&mergedAuthInfo, configAuthInfo) } @@ -272,11 +278,11 @@ func (config DirectClientConfig) getAuthInfo() AuthInfo { return mergedAuthInfo } -func (config DirectClientConfig) getCluster() Cluster { +func (config DirectClientConfig) getCluster() clientcmdapi.Cluster { clusterInfos := config.config.Clusters clusterInfoName := config.getClusterName() - var mergedClusterInfo Cluster + var mergedClusterInfo clientcmdapi.Cluster mergo.Merge(&mergedClusterInfo, defaultCluster) mergo.Merge(&mergedClusterInfo, envVarCluster) if configClusterInfo, exists := clusterInfos[clusterInfoName]; exists { diff --git a/pkg/client/clientcmd/client_config_test.go b/pkg/client/clientcmd/client_config_test.go index 39d8aeb3b45..d5a973c7a71 100644 --- a/pkg/client/clientcmd/client_config_test.go +++ b/pkg/client/clientcmd/client_config_test.go @@ -22,23 +22,24 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/latest" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" ) -func createValidTestConfig() *Config { +func createValidTestConfig() *clientcmdapi.Config { const ( server = "https://anything.com:8080" token = "the-token" ) - config := NewConfig() - config.Clusters["clean"] = Cluster{ + config := clientcmdapi.NewConfig() + config.Clusters["clean"] = clientcmdapi.Cluster{ Server: server, APIVersion: latest.Version, } - config.AuthInfos["clean"] = AuthInfo{ + config.AuthInfos["clean"] = clientcmdapi.AuthInfo{ Token: token, } - config.Contexts["clean"] = Context{ + config.Contexts["clean"] = clientcmdapi.Context{ Cluster: "clean", AuthInfo: "clean", } diff --git a/pkg/client/clientcmd/loader.go b/pkg/client/clientcmd/loader.go index a79e3b1920f..6fd5c4fd9d2 100644 --- a/pkg/client/clientcmd/loader.go +++ b/pkg/client/clientcmd/loader.go @@ -20,8 +20,11 @@ import ( "io/ioutil" "os" + "github.com/ghodss/yaml" "github.com/imdario/mergo" - "gopkg.in/v2/yaml" + + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + clientcmdlatest "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api/latest" ) const ( @@ -56,8 +59,8 @@ func NewClientConfigLoadingRules() *ClientConfigLoadingRules { // 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() +func (rules *ClientConfigLoadingRules) Load() (*clientcmdapi.Config, error) { + config := clientcmdapi.NewConfig() mergeConfigWithFile(config, rules.CommandLinePath) mergeConfigWithFile(config, rules.EnvVarPath) @@ -67,7 +70,7 @@ func (rules *ClientConfigLoadingRules) Load() (*Config, error) { return config, nil } -func mergeConfigWithFile(startingConfig *Config, filename string) error { +func mergeConfigWithFile(startingConfig *clientcmdapi.Config, filename string) error { if len(filename) == 0 { // no work to do return nil @@ -84,16 +87,15 @@ func mergeConfigWithFile(startingConfig *Config, filename string) error { } // LoadFromFile takes a filename and deserializes the contents into Config object -func LoadFromFile(filename string) (*Config, error) { - config := &Config{} +func LoadFromFile(filename string) (*clientcmdapi.Config, error) { + config := &clientcmdapi.Config{} kubeconfigBytes, err := ioutil.ReadFile(filename) if err != nil { return nil, err } - err = yaml.Unmarshal(kubeconfigBytes, &config) - if err != nil { + if err := clientcmdlatest.Codec.DecodeInto(kubeconfigBytes, config); err != nil { return nil, err } @@ -102,16 +104,20 @@ func LoadFromFile(filename string) (*Config, error) { // 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) +func WriteToFile(config clientcmdapi.Config, filename string) error { + json, err := clientcmdlatest.Codec.Encode(&config) if err != nil { return err } - err = ioutil.WriteFile(filename, content, 0644) + content, err := yaml.JSONToYAML(json) if err != nil { return err } + if err := ioutil.WriteFile(filename, content, 0644); err != nil { + return err + } + return nil } diff --git a/pkg/client/clientcmd/loader_test.go b/pkg/client/clientcmd/loader_test.go index a1063abdcaf..dc3eb86bd67 100644 --- a/pkg/client/clientcmd/loader_test.go +++ b/pkg/client/clientcmd/loader_test.go @@ -21,47 +21,50 @@ import ( "io/ioutil" "os" - "gopkg.in/v2/yaml" + "github.com/ghodss/yaml" + + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + clientcmdlatest "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api/latest" ) var ( - testConfigAlfa = Config{ - AuthInfos: map[string]AuthInfo{ + testConfigAlfa = clientcmdapi.Config{ + AuthInfos: map[string]clientcmdapi.AuthInfo{ "red-user": {Token: "red-token"}}, - Clusters: map[string]Cluster{ + Clusters: map[string]clientcmdapi.Cluster{ "cow-cluster": {Server: "http://cow.org:8080"}}, - Contexts: map[string]Context{ + Contexts: map[string]clientcmdapi.Context{ "federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"}}, } - testConfigBravo = Config{ - AuthInfos: map[string]AuthInfo{ + testConfigBravo = clientcmdapi.Config{ + AuthInfos: map[string]clientcmdapi.AuthInfo{ "black-user": {Token: "black-token"}}, - Clusters: map[string]Cluster{ + Clusters: map[string]clientcmdapi.Cluster{ "pig-cluster": {Server: "http://pig.org:8080"}}, - Contexts: map[string]Context{ + Contexts: map[string]clientcmdapi.Context{ "queen-anne-context": {AuthInfo: "black-user", Cluster: "pig-cluster", Namespace: "saw-ns"}}, } - testConfigCharlie = Config{ - AuthInfos: map[string]AuthInfo{ + testConfigCharlie = clientcmdapi.Config{ + AuthInfos: map[string]clientcmdapi.AuthInfo{ "green-user": {Token: "green-token"}}, - Clusters: map[string]Cluster{ + Clusters: map[string]clientcmdapi.Cluster{ "horse-cluster": {Server: "http://horse.org:8080"}}, - Contexts: map[string]Context{ + Contexts: map[string]clientcmdapi.Context{ "shaker-context": {AuthInfo: "green-user", Cluster: "horse-cluster", Namespace: "chisel-ns"}}, } - testConfigDelta = Config{ - AuthInfos: map[string]AuthInfo{ + testConfigDelta = clientcmdapi.Config{ + AuthInfos: map[string]clientcmdapi.AuthInfo{ "blue-user": {Token: "blue-token"}}, - Clusters: map[string]Cluster{ + Clusters: map[string]clientcmdapi.Cluster{ "chicken-cluster": {Server: "http://chicken.org:8080"}}, - Contexts: map[string]Context{ + Contexts: map[string]clientcmdapi.Context{ "gothic-context": {AuthInfo: "blue-user", Cluster: "chicken-cluster", Namespace: "plane-ns"}}, } - testConfigConflictAlfa = Config{ - AuthInfos: map[string]AuthInfo{ + testConfigConflictAlfa = clientcmdapi.Config{ + AuthInfos: map[string]clientcmdapi.AuthInfo{ "red-user": {Token: "a-different-red-token"}, "yellow-user": {Token: "yellow-token"}}, - Clusters: map[string]Cluster{ + Clusters: map[string]clientcmdapi.Cluster{ "cow-cluster": {Server: "http://a-different-cow.org:8080", InsecureSkipTLSVerify: true}, "donkey-cluster": {Server: "http://donkey.org:8080", InsecureSkipTLSVerify: true}}, CurrentContext: "federal-context", @@ -84,31 +87,42 @@ func ExampleMergingSomeWithConflict() { mergedConfig, err := loadingRules.Load() - output, err := yaml.Marshal(mergedConfig) + json, err := clientcmdlatest.Codec.Encode(mergedConfig) + if err != nil { + fmt.Printf("Unexpected error: %v", err) + } + output, err := yaml.JSONToYAML(json) if err != nil { fmt.Printf("Unexpected error: %v", err) } fmt.Printf("%v", string(output)) // Output: - // preferences: {} + // apiVersion: v1 // clusters: - // cow-cluster: + // - cluster: // server: http://cow.org:8080 - // donkey-cluster: - // server: http://donkey.org:8080 + // name: cow-cluster + // - cluster: // insecure-skip-tls-verify: true - // users: - // red-user: - // token: red-token - // yellow-user: - // token: yellow-token + // server: http://donkey.org:8080 + // name: donkey-cluster // contexts: - // federal-context: + // - context: // cluster: cow-cluster - // user: red-user // namespace: hammer-ns + // user: red-user + // name: federal-context // current-context: federal-context + // kind: Config + // preferences: {} + // users: + // - name: red-user + // user: + // token: red-token + // - name: yellow-user + // user: + // token: yellow-token } func ExampleMergingEverythingNoConflicts() { @@ -135,48 +149,66 @@ func ExampleMergingEverythingNoConflicts() { mergedConfig, err := loadingRules.Load() - output, err := yaml.Marshal(mergedConfig) + json, err := clientcmdlatest.Codec.Encode(mergedConfig) + if err != nil { + fmt.Printf("Unexpected error: %v", err) + } + output, err := yaml.JSONToYAML(json) if err != nil { fmt.Printf("Unexpected error: %v", err) } fmt.Printf("%v", string(output)) // Output: - // preferences: {} + // apiVersion: v1 // clusters: - // chicken-cluster: + // - cluster: // server: http://chicken.org:8080 - // cow-cluster: + // name: chicken-cluster + // - cluster: // server: http://cow.org:8080 - // horse-cluster: + // name: cow-cluster + // - cluster: // server: http://horse.org:8080 - // pig-cluster: + // name: horse-cluster + // - 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 + // name: pig-cluster // contexts: - // federal-context: + // - context: // cluster: cow-cluster - // user: red-user // namespace: hammer-ns - // gothic-context: + // user: red-user + // name: federal-context + // - context: // cluster: chicken-cluster - // user: blue-user // namespace: plane-ns - // queen-anne-context: + // user: blue-user + // name: gothic-context + // - context: // cluster: pig-cluster - // user: black-user // namespace: saw-ns - // shaker-context: + // user: black-user + // name: queen-anne-context + // - context: // cluster: horse-cluster - // user: green-user // namespace: chisel-ns + // user: green-user + // name: shaker-context // current-context: "" + // kind: Config + // preferences: {} + // users: + // - name: black-user + // user: + // token: black-token + // - name: blue-user + // user: + // token: blue-token + // - name: green-user + // user: + // token: green-token + // - name: red-user + // user: + // token: red-token } diff --git a/pkg/client/clientcmd/merged_client_builder.go b/pkg/client/clientcmd/merged_client_builder.go index a0fa856bb2a..ffbe4ab68e3 100644 --- a/pkg/client/clientcmd/merged_client_builder.go +++ b/pkg/client/clientcmd/merged_client_builder.go @@ -20,6 +20,7 @@ import ( "io" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" ) // DeferredLoadingClientConfig is a ClientConfig interface that is backed by a set of loading rules @@ -59,6 +60,15 @@ func (config DeferredLoadingClientConfig) createClientConfig() (ClientConfig, er return mergedClientConfig, nil } +func (config DeferredLoadingClientConfig) RawConfig() (clientcmdapi.Config, error) { + mergedConfig, err := config.createClientConfig() + if err != nil { + return clientcmdapi.Config{}, err + } + + return mergedConfig.RawConfig() +} + // ClientConfig implements ClientConfig func (config DeferredLoadingClientConfig) ClientConfig() (*client.Config, error) { mergedClientConfig, err := config.createClientConfig() diff --git a/pkg/client/clientcmd/merged_client_builder_test.go b/pkg/client/clientcmd/merged_client_builder_test.go index 8a048908e4d..2c659ebfc7b 100644 --- a/pkg/client/clientcmd/merged_client_builder_test.go +++ b/pkg/client/clientcmd/merged_client_builder_test.go @@ -85,7 +85,7 @@ func testBindClientConfig(cmd *cobra.Command) ClientConfig { cmd.PersistentFlags().StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") overrides := &ConfigOverrides{} - overrides.BindFlags(cmd.PersistentFlags(), RecommendedConfigOverrideFlags("")) + BindOverrideFlags(overrides, cmd.PersistentFlags(), RecommendedConfigOverrideFlags("")) clientConfig := NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin) return clientConfig diff --git a/pkg/client/clientcmd/overrides.go b/pkg/client/clientcmd/overrides.go index 6ccacafb06e..5534142ddff 100644 --- a/pkg/client/clientcmd/overrides.go +++ b/pkg/client/clientcmd/overrides.go @@ -18,13 +18,15 @@ package clientcmd import ( "github.com/spf13/pflag" + + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" ) // 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 + AuthInfo clientcmdapi.AuthInfo + ClusterInfo clientcmdapi.Cluster Namespace string CurrentContext string ClusterName string @@ -105,8 +107,8 @@ func RecommendedConfigOverrideFlags(prefix string) ConfigOverrideFlags { } } -// BindFlags is a convenience method to bind the specified flags to their associated variables -func (authInfo *AuthInfo) BindFlags(flags *pflag.FlagSet, flagNames AuthOverrideFlags) { +// BindAuthInfoFlags is a convenience method to bind the specified flags to their associated variables +func BindAuthInfoFlags(authInfo *clientcmdapi.AuthInfo, 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.") @@ -114,8 +116,8 @@ func (authInfo *AuthInfo) BindFlags(flags *pflag.FlagSet, flagNames AuthOverride 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) { +// BindClusterFlags is a convenience method to bind the specified flags to their associated variables +func BindClusterFlags(clusterInfo *clientcmdapi.Cluster, 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") @@ -123,10 +125,10 @@ func (clusterInfo *Cluster) BindFlags(flags *pflag.FlagSet, flagNames ClusterOve 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) +// BindOverrideFlags is a convenience method to bind the specified flags to their associated variables +func BindOverrideFlags(overrides *ConfigOverrides, flags *pflag.FlagSet, flagNames ConfigOverrideFlags) { + BindAuthInfoFlags(&overrides.AuthInfo, flags, flagNames.AuthOverrideFlags) + BindClusterFlags(&overrides.ClusterInfo, 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") diff --git a/pkg/client/clientcmd/types.go b/pkg/client/clientcmd/types.go deleted file mode 100644 index 3384c2f4557..00000000000 --- a/pkg/client/clientcmd/types.go +++ /dev/null @@ -1,83 +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 () - -// 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), - } -} diff --git a/pkg/client/clientcmd/validation.go b/pkg/client/clientcmd/validation.go index 1732962a721..6a2f63f0fba 100644 --- a/pkg/client/clientcmd/validation.go +++ b/pkg/client/clientcmd/validation.go @@ -22,6 +22,7 @@ import ( "os" "strings" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" utilerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" ) @@ -47,7 +48,7 @@ func IsContextNotFound(err error) bool { } // 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 { +func Validate(config clientcmdapi.Config) error { validationErrors := make([]error, 0) if len(config.CurrentContext) != 0 { @@ -73,7 +74,7 @@ func Validate(config Config) error { // 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 { +func ConfirmUsable(config clientcmdapi.Config, passedContextName string) error { validationErrors := make([]error, 0) var contextName string @@ -102,7 +103,7 @@ func ConfirmUsable(config Config, passedContextName string) error { } // validateClusterInfo looks for conflicts and errors in the cluster info -func validateClusterInfo(clusterName string, clusterInfo Cluster) []error { +func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) []error { validationErrors := make([]error, 0) if len(clusterInfo.Server) == 0 { @@ -120,7 +121,7 @@ func validateClusterInfo(clusterName string, clusterInfo Cluster) []error { } // validateAuthInfo looks for conflicts and errors in the auth info -func validateAuthInfo(authInfoName string, authInfo AuthInfo) []error { +func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []error { validationErrors := make([]error, 0) usingAuthPath := false @@ -163,7 +164,7 @@ func validateAuthInfo(authInfoName string, authInfo AuthInfo) []error { } // 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 { +func validateContext(contextName string, context clientcmdapi.Context, config clientcmdapi.Config) []error { validationErrors := make([]error, 0) if len(context.AuthInfo) == 0 { diff --git a/pkg/client/clientcmd/validation_test.go b/pkg/client/clientcmd/validation_test.go index 23c0d4b38e4..ee2f320ed06 100644 --- a/pkg/client/clientcmd/validation_test.go +++ b/pkg/client/clientcmd/validation_test.go @@ -22,30 +22,31 @@ import ( "strings" "testing" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" ) func TestConfirmUsableBadInfoButOkConfig(t *testing.T) { - config := NewConfig() - config.Clusters["missing ca"] = Cluster{ + config := clientcmdapi.NewConfig() + config.Clusters["missing ca"] = clientcmdapi.Cluster{ Server: "anything", CertificateAuthority: "missing", } - config.AuthInfos["error"] = AuthInfo{ + config.AuthInfos["error"] = clientcmdapi.AuthInfo{ AuthPath: "anything", Token: "here", } - config.Contexts["dirty"] = Context{ + config.Contexts["dirty"] = clientcmdapi.Context{ Cluster: "missing ca", AuthInfo: "error", } - config.Clusters["clean"] = Cluster{ + config.Clusters["clean"] = clientcmdapi.Cluster{ Server: "anything", } - config.AuthInfos["clean"] = AuthInfo{ + config.AuthInfos["clean"] = clientcmdapi.AuthInfo{ Token: "here", } - config.Contexts["clean"] = Context{ + config.Contexts["clean"] = clientcmdapi.Context{ Cluster: "clean", AuthInfo: "clean", } @@ -62,16 +63,16 @@ func TestConfirmUsableBadInfoButOkConfig(t *testing.T) { badValidation.testConfig(t) } func TestConfirmUsableBadInfoConfig(t *testing.T) { - config := NewConfig() - config.Clusters["missing ca"] = Cluster{ + config := clientcmdapi.NewConfig() + config.Clusters["missing ca"] = clientcmdapi.Cluster{ Server: "anything", CertificateAuthority: "missing", } - config.AuthInfos["error"] = AuthInfo{ + config.AuthInfos["error"] = clientcmdapi.AuthInfo{ AuthPath: "anything", Token: "here", } - config.Contexts["first"] = Context{ + config.Contexts["first"] = clientcmdapi.Context{ Cluster: "missing ca", AuthInfo: "error", } @@ -83,7 +84,7 @@ func TestConfirmUsableBadInfoConfig(t *testing.T) { test.testConfirmUsable("first", t) } func TestConfirmUsableEmptyConfig(t *testing.T) { - config := NewConfig() + config := clientcmdapi.NewConfig() test := configValidationTest{ config: config, expectedErrorSubstring: []string{"no context chosen"}, @@ -92,7 +93,7 @@ func TestConfirmUsableEmptyConfig(t *testing.T) { test.testConfirmUsable("", t) } func TestConfirmUsableMissingConfig(t *testing.T) { - config := NewConfig() + config := clientcmdapi.NewConfig() test := configValidationTest{ config: config, expectedErrorSubstring: []string{"context was not found for"}, @@ -101,7 +102,7 @@ func TestConfirmUsableMissingConfig(t *testing.T) { test.testConfirmUsable("not-here", t) } func TestValidateEmptyConfig(t *testing.T) { - config := NewConfig() + config := clientcmdapi.NewConfig() test := configValidationTest{ config: config, } @@ -109,7 +110,7 @@ func TestValidateEmptyConfig(t *testing.T) { test.testConfig(t) } func TestValidateMissingCurrentContextConfig(t *testing.T) { - config := NewConfig() + config := clientcmdapi.NewConfig() config.CurrentContext = "anything" test := configValidationTest{ config: config, @@ -119,7 +120,7 @@ func TestValidateMissingCurrentContextConfig(t *testing.T) { test.testConfig(t) } func TestIsContextNotFound(t *testing.T) { - config := NewConfig() + config := clientcmdapi.NewConfig() config.CurrentContext = "anything" err := Validate(*config) @@ -128,9 +129,9 @@ func TestIsContextNotFound(t *testing.T) { } } func TestValidateMissingReferencesConfig(t *testing.T) { - config := NewConfig() + config := clientcmdapi.NewConfig() config.CurrentContext = "anything" - config.Contexts["anything"] = Context{Cluster: "missing", AuthInfo: "missing"} + config.Contexts["anything"] = clientcmdapi.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"}, @@ -140,9 +141,9 @@ func TestValidateMissingReferencesConfig(t *testing.T) { test.testConfig(t) } func TestValidateEmptyContext(t *testing.T) { - config := NewConfig() + config := clientcmdapi.NewConfig() config.CurrentContext = "anything" - config.Contexts["anything"] = Context{} + config.Contexts["anything"] = clientcmdapi.Context{} test := configValidationTest{ config: config, expectedErrorSubstring: []string{"user was not specified for Context anything", "cluster was not specified for Context anything"}, @@ -153,8 +154,8 @@ func TestValidateEmptyContext(t *testing.T) { } func TestValidateEmptyClusterInfo(t *testing.T) { - config := NewConfig() - config.Clusters["empty"] = Cluster{} + config := clientcmdapi.NewConfig() + config.Clusters["empty"] = clientcmdapi.Cluster{} test := configValidationTest{ config: config, expectedErrorSubstring: []string{"no server found for"}, @@ -164,8 +165,8 @@ func TestValidateEmptyClusterInfo(t *testing.T) { test.testConfig(t) } func TestValidateMissingCAFileClusterInfo(t *testing.T) { - config := NewConfig() - config.Clusters["missing ca"] = Cluster{ + config := clientcmdapi.NewConfig() + config.Clusters["missing ca"] = clientcmdapi.Cluster{ Server: "anything", CertificateAuthority: "missing", } @@ -178,8 +179,8 @@ func TestValidateMissingCAFileClusterInfo(t *testing.T) { test.testConfig(t) } func TestValidateCleanClusterInfo(t *testing.T) { - config := NewConfig() - config.Clusters["clean"] = Cluster{ + config := clientcmdapi.NewConfig() + config.Clusters["clean"] = clientcmdapi.Cluster{ Server: "anything", } test := configValidationTest{ @@ -193,8 +194,8 @@ func TestValidateCleanWithCAClusterInfo(t *testing.T) { tempFile, _ := ioutil.TempFile("", "") defer os.Remove(tempFile.Name()) - config := NewConfig() - config.Clusters["clean"] = Cluster{ + config := clientcmdapi.NewConfig() + config.Clusters["clean"] = clientcmdapi.Cluster{ Server: "anything", CertificateAuthority: tempFile.Name(), } @@ -207,8 +208,8 @@ func TestValidateCleanWithCAClusterInfo(t *testing.T) { } func TestValidateEmptyAuthInfo(t *testing.T) { - config := NewConfig() - config.AuthInfos["error"] = AuthInfo{} + config := clientcmdapi.NewConfig() + config.AuthInfos["error"] = clientcmdapi.AuthInfo{} test := configValidationTest{ config: config, } @@ -217,8 +218,8 @@ func TestValidateEmptyAuthInfo(t *testing.T) { test.testConfig(t) } func TestValidatePathNotFoundAuthInfo(t *testing.T) { - config := NewConfig() - config.AuthInfos["error"] = AuthInfo{ + config := clientcmdapi.NewConfig() + config.AuthInfos["error"] = clientcmdapi.AuthInfo{ AuthPath: "missing", } test := configValidationTest{ @@ -230,8 +231,8 @@ func TestValidatePathNotFoundAuthInfo(t *testing.T) { test.testConfig(t) } func TestValidateCertFilesNotFoundAuthInfo(t *testing.T) { - config := NewConfig() - config.AuthInfos["error"] = AuthInfo{ + config := clientcmdapi.NewConfig() + config.AuthInfos["error"] = clientcmdapi.AuthInfo{ ClientCertificate: "missing", ClientKey: "missing", } @@ -247,8 +248,8 @@ func TestValidateCleanCertFilesAuthInfo(t *testing.T) { tempFile, _ := ioutil.TempFile("", "") defer os.Remove(tempFile.Name()) - config := NewConfig() - config.AuthInfos["clean"] = AuthInfo{ + config := clientcmdapi.NewConfig() + config.AuthInfos["clean"] = clientcmdapi.AuthInfo{ ClientCertificate: tempFile.Name(), ClientKey: tempFile.Name(), } @@ -263,8 +264,8 @@ func TestValidateCleanPathAuthInfo(t *testing.T) { tempFile, _ := ioutil.TempFile("", "") defer os.Remove(tempFile.Name()) - config := NewConfig() - config.AuthInfos["clean"] = AuthInfo{ + config := clientcmdapi.NewConfig() + config.AuthInfos["clean"] = clientcmdapi.AuthInfo{ AuthPath: tempFile.Name(), } test := configValidationTest{ @@ -275,8 +276,8 @@ func TestValidateCleanPathAuthInfo(t *testing.T) { test.testConfig(t) } func TestValidateCleanTokenAuthInfo(t *testing.T) { - config := NewConfig() - config.AuthInfos["clean"] = AuthInfo{ + config := clientcmdapi.NewConfig() + config.AuthInfos["clean"] = clientcmdapi.AuthInfo{ Token: "any-value", } test := configValidationTest{ @@ -288,7 +289,7 @@ func TestValidateCleanTokenAuthInfo(t *testing.T) { } type configValidationTest struct { - config *Config + config *clientcmdapi.Config expectedErrorSubstring []string } diff --git a/pkg/kubectl/cmd/cmd.go b/pkg/kubectl/cmd/cmd.go index 9ff3e8c0c56..ffaa99d2347 100644 --- a/pkg/kubectl/cmd/cmd.go +++ b/pkg/kubectl/cmd/cmd.go @@ -28,6 +28,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl" + cmdconfig "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/config" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -168,6 +169,7 @@ Find more information at https://github.com/GoogleCloudPlatform/kubernetes.`, cmds.AddCommand(f.NewCmdUpdate(out)) cmds.AddCommand(f.NewCmdDelete(out)) + cmds.AddCommand(cmdconfig.NewCmdConfig(out)) cmds.AddCommand(NewCmdNamespace(out)) cmds.AddCommand(f.NewCmdLog(out)) cmds.AddCommand(f.NewCmdRollingUpdate(out)) @@ -213,7 +215,7 @@ func DefaultClientConfig(flags *pflag.FlagSet) clientcmd.ClientConfig { flags.StringVar(&loadingRules.CommandLinePath, "kubeconfig", "", "Path to the kubeconfig file to use for CLI requests.") overrides := &clientcmd.ConfigOverrides{} - overrides.BindFlags(flags, clientcmd.RecommendedConfigOverrideFlags("")) + clientcmd.BindOverrideFlags(overrides, flags, clientcmd.RecommendedConfigOverrideFlags("")) clientConfig := clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, overrides, os.Stdin) return clientConfig diff --git a/pkg/kubectl/cmd/config/config.go b/pkg/kubectl/cmd/config/config.go new file mode 100644 index 00000000000..e73fb841635 --- /dev/null +++ b/pkg/kubectl/cmd/config/config.go @@ -0,0 +1,111 @@ +/* +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 config + +import ( + "io" + "os" + "strconv" + + "github.com/golang/glog" + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" +) + +type pathOptions struct { + local bool + global bool + specifiedFile string +} + +func NewCmdConfig(out io.Writer) *cobra.Command { + pathOptions := &pathOptions{} + + cmd := &cobra.Command{ + Use: "config ", + Short: "config modifies .kubeconfig files", + Long: `config modifies .kubeconfig files using subcommands like "kubectl config set current-context my-context"`, + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + + // file paths are common to all sub commands + cmd.PersistentFlags().BoolVar(&pathOptions.local, "local", true, "use the .kubeconfig in the currect directory") + cmd.PersistentFlags().BoolVar(&pathOptions.global, "global", false, "use the .kubeconfig from "+os.Getenv("HOME")) + cmd.PersistentFlags().StringVar(&pathOptions.specifiedFile, "kubeconfig", "", "use a particular .kubeconfig file") + + cmd.AddCommand(NewCmdConfigView(out, pathOptions)) + cmd.AddCommand(NewCmdConfigSetCluster(out, pathOptions)) + cmd.AddCommand(NewCmdConfigSetAuthInfo(out, pathOptions)) + cmd.AddCommand(NewCmdConfigSetContext(out, pathOptions)) + cmd.AddCommand(NewCmdConfigSet(out, pathOptions)) + cmd.AddCommand(NewCmdConfigUnset(out, pathOptions)) + cmd.AddCommand(NewCmdConfigUseContext(out, pathOptions)) + + return cmd +} + +func (o *pathOptions) getStartingConfig() (*clientcmdapi.Config, string, error) { + filename := "" + config := clientcmdapi.NewConfig() + switch { + case o.global: + filename = os.Getenv("HOME") + "/.kube/.kubeconfig" + config = getConfigFromFileOrDie(filename) + + case len(o.specifiedFile) > 0: + filename = o.specifiedFile + config = getConfigFromFileOrDie(filename) + + case o.local: + filename = ".kubeconfig" + config = getConfigFromFileOrDie(filename) + } + + return config, filename, nil +} + +// getConfigFromFileOrDie tries to read a kubeconfig file and if it can't, it calls exit. One exception, missing files result in empty configs, not an exit +func getConfigFromFileOrDie(filename string) *clientcmdapi.Config { + var err error + config, err := clientcmd.LoadFromFile(filename) + if err != nil && !os.IsNotExist(err) { + glog.FatalDepth(1, err) + } + + if config == nil { + config = clientcmdapi.NewConfig() + } + + return config +} + +func toBool(propertyValue string) (bool, error) { + boolValue := false + if len(propertyValue) != 0 { + var err error + boolValue, err = strconv.ParseBool(propertyValue) + if err != nil { + return false, err + } + } + + return boolValue, nil +} diff --git a/pkg/kubectl/cmd/config/config_test.go b/pkg/kubectl/cmd/config/config_test.go new file mode 100644 index 00000000000..069760872cb --- /dev/null +++ b/pkg/kubectl/cmd/config/config_test.go @@ -0,0 +1,374 @@ +/* +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 config + +import ( + "bytes" + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util" +) + +func newRedFederalCowHammerConfig() clientcmdapi.Config { + return clientcmdapi.Config{ + AuthInfos: map[string]clientcmdapi.AuthInfo{ + "red-user": {Token: "red-token"}}, + Clusters: map[string]clientcmdapi.Cluster{ + "cow-cluster": {Server: "http://cow.org:8080"}}, + Contexts: map[string]clientcmdapi.Context{ + "federal-context": {AuthInfo: "red-user", Cluster: "cow-cluster", Namespace: "hammer-ns"}}, + } +} + +type configCommandTest struct { + args []string + startingConfig clientcmdapi.Config + expectedConfig clientcmdapi.Config +} + +func TestSetCurrentContext(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.CurrentContext = "the-new-context" + test := configCommandTest{ + args: []string{"use-context", "the-new-context"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestSetIntoExistingStruct(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + a := expectedConfig.AuthInfos["red-user"] + authInfo := &a + authInfo.AuthPath = "new-path-value" + expectedConfig.AuthInfos["red-user"] = *authInfo + test := configCommandTest{ + args: []string{"set", "users.red-user.auth-path", "new-path-value"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestUnsetStruct(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + delete(expectedConfig.AuthInfos, "red-user") + test := configCommandTest{ + args: []string{"unset", "users.red-user"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestUnsetField(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.AuthInfos["red-user"] = *clientcmdapi.NewAuthInfo() + test := configCommandTest{ + args: []string{"unset", "users.red-user.token"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestSetIntoNewStruct(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + cluster := clientcmdapi.NewCluster() + cluster.Server = "new-server-value" + expectedConfig.Clusters["big-cluster"] = *cluster + test := configCommandTest{ + args: []string{"set", "clusters.big-cluster.server", "new-server-value"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestSetBoolean(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + cluster := clientcmdapi.NewCluster() + cluster.InsecureSkipTLSVerify = true + expectedConfig.Clusters["big-cluster"] = *cluster + test := configCommandTest{ + args: []string{"set", "clusters.big-cluster.insecure-skip-tls-verify", "true"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestSetIntoNewConfig(t *testing.T) { + expectedConfig := *clientcmdapi.NewConfig() + context := clientcmdapi.NewContext() + context.AuthInfo = "fake-user" + expectedConfig.Contexts["new-context"] = *context + test := configCommandTest{ + args: []string{"set", "contexts.new-context.user", "fake-user"}, + startingConfig: *clientcmdapi.NewConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestNewEmptyAuth(t *testing.T) { + expectedConfig := *clientcmdapi.NewConfig() + expectedConfig.AuthInfos["the-user-name"] = *clientcmdapi.NewAuthInfo() + test := configCommandTest{ + args: []string{"set-credentials", "the-user-name"}, + startingConfig: *clientcmdapi.NewConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestAdditionalAuth(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + authInfo := clientcmdapi.NewAuthInfo() + authInfo.AuthPath = "auth-path" + authInfo.ClientKey = "client-key" + authInfo.Token = "token" + expectedConfig.AuthInfos["another-user"] = *authInfo + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagAuthPath + "=auth-path", "--" + clientcmd.FlagKeyFile + "=client-key", "--" + clientcmd.FlagBearerToken + "=token"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestOverwriteExistingAuth(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + authInfo := clientcmdapi.NewAuthInfo() + authInfo.AuthPath = "auth-path" + expectedConfig.AuthInfos["red-user"] = *authInfo + test := configCommandTest{ + args: []string{"set-credentials", "red-user", "--" + clientcmd.FlagAuthPath + "=auth-path"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestNewEmptyCluster(t *testing.T) { + expectedConfig := *clientcmdapi.NewConfig() + expectedConfig.Clusters["new-cluster"] = *clientcmdapi.NewCluster() + test := configCommandTest{ + args: []string{"set-cluster", "new-cluster"}, + startingConfig: *clientcmdapi.NewConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestAdditionalCluster(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + cluster := *clientcmdapi.NewCluster() + cluster.APIVersion = "v1beta1" + cluster.CertificateAuthority = "ca-location" + cluster.InsecureSkipTLSVerify = true + cluster.Server = "serverlocation" + expectedConfig.Clusters["different-cluster"] = cluster + test := configCommandTest{ + args: []string{"set-cluster", "different-cluster", "--" + clientcmd.FlagAPIServer + "=serverlocation", "--" + clientcmd.FlagInsecure + "=true", "--" + clientcmd.FlagCAFile + "=ca-location", "--" + clientcmd.FlagAPIVersion + "=v1beta1"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestOverwriteExistingCluster(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + cluster := *clientcmdapi.NewCluster() + cluster.Server = "serverlocation" + expectedConfig.Clusters["cow-cluster"] = cluster + + test := configCommandTest{ + args: []string{"set-cluster", "cow-cluster", "--" + clientcmd.FlagAPIServer + "=serverlocation"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestNewEmptyContext(t *testing.T) { + expectedConfig := *clientcmdapi.NewConfig() + expectedConfig.Contexts["new-context"] = *clientcmdapi.NewContext() + test := configCommandTest{ + args: []string{"set-context", "new-context"}, + startingConfig: *clientcmdapi.NewConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestAdditionalContext(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + context := *clientcmdapi.NewContext() + context.Cluster = "some-cluster" + context.AuthInfo = "some-user" + context.Namespace = "different-namespace" + expectedConfig.Contexts["different-context"] = context + test := configCommandTest{ + args: []string{"set-context", "different-context", "--" + clientcmd.FlagClusterName + "=some-cluster", "--" + clientcmd.FlagAuthInfoName + "=some-user", "--" + clientcmd.FlagNamespace + "=different-namespace"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestOverwriteExistingContext(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + context := *clientcmdapi.NewContext() + context.Cluster = "clustername" + expectedConfig.Contexts["federal-context"] = context + + test := configCommandTest{ + args: []string{"set-context", "federal-context", "--" + clientcmd.FlagClusterName + "=clustername"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestToBool(t *testing.T) { + type test struct { + in string + out bool + err string + } + + tests := []test{ + {"", false, ""}, + {"true", true, ""}, + {"on", false, `strconv.ParseBool: parsing "on": invalid syntax`}, + } + + for _, curr := range tests { + b, err := toBool(curr.in) + if (len(curr.err) != 0) && err == nil { + t.Errorf("Expected error: %v, but got nil", curr.err) + } + if (len(curr.err) == 0) && err != nil { + t.Errorf("Unexpected error: %v", err) + } + if (err != nil) && (err.Error() != curr.err) { + t.Errorf("Expected %v, got %v", curr.err, err) + + } + if b != curr.out { + t.Errorf("Expected %v, got %v", curr.out, b) + } + } + +} + +func testConfigCommand(args []string, startingConfig clientcmdapi.Config) (string, clientcmdapi.Config) { + fakeKubeFile, _ := ioutil.TempFile("", "") + defer os.Remove(fakeKubeFile.Name()) + clientcmd.WriteToFile(startingConfig, fakeKubeFile.Name()) + + argsToUse := make([]string, 0, 2+len(args)) + argsToUse = append(argsToUse, "--kubeconfig="+fakeKubeFile.Name()) + argsToUse = append(argsToUse, args...) + + buf := bytes.NewBuffer([]byte{}) + + cmd := NewCmdConfig(buf) + cmd.SetArgs(argsToUse) + cmd.Execute() + + // outBytes, _ := ioutil.ReadFile(fakeKubeFile.Name()) + config := getConfigFromFileOrDie(fakeKubeFile.Name()) + + return buf.String(), *config +} + +func (test configCommandTest) run(t *testing.T) { + _, actualConfig := testConfigCommand(test.args, test.startingConfig) + + testSetNilMapsToEmpties(reflect.ValueOf(&test.expectedConfig)) + testSetNilMapsToEmpties(reflect.ValueOf(&actualConfig)) + + if !reflect.DeepEqual(test.expectedConfig, actualConfig) { + t.Errorf("diff: %v", util.ObjectDiff(test.expectedConfig, actualConfig)) + t.Errorf("expected: %#v\n actual: %#v", test.expectedConfig, actualConfig) + } +} +func testSetNilMapsToEmpties(curr reflect.Value) { + actualCurrValue := curr + if curr.Kind() == reflect.Ptr { + actualCurrValue = curr.Elem() + } + + switch actualCurrValue.Kind() { + case reflect.Map: + for _, mapKey := range actualCurrValue.MapKeys() { + currMapValue := actualCurrValue.MapIndex(mapKey) + + // our maps do not hold pointers to structs, they hold the structs themselves. This means that MapIndex returns the struct itself + // That in turn means that they have kinds of type.Struct, which is not a settable type. Because of this, we need to make new struct of that type + // copy all the data from the old value into the new value, then take the .addr of the new value to modify it in the next recursion. + // clear as mud + modifiableMapValue := reflect.New(currMapValue.Type()).Elem() + modifiableMapValue.Set(currMapValue) + + if modifiableMapValue.Kind() == reflect.Struct { + modifiableMapValue = modifiableMapValue.Addr() + } + + testSetNilMapsToEmpties(modifiableMapValue) + actualCurrValue.SetMapIndex(mapKey, reflect.Indirect(modifiableMapValue)) + } + + case reflect.Struct: + for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ { + currFieldValue := actualCurrValue.Field(fieldIndex) + + if currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil() { + newValue := reflect.MakeMap(currFieldValue.Type()) + currFieldValue.Set(newValue) + } else { + testSetNilMapsToEmpties(currFieldValue.Addr()) + } + } + + } + +} diff --git a/pkg/kubectl/cmd/config/create_authinfo.go b/pkg/kubectl/cmd/config/create_authinfo.go new file mode 100644 index 00000000000..813390d177f --- /dev/null +++ b/pkg/kubectl/cmd/config/create_authinfo.go @@ -0,0 +1,120 @@ +/* +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 config + +import ( + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" +) + +type createAuthInfoOptions struct { + pathOptions *pathOptions + name string + authPath string + clientCertificate string + clientKey string + token string +} + +func NewCmdConfigSetAuthInfo(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &createAuthInfoOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: "set-credentials name", + Short: "Sets a user entry in .kubeconfig", + Long: `Sets a user entry in .kubeconfig + + Specifying a name that already exists overwrites that user entry. + `, + Run: func(cmd *cobra.Command, args []string) { + if !options.complete(cmd) { + return + } + + err := options.run() + if err != nil { + fmt.Printf("%v\n", err) + } + }, + } + + cmd.Flags().StringVar(&options.authPath, clientcmd.FlagAuthPath, "", clientcmd.FlagAuthPath+" for the user entry in .kubeconfig") + cmd.Flags().StringVar(&options.clientCertificate, clientcmd.FlagCertFile, "", clientcmd.FlagCertFile+" for the user entry in .kubeconfig") + cmd.Flags().StringVar(&options.clientKey, clientcmd.FlagKeyFile, "", clientcmd.FlagKeyFile+" for the user entry in .kubeconfig") + cmd.Flags().StringVar(&options.token, clientcmd.FlagBearerToken, "", clientcmd.FlagBearerToken+" for the user entry in .kubeconfig") + + return cmd +} + +func (o createAuthInfoOptions) run() error { + err := o.validate() + if err != nil { + return err + } + + config, filename, err := o.pathOptions.getStartingConfig() + if err != nil { + return err + } + + authInfo := o.authInfo() + config.AuthInfos[o.name] = authInfo + + err = clientcmd.WriteToFile(*config, filename) + if err != nil { + return err + } + + return nil +} + +// authInfo builds an AuthInfo object from the options +func (o *createAuthInfoOptions) authInfo() clientcmdapi.AuthInfo { + authInfo := clientcmdapi.AuthInfo{ + AuthPath: o.authPath, + ClientCertificate: o.clientCertificate, + ClientKey: o.clientKey, + Token: o.token, + } + + return authInfo +} + +func (o *createAuthInfoOptions) complete(cmd *cobra.Command) bool { + args := cmd.Flags().Args() + if len(args) != 1 { + cmd.Help() + return false + } + + o.name = args[0] + return true +} + +func (o createAuthInfoOptions) validate() error { + if len(o.name) == 0 { + return errors.New("You must specify a non-empty user name") + } + + return nil +} diff --git a/pkg/kubectl/cmd/config/create_cluster.go b/pkg/kubectl/cmd/config/create_cluster.go new file mode 100644 index 00000000000..ee9b49b3a26 --- /dev/null +++ b/pkg/kubectl/cmd/config/create_cluster.go @@ -0,0 +1,124 @@ +/* +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 config + +import ( + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" +) + +type createClusterOptions struct { + pathOptions *pathOptions + name string + server string + apiVersion string + insecureSkipTLSVerify bool + certificateAuthority string +} + +func NewCmdConfigSetCluster(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &createClusterOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: "set-cluster name [server] [insecure-skip-tls-verify] [certificate-authority] [api-version]", + Short: "Sets a cluster entry in .kubeconfig", + Long: `Sets a cluster entry in .kubeconfig + + Specifying a name that already exists overwrites that cluster entry. + `, + Run: func(cmd *cobra.Command, args []string) { + if !options.complete(cmd) { + return + } + + err := options.run() + if err != nil { + fmt.Printf("%v\n", err) + } + }, + } + + cmd.Flags().StringVar(&options.server, clientcmd.FlagAPIServer, "", clientcmd.FlagAPIServer+" for the cluster entry in .kubeconfig") + cmd.Flags().StringVar(&options.apiVersion, clientcmd.FlagAPIVersion, "", clientcmd.FlagAPIVersion+" for the cluster entry in .kubeconfig") + cmd.Flags().BoolVar(&options.insecureSkipTLSVerify, clientcmd.FlagInsecure, false, clientcmd.FlagInsecure+" for the cluster entry in .kubeconfig") + cmd.Flags().StringVar(&options.certificateAuthority, clientcmd.FlagCAFile, "", clientcmd.FlagCAFile+" for the cluster entry in .kubeconfig") + + return cmd +} + +func (o createClusterOptions) run() error { + err := o.validate() + if err != nil { + return err + } + + config, filename, err := o.pathOptions.getStartingConfig() + if err != nil { + return err + } + + if config.Clusters == nil { + config.Clusters = make(map[string]clientcmdapi.Cluster) + } + + cluster := o.cluster() + config.Clusters[o.name] = cluster + + err = clientcmd.WriteToFile(*config, filename) + if err != nil { + return err + } + + return nil +} + +// cluster builds a Cluster object from the options +func (o *createClusterOptions) cluster() clientcmdapi.Cluster { + cluster := clientcmdapi.Cluster{ + Server: o.server, + APIVersion: o.apiVersion, + InsecureSkipTLSVerify: o.insecureSkipTLSVerify, + CertificateAuthority: o.certificateAuthority, + } + + return cluster +} + +func (o *createClusterOptions) complete(cmd *cobra.Command) bool { + args := cmd.Flags().Args() + if len(args) != 1 { + cmd.Help() + return false + } + + o.name = args[0] + return true +} + +func (o createClusterOptions) validate() error { + if len(o.name) == 0 { + return errors.New("You must specify a non-empty cluster name") + } + + return nil +} diff --git a/pkg/kubectl/cmd/config/create_context.go b/pkg/kubectl/cmd/config/create_context.go new file mode 100644 index 00000000000..c77fba7c0f3 --- /dev/null +++ b/pkg/kubectl/cmd/config/create_context.go @@ -0,0 +1,116 @@ +/* +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 config + +import ( + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" +) + +type createContextOptions struct { + pathOptions *pathOptions + name string + cluster string + authInfo string + namespace string +} + +func NewCmdConfigSetContext(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &createContextOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: "set-context name", + Short: "Sets a context entry in .kubeconfig", + Long: `Sets a context entry in .kubeconfig + + Specifying a name that already exists overwrites that context entry. + `, + Run: func(cmd *cobra.Command, args []string) { + if !options.complete(cmd) { + return + } + + err := options.run() + if err != nil { + fmt.Printf("%v\n", err) + } + }, + } + + cmd.Flags().StringVar(&options.cluster, clientcmd.FlagClusterName, "", clientcmd.FlagClusterName+" for the context entry in .kubeconfig") + cmd.Flags().StringVar(&options.authInfo, clientcmd.FlagAuthInfoName, "", clientcmd.FlagAuthInfoName+" for the context entry in .kubeconfig") + cmd.Flags().StringVar(&options.namespace, clientcmd.FlagNamespace, "", clientcmd.FlagNamespace+" for the context entry in .kubeconfig") + + return cmd +} + +func (o createContextOptions) run() error { + err := o.validate() + if err != nil { + return err + } + + config, filename, err := o.pathOptions.getStartingConfig() + if err != nil { + return err + } + + context := o.context() + config.Contexts[o.name] = context + + err = clientcmd.WriteToFile(*config, filename) + if err != nil { + return err + } + + return nil +} + +func (o *createContextOptions) context() clientcmdapi.Context { + context := clientcmdapi.Context{ + Cluster: o.cluster, + AuthInfo: o.authInfo, + Namespace: o.namespace, + } + + return context +} + +func (o *createContextOptions) complete(cmd *cobra.Command) bool { + args := cmd.Flags().Args() + if len(args) != 1 { + cmd.Help() + return false + } + + o.name = args[0] + return true +} + +func (o createContextOptions) validate() error { + if len(o.name) == 0 { + return errors.New("You must specify a non-empty context name") + } + + return nil +} diff --git a/pkg/kubectl/cmd/config/set.go b/pkg/kubectl/cmd/config/set.go new file mode 100644 index 00000000000..83c8b128862 --- /dev/null +++ b/pkg/kubectl/cmd/config/set.go @@ -0,0 +1,249 @@ +/* +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 config + +import ( + "errors" + "fmt" + "io" + "reflect" + "strings" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" +) + +const ( + cannotHaveStepsAfterError = "Cannot have steps after %v. %v are remaining" + additionStepRequiredUnlessUnsettingError = "Must have additional steps after %v unless you are unsetting it" +) + +type navigationSteps []string + +type setOptions struct { + pathOptions *pathOptions + propertyName string + propertyValue string +} + +func NewCmdConfigSet(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &setOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: "set property-name property-value", + Short: "Sets an individual value in a .kubeconfig file", + Long: `Sets an individual value in a .kubeconfig file + + property-name is a dot delimitted name where each token represents either a attribute name or a map key. Map keys may not contain dots. + property-value is the new value you wish to set. + + `, + Run: func(cmd *cobra.Command, args []string) { + if !options.complete(cmd) { + return + } + + err := options.run() + if err != nil { + fmt.Printf("%v\n", err) + } + }, + } + + return cmd +} + +func (o setOptions) run() error { + err := o.validate() + if err != nil { + return err + } + + config, filename, err := o.pathOptions.getStartingConfig() + if err != nil { + return err + } + + if len(filename) == 0 { + return errors.New("cannot set property without using a specific file") + } + + parts := strings.Split(o.propertyName, ".") + err = modifyConfig(reflect.ValueOf(config), parts, o.propertyValue, false) + if err != nil { + return err + } + + err = clientcmd.WriteToFile(*config, filename) + if err != nil { + return err + } + + return nil +} + +func (o *setOptions) complete(cmd *cobra.Command) bool { + endingArgs := cmd.Flags().Args() + if len(endingArgs) != 2 { + cmd.Help() + return false + } + + o.propertyValue = endingArgs[1] + o.propertyName = endingArgs[0] + return true +} + +func (o setOptions) validate() error { + if len(o.propertyValue) == 0 { + return errors.New("You cannot use set to unset a property") + } + + if len(o.propertyName) == 0 { + return errors.New("You must specify a property") + } + + return nil +} + +// moreStepsRemaining just makes code read cleaner +func moreStepsRemaining(remainder []string) bool { + return len(remainder) != 0 +} + +func (s navigationSteps) nextSteps() navigationSteps { + if len(s) < 2 { + return make([]string, 0, 0) + } else { + return s[1:] + } +} +func (s navigationSteps) moreStepsRemaining() bool { + return len(s) != 0 +} +func (s navigationSteps) nextStep() string { + return s[0] +} + +func modifyConfig(curr reflect.Value, steps navigationSteps, propertyValue string, unset bool) error { + shouldUnsetNextField := !steps.nextSteps().moreStepsRemaining() && unset + shouldSetThisField := !steps.moreStepsRemaining() && !unset + + actualCurrValue := curr + if curr.Kind() == reflect.Ptr { + actualCurrValue = curr.Elem() + } + + switch actualCurrValue.Kind() { + case reflect.Map: + if shouldSetThisField { + return fmt.Errorf("Can't set a map to a value: %v", actualCurrValue) + } + + mapKey := reflect.ValueOf(steps.nextStep()) + mapValueType := curr.Type().Elem().Elem() + + if shouldUnsetNextField { + actualCurrValue.SetMapIndex(mapKey, reflect.Value{}) + return nil + } + + currMapValue := actualCurrValue.MapIndex(mapKey) + + needToSetNewMapValue := currMapValue.Kind() == reflect.Invalid + if needToSetNewMapValue { + currMapValue = reflect.New(mapValueType).Elem() + actualCurrValue.SetMapIndex(mapKey, currMapValue) + } + + // our maps do not hold pointers to structs, they hold the structs themselves. This means that MapIndex returns the struct itself + // That in turn means that they have kinds of type.Struct, which is not a settable type. Because of this, we need to make new struct of that type + // copy all the data from the old value into the new value, then take the .addr of the new value to modify it in the next recursion. + // clear as mud + modifiableMapValue := reflect.New(currMapValue.Type()).Elem() + modifiableMapValue.Set(currMapValue) + + if modifiableMapValue.Kind() == reflect.Struct { + modifiableMapValue = modifiableMapValue.Addr() + } + err := modifyConfig(modifiableMapValue, steps.nextSteps(), propertyValue, unset) + if err != nil { + return err + } + + actualCurrValue.SetMapIndex(mapKey, reflect.Indirect(modifiableMapValue)) + return nil + + case reflect.String: + if steps.moreStepsRemaining() { + return fmt.Errorf("Can't have more steps after a string. %v", steps) + } + actualCurrValue.SetString(propertyValue) + return nil + + case reflect.Bool: + if steps.moreStepsRemaining() { + return fmt.Errorf("Can't have more steps after a bool. %v", steps) + } + boolValue, err := toBool(propertyValue) + if err != nil { + return err + } + actualCurrValue.SetBool(boolValue) + return nil + + case reflect.Struct: + if !steps.moreStepsRemaining() { + return fmt.Errorf("Can't set a struct to a value: %v", actualCurrValue) + } + + for fieldIndex := 0; fieldIndex < actualCurrValue.NumField(); fieldIndex++ { + currFieldValue := actualCurrValue.Field(fieldIndex) + currFieldType := actualCurrValue.Type().Field(fieldIndex) + currYamlTag := currFieldType.Tag.Get("json") + currFieldTypeYamlName := strings.Split(currYamlTag, ",")[0] + + if currFieldTypeYamlName == steps.nextStep() { + thisMapHasNoValue := (currFieldValue.Kind() == reflect.Map && currFieldValue.IsNil()) + + if thisMapHasNoValue { + newValue := reflect.MakeMap(currFieldValue.Type()) + currFieldValue.Set(newValue) + + if shouldUnsetNextField { + return nil + } + } + + if shouldUnsetNextField { + // if we're supposed to unset the value or if the value is a map that doesn't exist, create a new value and overwrite + newValue := reflect.New(currFieldValue.Type()).Elem() + currFieldValue.Set(newValue) + return nil + } + + return modifyConfig(currFieldValue.Addr(), steps.nextSteps(), propertyValue, unset) + } + } + + return fmt.Errorf("Unable to locate path %v under %v", steps, actualCurrValue) + + } + + return fmt.Errorf("Unrecognized type: %v", actualCurrValue) +} diff --git a/pkg/kubectl/cmd/config/unset.go b/pkg/kubectl/cmd/config/unset.go new file mode 100644 index 00000000000..2ac3c28f3e5 --- /dev/null +++ b/pkg/kubectl/cmd/config/unset.go @@ -0,0 +1,107 @@ +/* +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 config + +import ( + "errors" + "fmt" + "io" + "reflect" + "strings" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" +) + +type unsetOptions struct { + pathOptions *pathOptions + propertyName string +} + +func NewCmdConfigUnset(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &unsetOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: "unset property-name", + Short: "Unsets an individual value in a .kubeconfig file", + Long: `Unsets an individual value in a .kubeconfig file + + property-name is a dot delimitted name where each token represents either a attribute name or a map key. Map keys may not contain dots. + `, + Run: func(cmd *cobra.Command, args []string) { + if !options.complete(cmd) { + return + } + + err := options.run() + if err != nil { + fmt.Printf("%v\n", err) + } + }, + } + + return cmd +} + +func (o unsetOptions) run() error { + err := o.validate() + if err != nil { + return err + } + + config, filename, err := o.pathOptions.getStartingConfig() + if err != nil { + return err + } + + if len(filename) == 0 { + return errors.New("cannot set property without using a specific file") + } + + parts := strings.Split(o.propertyName, ".") + err = modifyConfig(reflect.ValueOf(config), parts, "", true) + if err != nil { + return err + } + + err = clientcmd.WriteToFile(*config, filename) + if err != nil { + return err + } + + return nil +} + +func (o *unsetOptions) complete(cmd *cobra.Command) bool { + endingArgs := cmd.Flags().Args() + if len(endingArgs) != 1 { + cmd.Help() + return false + } + + o.propertyName = endingArgs[0] + return true +} + +func (o unsetOptions) validate() error { + if len(o.propertyName) == 0 { + return errors.New("You must specify a property") + } + + return nil +} diff --git a/pkg/kubectl/cmd/config/use_context.go b/pkg/kubectl/cmd/config/use_context.go new file mode 100644 index 00000000000..1ac50fef1d3 --- /dev/null +++ b/pkg/kubectl/cmd/config/use_context.go @@ -0,0 +1,98 @@ +/* +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 config + +import ( + "errors" + "fmt" + "io" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" +) + +type useContextOptions struct { + pathOptions *pathOptions + contextName string +} + +func NewCmdConfigUseContext(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &useContextOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: "use-context context-name", + Short: "Sets the current-context in a .kubeconfig file", + Long: `Sets the current-context in a .kubeconfig file`, + Run: func(cmd *cobra.Command, args []string) { + if !options.complete(cmd) { + return + } + + err := options.run() + if err != nil { + fmt.Printf("%v\n", err) + } + }, + } + + return cmd +} + +func (o useContextOptions) run() error { + err := o.validate() + if err != nil { + return err + } + + config, filename, err := o.pathOptions.getStartingConfig() + if err != nil { + return err + } + + if len(filename) == 0 { + return errors.New("cannot set current-context without using a specific file") + } + + config.CurrentContext = o.contextName + + err = clientcmd.WriteToFile(*config, filename) + if err != nil { + return err + } + + return nil +} + +func (o *useContextOptions) complete(cmd *cobra.Command) bool { + endingArgs := cmd.Flags().Args() + if len(endingArgs) != 1 { + cmd.Help() + return false + } + + o.contextName = endingArgs[0] + return true +} + +func (o useContextOptions) validate() error { + if len(o.contextName) == 0 { + return errors.New("You must specify a current-context") + } + + return nil +} diff --git a/pkg/kubectl/cmd/config/view.go b/pkg/kubectl/cmd/config/view.go new file mode 100644 index 00000000000..ea678d0ba41 --- /dev/null +++ b/pkg/kubectl/cmd/config/view.go @@ -0,0 +1,99 @@ +/* +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 config + +import ( + "fmt" + "io" + + "github.com/ghodss/yaml" + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" + clientcmdapi "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd/api" +) + +type viewOptions struct { + pathOptions *pathOptions + merge bool +} + +func NewCmdConfigView(out io.Writer, pathOptions *pathOptions) *cobra.Command { + options := &viewOptions{pathOptions: pathOptions} + + cmd := &cobra.Command{ + Use: "view", + Short: "displays the specified .kubeconfig file or a merged result", + Long: `displays the specified .kubeconfig file or a merged result`, + Run: func(cmd *cobra.Command, args []string) { + err := options.run() + if err != nil { + fmt.Printf("%v\n", err) + } + }, + } + + cmd.Flags().BoolVar(&options.merge, "merge", false, "merge together the full hierarchy of .kubeconfig files") + + return cmd +} + +func (o viewOptions) run() error { + err := o.validate() + if err != nil { + return err + } + + config, _, err := o.getStartingConfig() + if err != nil { + return err + } + + content, err := yaml.Marshal(config) + if err != nil { + return err + } + + fmt.Printf("%v", string(content)) + + return nil +} + +func (o viewOptions) validate() error { + return nil +} + +// getStartingConfig returns the Config object built from the sources specified by the options, the filename read (only if it was a single file), and an error if something goes wrong +func (o *viewOptions) getStartingConfig() (*clientcmdapi.Config, string, error) { + switch { + case o.merge: + loadingRules := clientcmd.NewClientConfigLoadingRules() + loadingRules.EnvVarPath = "" + loadingRules.CommandLinePath = o.pathOptions.specifiedFile + + overrides := &clientcmd.ConfigOverrides{} + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) + + config, err := clientConfig.RawConfig() + if err != nil { + return nil, "", fmt.Errorf("Error getting config: %v", err) + } + return &config, "", nil + default: + return o.pathOptions.getStartingConfig() + } +}