Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Let .kubeconfig be a single-source config for API clients #4517

Merged
merged 1 commit into from
Feb 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 72 additions & 13 deletions docs/kubectl.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions pkg/client/clientcmd/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type Cluster struct {
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"`
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
CertificateAuthorityData []byte `json:"certificate-authority-data,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"`
}
Expand All @@ -67,10 +69,18 @@ type AuthInfo struct {
AuthPath string `json:"auth-path,omitempty"`
// ClientCertificate is the path to a client cert file for TLS.
ClientCertificate string `json:"client-certificate,omitempty"`
// ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate
ClientCertificateData []byte `json:"client-certificate-data,omitempty"`
// ClientKey is the path to a client key file for TLS.
ClientKey string `json:"client-key,omitempty"`
// ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey
ClientKeyData []byte `json:"client-key-data,omitempty"`
// Token is the bearer token for authentication to the kubernetes cluster.
Token string `json:"token,omitempty"`
// Username is the username for basic authentication to the kubernetes cluster.
Username string `json:"username,omitempty"`
// Password is the password for basic authentication to the kubernetes cluster.
Password string `json:"password,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"`
}
Expand Down
10 changes: 10 additions & 0 deletions pkg/client/clientcmd/api/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ type Cluster struct {
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"`
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
CertificateAuthorityData []byte `json:"certificate-authority-data,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"`
}
Expand All @@ -67,10 +69,18 @@ type AuthInfo struct {
AuthPath string `json:"auth-path,omitempty"`
// ClientCertificate is the path to a client cert file for TLS.
ClientCertificate string `json:"client-certificate,omitempty"`
// ClientCertificateData contains PEM-encoded data from a client cert file for TLS. Overrides ClientCertificate
ClientCertificateData []byte `json:"client-certificate-data,omitempty"`
// ClientKey is the path to a client key file for TLS.
ClientKey string `json:"client-key,omitempty"`
// ClientKeyData contains PEM-encoded data from a client key file for TLS. Overrides ClientKey
ClientKeyData []byte `json:"client-key-data,omitempty"`
// Token is the bearer token for authentication to the kubernetes cluster.
Token string `json:"token,omitempty"`
// Username is the username for basic authentication to the kubernetes cluster.
Username string `json:"username,omitempty"`
// Password is the password for basic authentication to the kubernetes cluster.
Password string `json:"password,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"`
}
Expand Down
11 changes: 9 additions & 2 deletions pkg/client/clientcmd/client_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo,
// configClusterInfo holds the information identify the server provided by .kubeconfig
configClientConfig := &client.Config{}
configClientConfig.CAFile = configClusterInfo.CertificateAuthority
configClientConfig.CAData = configClusterInfo.CertificateAuthorityData
configClientConfig.Insecure = configClusterInfo.InsecureSkipTLSVerify
mergo.Merge(mergedConfig, configClientConfig)

