diff --git a/docs/kubectl.md b/docs/kubectl.md index ad5a919ae3..5f02b0dff2 100644 --- a/docs/kubectl.md +++ b/docs/kubectl.md @@ -32,10 +32,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -73,10 +75,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -140,10 +144,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -179,10 +185,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -227,10 +235,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -279,10 +289,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -345,10 +357,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -364,13 +378,13 @@ Usage: kubectl config [command] Available Commands: - view displays merged .kubeconfig settings or a specified .kubeconfig file. - set-cluster name [--server=server] [--certificate-authority=path/to/certficate/authority] [--api-version=apiversion] [--insecure-skip-tls-verify=true] Sets a cluster entry in .kubeconfig - set-credentials name [--auth-path=path/to/auth/file] [--client-certificate=path/to/certficate/file] [--client-key=path/to/key/file] [--token=bearer_token_string] Sets a user entry in .kubeconfig - set-context name [--cluster=cluster-nickname] [--user=user-nickname] [--namespace=namespace] Sets a context entry in .kubeconfig - set property-name property-value Sets an individual value in a .kubeconfig file - unset property-name Unsets an individual value in a .kubeconfig file - use-context context-name Sets the current-context in a .kubeconfig file + view displays merged .kubeconfig settings or a specified .kubeconfig file. + set-cluster name [--server=server] [--certificate-authority=path/to/certficate/authority] [--api-version=apiversion] [--insecure-skip-tls-verify=true] Sets a cluster entry in .kubeconfig + set-credentials name [--auth-path=authfile] [--client-certificate=certfile] [--client-key=keyfile] [--token=bearer_token] [--username=basic_user] [--password=basic_password] Sets a user entry in .kubeconfig + set-context name [--cluster=cluster-nickname] [--user=user-nickname] [--namespace=namespace] Sets a context entry in .kubeconfig + set property-name property-value Sets an individual value in a .kubeconfig file + unset property-name Unsets an individual value in a .kubeconfig file + use-context context-name Sets the current-context in a .kubeconfig file Global Flags: @@ -394,10 +408,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -464,10 +480,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -508,10 +526,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -520,15 +540,28 @@ Global Flags: #### config set-credentials Sets a user entry in .kubeconfig - Specifying a name that already exists will merge new fields on top of existing values for those fields. - e.g. - kubectl config set-credentials cluster-admin --client-key=~/.kube/cluster-admin/.kubecfg.key - only sets the client-key field on the cluster-admin user entry without touching other values. - + + Specifying a name that already exists will merge new fields on top of existing + values. For example, the following only sets the "client-key" field on the + "cluster-admin" entry, without touching other values: + + set-credentials cluster-admin --client-key=~/.kube/admin.key + + Client-certificate flags: + --client-certificate=certfile --client-key=keyfile + + Bearer token flags: + --token=bearer_token + + Basic auth flags: + --username=basic_user --password=basic_password + + Bearer token and basic auth are mutually exclusive. + Usage: ``` - kubectl config set-credentials name [--auth-path=path/to/auth/file] [--client-certificate=path/to/certficate/file] [--client-key=path/to/key/file] [--token=bearer_token_string] [flags] + kubectl config set-credentials name [--auth-path=authfile] [--client-certificate=certfile] [--client-key=keyfile] [--token=bearer_token] [--username=basic_user] [--password=basic_password] [flags] Global Flags: @@ -552,10 +585,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -596,10 +631,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -640,10 +677,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -682,10 +721,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -721,10 +762,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -760,10 +803,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -806,10 +851,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -859,10 +906,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -912,10 +961,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -975,10 +1026,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -1027,10 +1080,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -1089,10 +1144,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging @@ -1149,10 +1206,12 @@ Global Flags: --logtostderr=true: log to standard error instead of files --match-server-version=false: Require server version to match client version --namespace="": If present, the namespace scope for this CLI request. + --password="": Password for basic authentication to the API server. -s, --server="": The address and port of the Kubernetes API server --stderrthreshold=2: logs at or above this threshold go to stderr --token="": Bearer token for authentication to the API server. --user="": The name of the kubeconfig user to use + --username="": Username for basic authentication to the API server. --v=0: log level for V logs --validate=false: If true, use a schema to validate the input before sending it --vmodule=: comma-separated list of pattern=N settings for file-filtered logging diff --git a/pkg/client/clientcmd/api/types.go b/pkg/client/clientcmd/api/types.go index 5b90969f5e..ab126071a4 100644 --- a/pkg/client/clientcmd/api/types.go +++ b/pkg/client/clientcmd/api/types.go @@ -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"` } @@ -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"` } diff --git a/pkg/client/clientcmd/api/v1/types.go b/pkg/client/clientcmd/api/v1/types.go index b5dcf83c54..e3ee4ca289 100644 --- a/pkg/client/clientcmd/api/v1/types.go +++ b/pkg/client/clientcmd/api/v1/types.go @@ -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"` } @@ -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"` } diff --git a/pkg/client/clientcmd/client_config.go b/pkg/client/clientcmd/client_config.go index 06b732b936..3b2990339c 100644 --- a/pkg/client/clientcmd/client_config.go +++ b/pkg/client/clientcmd/client_config.go @@ -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) @@ -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. @@ -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 } diff --git a/pkg/client/clientcmd/client_config_test.go b/pkg/client/clientcmd/client_config_test.go index 171bf70f3a..ee9f273162 100644 --- a/pkg/client/clientcmd/client_config_test.go +++ b/pkg/client/clientcmd/client_config_test.go @@ -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{}) @@ -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) + } +} diff --git a/pkg/client/clientcmd/loader.go b/pkg/client/clientcmd/loader.go index 6d1c9d93ba..0b5ad460b6 100644 --- a/pkg/client/clientcmd/loader.go +++ b/pkg/client/clientcmd/loader.go @@ -173,36 +173,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 } - - if err := ioutil.WriteFile(filename, content, 0644); err != nil { - return err - } - return nil } + +// 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 + } + content, err := yaml.JSONToYAML(json) + if err != nil { + return nil, err + } + return content, nil +} diff --git a/pkg/client/clientcmd/overrides.go b/pkg/client/clientcmd/overrides.go index a6c0aa0952..0b2e01f3be 100644 --- a/pkg/client/clientcmd/overrides.go +++ b/pkg/client/clientcmd/overrides.go @@ -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 @@ -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 @@ -89,6 +93,8 @@ func RecommendedAuthOverrideFlags(prefix string) AuthOverrideFlags { ClientCertificate: prefix + FlagCertFile, ClientKey: prefix + FlagKeyFile, Token: prefix + FlagBearerToken, + Username: prefix + FlagUsername, + Password: prefix + FlagPassword, } } @@ -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 diff --git a/pkg/client/clientcmd/validation.go b/pkg/client/clientcmd/validation.go index aa9b9e64e7..f922513e33 100644 --- a/pkg/client/clientcmd/validation.go +++ b/pkg/client/clientcmd/validation.go @@ -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() @@ -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") @@ -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)) + } } } diff --git a/pkg/client/clientcmd/validation_test.go b/pkg/client/clientcmd/validation_test.go index ee2f320ed0..0bb16cdb6e 100644 --- a/pkg/client/clientcmd/validation_test.go +++ b/pkg/client/clientcmd/validation_test.go @@ -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()) @@ -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 diff --git a/pkg/client/helper.go b/pkg/client/helper.go index 91c2151d8b..0ae507257f 100644 --- a/pkg/client/helper.go +++ b/pkg/client/helper.go @@ -354,7 +354,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" diff --git a/pkg/kubectl/cmd/config/config_test.go b/pkg/kubectl/cmd/config/config_test.go index 60949928e5..3be8f0ab1c 100644 --- a/pkg/kubectl/cmd/config/config_test.go +++ b/pkg/kubectl/cmd/config/config_test.go @@ -21,6 +21,7 @@ import ( "io/ioutil" "os" "reflect" + "strings" "testing" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/clientcmd" @@ -40,9 +41,10 @@ func newRedFederalCowHammerConfig() clientcmdapi.Config { } type configCommandTest struct { - args []string - startingConfig clientcmdapi.Config - expectedConfig clientcmdapi.Config + args []string + startingConfig clientcmdapi.Config + expectedConfig clientcmdapi.Config + expectedOutputs []string } func TestSetCurrentContext(t *testing.T) { @@ -154,11 +156,10 @@ 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"}, + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagAuthPath + "=auth-path", "--" + clientcmd.FlagBearerToken + "=token"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } @@ -166,6 +167,225 @@ func TestAdditionalAuth(t *testing.T) { test.run(t) } +func TestEmptyTokenAndCertAllowed(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + authInfo := clientcmdapi.NewAuthInfo() + authInfo.ClientCertificate = "cert-file" + expectedConfig.AuthInfos["another-user"] = *authInfo + + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=cert-file", "--" + clientcmd.FlagBearerToken + "="}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestTokenAndCertAllowed(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + authInfo := clientcmdapi.NewAuthInfo() + authInfo.Token = "token" + authInfo.ClientCertificate = "cert-file" + expectedConfig.AuthInfos["another-user"] = *authInfo + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=cert-file", "--" + clientcmd.FlagBearerToken + "=token"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestTokenAndBasicDisallowed(t *testing.T) { + expectedConfig := newRedFederalCowHammerConfig() + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagUsername + "=myuser", "--" + clientcmd.FlagBearerToken + "=token"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: expectedConfig, + expectedOutputs: []string{"--token", "--username"}, + } + + test.run(t) +} + +func TestBasicClearsToken(t *testing.T) { + authInfoWithToken := clientcmdapi.NewAuthInfo() + authInfoWithToken.Token = "token" + + authInfoWithBasic := clientcmdapi.NewAuthInfo() + authInfoWithBasic.Username = "myuser" + authInfoWithBasic.Password = "mypass" + + startingConfig := newRedFederalCowHammerConfig() + startingConfig.AuthInfos["another-user"] = *authInfoWithToken + + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.AuthInfos["another-user"] = *authInfoWithBasic + + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagUsername + "=myuser", "--" + clientcmd.FlagPassword + "=mypass"}, + startingConfig: startingConfig, + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestTokenClearsBasic(t *testing.T) { + authInfoWithBasic := clientcmdapi.NewAuthInfo() + authInfoWithBasic.Username = "myuser" + authInfoWithBasic.Password = "mypass" + + authInfoWithToken := clientcmdapi.NewAuthInfo() + authInfoWithToken.Token = "token" + + startingConfig := newRedFederalCowHammerConfig() + startingConfig.AuthInfos["another-user"] = *authInfoWithBasic + + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.AuthInfos["another-user"] = *authInfoWithToken + + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagBearerToken + "=token"}, + startingConfig: startingConfig, + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestTokenLeavesCert(t *testing.T) { + authInfoWithCerts := clientcmdapi.NewAuthInfo() + authInfoWithCerts.ClientCertificate = "cert" + authInfoWithCerts.ClientCertificateData = []byte("certdata") + authInfoWithCerts.ClientKey = "key" + authInfoWithCerts.ClientKeyData = []byte("keydata") + + authInfoWithTokenAndCerts := clientcmdapi.NewAuthInfo() + authInfoWithTokenAndCerts.Token = "token" + authInfoWithTokenAndCerts.ClientCertificate = "cert" + authInfoWithTokenAndCerts.ClientCertificateData = []byte("certdata") + authInfoWithTokenAndCerts.ClientKey = "key" + authInfoWithTokenAndCerts.ClientKeyData = []byte("keydata") + + startingConfig := newRedFederalCowHammerConfig() + startingConfig.AuthInfos["another-user"] = *authInfoWithCerts + + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.AuthInfos["another-user"] = *authInfoWithTokenAndCerts + + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagBearerToken + "=token"}, + startingConfig: startingConfig, + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestCertLeavesToken(t *testing.T) { + authInfoWithToken := clientcmdapi.NewAuthInfo() + authInfoWithToken.Token = "token" + + authInfoWithTokenAndCerts := clientcmdapi.NewAuthInfo() + authInfoWithTokenAndCerts.Token = "token" + authInfoWithTokenAndCerts.ClientCertificate = "cert" + authInfoWithTokenAndCerts.ClientKey = "key" + + startingConfig := newRedFederalCowHammerConfig() + startingConfig.AuthInfos["another-user"] = *authInfoWithToken + + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.AuthInfos["another-user"] = *authInfoWithTokenAndCerts + + test := configCommandTest{ + args: []string{"set-credentials", "another-user", "--" + clientcmd.FlagCertFile + "=cert", "--" + clientcmd.FlagKeyFile + "=key"}, + startingConfig: startingConfig, + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestCAClearsInsecure(t *testing.T) { + clusterInfoWithInsecure := clientcmdapi.NewCluster() + clusterInfoWithInsecure.InsecureSkipTLSVerify = true + + clusterInfoWithCA := clientcmdapi.NewCluster() + clusterInfoWithCA.CertificateAuthority = "cafile" + + startingConfig := newRedFederalCowHammerConfig() + startingConfig.Clusters["another-cluster"] = *clusterInfoWithInsecure + + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.Clusters["another-cluster"] = *clusterInfoWithCA + + test := configCommandTest{ + args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=cafile"}, + startingConfig: startingConfig, + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestCAClearsCAData(t *testing.T) { + clusterInfoWithCAData := clientcmdapi.NewCluster() + clusterInfoWithCAData.CertificateAuthorityData = []byte("cadata") + + clusterInfoWithCA := clientcmdapi.NewCluster() + clusterInfoWithCA.CertificateAuthority = "cafile" + + startingConfig := newRedFederalCowHammerConfig() + startingConfig.Clusters["another-cluster"] = *clusterInfoWithCAData + + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.Clusters["another-cluster"] = *clusterInfoWithCA + + test := configCommandTest{ + args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=cafile", "--" + clientcmd.FlagInsecure + "=false"}, + startingConfig: startingConfig, + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestInsecureClearsCA(t *testing.T) { + clusterInfoWithInsecure := clientcmdapi.NewCluster() + clusterInfoWithInsecure.InsecureSkipTLSVerify = true + + clusterInfoWithCA := clientcmdapi.NewCluster() + clusterInfoWithCA.CertificateAuthority = "cafile" + clusterInfoWithCA.CertificateAuthorityData = []byte("cadata") + + startingConfig := newRedFederalCowHammerConfig() + startingConfig.Clusters["another-cluster"] = *clusterInfoWithCA + + expectedConfig := newRedFederalCowHammerConfig() + expectedConfig.Clusters["another-cluster"] = *clusterInfoWithInsecure + + test := configCommandTest{ + args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagInsecure + "=true"}, + startingConfig: startingConfig, + expectedConfig: expectedConfig, + } + + test.run(t) +} + +func TestCAAndInsecureDisallowed(t *testing.T) { + test := configCommandTest{ + args: []string{"set-cluster", "another-cluster", "--" + clientcmd.FlagCAFile + "=cafile", "--" + clientcmd.FlagInsecure + "=true"}, + startingConfig: newRedFederalCowHammerConfig(), + expectedConfig: newRedFederalCowHammerConfig(), + expectedOutputs: []string{"certificate", "insecure"}, + } + + test.run(t) +} + func TestMergeExistingAuth(t *testing.T) { expectedConfig := newRedFederalCowHammerConfig() authInfo := expectedConfig.AuthInfos["red-user"] @@ -197,11 +417,11 @@ func TestAdditionalCluster(t *testing.T) { cluster := *clientcmdapi.NewCluster() cluster.APIVersion = "v1beta1" cluster.CertificateAuthority = "ca-location" - cluster.InsecureSkipTLSVerify = true + cluster.InsecureSkipTLSVerify = false 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"}, + args: []string{"set-cluster", "different-cluster", "--" + clientcmd.FlagAPIServer + "=serverlocation", "--" + clientcmd.FlagInsecure + "=false", "--" + clientcmd.FlagCAFile + "=ca-location", "--" + clientcmd.FlagAPIVersion + "=v1beta1"}, startingConfig: newRedFederalCowHammerConfig(), expectedConfig: expectedConfig, } @@ -321,7 +541,7 @@ func testConfigCommand(args []string, startingConfig clientcmdapi.Config) (strin } func (test configCommandTest) run(t *testing.T) { - _, actualConfig := testConfigCommand(test.args, test.startingConfig) + out, actualConfig := testConfigCommand(test.args, test.startingConfig) testSetNilMapsToEmpties(reflect.ValueOf(&test.expectedConfig)) testSetNilMapsToEmpties(reflect.ValueOf(&actualConfig)) @@ -330,6 +550,12 @@ func (test configCommandTest) run(t *testing.T) { t.Errorf("diff: %v", util.ObjectDiff(test.expectedConfig, actualConfig)) t.Errorf("expected: %#v\n actual: %#v", test.expectedConfig, actualConfig) } + + for _, expectedOutput := range test.expectedOutputs { + if !strings.Contains(out, expectedOutput) { + t.Errorf("expected '%s' in output, got '%s'", expectedOutput, out) + } + } } func testSetNilMapsToEmpties(curr reflect.Value) { actualCurrValue := curr diff --git a/pkg/kubectl/cmd/config/create_authinfo.go b/pkg/kubectl/cmd/config/create_authinfo.go index 814988bfb1..22860f1c59 100644 --- a/pkg/kubectl/cmd/config/create_authinfo.go +++ b/pkg/kubectl/cmd/config/create_authinfo.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "io" + "strings" "github.com/spf13/cobra" @@ -35,20 +36,35 @@ type createAuthInfoOptions struct { clientCertificate util.StringFlag clientKey util.StringFlag token util.StringFlag + username util.StringFlag + password util.StringFlag } func NewCmdConfigSetAuthInfo(out io.Writer, pathOptions *pathOptions) *cobra.Command { options := &createAuthInfoOptions{pathOptions: pathOptions} cmd := &cobra.Command{ - Use: fmt.Sprintf("set-credentials name [--%v=path/to/auth/file] [--%v=path/to/certficate/file] [--%v=path/to/key/file] [--%v=bearer_token_string]", clientcmd.FlagAuthPath, clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken), + Use: fmt.Sprintf("set-credentials name [--%v=authfile] [--%v=certfile] [--%v=keyfile] [--%v=bearer_token] [--%v=basic_user] [--%v=basic_password]", clientcmd.FlagAuthPath, clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword), Short: "Sets a user entry in .kubeconfig", - Long: `Sets a user entry in .kubeconfig - Specifying a name that already exists will merge new fields on top of existing values for those fields. - e.g. - kubectl config set-credentials cluster-admin --client-key=~/.kube/cluster-admin/.kubecfg.key - only sets the client-key field on the cluster-admin user entry without touching other values. - `, + Long: fmt.Sprintf(`Sets a user entry in .kubeconfig + + Specifying a name that already exists will merge new fields on top of existing + values. For example, the following only sets the "client-key" field on the + "cluster-admin" entry, without touching other values: + + set-credentials cluster-admin --client-key=~/.kube/admin.key + + Client-certificate flags: + --%v=certfile --%v=keyfile + + Bearer token flags: + --%v=bearer_token + + Basic auth flags: + --%v=basic_user --%v=basic_password + + Bearer token and basic auth are mutually exclusive. +`, clientcmd.FlagCertFile, clientcmd.FlagKeyFile, clientcmd.FlagBearerToken, clientcmd.FlagUsername, clientcmd.FlagPassword), Run: func(cmd *cobra.Command, args []string) { if !options.complete(cmd) { return @@ -56,7 +72,7 @@ func NewCmdConfigSetAuthInfo(out io.Writer, pathOptions *pathOptions) *cobra.Com err := options.run() if err != nil { - fmt.Printf("%v\n", err) + fmt.Fprintf(out, "%v\n", err) } }, } @@ -65,6 +81,8 @@ func NewCmdConfigSetAuthInfo(out io.Writer, pathOptions *pathOptions) *cobra.Com cmd.Flags().Var(&options.clientCertificate, clientcmd.FlagCertFile, clientcmd.FlagCertFile+" for the user entry in .kubeconfig") cmd.Flags().Var(&options.clientKey, clientcmd.FlagKeyFile, clientcmd.FlagKeyFile+" for the user entry in .kubeconfig") cmd.Flags().Var(&options.token, clientcmd.FlagBearerToken, clientcmd.FlagBearerToken+" for the user entry in .kubeconfig") + cmd.Flags().Var(&options.username, clientcmd.FlagUsername, clientcmd.FlagUsername+" for the user entry in .kubeconfig") + cmd.Flags().Var(&options.password, clientcmd.FlagPassword, clientcmd.FlagPassword+" for the user entry in .kubeconfig") return cmd } @@ -95,17 +113,48 @@ func (o createAuthInfoOptions) run() error { func (o *createAuthInfoOptions) modifyAuthInfo(existingAuthInfo clientcmdapi.AuthInfo) clientcmdapi.AuthInfo { modifiedAuthInfo := existingAuthInfo + var setToken, setBasic bool + if o.authPath.Provided() { modifiedAuthInfo.AuthPath = o.authPath.Value() } + if o.clientCertificate.Provided() { modifiedAuthInfo.ClientCertificate = o.clientCertificate.Value() + if len(modifiedAuthInfo.ClientCertificate) > 0 { + modifiedAuthInfo.ClientCertificateData = nil + } } if o.clientKey.Provided() { modifiedAuthInfo.ClientKey = o.clientKey.Value() + if len(modifiedAuthInfo.ClientKey) > 0 { + modifiedAuthInfo.ClientKeyData = nil + } } + if o.token.Provided() { modifiedAuthInfo.Token = o.token.Value() + setToken = len(modifiedAuthInfo.Token) > 0 + } + + if o.username.Provided() { + modifiedAuthInfo.Username = o.username.Value() + setBasic = setBasic || len(modifiedAuthInfo.Username) > 0 + } + if o.password.Provided() { + modifiedAuthInfo.Password = o.password.Value() + setBasic = setBasic || len(modifiedAuthInfo.Password) > 0 + } + + // If any auth info was set, make sure any other existing auth types are cleared + if setToken || setBasic { + if !setToken { + modifiedAuthInfo.Token = "" + } + if !setBasic { + modifiedAuthInfo.Username = "" + modifiedAuthInfo.Password = "" + } } return modifiedAuthInfo @@ -126,6 +175,16 @@ func (o createAuthInfoOptions) validate() error { if len(o.name) == 0 { return errors.New("You must specify a non-empty user name") } + methods := []string{} + if len(o.token.Value()) > 0 { + methods = append(methods, fmt.Sprintf("--%v", clientcmd.FlagBearerToken)) + } + if len(o.username.Value()) > 0 || len(o.password.Value()) > 0 { + methods = append(methods, fmt.Sprintf("--%v/--%v", clientcmd.FlagUsername, clientcmd.FlagPassword)) + } + if len(methods) > 1 { + return fmt.Errorf("You cannot specify more than one authentication method at the same time: %v", strings.Join(methods, ", ")) + } return nil } diff --git a/pkg/kubectl/cmd/config/create_cluster.go b/pkg/kubectl/cmd/config/create_cluster.go index 933aafb360..a1a2bde679 100644 --- a/pkg/kubectl/cmd/config/create_cluster.go +++ b/pkg/kubectl/cmd/config/create_cluster.go @@ -56,7 +56,7 @@ func NewCmdConfigSetCluster(out io.Writer, pathOptions *pathOptions) *cobra.Comm err := options.run() if err != nil { - fmt.Printf("%v\n", err) + fmt.Fprintf(out, "%v\n", err) } }, } @@ -109,9 +109,19 @@ func (o *createClusterOptions) modifyCluster(existingCluster clientcmdapi.Cluste } if o.insecureSkipTLSVerify.Provided() { modifiedCluster.InsecureSkipTLSVerify = o.insecureSkipTLSVerify.Value() + // Specifying insecure mode clears any certificate authority + if modifiedCluster.InsecureSkipTLSVerify { + modifiedCluster.CertificateAuthority = "" + modifiedCluster.CertificateAuthorityData = nil + } } if o.certificateAuthority.Provided() { modifiedCluster.CertificateAuthority = o.certificateAuthority.Value() + // Specifying a certificate authority file clears certificate authority data and insecure mode + if modifiedCluster.CertificateAuthority != "" { + modifiedCluster.CertificateAuthorityData = nil + modifiedCluster.InsecureSkipTLSVerify = false + } } return modifiedCluster @@ -132,6 +142,9 @@ func (o createClusterOptions) validate() error { if len(o.name) == 0 { return errors.New("You must specify a non-empty cluster name") } + if o.insecureSkipTLSVerify.Value() && o.certificateAuthority.Value() != "" { + return errors.New("You cannot specify a certificate authority and insecure mode at the same time") + } return nil }