From f97422c8bd57692f5a1a3aa6dc6abc31051ebc82 Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Wed, 6 May 2020 01:01:09 -0400 Subject: [PATCH] exec credential provider: wire in cluster info Signed-off-by: Monis Khan --- .../pkg/util/webhook/authentication.go | 2 +- .../pkg/apis/clientauthentication/types.go | 44 ++++ .../clientauthentication/v1beta1/types.go | 53 ++++- .../plugin/pkg/client/auth/exec/exec.go | 50 +++-- .../plugin/pkg/client/auth/exec/exec_test.go | 208 +++++++++++++++++- staging/src/k8s.io/client-go/rest/config.go | 48 +++- .../src/k8s.io/client-go/rest/config_test.go | 37 +++- .../src/k8s.io/client-go/rest/transport.go | 18 +- .../tools/clientcmd/client_config.go | 9 +- .../tools/clientcmd/client_config_test.go | 22 +- 10 files changed, 431 insertions(+), 60 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/util/webhook/authentication.go b/staging/src/k8s.io/apiserver/pkg/util/webhook/authentication.go index 042879dade9..7d1a67c5070 100644 --- a/staging/src/k8s.io/apiserver/pkg/util/webhook/authentication.go +++ b/staging/src/k8s.io/apiserver/pkg/util/webhook/authentication.go @@ -246,7 +246,7 @@ func restConfigFromKubeconfig(configAuthInfo *clientcmdapi.AuthInfo) (*rest.Conf config.Password = configAuthInfo.Password } if configAuthInfo.Exec != nil { - config.ExecProvider = configAuthInfo.Exec.DeepCopy() + config.Exec.ExecProvider = configAuthInfo.Exec.DeepCopy() } if configAuthInfo.AuthProvider != nil { return nil, fmt.Errorf("auth provider not supported") diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go index 6fb53cecf94..ec6f4f28cb5 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go @@ -18,6 +18,7 @@ package clientauthentication import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -49,6 +50,10 @@ type ExecCredentialSpec struct { // interactive prompt. // +optional Interactive bool + + // Cluster contains information to allow an exec plugin to communicate + // with the kubernetes cluster being authenticated to. + Cluster Cluster } // ExecCredentialStatus holds credentials for the transport to use. @@ -75,3 +80,42 @@ type Response struct { // Code is the HTTP status code returned by the server. Code int32 } + +// Cluster contains information to allow an exec plugin to communicate +// with the kubernetes cluster being authenticated to. +type Cluster struct { + // Server is the address of the kubernetes cluster (https://hostname:port). + Server string + // ServerName is passed to the server for SNI and is used in the client to check server + // certificates against. If ServerName is empty, the hostname used to contact the + // server is used. + // +optional + ServerName string + // CAData contains PEM-encoded certificate authority certificates. + // If empty, system roots should be used. + // +listType=atomic + // +optional + CAData []byte + // Config holds additional config data that is specific to the exec + // plugin with regards to the cluster being authenticated to. + // + // This data is sourced from the clientcmd Cluster object's extensions[exec] field: + // + // clusters: + // - name: my-cluster + // cluster: + // ... + // extensions: + // - name: exec # reserved extension name for per cluster exec config + // extension: + // audience: 06e3fbd18de8 # arbitrary config + // + // In some environments, the user config may be exactly the same across many clusters + // (i.e. call this exec plugin) minus some details that are specific to each cluster + // such as the audience. This field allows the per cluster config to be directly + // specified with the cluster info. Using this field to store secret data is not + // recommended as one of the prime benefits of exec plugins is that no secrets need + // to be stored directly in the kubeconfig. + // +optional + Config runtime.Object +} diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/types.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/types.go index d6e267452e9..e575ded23f6 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/types.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/types.go @@ -18,17 +18,17 @@ package v1beta1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// ExecCredentials is used by exec-based plugins to communicate credentials to +// ExecCredential is used by exec-based plugins to communicate credentials to // HTTP transports. type ExecCredential struct { metav1.TypeMeta `json:",inline"` - // Spec holds information passed to the plugin by the transport. This contains - // request and runtime specific information, such as if the session is interactive. + // Spec holds information passed to the plugin by the transport. Spec ExecCredentialSpec `json:"spec,omitempty"` // Status is filled in by the plugin and holds the credentials that the transport @@ -37,9 +37,13 @@ type ExecCredential struct { Status *ExecCredentialStatus `json:"status,omitempty"` } -// ExecCredenitalSpec holds request and runtime specific information provided by +// ExecCredentialSpec holds request and runtime specific information provided by // the transport. -type ExecCredentialSpec struct{} +type ExecCredentialSpec struct { + // Cluster contains information to allow an exec plugin to communicate + // with the kubernetes cluster being authenticated to. + Cluster Cluster `json:"cluster"` +} // ExecCredentialStatus holds credentials for the transport to use. // @@ -57,3 +61,42 @@ type ExecCredentialStatus struct { // PEM-encoded private key for the above certificate. ClientKeyData string `json:"clientKeyData,omitempty"` } + +// Cluster contains information to allow an exec plugin to communicate +// with the kubernetes cluster being authenticated to. +type Cluster struct { + // Server is the address of the kubernetes cluster (https://hostname:port). + Server string `json:"server"` + // ServerName is passed to the server for SNI and is used in the client to check server + // certificates against. If ServerName is empty, the hostname used to contact the + // server is used. + // +optional + ServerName string `json:"serverName,omitempty"` + // CAData contains PEM-encoded certificate authority certificates. + // If empty, system roots should be used. + // +listType=atomic + // +optional + CAData []byte `json:"caData,omitempty"` + // Config holds additional config data that is specific to the exec + // plugin with regards to the cluster being authenticated to. + // + // This data is sourced from the clientcmd Cluster object's extensions[exec] field: + // + // clusters: + // - name: my-cluster + // cluster: + // ... + // extensions: + // - name: exec # reserved extension name for per cluster exec config + // extension: + // audience: 06e3fbd18de8 # arbitrary config + // + // In some environments, the user config may be exactly the same across many clusters + // (i.e. call this exec plugin) minus some details that are specific to each cluster + // such as the audience. This field allows the per cluster config to be directly + // specified with the cluster info. Using this field to store secret data is not + // recommended as one of the prime benefits of exec plugins is that no secrets need + // to be stored directly in the kubeconfig. + // +optional + Config runtime.RawExtension `json:"config,omitempty"` +} diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go index 627bb2de94b..dcf7c3877e6 100644 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go @@ -87,8 +87,15 @@ func newCache() *cache { var spewConfig = &spew.ConfigState{DisableMethods: true, Indent: " "} -func cacheKey(c *api.ExecConfig) string { - return spewConfig.Sprint(c) +func cacheKey(conf *api.ExecConfig, cluster clientauthentication.Cluster) string { + key := struct { + conf *api.ExecConfig + cluster clientauthentication.Cluster + }{ + conf: conf, + cluster: cluster, + } + return spewConfig.Sprint(key) } type cache struct { @@ -155,12 +162,12 @@ func (s *sometimes) Do(f func()) { } // GetAuthenticator returns an exec-based plugin for providing client credentials. -func GetAuthenticator(config *api.ExecConfig) (*Authenticator, error) { - return newAuthenticator(globalCache, config) +func GetAuthenticator(config *api.ExecConfig, cluster clientauthentication.Cluster) (*Authenticator, error) { + return newAuthenticator(globalCache, config, cluster) } -func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error) { - key := cacheKey(config) +func newAuthenticator(c *cache, config *api.ExecConfig, cluster clientauthentication.Cluster) (*Authenticator, error) { + key := cacheKey(config, cluster) if a, ok := c.get(key); ok { return a, nil } @@ -171,9 +178,10 @@ func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error) } a := &Authenticator{ - cmd: config.Command, - args: config.Args, - group: gv, + cmd: config.Command, + args: config.Args, + group: gv, + cluster: cluster, installHint: config.InstallHint, sometimes: &sometimes{ @@ -200,10 +208,11 @@ func newAuthenticator(c *cache, config *api.ExecConfig) (*Authenticator, error) // The plugin input and output are defined by the API group client.authentication.k8s.io. type Authenticator struct { // Set by the config - cmd string - args []string - group schema.GroupVersion - env []string + cmd string + args []string + group schema.GroupVersion + env []string + cluster clientauthentication.Cluster // Used to avoid log spew by rate limiting install hint printing. We didn't do // this by interval based rate limiting alone since that way may have prevented @@ -365,21 +374,16 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err Spec: clientauthentication.ExecCredentialSpec{ Response: r, Interactive: a.interactive, + Cluster: a.cluster, }, } env := append(a.environ(), a.env...) - if a.group == v1alpha1.SchemeGroupVersion { - // Input spec disabled for beta due to lack of use. Possibly re-enable this later if - // someone wants it back. - // - // See: https://github.com/kubernetes/kubernetes/issues/61796 - data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred) - if err != nil { - return fmt.Errorf("encode ExecCredentials: %v", err) - } - env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data)) + data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred) + if err != nil { + return fmt.Errorf("encode ExecCredentials: %v", err) } + env = append(env, fmt.Sprintf("%s=%s", execInfoEnv, data)) stdout := &bytes.Buffer{} cmd := exec.Command(a.cmd, a.args...) diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go index 43b9c08701a..b575cfae3f6 100644 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go @@ -117,6 +117,21 @@ func TestCacheKey(t *testing.T) { }, APIVersion: "client.authentication.k8s.io/v1alpha1", } + c1c := clientauthentication.Cluster{ + Server: "foo", + ServerName: "bar", + CAData: []byte("baz"), + Config: &runtime.Unknown{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "", + Kind: "", + }, + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), + ContentEncoding: "", + ContentType: "application/json", + }, + } + c2 := &api.ExecConfig{ Command: "foo-bar", Args: []string{"1", "2"}, @@ -127,6 +142,21 @@ func TestCacheKey(t *testing.T) { }, APIVersion: "client.authentication.k8s.io/v1alpha1", } + c2c := clientauthentication.Cluster{ + Server: "foo", + ServerName: "bar", + CAData: []byte("baz"), + Config: &runtime.Unknown{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "", + Kind: "", + }, + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), + ContentEncoding: "", + ContentType: "application/json", + }, + } + c3 := &api.ExecConfig{ Command: "foo-bar", Args: []string{"1", "2"}, @@ -136,9 +166,49 @@ func TestCacheKey(t *testing.T) { }, APIVersion: "client.authentication.k8s.io/v1alpha1", } - key1 := cacheKey(c1) - key2 := cacheKey(c2) - key3 := cacheKey(c3) + c3c := clientauthentication.Cluster{ + Server: "foo", + ServerName: "bar", + CAData: []byte("baz"), + Config: &runtime.Unknown{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "", + Kind: "", + }, + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), + ContentEncoding: "", + ContentType: "application/json", + }, + } + + c4 := &api.ExecConfig{ + Command: "foo-bar", + Args: []string{"1", "2"}, + Env: []api.ExecEnvVar{ + {Name: "3", Value: "4"}, + {Name: "5", Value: "6"}, + }, + APIVersion: "client.authentication.k8s.io/v1alpha1", + } + c4c := clientauthentication.Cluster{ + Server: "foo", + ServerName: "bar", + CAData: []byte("baz"), + Config: &runtime.Unknown{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "", + Kind: "", + }, + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`), + ContentEncoding: "", + ContentType: "application/json", + }, + } + + key1 := cacheKey(c1, c1c) + key2 := cacheKey(c2, c2c) + key3 := cacheKey(c3, c3c) + key4 := cacheKey(c4, c4c) if key1 != key2 { t.Error("key1 and key2 didn't match") } @@ -148,6 +218,9 @@ func TestCacheKey(t *testing.T) { if key2 == key3 { t.Error("key2 and key3 matched") } + if key3 == key4 { + t.Error("key3 and key4 matched") + } } func compJSON(t *testing.T, got, want []byte) { @@ -173,6 +246,7 @@ func TestRefreshCreds(t *testing.T) { name string config api.ExecConfig exitCode int + cluster clientauthentication.Cluster output string interactive bool response *clientauthentication.Response @@ -393,6 +467,16 @@ func TestRefreshCreds(t *testing.T) { config: api.ExecConfig{ APIVersion: "client.authentication.k8s.io/v1beta1", }, + wantInput: `{ + "kind": "ExecCredential", + "apiVersion": "client.authentication.k8s.io/v1beta1", + "spec": { + "cluster": { + "server": "", + "config": null + } + } + }`, output: `{ "kind": "ExecCredential", "apiVersion": "client.authentication.k8s.io/v1beta1", @@ -407,6 +491,16 @@ func TestRefreshCreds(t *testing.T) { config: api.ExecConfig{ APIVersion: "client.authentication.k8s.io/v1beta1", }, + wantInput: `{ + "kind": "ExecCredential", + "apiVersion": "client.authentication.k8s.io/v1beta1", + "spec": { + "cluster": { + "server": "", + "config": null + } + } + }`, output: `{ "kind": "ExecCredential", "apiVersion": "client.authentication.k8s.io/v1beta1", @@ -473,6 +567,106 @@ func TestRefreshCreds(t *testing.T) { wantErr: true, wantErrSubstr: "73", }, + { + name: "alpha-with-cluster-is-ignored", + config: api.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1alpha1", + }, + cluster: clientauthentication.Cluster{ + Server: "foo", + ServerName: "bar", + CAData: []byte("baz"), + Config: &runtime.Unknown{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "", + Kind: "", + }, + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`), + ContentEncoding: "", + ContentType: "application/json", + }, + }, + response: &clientauthentication.Response{ + Header: map[string][]string{ + "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, + }, + Code: 401, + }, + wantInput: `{ + "kind":"ExecCredential", + "apiVersion":"client.authentication.k8s.io/v1alpha1", + "spec": { + "response": { + "header": { + "WWW-Authenticate": [ + "Basic realm=\"Access to the staging site\", charset=\"UTF-8\"" + ] + }, + "code": 401 + } + } + }`, + output: `{ + "kind": "ExecCredential", + "apiVersion": "client.authentication.k8s.io/v1alpha1", + "status": { + "token": "foo-bar" + } + }`, + wantCreds: credentials{token: "foo-bar"}, + }, + { + name: "beta-with-cluster-is-serialized", + config: api.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + cluster: clientauthentication.Cluster{ + Server: "foo", + ServerName: "bar", + CAData: []byte("baz"), + Config: &runtime.Unknown{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "", + Kind: "", + }, + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"snorlax"}}`), + ContentEncoding: "", + ContentType: "application/json", + }, + }, + response: &clientauthentication.Response{ + Header: map[string][]string{ + "WWW-Authenticate": {`Basic realm="Access to the staging site", charset="UTF-8"`}, + }, + Code: 401, + }, + wantInput: `{ + "kind":"ExecCredential", + "apiVersion":"client.authentication.k8s.io/v1beta1", + "spec": { + "cluster": { + "server": "foo", + "serverName": "bar", + "caData": "YmF6", + "config": { + "apiVersion": "group/v1", + "kind": "PluginConfig", + "spec": { + "audience": "snorlax" + } + } + } + } + }`, + output: `{ + "kind": "ExecCredential", + "apiVersion": "client.authentication.k8s.io/v1beta1", + "status": { + "token": "foo-bar" + } + }`, + wantCreds: credentials{token: "foo-bar"}, + }, } for _, test := range tests { @@ -491,7 +685,7 @@ func TestRefreshCreds(t *testing.T) { }) } - a, err := newAuthenticator(newCache(), &c) + a, err := newAuthenticator(newCache(), &c, test.cluster) if err != nil { t.Fatal(err) } @@ -569,7 +763,7 @@ func TestRoundTripper(t *testing.T) { Command: "./testdata/test-plugin.sh", APIVersion: "client.authentication.k8s.io/v1alpha1", } - a, err := newAuthenticator(newCache(), &c) + a, err := newAuthenticator(newCache(), &c, clientauthentication.Cluster{}) if err != nil { t.Fatal(err) } @@ -694,7 +888,7 @@ func TestTLSCredentials(t *testing.T) { a, err := newAuthenticator(newCache(), &api.ExecConfig{ Command: "./testdata/test-plugin.sh", APIVersion: "client.authentication.k8s.io/v1alpha1", - }) + }, clientauthentication.Cluster{}) if err != nil { t.Fatal(err) } @@ -784,7 +978,7 @@ func TestConcurrentUpdateTransportConfig(t *testing.T) { Command: "./testdata/test-plugin.sh", APIVersion: "client.authentication.k8s.io/v1alpha1", } - a, err := newAuthenticator(newCache(), &c) + a, err := newAuthenticator(newCache(), &c, clientauthentication.Cluster{}) if err != nil { t.Fatal(err) } diff --git a/staging/src/k8s.io/client-go/rest/config.go b/staging/src/k8s.io/client-go/rest/config.go index fe132342efe..654cd1f9f44 100644 --- a/staging/src/k8s.io/client-go/rest/config.go +++ b/staging/src/k8s.io/client-go/rest/config.go @@ -87,7 +87,7 @@ type Config struct { AuthConfigPersister AuthProviderConfigPersister // Exec-based authentication provider. - ExecProvider *clientcmdapi.ExecConfig + Exec Exec // TLSClientConfig contains settings to enable transport layer security TLSClientConfig @@ -160,6 +160,15 @@ func (sanitizedAuthConfigPersister) String() string { return "rest.AuthProviderConfigPersister(--- REDACTED ---)" } +type sanitizedObject struct{ runtime.Object } + +func (sanitizedObject) GoString() string { + return "runtime.Object(--- REDACTED ---)" +} +func (sanitizedObject) String() string { + return "runtime.Object(--- REDACTED ---)" +} + // GoString implements fmt.GoStringer and sanitizes sensitive fields of Config // to prevent accidental leaking via logs. func (c *Config) GoString() string { @@ -183,10 +192,40 @@ func (c *Config) String() string { if cc.AuthConfigPersister != nil { cc.AuthConfigPersister = sanitizedAuthConfigPersister{cc.AuthConfigPersister} } - + if cc.Exec.Config != nil { + cc.Exec.Config = sanitizedObject{Object: cc.Exec.Config} + } return fmt.Sprintf("%#v", cc) } +// Exec plugin authentication provider. +type Exec struct { + // ExecProvider provides the config needed to execute the exec plugin. + ExecProvider *clientcmdapi.ExecConfig + + // Config holds additional config data that is specific to the exec + // plugin with regards to the cluster being authenticated to. + // + // This data is sourced from the clientcmd Cluster object's extensions[exec] field: + // + // clusters: + // - name: my-cluster + // cluster: + // ... + // extensions: + // - name: exec # reserved extension name for per cluster exec config + // extension: + // audience: 06e3fbd18de8 # arbitrary config + // + // In some environments, the user config may be exactly the same across many clusters + // (i.e. call this exec plugin) minus some details that are specific to each cluster + // such as the audience. This field allows the per cluster config to be directly + // specified with the cluster info. Using this field to store secret data is not + // recommended as one of the prime benefits of exec plugins is that no secrets need + // to be stored directly in the kubeconfig. + Config runtime.Object +} + // ImpersonationConfig has all the available impersonation options type ImpersonationConfig struct { // UserName is the username to impersonate on each request. @@ -603,7 +642,10 @@ func CopyConfig(config *Config) *Config { }, AuthProvider: config.AuthProvider, AuthConfigPersister: config.AuthConfigPersister, - ExecProvider: config.ExecProvider, + Exec: Exec{ + ExecProvider: config.Exec.ExecProvider, + Config: config.Exec.Config, + }, TLSClientConfig: TLSClientConfig{ Insecure: config.TLSClientConfig.Insecure, ServerName: config.TLSClientConfig.ServerName, diff --git a/staging/src/k8s.io/client-go/rest/config_test.go b/staging/src/k8s.io/client-go/rest/config_test.go index 1ccd14023c5..a691b13f06c 100644 --- a/staging/src/k8s.io/client-go/rest/config_test.go +++ b/staging/src/k8s.io/client-go/rest/config_test.go @@ -337,6 +337,11 @@ func TestAnonymousConfig(t *testing.T) { func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) { *r = fakeProxyFunc }, + func(r *runtime.Object, f fuzz.Continue) { + unknown := &runtime.Unknown{} + f.Fuzz(unknown) + *r = unknown + }, ) for i := 0; i < 20; i++ { original := &Config{} @@ -353,7 +358,8 @@ func TestAnonymousConfig(t *testing.T) { expected.Password = "" expected.AuthProvider = nil expected.AuthConfigPersister = nil - expected.ExecProvider = nil + expected.Exec.ExecProvider = nil + expected.Exec.Config = nil expected.TLSClientConfig.CertData = nil expected.TLSClientConfig.CertFile = "" expected.TLSClientConfig.KeyData = nil @@ -428,6 +434,11 @@ func TestCopyConfig(t *testing.T) { func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) { *r = fakeProxyFunc }, + func(r *runtime.Object, f fuzz.Continue) { + unknown := &runtime.Unknown{} + f.Fuzz(unknown) + *r = unknown + }, ) for i := 0; i < 20; i++ { original := &Config{} @@ -524,9 +535,12 @@ func TestConfigStringer(t *testing.T) { AuthProvider: &clientcmdapi.AuthProviderConfig{ Config: map[string]string{"secret": "s3cr3t"}, }, - ExecProvider: &clientcmdapi.ExecConfig{ - Args: []string{"secret"}, - Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, + Exec: Exec{ + ExecProvider: &clientcmdapi.ExecConfig{ + Args: []string{"secret"}, + Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, + }, + Config: &runtime.Unknown{Raw: []byte("super secret password")}, }, }, expectContent: []string{ @@ -545,6 +559,8 @@ func TestConfigStringer(t *testing.T) { formatBytes([]byte("fake key")), "secret", "s3cr3t", + "super secret password", + formatBytes([]byte("super secret password")), }, }, } @@ -587,10 +603,13 @@ func TestConfigSprint(t *testing.T) { Config: map[string]string{"secret": "s3cr3t"}, }, AuthConfigPersister: fakeAuthProviderConfigPersister{}, - ExecProvider: &clientcmdapi.ExecConfig{ - Command: "sudo", - Args: []string{"secret"}, - Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, + Exec: Exec{ + ExecProvider: &clientcmdapi.ExecConfig{ + Command: "sudo", + Args: []string{"secret"}, + Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, + }, + Config: &runtime.Unknown{Raw: []byte("super secret password")}, }, TLSClientConfig: TLSClientConfig{ CertFile: "a.crt", @@ -611,7 +630,7 @@ func TestConfigSprint(t *testing.T) { Proxy: fakeProxyFunc, } want := fmt.Sprintf( - `&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), WarningHandler:rest.fakeWarningHandler{}, Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`, + `&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), Exec:rest.Exec{ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, Config:runtime.Object(--- REDACTED ---)}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), WarningHandler:rest.fakeWarningHandler{}, Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`, c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc, ) diff --git a/staging/src/k8s.io/client-go/rest/transport.go b/staging/src/k8s.io/client-go/rest/transport.go index 450edc6edde..22407273685 100644 --- a/staging/src/k8s.io/client-go/rest/transport.go +++ b/staging/src/k8s.io/client-go/rest/transport.go @@ -19,8 +19,10 @@ package rest import ( "crypto/tls" "errors" + "fmt" "net/http" + "k8s.io/client-go/pkg/apis/clientauthentication" "k8s.io/client-go/plugin/pkg/client/auth/exec" "k8s.io/client-go/transport" ) @@ -89,12 +91,22 @@ func (c *Config) TransportConfig() (*transport.Config, error) { Proxy: c.Proxy, } - if c.ExecProvider != nil && c.AuthProvider != nil { + if c.Exec.ExecProvider != nil && c.AuthProvider != nil { return nil, errors.New("execProvider and authProvider cannot be used in combination") } - if c.ExecProvider != nil { - provider, err := exec.GetAuthenticator(c.ExecProvider) + if c.Exec.ExecProvider != nil { + caData, err := dataFromSliceOrFile(c.CAData, c.CAFile) + if err != nil { + return nil, fmt.Errorf("failed to load CA bundle for execProvider: %v", err) + } + cluster := clientauthentication.Cluster{ + Server: c.Host, + ServerName: c.TLSClientConfig.ServerName, + CAData: caData, + Config: c.Exec.Config, + } + provider, err := exec.GetAuthenticator(c.Exec.ExecProvider, cluster) if err != nil { return nil, err } diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/client_config.go b/staging/src/k8s.io/client-go/tools/clientcmd/client_config.go index 690afce0cd6..361ad43d7a9 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/client_config.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/client_config.go @@ -189,7 +189,7 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) { authInfoName, _ := config.getAuthInfoName() persister = PersisterForUser(config.configAccess, authInfoName) } - userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister) + userAuthPartialConfig, err := config.getUserIdentificationPartialConfig(configAuthInfo, config.fallbackReader, persister, configClusterInfo) if err != nil { return nil, err } @@ -232,7 +232,7 @@ func getServerIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, // 2. configAuthInfo.auth-path (this file can contain information that conflicts with #1, and we want #1 to win the priority) // 3. if there is not enough information to identify the user, load try the ~/.kubernetes_auth file // 4. if there is not enough information to identify the user, prompt if possible -func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister) (*restclient.Config, error) { +func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthInfo clientcmdapi.AuthInfo, fallbackReader io.Reader, persistAuthConfig restclient.AuthProviderConfigPersister, configClusterInfo clientcmdapi.Cluster) (*restclient.Config, error) { mergedConfig := &restclient.Config{} // blindly overwrite existing values based on precedence @@ -269,8 +269,9 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI mergedConfig.AuthConfigPersister = persistAuthConfig } if configAuthInfo.Exec != nil { - mergedConfig.ExecProvider = configAuthInfo.Exec + mergedConfig.Exec.ExecProvider = configAuthInfo.Exec mergedConfig.ExecProvider.InstallHint = cleanANSIEscapeCodes(mergedConfig.ExecProvider.InstallHint) + mergedConfig.Exec.Config = configClusterInfo.Extensions["exec"] // this key is reserved in the extensions list for exec plugin config } // if there still isn't enough information to authenticate the user, try prompting @@ -313,7 +314,7 @@ func canIdentifyUser(config restclient.Config) bool { (len(config.CertFile) > 0 || len(config.CertData) > 0) || len(config.BearerToken) > 0 || config.AuthProvider != nil || - config.ExecProvider != nil + config.Exec.ExecProvider != nil } // cleanANSIEscapeCodes takes an arbitrary string and ensures that there are no diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go b/staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go index 0819ed53477..08cf75d83e6 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/client_config_test.go @@ -23,10 +23,11 @@ import ( "strings" "testing" + "github.com/imdario/mergo" + + "k8s.io/apimachinery/pkg/runtime" restclient "k8s.io/client-go/rest" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - - "github.com/imdario/mergo" ) func TestMergoSemantics(t *testing.T) { @@ -834,6 +835,11 @@ apiVersion: v1 clusters: - cluster: server: https://localhost:8080 + extensions: + - name: exec + extension: + audience: foo + other: bar name: foo-cluster contexts: - context: @@ -865,10 +871,16 @@ users: if err != nil { t.Error(err) } - if !reflect.DeepEqual(config.ExecProvider.Args, []string{"arg-1", "arg-2"}) { - t.Errorf("Got args %v when they should be %v\n", config.ExecProvider.Args, []string{"arg-1", "arg-2"}) + if !reflect.DeepEqual(config.Exec.ExecProvider.Args, []string{"arg-1", "arg-2"}) { + t.Errorf("Got args %v when they should be %v\n", config.Exec.ExecProvider.Args, []string{"arg-1", "arg-2"}) + } + want := &runtime.Unknown{ + Raw: []byte(`{"audience":"foo","other":"bar"}`), + ContentType: "application/json", + } + if !reflect.DeepEqual(config.Exec.Config, want) { + t.Errorf("Got config %v when it should be %v\n", config.Exec.Config, want) } - } func TestCleanANSIEscapeCodes(t *testing.T) {