Expand Down Expand Up @@ -169,9 +170,15 @@ func getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fa
if len(configAuthInfo.Token) > 0 {
mergedConfig.BearerToken = configAuthInfo.Token
}
if len(configAuthInfo.ClientCertificate) > 0 {
if len(configAuthInfo.ClientCertificate) > 0 || len(configAuthInfo.ClientCertificateData) > 0 {
mergedConfig.CertFile = configAuthInfo.ClientCertificate
mergedConfig.CertData = configAuthInfo.ClientCertificateData
mergedConfig.KeyFile = configAuthInfo.ClientKey
mergedConfig.KeyData = configAuthInfo.ClientKeyData
}
if len(configAuthInfo.Username) > 0 || len(configAuthInfo.Password) > 0 {
mergedConfig.Username = configAuthInfo.Username
mergedConfig.Password = configAuthInfo.Password
}

// if there isn't sufficient information to authenticate the user to the server, merge in ~/.kubernetes_auth.
Expand Down Expand Up @@ -228,7 +235,7 @@ func makeServerIdentificationConfig(info clientauth.Info) client.Config {

func canIdentifyUser(config client.Config) bool {
return len(config.Username) > 0 ||
len(config.CertFile) > 0 ||
(len(config.CertFile) > 0 || len(config.CertData) > 0) ||
len(config.BearerToken) > 0

}
Expand Down
71 changes: 71 additions & 0 deletions pkg/client/clientcmd/client_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,71 @@ func TestMergeContext(t *testing.T) {
matchStringArg(namespace, actual, t)
}

func TestCertificateData(t *testing.T) {
caData := []byte("ca-data")
certData := []byte("cert-data")
keyData := []byte("key-data")

config := clientcmdapi.NewConfig()
config.Clusters["clean"] = clientcmdapi.Cluster{
Server: "https://localhost:8443",
APIVersion: latest.Version,
CertificateAuthorityData: caData,
}
config.AuthInfos["clean"] = clientcmdapi.AuthInfo{
ClientCertificateData: certData,
ClientKeyData: keyData,
}
config.Contexts["clean"] = clientcmdapi.Context{
Cluster: "clean",
AuthInfo: "clean",
}
config.CurrentContext = "clean"

clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{})

clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

// Make sure cert data gets into config (will override file paths)
matchByteArg(caData, clientConfig.TLSClientConfig.CAData, t)
matchByteArg(certData, clientConfig.TLSClientConfig.CertData, t)
matchByteArg(keyData, clientConfig.TLSClientConfig.KeyData, t)
}

func TestBasicAuthData(t *testing.T) {
username := "myuser"
password := "mypass"

config := clientcmdapi.NewConfig()
config.Clusters["clean"] = clientcmdapi.Cluster{
Server: "https://localhost:8443",
APIVersion: latest.Version,
}
config.AuthInfos["clean"] = clientcmdapi.AuthInfo{
Username: username,
Password: password,
}
config.Contexts["clean"] = clientcmdapi.Context{
Cluster: "clean",
AuthInfo: "clean",
}
config.CurrentContext = "clean"

clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{})

clientConfig, err := clientBuilder.ClientConfig()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}

// Make sure basic auth data gets into config
matchStringArg(username, clientConfig.Username, t)
matchStringArg(password, clientConfig.Password, t)
}

func TestCreateClean(t *testing.T) {
config := createValidTestConfig()
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{})
Expand Down Expand Up @@ -123,3 +188,9 @@ func matchStringArg(expected, got string, t *testing.T) {
t.Errorf("Expected %v, got %v", expected, got)
}
}

