Skip to content

Commit

Permalink
Let .kubeconfig populate ca/cert/key data, and basic-auth username/pa…
Browse files Browse the repository at this point in the history
…ssword in client configs
  • Loading branch information
liggitt committed Feb 19, 2015
1 parent 413e1db commit abb38cf
Show file tree
Hide file tree
Showing 13 changed files with 588 additions and 56 deletions.
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
Loading

0 comments on commit abb38cf

Please sign in to comment.