func matchByteArg(expected, got []byte, t *testing.T) {
if !reflect.DeepEqual(expected, got) {
t.Errorf("Expected %v, got %v", expected, got)
}
}
36 changes: 23 additions & 13 deletions pkg/client/clientcmd/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,36 +153,46 @@ func resolveLocalPath(startingDir, path string) string {

// LoadFromFile takes a filename and deserializes the contents into Config object
func LoadFromFile(filename string) (*clientcmdapi.Config, error) {
config := &clientcmdapi.Config{}

kubeconfigBytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return Load(kubeconfigBytes)
}

if err := clientcmdlatest.Codec.DecodeInto(kubeconfigBytes, config); err != nil {
// Load takes a byte slice and deserializes the contents into Config object.
// Encapsulates deserialization without assuming the source is a file.
func Load(data []byte) (*clientcmdapi.Config, error) {
config := &clientcmdapi.Config{}
if err := clientcmdlatest.Codec.DecodeInto(data, config); err != nil {
return nil, err
}

return config, nil
}

// WriteToFile serializes the config to yaml and writes it out to a file. If no present, it creates the file with 0644. If it is present
// WriteToFile serializes the config to yaml and writes it out to a file. If not present, it creates the file with the mode 0600. If it is present
// it stomps the contents
func WriteToFile(config clientcmdapi.Config, filename string) error {
json, err := clientcmdlatest.Codec.Encode(&config)
content, err := Write(config)
if err != nil {
return err
}

content, err := yaml.JSONToYAML(json)
if err != nil {
if err := ioutil.WriteFile(filename, content, 0600); err != nil {
return err
}
return nil
}

if err := ioutil.WriteFile(filename, content, 0644); err != nil {
return err
// Write serializes the config to yaml.
// Encapsulates serialization without assuming the destination is a file.
func Write(config clientcmdapi.Config) ([]byte, error) {
json, err := clientcmdlatest.Codec.Encode(&config)
if err != nil {
return nil, err
}

return nil
content, err := yaml.JSONToYAML(json)
if err != nil {
return nil, err
}
return content, nil
}
8 changes: 8 additions & 0 deletions pkg/client/clientcmd/overrides.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ type AuthOverrideFlags struct {
ClientCertificate string
ClientKey string
Token string
Username string
Password string
}

// ContextOverrideFlags holds the flag names to be used for binding command line flags for Cluster objects
Expand Down Expand Up @@ -80,6 +82,8 @@ const (
FlagKeyFile = "client-key"
FlagCAFile = "certificate-authority"
FlagBearerToken = "token"
FlagUsername = "username"
FlagPassword = "password"
)

// RecommendedAuthOverrideFlags is a convenience method to return recommended flag names prefixed with a string of your choosing
Expand All @@ -89,6 +93,8 @@ func RecommendedAuthOverrideFlags(prefix string) AuthOverrideFlags {
ClientCertificate: prefix + FlagCertFile,
ClientKey: prefix + FlagKeyFile,
Token: prefix + FlagBearerToken,
Username: prefix + FlagUsername,
Password: prefix + FlagPassword,
}
}

Expand Down Expand Up @@ -127,6 +133,8 @@ func BindAuthInfoFlags(authInfo *clientcmdapi.AuthInfo, flags *pflag.FlagSet, fl
flags.StringVar(&authInfo.ClientCertificate, flagNames.ClientCertificate, "", "Path to a client key file for TLS.")
flags.StringVar(&authInfo.ClientKey, flagNames.ClientKey, "", "Path to a client key file for TLS.")
flags.StringVar(&authInfo.Token, flagNames.Token, "", "Bearer token for authentication to the API server.")
flags.StringVar(&authInfo.Username, flagNames.Username, "", "Username for basic authentication to the API server.")
flags.StringVar(&authInfo.Password, flagNames.Password, "", "Password for basic authentication to the API server.")
}

// BindClusterFlags is a convenience method to bind the specified flags to their associated variables
Expand Down
43 changes: 33 additions & 10 deletions pkg/client/clientcmd/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) [
if len(clusterInfo.Server) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("no server found for %v", clusterName))
}
// Make sure CA data and CA file aren't both specified
if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName))
}
if len(clusterInfo.CertificateAuthority) != 0 {
clientCertCA, err := os.Open(clusterInfo.CertificateAuthority)
defer clientCertCA.Close()
Expand All @@ -129,6 +133,9 @@ func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []err
if len(authInfo.Token) != 0 {
methods = append(methods, "token")
}
if len(authInfo.Username) != 0 || len(authInfo.Password) != 0 {
methods = append(methods, "basicAuth")
}
if len(authInfo.AuthPath) != 0 {
usingAuthPath = true
methods = append(methods, "authFile")
Expand All @@ -140,18 +147,34 @@ func validateAuthInfo(authInfoName string, authInfo clientcmdapi.AuthInfo) []err
validationErrors = append(validationErrors, fmt.Errorf("unable to read auth-path %v for %v due to %v", authInfo.AuthPath, authInfoName, err))
}
}
if len(authInfo.ClientCertificate) != 0 {
methods = append(methods, "clientCert")

clientCertFile, err := os.Open(authInfo.ClientCertificate)
defer clientCertFile.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
if len(authInfo.ClientCertificate) != 0 || len(authInfo.ClientCertificateData) != 0 {
// Make sure cert data and file aren't both specified
if len(authInfo.ClientCertificate) != 0 && len(authInfo.ClientCertificateData) != 0 {
validationErrors = append(validationErrors, fmt.Errorf("client-cert-data and client-cert are both specified for %v. client-cert-data will override.", authInfoName))
}
clientKeyFile, err := os.Open(authInfo.ClientKey)
defer clientKeyFile.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
// Make sure key data and file aren't both specified
if len(authInfo.ClientKey) != 0 && len(authInfo.ClientKeyData) != 0 {
validationErrors = append(validationErrors, fmt.Errorf("client-key-data and client-key are both specified for %v. client-key-data will override.", authInfoName))
}
// Make sure a key is specified
if len(authInfo.ClientKey) == 0 && len(authInfo.ClientKeyData) == 0 {
validationErrors = append(validationErrors, fmt.Errorf("client-key-data or client-key must be specified for %v to use the clientCert authentication method.", authInfoName))
}

if len(authInfo.ClientCertificate) != 0 {
clientCertFile, err := os.Open(authInfo.ClientCertificate)
defer clientCertFile.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-cert %v for %v due to %v", authInfo.ClientCertificate, authInfoName, err))
}
}
if len(authInfo.ClientKey) != 0 {
clientKeyFile, err := os.Open(authInfo.ClientKey)
defer clientKeyFile.Close()
if err != nil {
validationErrors = append(validationErrors, fmt.Errorf("unable to read client-key %v for %v due to %v", authInfo.ClientKey, authInfoName, err))
}
}
}

Expand Down
34 changes: 34 additions & 0 deletions pkg/client/clientcmd/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,25 @@ func TestValidateCertFilesNotFoundAuthInfo(t *testing.T) {
test.testAuthInfo("error", t)
test.testConfig(t)
}
func TestValidateCertDataOverridesFiles(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "")
defer os.Remove(tempFile.Name())

config := clientcmdapi.NewConfig()
config.AuthInfos["clean"] = clientcmdapi.AuthInfo{
ClientCertificate: tempFile.Name(),
ClientCertificateData: []byte("certdata"),
ClientKey: tempFile.Name(),
ClientKeyData: []byte("keydata"),
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"client-cert-data and client-cert are both specified", "client-key-data and client-key are both specified"},
}

test.testAuthInfo("clean", t)
test.testConfig(t)
}
func TestValidateCleanCertFilesAuthInfo(t *testing.T) {
tempFile, _ := ioutil.TempFile("", "")
defer os.Remove(tempFile.Name())
Expand Down Expand Up @@ -288,6 +307,21 @@ func TestValidateCleanTokenAuthInfo(t *testing.T) {
test.testConfig(t)
}

func TestValidateMultipleMethodsAuthInfo(t *testing.T) {
config := clientcmdapi.NewConfig()
config.AuthInfos["error"] = clientcmdapi.AuthInfo{
Token: "token",
Username: "username",
}
test := configValidationTest{
config: config,
expectedErrorSubstring: []string{"more than one authentication method", "token", "basicAuth"},
}

test.testAuthInfo("error", t)
test.testConfig(t)
}

type configValidationTest struct {
config *clientcmdapi.Config
expectedErrorSubstring []string
Expand Down
4 changes: 3 additions & 1 deletion pkg/client/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,9 @@ func IsConfigTransportTLS(config Config) bool {
func defaultServerUrlFor(config *Config) (*url.URL, error) {
// TODO: move the default to secure when the apiserver supports TLS by default
// config.Insecure is taken to mean "I want HTTPS but don't bother checking the certs against a CA."
defaultTLS := config.CertFile != "" || config.Insecure
hasCA := len(config.CAFile) != 0 || len(config.CAData) != 0
hasCert := len(config.CertFile) != 0 || len(config.CertData) != 0
defaultTLS := hasCA || hasCert || config.Insecure
host := config.Host
if host == "" {
host = "localhost"
Expand Down