From f97422c8bd57692f5a1a3aa6dc6abc31051ebc82 Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Wed, 6 May 2020 01:01:09 -0400 Subject: [PATCH 1/6] 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) { From 99566970ceceb7e60f48cf2ff3015586fe50cc0e Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Wed, 6 May 2020 01:28:11 -0400 Subject: [PATCH 2/6] Generated conversions Signed-off-by: Monis Khan --- .../v1alpha1/conversion.go | 26 +++++++++++ .../v1alpha1/zz_generated.conversion.go | 16 +++---- .../v1beta1/conversion.go | 6 +-- .../v1beta1/zz_generated.conversion.go | 46 +++++++++++++++++++ .../v1beta1/zz_generated.deepcopy.go | 25 +++++++++- .../zz_generated.deepcopy.go | 25 ++++++++++ 6 files changed, 130 insertions(+), 14 deletions(-) create mode 100644 staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/conversion.go diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/conversion.go new file mode 100644 index 00000000000..07e3acc26de --- /dev/null +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/conversion.go @@ -0,0 +1,26 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/client-go/pkg/apis/clientauthentication" +) + +func Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { + return autoConvert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(in, out, s) +} diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/zz_generated.conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/zz_generated.conversion.go index 461c20b2982..b0e503af493 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/zz_generated.conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/zz_generated.conversion.go @@ -51,11 +51,6 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } - if err := s.AddGeneratedConversionFunc((*clientauthentication.ExecCredentialSpec)(nil), (*ExecCredentialSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { - return Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(a.(*clientauthentication.ExecCredentialSpec), b.(*ExecCredentialSpec), scope) - }); err != nil { - return err - } if err := s.AddGeneratedConversionFunc((*ExecCredentialStatus)(nil), (*clientauthentication.ExecCredentialStatus)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_ExecCredentialStatus_To_clientauthentication_ExecCredentialStatus(a.(*ExecCredentialStatus), b.(*clientauthentication.ExecCredentialStatus), scope) }); err != nil { @@ -76,6 +71,11 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddConversionFunc((*clientauthentication.ExecCredentialSpec)(nil), (*ExecCredentialSpec)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(a.(*clientauthentication.ExecCredentialSpec), b.(*ExecCredentialSpec), scope) + }); err != nil { + return err + } return nil } @@ -119,14 +119,10 @@ func Convert_v1alpha1_ExecCredentialSpec_To_clientauthentication_ExecCredentialS func autoConvert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { out.Response = (*Response)(unsafe.Pointer(in.Response)) out.Interactive = in.Interactive + // WARNING: in.Cluster requires manual conversion: does not exist in peer-type return nil } -// Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec is an autogenerated conversion function. -func Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { - return autoConvert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(in, out, s) -} - func autoConvert_v1alpha1_ExecCredentialStatus_To_clientauthentication_ExecCredentialStatus(in *ExecCredentialStatus, out *clientauthentication.ExecCredentialStatus, s conversion.Scope) error { out.ExpirationTimestamp = (*v1.Time)(unsafe.Pointer(in.ExpirationTimestamp)) out.Token = in.Token diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go index f543806ac95..4bfc05f3d75 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go @@ -17,10 +17,10 @@ limitations under the License. package v1beta1 import ( - conversion "k8s.io/apimachinery/pkg/conversion" - clientauthentication "k8s.io/client-go/pkg/apis/clientauthentication" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/client-go/pkg/apis/clientauthentication" ) func Convert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { - return nil + return autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in, out, s) } diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go index 0e533e46576..2432af55760 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go @@ -36,6 +36,16 @@ func init() { // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*Cluster)(nil), (*clientauthentication.Cluster)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_Cluster_To_clientauthentication_Cluster(a.(*Cluster), b.(*clientauthentication.Cluster), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*clientauthentication.Cluster)(nil), (*Cluster)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_clientauthentication_Cluster_To_v1beta1_Cluster(a.(*clientauthentication.Cluster), b.(*Cluster), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*ExecCredential)(nil), (*clientauthentication.ExecCredential)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_ExecCredential_To_clientauthentication_ExecCredential(a.(*ExecCredential), b.(*clientauthentication.ExecCredential), scope) }); err != nil { @@ -69,6 +79,36 @@ func RegisterConversions(s *runtime.Scheme) error { return nil } +func autoConvert_v1beta1_Cluster_To_clientauthentication_Cluster(in *Cluster, out *clientauthentication.Cluster, s conversion.Scope) error { + out.Server = in.Server + out.ServerName = in.ServerName + out.CAData = *(*[]byte)(unsafe.Pointer(&in.CAData)) + if err := runtime.Convert_runtime_RawExtension_To_runtime_Object(&in.Config, &out.Config, s); err != nil { + return err + } + return nil +} + +// Convert_v1beta1_Cluster_To_clientauthentication_Cluster is an autogenerated conversion function. +func Convert_v1beta1_Cluster_To_clientauthentication_Cluster(in *Cluster, out *clientauthentication.Cluster, s conversion.Scope) error { + return autoConvert_v1beta1_Cluster_To_clientauthentication_Cluster(in, out, s) +} + +func autoConvert_clientauthentication_Cluster_To_v1beta1_Cluster(in *clientauthentication.Cluster, out *Cluster, s conversion.Scope) error { + out.Server = in.Server + out.ServerName = in.ServerName + out.CAData = *(*[]byte)(unsafe.Pointer(&in.CAData)) + if err := runtime.Convert_runtime_Object_To_runtime_RawExtension(&in.Config, &out.Config, s); err != nil { + return err + } + return nil +} + +// Convert_clientauthentication_Cluster_To_v1beta1_Cluster is an autogenerated conversion function. +func Convert_clientauthentication_Cluster_To_v1beta1_Cluster(in *clientauthentication.Cluster, out *Cluster, s conversion.Scope) error { + return autoConvert_clientauthentication_Cluster_To_v1beta1_Cluster(in, out, s) +} + func autoConvert_v1beta1_ExecCredential_To_clientauthentication_ExecCredential(in *ExecCredential, out *clientauthentication.ExecCredential, s conversion.Scope) error { if err := Convert_v1beta1_ExecCredentialSpec_To_clientauthentication_ExecCredentialSpec(&in.Spec, &out.Spec, s); err != nil { return err @@ -96,6 +136,9 @@ func Convert_clientauthentication_ExecCredential_To_v1beta1_ExecCredential(in *c } func autoConvert_v1beta1_ExecCredentialSpec_To_clientauthentication_ExecCredentialSpec(in *ExecCredentialSpec, out *clientauthentication.ExecCredentialSpec, s conversion.Scope) error { + if err := Convert_v1beta1_Cluster_To_clientauthentication_Cluster(&in.Cluster, &out.Cluster, s); err != nil { + return err + } return nil } @@ -107,6 +150,9 @@ func Convert_v1beta1_ExecCredentialSpec_To_clientauthentication_ExecCredentialSp func autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { // WARNING: in.Response requires manual conversion: does not exist in peer-type // WARNING: in.Interactive requires manual conversion: does not exist in peer-type + if err := Convert_clientauthentication_Cluster_To_v1beta1_Cluster(&in.Cluster, &out.Cluster, s); err != nil { + return err + } return nil } diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.deepcopy.go index 736b8cf00dd..5fe29d47518 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.deepcopy.go @@ -24,11 +24,33 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *out = *in + if in.CAData != nil { + in, out := &in.CAData, &out.CAData + *out = make([]byte, len(*in)) + copy(*out, *in) + } + in.Config.DeepCopyInto(&out.Config) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExecCredential) DeepCopyInto(out *ExecCredential) { *out = *in out.TypeMeta = in.TypeMeta - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) if in.Status != nil { in, out := &in.Status, &out.Status *out = new(ExecCredentialStatus) @@ -58,6 +80,7 @@ func (in *ExecCredential) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExecCredentialSpec) DeepCopyInto(out *ExecCredentialSpec) { *out = *in + in.Cluster.DeepCopyInto(&out.Cluster) return } diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/zz_generated.deepcopy.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/zz_generated.deepcopy.go index c568a6fc8a4..f0c23b2f2fc 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/zz_generated.deepcopy.go @@ -24,6 +24,30 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *out = *in + if in.CAData != nil { + in, out := &in.CAData, &out.CAData + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Config != nil { + out.Config = in.Config.DeepCopyObject() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExecCredential) DeepCopyInto(out *ExecCredential) { *out = *in @@ -63,6 +87,7 @@ func (in *ExecCredentialSpec) DeepCopyInto(out *ExecCredentialSpec) { *out = new(Response) (*in).DeepCopyInto(*out) } + in.Cluster.DeepCopyInto(&out.Cluster) return } From 0284139660dcabfe8816b3136f12d79a5a236db1 Mon Sep 17 00:00:00 2001 From: Monis Khan Date: Wed, 6 May 2020 02:17:37 -0400 Subject: [PATCH 3/6] Generated bazel Signed-off-by: Monis Khan --- .../client-go/pkg/apis/clientauthentication/v1alpha1/BUILD | 1 + staging/src/k8s.io/client-go/rest/BUILD | 1 + 2 files changed, 2 insertions(+) diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/BUILD b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/BUILD index d3d66e5d2f4..8ad512b7cc8 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/BUILD +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/BUILD @@ -8,6 +8,7 @@ load( go_library( name = "go_default_library", srcs = [ + "conversion.go", "doc.go", "register.go", "types.go", diff --git a/staging/src/k8s.io/client-go/rest/BUILD b/staging/src/k8s.io/client-go/rest/BUILD index 17d6b3e1656..caf7eaa2b25 100644 --- a/staging/src/k8s.io/client-go/rest/BUILD +++ b/staging/src/k8s.io/client-go/rest/BUILD @@ -71,6 +71,7 @@ go_library( "//staging/src/k8s.io/apimachinery/pkg/util/net:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", + "//staging/src/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library", "//staging/src/k8s.io/client-go/pkg/version:go_default_library", "//staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec:go_default_library", "//staging/src/k8s.io/client-go/rest/watch:go_default_library", From c4299d15d5289768808034676858e76a177eeae5 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Thu, 29 Oct 2020 13:38:42 -0400 Subject: [PATCH 4/6] exec credential provider: ProvideClusterInfo and kubeconfig shadow - The main idea here is that we want to 1) prevent potentially large CA bundles from being set in an exec plugin's environment and 2) ensure that the exec plugin is getting everything it needs in order to talk to a cluster. - Avoid breaking existing manual declarations of rest.Config instances by moving exec Cluster to kubeconfig internal type. - Use client.authentication.k8s.io/exec to qualify exec cluster extension. - Deep copy the exec Cluster.Config when we copy a rest.Config. Signed-off-by: Andrew Keesler --- api/api-rules/violation_exceptions.list | 4 + .../pkg/util/webhook/authentication.go | 2 +- .../pkg/apis/clientauthentication/types.go | 38 +- .../v1alpha1/conversion.go | 1 + .../clientauthentication/v1alpha1/types.go | 2 +- .../v1beta1/conversion.go | 2 + .../clientauthentication/v1beta1/types.go | 34 +- .../v1beta1/zz_generated.conversion.go | 32 +- .../v1beta1/zz_generated.deepcopy.go | 10 +- .../zz_generated.deepcopy.go | 10 +- .../plugin/pkg/client/auth/exec/exec.go | 32 +- .../plugin/pkg/client/auth/exec/exec_test.go | 173 +++++--- staging/src/k8s.io/client-go/rest/BUILD | 4 + staging/src/k8s.io/client-go/rest/config.go | 45 +- .../src/k8s.io/client-go/rest/config_test.go | 30 +- staging/src/k8s.io/client-go/rest/exec.go | 85 ++++ .../src/k8s.io/client-go/rest/exec_test.go | 384 ++++++++++++++++++ .../src/k8s.io/client-go/rest/testdata/ca.pem | 1 + .../src/k8s.io/client-go/rest/transport.go | 23 +- .../client-go/tools/clientcmd/api/types.go | 36 +- .../client-go/tools/clientcmd/api/v1/types.go | 7 + .../api/v1/zz_generated.conversion.go | 23 +- .../clientcmd/api/zz_generated.deepcopy.go | 3 + .../tools/clientcmd/client_config.go | 11 +- .../tools/clientcmd/client_config_test.go | 14 +- 25 files changed, 831 insertions(+), 175 deletions(-) create mode 100644 staging/src/k8s.io/client-go/rest/exec.go create mode 100644 staging/src/k8s.io/client-go/rest/exec_test.go create mode 100644 staging/src/k8s.io/client-go/rest/testdata/ca.pem diff --git a/api/api-rules/violation_exceptions.list b/api/api-rules/violation_exceptions.list index cfe891e4ab7..5adee6a3dd3 100644 --- a/api/api-rules/violation_exceptions.list +++ b/api/api-rules/violation_exceptions.list @@ -466,6 +466,10 @@ API rule violation: names_match,k8s.io/apimachinery/pkg/runtime,Unknown,Raw API rule violation: names_match,k8s.io/apimachinery/pkg/util/intstr,IntOrString,IntVal API rule violation: names_match,k8s.io/apimachinery/pkg/util/intstr,IntOrString,StrVal API rule violation: names_match,k8s.io/apimachinery/pkg/util/intstr,IntOrString,Type +API rule violation: names_match,k8s.io/client-go/pkg/apis/clientauthentication/v1beta1,Cluster,CertificateAuthorityData +API rule violation: names_match,k8s.io/client-go/pkg/apis/clientauthentication/v1beta1,Cluster,InsecureSkipTLSVerify +API rule violation: names_match,k8s.io/client-go/pkg/apis/clientauthentication/v1beta1,Cluster,ProxyURL +API rule violation: names_match,k8s.io/client-go/pkg/apis/clientauthentication/v1beta1,Cluster,TLSServerName API rule violation: names_match,k8s.io/cloud-provider/app/apis/config/v1alpha1,CloudControllerManagerConfiguration,Generic API rule violation: names_match,k8s.io/cloud-provider/app/apis/config/v1alpha1,CloudControllerManagerConfiguration,KubeCloudShared API rule violation: names_match,k8s.io/cloud-provider/app/apis/config/v1alpha1,CloudControllerManagerConfiguration,NodeStatusUpdateFrequency 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 7d1a67c5070..042879dade9 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.Exec.ExecProvider = configAuthInfo.Exec.DeepCopy() + config.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 ec6f4f28cb5..d99ca818792 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 @@ -23,7 +23,7 @@ import ( // +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 @@ -38,7 +38,7 @@ type ExecCredential struct { Status *ExecCredentialStatus } -// ExecCredenitalSpec holds request and runtime specific information provided by +// ExecCredentialSpec holds request and runtime specific information provided by // the transport. type ExecCredentialSpec struct { // Response is populated when the transport encounters HTTP status codes, such as 401, @@ -51,9 +51,12 @@ type ExecCredentialSpec struct { // +optional Interactive bool - // Cluster contains information to allow an exec plugin to communicate - // with the kubernetes cluster being authenticated to. - Cluster Cluster + // Cluster contains information to allow an exec plugin to communicate with the + // kubernetes cluster being authenticated to. Note that Cluster is non-nil only + // when provideClusterInfo is set to true in the exec provider config (i.e., + // ExecConfig.ProvideClusterInfo). + // +optional + Cluster *Cluster } // ExecCredentialStatus holds credentials for the transport to use. @@ -83,19 +86,32 @@ type Response struct { // Cluster contains information to allow an exec plugin to communicate // with the kubernetes cluster being authenticated to. +// +// To ensure that this struct contains everything someone would need to communicate +// with a kubernetes cluster (just like they would via a kubeconfig), the fields +// should shadow "k8s.io/client-go/tools/clientcmd/api/v1".Cluster, with the exception +// of CertificateAuthority, since CA data will always be passed to the plugin as bytes. 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. + // TLSServerName 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 + TLSServerName string + // InsecureSkipTLSVerify skips the validity check for the server's certificate. + // This will make your HTTPS connections insecure. + // +optional + InsecureSkipTLSVerify bool // CAData contains PEM-encoded certificate authority certificates. // If empty, system roots should be used. // +listType=atomic // +optional - CAData []byte + CertificateAuthorityData []byte + // ProxyURL is the URL to the proxy to be used for all requests to this + // cluster. + // +optional + ProxyURL string // Config holds additional config data that is specific to the exec // plugin with regards to the cluster being authenticated to. // @@ -106,7 +122,7 @@ type Cluster struct { // cluster: // ... // extensions: - // - name: exec # reserved extension name for per cluster exec config + // - name: client.authentication.k8s.io/exec # reserved extension name for per cluster exec config // extension: // audience: 06e3fbd18de8 # arbitrary config // diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/conversion.go index 07e3acc26de..572e049f81d 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/conversion.go @@ -22,5 +22,6 @@ import ( ) func Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { + // This conversion intentionally omits the Cluster field which is only supported in newer versions. return autoConvert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(in, out, s) } diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/types.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/types.go index c714e2457ac..1080fddb7ed 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/types.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/types.go @@ -37,7 +37,7 @@ 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 { // Response is populated when the transport encounters HTTP status codes, such as 401, diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go index 4bfc05f3d75..441b7c44bd9 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/conversion.go @@ -22,5 +22,7 @@ import ( ) func Convert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { + // This conversion intentionally omits the Response and Interactive fields, which were only + // supported in v1alpha1. return autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in, out, s) } 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 e575ded23f6..a3119f154ad 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 @@ -40,9 +40,12 @@ type ExecCredential struct { // ExecCredentialSpec holds request and runtime specific information provided by // the transport. type ExecCredentialSpec struct { - // Cluster contains information to allow an exec plugin to communicate - // with the kubernetes cluster being authenticated to. - Cluster Cluster `json:"cluster"` + // Cluster contains information to allow an exec plugin to communicate with the + // kubernetes cluster being authenticated to. Note that Cluster is non-nil only + // when provideClusterInfo is set to true in the exec provider config (i.e., + // ExecConfig.ProvideClusterInfo). + // +optional + Cluster *Cluster `json:"cluster,omitempty"` } // ExecCredentialStatus holds credentials for the transport to use. @@ -64,19 +67,32 @@ type ExecCredentialStatus struct { // Cluster contains information to allow an exec plugin to communicate // with the kubernetes cluster being authenticated to. +// +// To ensure that this struct contains everything someone would need to communicate +// with a kubernetes cluster (just like they would via a kubeconfig), the fields +// should shadow "k8s.io/client-go/tools/clientcmd/api/v1".Cluster, with the exception +// of CertificateAuthority, since CA data will always be passed to the plugin as bytes. 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. + // TLSServerName 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"` + TLSServerName string `json:"tls-server-name,omitempty"` + // InsecureSkipTLSVerify skips the validity check for the server's certificate. + // This will make your HTTPS connections insecure. + // +optional + InsecureSkipTLSVerify bool `json:"insecure-skip-tls-verify,omitempty"` // CAData contains PEM-encoded certificate authority certificates. // If empty, system roots should be used. // +listType=atomic // +optional - CAData []byte `json:"caData,omitempty"` + CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"` + // ProxyURL is the URL to the proxy to be used for all requests to this + // cluster. + // +optional + ProxyURL string `json:"proxy-url,omitempty"` // Config holds additional config data that is specific to the exec // plugin with regards to the cluster being authenticated to. // @@ -87,7 +103,7 @@ type Cluster struct { // cluster: // ... // extensions: - // - name: exec # reserved extension name for per cluster exec config + // - name: client.authentication.k8s.io/exec # reserved extension name for per cluster exec config // extension: // audience: 06e3fbd18de8 # arbitrary config // diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go index 2432af55760..90f7935fefe 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.conversion.go @@ -81,8 +81,10 @@ func RegisterConversions(s *runtime.Scheme) error { func autoConvert_v1beta1_Cluster_To_clientauthentication_Cluster(in *Cluster, out *clientauthentication.Cluster, s conversion.Scope) error { out.Server = in.Server - out.ServerName = in.ServerName - out.CAData = *(*[]byte)(unsafe.Pointer(&in.CAData)) + out.TLSServerName = in.TLSServerName + out.InsecureSkipTLSVerify = in.InsecureSkipTLSVerify + out.CertificateAuthorityData = *(*[]byte)(unsafe.Pointer(&in.CertificateAuthorityData)) + out.ProxyURL = in.ProxyURL if err := runtime.Convert_runtime_RawExtension_To_runtime_Object(&in.Config, &out.Config, s); err != nil { return err } @@ -96,8 +98,10 @@ func Convert_v1beta1_Cluster_To_clientauthentication_Cluster(in *Cluster, out *c func autoConvert_clientauthentication_Cluster_To_v1beta1_Cluster(in *clientauthentication.Cluster, out *Cluster, s conversion.Scope) error { out.Server = in.Server - out.ServerName = in.ServerName - out.CAData = *(*[]byte)(unsafe.Pointer(&in.CAData)) + out.TLSServerName = in.TLSServerName + out.InsecureSkipTLSVerify = in.InsecureSkipTLSVerify + out.CertificateAuthorityData = *(*[]byte)(unsafe.Pointer(&in.CertificateAuthorityData)) + out.ProxyURL = in.ProxyURL if err := runtime.Convert_runtime_Object_To_runtime_RawExtension(&in.Config, &out.Config, s); err != nil { return err } @@ -136,8 +140,14 @@ func Convert_clientauthentication_ExecCredential_To_v1beta1_ExecCredential(in *c } func autoConvert_v1beta1_ExecCredentialSpec_To_clientauthentication_ExecCredentialSpec(in *ExecCredentialSpec, out *clientauthentication.ExecCredentialSpec, s conversion.Scope) error { - if err := Convert_v1beta1_Cluster_To_clientauthentication_Cluster(&in.Cluster, &out.Cluster, s); err != nil { - return err + if in.Cluster != nil { + in, out := &in.Cluster, &out.Cluster + *out = new(clientauthentication.Cluster) + if err := Convert_v1beta1_Cluster_To_clientauthentication_Cluster(*in, *out, s); err != nil { + return err + } + } else { + out.Cluster = nil } return nil } @@ -150,8 +160,14 @@ func Convert_v1beta1_ExecCredentialSpec_To_clientauthentication_ExecCredentialSp func autoConvert_clientauthentication_ExecCredentialSpec_To_v1beta1_ExecCredentialSpec(in *clientauthentication.ExecCredentialSpec, out *ExecCredentialSpec, s conversion.Scope) error { // WARNING: in.Response requires manual conversion: does not exist in peer-type // WARNING: in.Interactive requires manual conversion: does not exist in peer-type - if err := Convert_clientauthentication_Cluster_To_v1beta1_Cluster(&in.Cluster, &out.Cluster, s); err != nil { - return err + if in.Cluster != nil { + in, out := &in.Cluster, &out.Cluster + *out = new(Cluster) + if err := Convert_clientauthentication_Cluster_To_v1beta1_Cluster(*in, *out, s); err != nil { + return err + } + } else { + out.Cluster = nil } return nil } diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.deepcopy.go index 5fe29d47518..3a72ece0c6f 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1/zz_generated.deepcopy.go @@ -27,8 +27,8 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in - if in.CAData != nil { - in, out := &in.CAData, &out.CAData + if in.CertificateAuthorityData != nil { + in, out := &in.CertificateAuthorityData, &out.CertificateAuthorityData *out = make([]byte, len(*in)) copy(*out, *in) } @@ -80,7 +80,11 @@ func (in *ExecCredential) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ExecCredentialSpec) DeepCopyInto(out *ExecCredentialSpec) { *out = *in - in.Cluster.DeepCopyInto(&out.Cluster) + if in.Cluster != nil { + in, out := &in.Cluster, &out.Cluster + *out = new(Cluster) + (*in).DeepCopyInto(*out) + } return } diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/zz_generated.deepcopy.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/zz_generated.deepcopy.go index f0c23b2f2fc..045b07f5b05 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/zz_generated.deepcopy.go @@ -27,8 +27,8 @@ import ( // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Cluster) DeepCopyInto(out *Cluster) { *out = *in - if in.CAData != nil { - in, out := &in.CAData, &out.CAData + if in.CertificateAuthorityData != nil { + in, out := &in.CertificateAuthorityData, &out.CertificateAuthorityData *out = make([]byte, len(*in)) copy(*out, *in) } @@ -87,7 +87,11 @@ func (in *ExecCredentialSpec) DeepCopyInto(out *ExecCredentialSpec) { *out = new(Response) (*in).DeepCopyInto(*out) } - in.Cluster.DeepCopyInto(&out.Cluster) + if in.Cluster != nil { + in, out := &in.Cluster, &out.Cluster + *out = new(Cluster) + (*in).DeepCopyInto(*out) + } return } 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 dcf7c3877e6..08c59a8c2e5 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,10 +87,10 @@ func newCache() *cache { var spewConfig = &spew.ConfigState{DisableMethods: true, Indent: " "} -func cacheKey(conf *api.ExecConfig, cluster clientauthentication.Cluster) string { +func cacheKey(conf *api.ExecConfig, cluster *clientauthentication.Cluster) string { key := struct { conf *api.ExecConfig - cluster clientauthentication.Cluster + cluster *clientauthentication.Cluster }{ conf: conf, cluster: cluster, @@ -162,11 +162,11 @@ func (s *sometimes) Do(f func()) { } // GetAuthenticator returns an exec-based plugin for providing client credentials. -func GetAuthenticator(config *api.ExecConfig, cluster clientauthentication.Cluster) (*Authenticator, error) { +func GetAuthenticator(config *api.ExecConfig, cluster *clientauthentication.Cluster) (*Authenticator, error) { return newAuthenticator(globalCache, config, cluster) } -func newAuthenticator(c *cache, config *api.ExecConfig, cluster clientauthentication.Cluster) (*Authenticator, error) { +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 @@ -178,10 +178,11 @@ func newAuthenticator(c *cache, config *api.ExecConfig, cluster clientauthentica } a := &Authenticator{ - cmd: config.Command, - args: config.Args, - group: gv, - cluster: cluster, + cmd: config.Command, + args: config.Args, + group: gv, + cluster: cluster, + provideClusterInfo: config.ProvideClusterInfo, installHint: config.InstallHint, sometimes: &sometimes{ @@ -208,11 +209,12 @@ func newAuthenticator(c *cache, config *api.ExecConfig, cluster clientauthentica // 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 - cluster clientauthentication.Cluster + cmd string + args []string + group schema.GroupVersion + env []string + cluster *clientauthentication.Cluster + provideClusterInfo bool // 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 @@ -374,9 +376,11 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err Spec: clientauthentication.ExecCredentialSpec{ Response: r, Interactive: a.interactive, - Cluster: a.cluster, }, } + if a.provideClusterInfo { + cred.Spec.Cluster = a.cluster + } env := append(a.environ(), a.env...) data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred) 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 b575cfae3f6..6aee2e25216 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 @@ -115,12 +115,13 @@ func TestCacheKey(t *testing.T) { {Name: "5", Value: "6"}, {Name: "7", Value: "8"}, }, - APIVersion: "client.authentication.k8s.io/v1alpha1", + APIVersion: "client.authentication.k8s.io/v1alpha1", + ProvideClusterInfo: true, } - c1c := clientauthentication.Cluster{ - Server: "foo", - ServerName: "bar", - CAData: []byte("baz"), + c1c := &clientauthentication.Cluster{ + Server: "foo", + TLSServerName: "bar", + CertificateAuthorityData: []byte("baz"), Config: &runtime.Unknown{ TypeMeta: runtime.TypeMeta{ APIVersion: "", @@ -140,12 +141,13 @@ func TestCacheKey(t *testing.T) { {Name: "5", Value: "6"}, {Name: "7", Value: "8"}, }, - APIVersion: "client.authentication.k8s.io/v1alpha1", + APIVersion: "client.authentication.k8s.io/v1alpha1", + ProvideClusterInfo: true, } - c2c := clientauthentication.Cluster{ - Server: "foo", - ServerName: "bar", - CAData: []byte("baz"), + c2c := &clientauthentication.Cluster{ + Server: "foo", + TLSServerName: "bar", + CertificateAuthorityData: []byte("baz"), Config: &runtime.Unknown{ TypeMeta: runtime.TypeMeta{ APIVersion: "", @@ -166,10 +168,10 @@ func TestCacheKey(t *testing.T) { }, APIVersion: "client.authentication.k8s.io/v1alpha1", } - c3c := clientauthentication.Cluster{ - Server: "foo", - ServerName: "bar", - CAData: []byte("baz"), + c3c := &clientauthentication.Cluster{ + Server: "foo", + TLSServerName: "bar", + CertificateAuthorityData: []byte("baz"), Config: &runtime.Unknown{ TypeMeta: runtime.TypeMeta{ APIVersion: "", @@ -190,10 +192,10 @@ func TestCacheKey(t *testing.T) { }, APIVersion: "client.authentication.k8s.io/v1alpha1", } - c4c := clientauthentication.Cluster{ - Server: "foo", - ServerName: "bar", - CAData: []byte("baz"), + c4c := &clientauthentication.Cluster{ + Server: "foo", + TLSServerName: "bar", + CertificateAuthorityData: []byte("baz"), Config: &runtime.Unknown{ TypeMeta: runtime.TypeMeta{ APIVersion: "", @@ -205,10 +207,49 @@ func TestCacheKey(t *testing.T) { }, } + // c5/c5c should be the same as c4/c4c, except c5 has ProvideClusterInfo set to true. + c5 := &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", + ProvideClusterInfo: true, + } + c5c := &clientauthentication.Cluster{ + Server: "foo", + TLSServerName: "bar", + CertificateAuthorityData: []byte("baz"), + Config: &runtime.Unknown{ + TypeMeta: runtime.TypeMeta{ + APIVersion: "", + Kind: "", + }, + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"audience":"panda"}}`), + ContentEncoding: "", + ContentType: "application/json", + }, + } + + // c6 should be the same as c4, except c6 is passed with a nil cluster + c6 := &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", + } + key1 := cacheKey(c1, c1c) key2 := cacheKey(c2, c2c) key3 := cacheKey(c3, c3c) key4 := cacheKey(c4, c4c) + key5 := cacheKey(c5, c5c) + key6 := cacheKey(c6, nil) if key1 != key2 { t.Error("key1 and key2 didn't match") } @@ -221,6 +262,12 @@ func TestCacheKey(t *testing.T) { if key3 == key4 { t.Error("key3 and key4 matched") } + if key4 == key5 { + t.Error("key3 and key4 matched") + } + if key6 == key4 { + t.Error("key6 and key4 matched") + } } func compJSON(t *testing.T, got, want []byte) { @@ -246,7 +293,7 @@ func TestRefreshCreds(t *testing.T) { name string config api.ExecConfig exitCode int - cluster clientauthentication.Cluster + cluster *clientauthentication.Cluster output string interactive bool response *clientauthentication.Response @@ -470,12 +517,7 @@ func TestRefreshCreds(t *testing.T) { wantInput: `{ "kind": "ExecCredential", "apiVersion": "client.authentication.k8s.io/v1beta1", - "spec": { - "cluster": { - "server": "", - "config": null - } - } + "spec": {} }`, output: `{ "kind": "ExecCredential", @@ -494,12 +536,7 @@ func TestRefreshCreds(t *testing.T) { wantInput: `{ "kind": "ExecCredential", "apiVersion": "client.authentication.k8s.io/v1beta1", - "spec": { - "cluster": { - "server": "", - "config": null - } - } + "spec": {} }`, output: `{ "kind": "ExecCredential", @@ -572,10 +609,10 @@ func TestRefreshCreds(t *testing.T) { config: api.ExecConfig{ APIVersion: "client.authentication.k8s.io/v1alpha1", }, - cluster: clientauthentication.Cluster{ - Server: "foo", - ServerName: "bar", - CAData: []byte("baz"), + cluster: &clientauthentication.Cluster{ + Server: "foo", + TLSServerName: "bar", + CertificateAuthorityData: []byte("baz"), Config: &runtime.Unknown{ TypeMeta: runtime.TypeMeta{ APIVersion: "", @@ -616,14 +653,15 @@ func TestRefreshCreds(t *testing.T) { wantCreds: credentials{token: "foo-bar"}, }, { - name: "beta-with-cluster-is-serialized", + name: "beta-with-cluster-and-provide-cluster-info-is-serialized", config: api.ExecConfig{ - APIVersion: "client.authentication.k8s.io/v1beta1", + APIVersion: "client.authentication.k8s.io/v1beta1", + ProvideClusterInfo: true, }, - cluster: clientauthentication.Cluster{ - Server: "foo", - ServerName: "bar", - CAData: []byte("baz"), + cluster: &clientauthentication.Cluster{ + Server: "foo", + TLSServerName: "bar", + CertificateAuthorityData: []byte("baz"), Config: &runtime.Unknown{ TypeMeta: runtime.TypeMeta{ APIVersion: "", @@ -646,8 +684,8 @@ func TestRefreshCreds(t *testing.T) { "spec": { "cluster": { "server": "foo", - "serverName": "bar", - "caData": "YmF6", + "tls-server-name": "bar", + "certificate-authority-data": "YmF6", "config": { "apiVersion": "group/v1", "kind": "PluginConfig", @@ -667,6 +705,45 @@ func TestRefreshCreds(t *testing.T) { }`, wantCreds: credentials{token: "foo-bar"}, }, + { + name: "beta-with-cluster-and-without-provide-cluster-info-is-not-serialized", + config: api.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + cluster: &clientauthentication.Cluster{ + Server: "foo", + TLSServerName: "bar", + CertificateAuthorityData: []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": {} + }`, + output: `{ + "kind": "ExecCredential", + "apiVersion": "client.authentication.k8s.io/v1beta1", + "status": { + "token": "foo-bar" + } + }`, + wantCreds: credentials{token: "foo-bar"}, + }, } for _, test := range tests { @@ -763,7 +840,7 @@ func TestRoundTripper(t *testing.T) { Command: "./testdata/test-plugin.sh", APIVersion: "client.authentication.k8s.io/v1alpha1", } - a, err := newAuthenticator(newCache(), &c, clientauthentication.Cluster{}) + a, err := newAuthenticator(newCache(), &c, nil) if err != nil { t.Fatal(err) } @@ -849,7 +926,7 @@ func TestTokenPresentCancelsExecAction(t *testing.T) { a, err := newAuthenticator(newCache(), &api.ExecConfig{ Command: "./testdata/test-plugin.sh", APIVersion: "client.authentication.k8s.io/v1alpha1", - }) + }, nil) if err != nil { t.Fatal(err) } @@ -888,7 +965,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{}) + }, nil) if err != nil { t.Fatal(err) } @@ -978,7 +1055,7 @@ func TestConcurrentUpdateTransportConfig(t *testing.T) { Command: "./testdata/test-plugin.sh", APIVersion: "client.authentication.k8s.io/v1alpha1", } - a, err := newAuthenticator(newCache(), &c, clientauthentication.Cluster{}) + a, err := newAuthenticator(newCache(), &c, nil) if err != nil { t.Fatal(err) } @@ -1045,7 +1122,7 @@ func TestInstallHintRateLimit(t *testing.T) { APIVersion: "client.authentication.k8s.io/v1alpha1", InstallHint: "some install hint", } - a, err := newAuthenticator(newCache(), &c) + a, err := newAuthenticator(newCache(), &c, nil) if err != nil { t.Fatal(err) } diff --git a/staging/src/k8s.io/client-go/rest/BUILD b/staging/src/k8s.io/client-go/rest/BUILD index caf7eaa2b25..e2607d93b13 100644 --- a/staging/src/k8s.io/client-go/rest/BUILD +++ b/staging/src/k8s.io/client-go/rest/BUILD @@ -11,11 +11,13 @@ go_test( srcs = [ "client_test.go", "config_test.go", + "exec_test.go", "plugin_test.go", "request_test.go", "url_utils_test.go", "urlbackoff_test.go", ], + data = glob(["testdata/**"]), embed = [":go_default_library"], deps = [ "//staging/src/k8s.io/api/core/v1:go_default_library", @@ -33,6 +35,7 @@ go_test( "//staging/src/k8s.io/apimachinery/pkg/util/intstr:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/watch:go_default_library", "//staging/src/k8s.io/client-go/kubernetes/scheme:go_default_library", + "//staging/src/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library", "//staging/src/k8s.io/client-go/rest/watch:go_default_library", "//staging/src/k8s.io/client-go/tools/clientcmd/api:go_default_library", "//staging/src/k8s.io/client-go/transport:go_default_library", @@ -50,6 +53,7 @@ go_library( srcs = [ "client.go", "config.go", + "exec.go", "plugin.go", "request.go", "transport.go", diff --git a/staging/src/k8s.io/client-go/rest/config.go b/staging/src/k8s.io/client-go/rest/config.go index 654cd1f9f44..c9d72288776 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. - Exec Exec + ExecProvider *clientcmdapi.ExecConfig // TLSClientConfig contains settings to enable transport layer security TLSClientConfig @@ -192,40 +192,12 @@ 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} + if cc.ExecProvider != nil && cc.ExecProvider.Config != nil { + cc.ExecProvider.Config = sanitizedObject{Object: cc.ExecProvider.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. @@ -627,7 +599,7 @@ func AnonymousClientConfig(config *Config) *Config { // CopyConfig returns a copy of the given config func CopyConfig(config *Config) *Config { - return &Config{ + c := &Config{ Host: config.Host, APIPath: config.APIPath, ContentConfig: config.ContentConfig, @@ -642,10 +614,7 @@ func CopyConfig(config *Config) *Config { }, AuthProvider: config.AuthProvider, AuthConfigPersister: config.AuthConfigPersister, - Exec: Exec{ - ExecProvider: config.Exec.ExecProvider, - Config: config.Exec.Config, - }, + ExecProvider: config.ExecProvider, TLSClientConfig: TLSClientConfig{ Insecure: config.TLSClientConfig.Insecure, ServerName: config.TLSClientConfig.ServerName, @@ -669,4 +638,8 @@ func CopyConfig(config *Config) *Config { Dial: config.Dial, Proxy: config.Proxy, } + if config.ExecProvider != nil && config.ExecProvider.Config != nil { + c.ExecProvider.Config = config.ExecProvider.Config.DeepCopyObject() + } + return c } 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 a691b13f06c..bd857e0adfa 100644 --- a/staging/src/k8s.io/client-go/rest/config_test.go +++ b/staging/src/k8s.io/client-go/rest/config_test.go @@ -358,8 +358,7 @@ func TestAnonymousConfig(t *testing.T) { expected.Password = "" expected.AuthProvider = nil expected.AuthConfigPersister = nil - expected.Exec.ExecProvider = nil - expected.Exec.Config = nil + expected.ExecProvider = nil expected.TLSClientConfig.CertData = nil expected.TLSClientConfig.CertFile = "" expected.TLSClientConfig.KeyData = nil @@ -535,12 +534,10 @@ func TestConfigStringer(t *testing.T) { AuthProvider: &clientcmdapi.AuthProviderConfig{ Config: map[string]string{"secret": "s3cr3t"}, }, - Exec: Exec{ - ExecProvider: &clientcmdapi.ExecConfig{ - Args: []string{"secret"}, - Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, - }, - Config: &runtime.Unknown{Raw: []byte("super secret password")}, + ExecProvider: &clientcmdapi.ExecConfig{ + Args: []string{"secret"}, + Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, + Config: &runtime.Unknown{Raw: []byte("here is some config data")}, }, }, expectContent: []string{ @@ -559,7 +556,7 @@ func TestConfigStringer(t *testing.T) { formatBytes([]byte("fake key")), "secret", "s3cr3t", - "super secret password", + "here is some config data", formatBytes([]byte("super secret password")), }, }, @@ -603,13 +600,12 @@ func TestConfigSprint(t *testing.T) { Config: map[string]string{"secret": "s3cr3t"}, }, AuthConfigPersister: fakeAuthProviderConfigPersister{}, - 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")}, + ExecProvider: &clientcmdapi.ExecConfig{ + Command: "sudo", + Args: []string{"secret"}, + Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}}, + ProvideClusterInfo: true, + Config: &runtime.Unknown{Raw: []byte("super secret password")}, }, TLSClientConfig: TLSClientConfig{ CertFile: "a.crt", @@ -630,7 +626,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 ---), 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)}`, + `&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.ExecConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: "", ProvideClusterInfo: true, 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/exec.go b/staging/src/k8s.io/client-go/rest/exec.go new file mode 100644 index 00000000000..5f3b43c55a8 --- /dev/null +++ b/staging/src/k8s.io/client-go/rest/exec.go @@ -0,0 +1,85 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rest + +import ( + "fmt" + "net/http" + "net/url" + + "k8s.io/client-go/pkg/apis/clientauthentication" + clientauthenticationapi "k8s.io/client-go/pkg/apis/clientauthentication" +) + +// This file contains Config logic related to exec credential plugins. + +// ConfigToExecCluster creates a clientauthenticationapi.Cluster with the corresponding fields from +// the provided Config. +func ConfigToExecCluster(config *Config) (*clientauthenticationapi.Cluster, error) { + caData, err := dataFromSliceOrFile(config.CAData, config.CAFile) + if err != nil { + return nil, fmt.Errorf("failed to load CA bundle for execProvider: %v", err) + } + + var proxyURL string + if config.Proxy != nil { + req, err := http.NewRequest("", config.Host, nil) + if err != nil { + return nil, fmt.Errorf("failed to create proxy URL request for execProvider: %w", err) + } + url, err := config.Proxy(req) + if err != nil { + return nil, fmt.Errorf("failed to get proxy URL for execProvider: %w", err) + } + if url != nil { + proxyURL = url.String() + } + } + + return &clientauthentication.Cluster{ + Server: config.Host, + TLSServerName: config.ServerName, + InsecureSkipTLSVerify: config.Insecure, + CertificateAuthorityData: caData, + ProxyURL: proxyURL, + Config: config.ExecProvider.Config, + }, nil +} + +// ExecClusterToConfig creates a Config with the corresponding fields from the provided +// clientauthenticationapi.Cluster. The returned Config will be anonymous (i.e., it will not have +// any authentication-related fields set). +func ExecClusterToConfig(cluster *clientauthentication.Cluster) (*Config, error) { + var proxy func(*http.Request) (*url.URL, error) + if cluster.ProxyURL != "" { + proxyURL, err := url.Parse(cluster.ProxyURL) + if err != nil { + return nil, fmt.Errorf("cannot parse proxy URL: %w", err) + } + proxy = http.ProxyURL(proxyURL) + } + + return &Config{ + Host: cluster.Server, + TLSClientConfig: TLSClientConfig{ + Insecure: cluster.InsecureSkipTLSVerify, + ServerName: cluster.TLSServerName, + CAData: cluster.CertificateAuthorityData, + }, + Proxy: proxy, + }, nil +} diff --git a/staging/src/k8s.io/client-go/rest/exec_test.go b/staging/src/k8s.io/client-go/rest/exec_test.go new file mode 100644 index 00000000000..705b776aa2e --- /dev/null +++ b/staging/src/k8s.io/client-go/rest/exec_test.go @@ -0,0 +1,384 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rest + +import ( + "context" + "errors" + "net" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + fuzz "github.com/google/gofuzz" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/pkg/apis/clientauthentication" + clientauthenticationapi "k8s.io/client-go/pkg/apis/clientauthentication" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/transport" + "k8s.io/client-go/util/flowcontrol" +) + +func TestConfigToExecCluster(t *testing.T) { + t.Parallel() + + const proxyURL = "https://some-proxy-url.com/tuna/fish" + proxy := func(r *http.Request) (*url.URL, error) { + return url.Parse(proxyURL) + } + + tests := []struct { + name string + in Config + wantOut clientauthenticationapi.Cluster + wantErrorPrefix string + }{ + { + name: "CA data from memory", + in: Config{ + ExecProvider: &clientcmdapi.ExecConfig{ + ProvideClusterInfo: true, + Config: &runtime.Unknown{ + Raw: []byte("stuff"), + }, + }, + Host: "some-host", + TLSClientConfig: TLSClientConfig{ + ServerName: "some-server-name", + Insecure: true, + CAData: []byte("some-ca-data"), + }, + Proxy: proxy, + }, + wantOut: clientauthenticationapi.Cluster{ + Server: "some-host", + TLSServerName: "some-server-name", + InsecureSkipTLSVerify: true, + CertificateAuthorityData: []byte("some-ca-data"), + ProxyURL: proxyURL, + Config: &runtime.Unknown{ + Raw: []byte("stuff"), + }, + }, + }, + { + name: "CA data from file", + in: Config{ + ExecProvider: &clientcmdapi.ExecConfig{ + ProvideClusterInfo: true, + Config: &runtime.Unknown{ + Raw: []byte("stuff"), + }, + }, + Host: "some-host", + TLSClientConfig: TLSClientConfig{ + ServerName: "some-server-name", + Insecure: true, + CAFile: "testdata/ca.pem", + }, + Proxy: proxy, + }, + wantOut: clientauthenticationapi.Cluster{ + Server: "some-host", + TLSServerName: "some-server-name", + InsecureSkipTLSVerify: true, + CertificateAuthorityData: []byte("a CA bundle lives here"), + ProxyURL: proxyURL, + Config: &runtime.Unknown{ + Raw: []byte("stuff"), + }, + }, + }, + { + name: "no CA data", + in: Config{ + ExecProvider: &clientcmdapi.ExecConfig{ + ProvideClusterInfo: true, + }, + TLSClientConfig: TLSClientConfig{ + CAFile: "this-file-does-not-exist", + }, + }, + wantErrorPrefix: "failed to load CA bundle for execProvider: ", + }, + { + name: "nil proxy", + in: Config{ + ExecProvider: &clientcmdapi.ExecConfig{ + ProvideClusterInfo: true, + Config: &runtime.Unknown{ + Raw: []byte("stuff"), + }, + }, + Host: "some-host", + TLSClientConfig: TLSClientConfig{ + ServerName: "some-server-name", + Insecure: true, + CAFile: "testdata/ca.pem", + }, + }, + wantOut: clientauthenticationapi.Cluster{ + Server: "some-host", + TLSServerName: "some-server-name", + InsecureSkipTLSVerify: true, + CertificateAuthorityData: []byte("a CA bundle lives here"), + Config: &runtime.Unknown{ + Raw: []byte("stuff"), + }, + }, + }, + { + name: "bad proxy", + in: Config{ + ExecProvider: &clientcmdapi.ExecConfig{ + ProvideClusterInfo: true, + }, + Proxy: func(_ *http.Request) (*url.URL, error) { + return nil, errors.New("some proxy error") + }, + }, + wantErrorPrefix: "failed to get proxy URL for execProvider: some proxy error", + }, + { + name: "proxy returns nil", + in: Config{ + ExecProvider: &clientcmdapi.ExecConfig{ + ProvideClusterInfo: true, + }, + Proxy: func(_ *http.Request) (*url.URL, error) { + return nil, nil + }, + Host: "some-host", + TLSClientConfig: TLSClientConfig{ + ServerName: "some-server-name", + Insecure: true, + CAFile: "testdata/ca.pem", + }, + }, + wantOut: clientauthenticationapi.Cluster{ + Server: "some-host", + TLSServerName: "some-server-name", + InsecureSkipTLSVerify: true, + CertificateAuthorityData: []byte("a CA bundle lives here"), + }, + }, + { + name: "invalid config host", + in: Config{ + ExecProvider: &clientcmdapi.ExecConfig{ + ProvideClusterInfo: true, + }, + Proxy: func(_ *http.Request) (*url.URL, error) { + return nil, nil + }, + Host: "invalid-config-host\n", + }, + wantErrorPrefix: "failed to create proxy URL request for execProvider: ", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + out, err := ConfigToExecCluster(&test.in) + if test.wantErrorPrefix != "" { + if err == nil { + t.Error("wanted error") + } else if !strings.HasPrefix(err.Error(), test.wantErrorPrefix) { + t.Errorf("wanted error prefix %q, got %q", test.wantErrorPrefix, err.Error()) + } + } else if diff := cmp.Diff(&test.wantOut, out); diff != "" { + t.Errorf("unexpected returned cluster: -got, +want:\n %s", diff) + } + }) + } +} + +func TestConfigToExecClusterRoundtrip(t *testing.T) { + t.Parallel() + + f := fuzz.New().NilChance(0.5).NumElements(1, 1) + f.Funcs( + func(r *runtime.Codec, f fuzz.Continue) { + codec := &fakeCodec{} + f.Fuzz(codec) + *r = codec + }, + func(r *http.RoundTripper, f fuzz.Continue) { + roundTripper := &fakeRoundTripper{} + f.Fuzz(roundTripper) + *r = roundTripper + }, + func(fn *func(http.RoundTripper) http.RoundTripper, f fuzz.Continue) { + *fn = fakeWrapperFunc + }, + func(fn *transport.WrapperFunc, f fuzz.Continue) { + *fn = fakeWrapperFunc + }, + func(r *runtime.NegotiatedSerializer, f fuzz.Continue) { + serializer := &fakeNegotiatedSerializer{} + f.Fuzz(serializer) + *r = serializer + }, + func(r *flowcontrol.RateLimiter, f fuzz.Continue) { + limiter := &fakeLimiter{} + f.Fuzz(limiter) + *r = limiter + }, + func(h *WarningHandler, f fuzz.Continue) { + *h = &fakeWarningHandler{} + }, + // Authentication does not require fuzzer + func(r *AuthProviderConfigPersister, f fuzz.Continue) {}, + func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) { + r.Config = map[string]string{} + }, + func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) { + *r = fakeDialFunc + }, + 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 < 100; i++ { + expected := &Config{} + f.Fuzz(expected) + + // This is the list of known fields that this roundtrip doesn't care about. We should add new + // fields to this list if we don't want to roundtrip them on exec cluster conversion. + expected.APIPath = "" + expected.ContentConfig = ContentConfig{} + expected.Username = "" + expected.Password = "" + expected.BearerToken = "" + expected.BearerTokenFile = "" + expected.Impersonate = ImpersonationConfig{} + expected.AuthProvider = nil + expected.AuthConfigPersister = nil + expected.ExecProvider = &clientcmdapi.ExecConfig{} // ConfigToExecCluster assumes != nil. + expected.TLSClientConfig.CertFile = "" + expected.TLSClientConfig.KeyFile = "" + expected.TLSClientConfig.CAFile = "" + expected.TLSClientConfig.CertData = nil + expected.TLSClientConfig.KeyData = nil + expected.TLSClientConfig.NextProtos = nil + expected.UserAgent = "" + expected.DisableCompression = false + expected.Transport = nil + expected.WrapTransport = nil + expected.QPS = 0.0 + expected.Burst = 0 + expected.RateLimiter = nil + expected.WarningHandler = nil + expected.Timeout = 0 + expected.Dial = nil + + // Manually set URLs so we don't get an error when parsing these during the roundtrip. + if expected.Host != "" { + expected.Host = "https://some-server-url.com/tuna/fish" + } + if expected.Proxy != nil { + expected.Proxy = func(_ *http.Request) (*url.URL, error) { + return url.Parse("https://some-proxy-url.com/tuna/fish") + } + } + + cluster, err := ConfigToExecCluster(expected) + if err != nil { + t.Fatal(err) + } + + actual, err := ExecClusterToConfig(cluster) + if err != nil { + t.Fatal(err) + } + + if actual.Proxy != nil { + actualURL, actualErr := actual.Proxy(nil) + expectedURL, expectedErr := expected.Proxy(nil) + if actualErr != nil { + t.Fatalf("failed to get url from actual proxy func: %s", actualErr.Error()) + } + if expectedErr != nil { + t.Fatalf("failed to get url from expected proxy func: %s", actualErr.Error()) + } + if diff := cmp.Diff(actualURL, expectedURL); diff != "" { + t.Fatal("we dropped the Config.Proxy field during conversion") + } + } + actual.Proxy = nil + expected.Proxy = nil + + if actual.ExecProvider != nil { + t.Fatal("expected actual Config.ExecProvider field to be set to nil") + } + actual.ExecProvider = nil + expected.ExecProvider = nil + + if diff := cmp.Diff(actual, expected); diff != "" { + t.Fatalf("we dropped some Config fields during roundtrip, -got, +want:\n %s", diff) + } + } +} + +func TestExecClusterToConfigRoundtrip(t *testing.T) { + t.Parallel() + + f := fuzz.New().NilChance(0.5).NumElements(1, 1) + f.Funcs( + func(r *runtime.Object, f fuzz.Continue) { + // We don't expect the clientauthentication.Cluster.Config to show up in the Config that + // comes back from the roundtrip, so just set it to nil. + *r = nil + }, + ) + for i := 0; i < 100; i++ { + expected := &clientauthentication.Cluster{} + f.Fuzz(expected) + + // Manually set URLs so we don't get an error when parsing these during the roundtrip. + if expected.Server != "" { + expected.Server = "https://some-server-url.com/tuna/fish" + } + if expected.ProxyURL != "" { + expected.ProxyURL = "https://some-proxy-url.com/tuna/fish" + } + + config, err := ExecClusterToConfig(expected) + if err != nil { + t.Fatal(err) + } + + // ConfigToExecCluster assumes config.ExecProvider is not nil. + config.ExecProvider = &clientcmdapi.ExecConfig{} + + actual, err := ConfigToExecCluster(config) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(actual, expected); diff != "" { + t.Fatalf("we dropped some Cluster fields during roundtrip: -got, +want:\n %s", diff) + } + } +} diff --git a/staging/src/k8s.io/client-go/rest/testdata/ca.pem b/staging/src/k8s.io/client-go/rest/testdata/ca.pem new file mode 100644 index 00000000000..4c8eaeb7384 --- /dev/null +++ b/staging/src/k8s.io/client-go/rest/testdata/ca.pem @@ -0,0 +1 @@ +a CA bundle lives here \ No newline at end of file diff --git a/staging/src/k8s.io/client-go/rest/transport.go b/staging/src/k8s.io/client-go/rest/transport.go index 22407273685..87792750ad3 100644 --- a/staging/src/k8s.io/client-go/rest/transport.go +++ b/staging/src/k8s.io/client-go/rest/transport.go @@ -19,7 +19,6 @@ package rest import ( "crypto/tls" "errors" - "fmt" "net/http" "k8s.io/client-go/pkg/apis/clientauthentication" @@ -91,22 +90,20 @@ func (c *Config) TransportConfig() (*transport.Config, error) { Proxy: c.Proxy, } - if c.Exec.ExecProvider != nil && c.AuthProvider != nil { + if c.ExecProvider != nil && c.AuthProvider != nil { return nil, errors.New("execProvider and authProvider cannot be used in combination") } - 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) + if c.ExecProvider != nil { + var cluster *clientauthentication.Cluster + if c.ExecProvider.ProvideClusterInfo { + var err error + cluster, err = ConfigToExecCluster(c) + if err != nil { + return nil, 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) + provider, err := exec.GetAuthenticator(c.ExecProvider, cluster) if err != nil { return nil, err } diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go index 829424dcf3f..d3d39530497 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/types.go @@ -215,6 +215,36 @@ type ExecConfig struct { // present. For example, `brew install foo-cli` might be a good InstallHint for // foo-cli on Mac OS systems. InstallHint string `json:"installHint,omitempty"` + + // ProvideClusterInfo determines whether or not to provide cluster information, + // which could potentially contain very large CA data, to this exec plugin as a + // part of the KUBERNETES_EXEC_INFO environment variable. By default, it is set + // to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for + // reading this environment variable. + ProvideClusterInfo bool `json:"provideClusterInfo"` + + // 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: client.authentication.k8s.io/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. + // +k8s:conversion-gen=false + Config runtime.Object } var _ fmt.Stringer = new(ExecConfig) @@ -237,7 +267,11 @@ func (c ExecConfig) String() string { if len(c.Env) > 0 { env = "[]ExecEnvVar{--- REDACTED ---}" } - return fmt.Sprintf("api.AuthProviderConfig{Command: %q, Args: %#v, Env: %s, APIVersion: %q}", c.Command, args, env, c.APIVersion) + config := "runtime.Object(nil)" + if c.Config != nil { + config = "runtime.Object(--- REDACTED ---)" + } + return fmt.Sprintf("api.ExecConfig{Command: %q, Args: %#v, Env: %s, APIVersion: %q, ProvideClusterInfo: %t, Config: %s}", c.Command, args, env, c.APIVersion, c.ProvideClusterInfo, config) } // ExecEnvVar is used for setting environment variables when executing an exec-based diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go index 0395f860f36..70d8c20d5e9 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go @@ -214,6 +214,13 @@ type ExecConfig struct { // present. For example, `brew install foo-cli` might be a good InstallHint for // foo-cli on Mac OS systems. InstallHint string `json:"installHint,omitempty"` + + // ProvideClusterInfo determines whether or not to provide cluster information, + // which could potentially contain very large CA data, to this exec plugin as a + // part of the KUBERNETES_EXEC_INFO environment variable. By default, it is set + // to false. Package k8s.io/client-go/tools/auth/exec provides helper methods for + // reading this environment variable. + ProvideClusterInfo bool `json:"provideClusterInfo"` } // ExecEnvVar is used for setting environment variables when executing an exec-based diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go index bf9eaeca322..26e96529d38 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go @@ -171,7 +171,15 @@ func autoConvert_v1_AuthInfo_To_api_AuthInfo(in *AuthInfo, out *api.AuthInfo, s out.Username = in.Username out.Password = in.Password out.AuthProvider = (*api.AuthProviderConfig)(unsafe.Pointer(in.AuthProvider)) - out.Exec = (*api.ExecConfig)(unsafe.Pointer(in.Exec)) + if in.Exec != nil { + in, out := &in.Exec, &out.Exec + *out = new(api.ExecConfig) + if err := Convert_v1_ExecConfig_To_api_ExecConfig(*in, *out, s); err != nil { + return err + } + } else { + out.Exec = nil + } if err := Convert_Slice_v1_NamedExtension_To_Map_string_To_runtime_Object(&in.Extensions, &out.Extensions, s); err != nil { return err } @@ -197,7 +205,15 @@ func autoConvert_api_AuthInfo_To_v1_AuthInfo(in *api.AuthInfo, out *AuthInfo, s out.Username = in.Username out.Password = in.Password out.AuthProvider = (*AuthProviderConfig)(unsafe.Pointer(in.AuthProvider)) - out.Exec = (*ExecConfig)(unsafe.Pointer(in.Exec)) + if in.Exec != nil { + in, out := &in.Exec, &out.Exec + *out = new(ExecConfig) + if err := Convert_api_ExecConfig_To_v1_ExecConfig(*in, *out, s); err != nil { + return err + } + } else { + out.Exec = nil + } if err := Convert_Map_string_To_runtime_Object_To_Slice_v1_NamedExtension(&in.Extensions, &out.Extensions, s); err != nil { return err } @@ -359,6 +375,7 @@ func autoConvert_v1_ExecConfig_To_api_ExecConfig(in *ExecConfig, out *api.ExecCo out.Env = *(*[]api.ExecEnvVar)(unsafe.Pointer(&in.Env)) out.APIVersion = in.APIVersion out.InstallHint = in.InstallHint + out.ProvideClusterInfo = in.ProvideClusterInfo return nil } @@ -373,6 +390,8 @@ func autoConvert_api_ExecConfig_To_v1_ExecConfig(in *api.ExecConfig, out *ExecCo out.Env = *(*[]ExecEnvVar)(unsafe.Pointer(&in.Env)) out.APIVersion = in.APIVersion out.InstallHint = in.InstallHint + out.ProvideClusterInfo = in.ProvideClusterInfo + // INFO: in.Config opted out of conversion generation return nil } diff --git a/staging/src/k8s.io/client-go/tools/clientcmd/api/zz_generated.deepcopy.go b/staging/src/k8s.io/client-go/tools/clientcmd/api/zz_generated.deepcopy.go index 3240a7a98dc..a04de6260ba 100644 --- a/staging/src/k8s.io/client-go/tools/clientcmd/api/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/client-go/tools/clientcmd/api/zz_generated.deepcopy.go @@ -267,6 +267,9 @@ func (in *ExecConfig) DeepCopyInto(out *ExecConfig) { *out = make([]ExecEnvVar, len(*in)) copy(*out, *in) } + if in.Config != nil { + out.Config = in.Config.DeepCopyObject() + } return } 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 361ad43d7a9..8a8b80363b4 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 @@ -34,6 +34,11 @@ import ( "github.com/imdario/mergo" ) +const ( + // clusterExtensionKey is reserved in the cluster extensions list for exec plugin config. + clusterExtensionKey = "client.authentication.k8s.io/exec" +) + var ( // ClusterDefaults has the same behavior as the old EnvVar and DefaultCluster fields // DEPRECATED will be replaced @@ -269,9 +274,9 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI mergedConfig.AuthConfigPersister = persistAuthConfig } if configAuthInfo.Exec != nil { - mergedConfig.Exec.ExecProvider = configAuthInfo.Exec + mergedConfig.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 + mergedConfig.ExecProvider.Config = configClusterInfo.Extensions[clusterExtensionKey] } // if there still isn't enough information to authenticate the user, try prompting @@ -314,7 +319,7 @@ func canIdentifyUser(config restclient.Config) bool { (len(config.CertFile) > 0 || len(config.CertData) > 0) || len(config.BearerToken) > 0 || config.AuthProvider != nil || - config.Exec.ExecProvider != nil + config.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 08cf75d83e6..7ed3a1d7c2e 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 @@ -836,7 +836,7 @@ clusters: - cluster: server: https://localhost:8080 extensions: - - name: exec + - name: client.authentication.k8s.io/exec extension: audience: foo other: bar @@ -858,6 +858,7 @@ users: - arg-1 - arg-2 command: foo-command + provideClusterInfo: true ` tmpfile, err := ioutil.TempFile("", "kubeconfig") if err != nil { @@ -871,15 +872,18 @@ users: if err != nil { t.Error(err) } - 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"}) + 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 !config.ExecProvider.ProvideClusterInfo { + t.Error("Wanted provider cluster info to be true") } 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) + if !reflect.DeepEqual(config.ExecProvider.Config, want) { + t.Errorf("Got config %v when it should be %v\n", config.ExecProvider.Config, want) } } From 875a46bd7c1b79f1fae9cd189eec5fc9c3fbf1bc Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Thu, 29 Oct 2020 13:38:50 -0400 Subject: [PATCH 5/6] exec credential provider: k8s.io/client-go/tools/auth/exec helper Exec plugin implementations should be able to call LoadExecCredentialFromEnv() in order to get everything they need to operate (i.e., cluster information (as long as it is passed in) and optionally per-cluster configuration). Signed-off-by: Andrew Keesler --- staging/src/k8s.io/client-go/tools/auth/BUILD | 5 +- .../k8s.io/client-go/tools/auth/exec/BUILD | 54 +++++ .../k8s.io/client-go/tools/auth/exec/exec.go | 110 +++++++++ .../client-go/tools/auth/exec/exec_test.go | 210 ++++++++++++++++++ .../client-go/tools/auth/exec/types_test.go | 151 +++++++++++++ 5 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 staging/src/k8s.io/client-go/tools/auth/exec/BUILD create mode 100644 staging/src/k8s.io/client-go/tools/auth/exec/exec.go create mode 100644 staging/src/k8s.io/client-go/tools/auth/exec/exec_test.go create mode 100644 staging/src/k8s.io/client-go/tools/auth/exec/types_test.go diff --git a/staging/src/k8s.io/client-go/tools/auth/BUILD b/staging/src/k8s.io/client-go/tools/auth/BUILD index 1f44e2db68b..1ecf2715dfd 100644 --- a/staging/src/k8s.io/client-go/tools/auth/BUILD +++ b/staging/src/k8s.io/client-go/tools/auth/BUILD @@ -29,6 +29,9 @@ filegroup( filegroup( name = "all-srcs", - srcs = [":package-srcs"], + srcs = [ + ":package-srcs", + "//staging/src/k8s.io/client-go/tools/auth/exec:all-srcs", + ], tags = ["automanaged"], ) diff --git a/staging/src/k8s.io/client-go/tools/auth/exec/BUILD b/staging/src/k8s.io/client-go/tools/auth/exec/BUILD new file mode 100644 index 00000000000..bcc6bfb0174 --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/auth/exec/BUILD @@ -0,0 +1,54 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["exec.go"], + importmap = "k8s.io/kubernetes/vendor/k8s.io/client-go/tools/auth/exec", + importpath = "k8s.io/client-go/tools/auth/exec", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/serializer:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/runtime:go_default_library", + "//staging/src/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library", + "//staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1:go_default_library", + "//staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1:go_default_library", + "//staging/src/k8s.io/client-go/rest:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = [ + "exec_test.go", + "types_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", + "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1:go_default_library", + "//staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1beta1:go_default_library", + "//staging/src/k8s.io/client-go/rest:go_default_library", + "//staging/src/k8s.io/client-go/tools/clientcmd/api/v1:go_default_library", + "//vendor/github.com/google/go-cmp/cmp:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) diff --git a/staging/src/k8s.io/client-go/tools/auth/exec/exec.go b/staging/src/k8s.io/client-go/tools/auth/exec/exec.go new file mode 100644 index 00000000000..246de2ef103 --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/auth/exec/exec.go @@ -0,0 +1,110 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package exec contains helper utilities for exec credential plugins. +package exec + +import ( + "errors" + "fmt" + "os" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/pkg/apis/clientauthentication" + "k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1" + "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + "k8s.io/client-go/rest" +) + +const execInfoEnv = "KUBERNETES_EXEC_INFO" + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +func init() { + metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(v1beta1.AddToScheme(scheme)) + utilruntime.Must(clientauthentication.AddToScheme(scheme)) +} + +// LoadExecCredentialFromEnv is a helper-wrapper around LoadExecCredential that loads from the +// well-known KUBERNETES_EXEC_INFO environment variable. +// +// When the KUBERNETES_EXEC_INFO environment variable is not set or is empty, then this function +// will immediately return an error. +func LoadExecCredentialFromEnv() (runtime.Object, *rest.Config, error) { + env := os.Getenv(execInfoEnv) + if env == "" { + return nil, nil, errors.New("KUBERNETES_EXEC_INFO env var is unset or empty") + } + return LoadExecCredential([]byte(env)) +} + +// LoadExecCredential loads the configuration needed for an exec plugin to communicate with a +// cluster. +// +// LoadExecCredential expects the provided data to be a serialized client.authentication.k8s.io +// ExecCredential object (of any version). If the provided data is invalid (i.e., it cannot be +// unmarshalled into any known client.authentication.k8s.io ExecCredential version), an error will +// be returned. A successfully unmarshalled ExecCredential will be returned as the first return +// value. +// +// If the provided data is successfully unmarshalled, but it does not contain cluster information +// (i.e., ExecCredential.Spec.Cluster == nil), then the returned rest.Config and error will be nil. +// +// Note that the returned rest.Config will use anonymous authentication, since the exec plugin has +// not returned credentials for this cluster yet. +func LoadExecCredential(data []byte) (runtime.Object, *rest.Config, error) { + obj, gvk, err := codecs.UniversalDeserializer().Decode(data, nil, nil) + if err != nil { + return nil, nil, fmt.Errorf("decode: %w", err) + } + + expectedGK := schema.GroupKind{ + Group: clientauthentication.SchemeGroupVersion.Group, + Kind: "ExecCredential", + } + if gvk.GroupKind() != expectedGK { + return nil, nil, fmt.Errorf( + "invalid group/kind: wanted %s, got %s", + expectedGK.String(), + gvk.GroupKind().String(), + ) + } + + // Explicitly convert object here so that we can return a nicer error message above for when the + // data represents an invalid type. + var execCredential clientauthentication.ExecCredential + if err := scheme.Convert(obj, &execCredential, nil); err != nil { + return nil, nil, fmt.Errorf("cannot convert to ExecCredential: %w", err) + } + + if execCredential.Spec.Cluster == nil { + return nil, nil, errors.New("ExecCredential does not contain cluster information") + } + + restConfig, err := rest.ExecClusterToConfig(execCredential.Spec.Cluster) + if err != nil { + return nil, nil, fmt.Errorf("cannot create rest.Config: %w", err) + } + + return obj, restConfig, nil +} diff --git a/staging/src/k8s.io/client-go/tools/auth/exec/exec_test.go b/staging/src/k8s.io/client-go/tools/auth/exec/exec_test.go new file mode 100644 index 00000000000..fd1b1b0b5dd --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/auth/exec/exec_test.go @@ -0,0 +1,210 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package exec + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + clientauthenticationv1alpha1 "k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1" + clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + "k8s.io/client-go/rest" +) + +// restInfo holds the rest.Client fields that we care about for test assertions. +type restInfo struct { + host string + tlsClientConfig rest.TLSClientConfig + proxyURL string +} + +func TestLoadExecCredential(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + data []byte + wantExecCredential runtime.Object + wantRESTInfo restInfo + wantErrorPrefix string + }{ + { + name: "v1beta1 happy path", + data: marshal(t, clientauthenticationv1beta1.SchemeGroupVersion, &clientauthenticationv1beta1.ExecCredential{ + Spec: clientauthenticationv1beta1.ExecCredentialSpec{ + Cluster: &clientauthenticationv1beta1.Cluster{ + Server: "https://some-server/some/path", + TLSServerName: "some-server-name", + InsecureSkipTLSVerify: true, + CertificateAuthorityData: []byte("some-ca-data"), + ProxyURL: "https://some-proxy-url:12345", + Config: runtime.RawExtension{ + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"names":["marshmallow","zelda"]}}`), + }, + }, + }, + }), + wantExecCredential: &clientauthenticationv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(), + }, + Spec: clientauthenticationv1beta1.ExecCredentialSpec{ + Cluster: &clientauthenticationv1beta1.Cluster{ + Server: "https://some-server/some/path", + TLSServerName: "some-server-name", + InsecureSkipTLSVerify: true, + CertificateAuthorityData: []byte("some-ca-data"), + ProxyURL: "https://some-proxy-url:12345", + Config: runtime.RawExtension{ + Raw: []byte(`{"apiVersion":"group/v1","kind":"PluginConfig","spec":{"names":["marshmallow","zelda"]}}`), + }, + }, + }, + }, + wantRESTInfo: restInfo{ + host: "https://some-server/some/path", + tlsClientConfig: rest.TLSClientConfig{ + Insecure: true, + ServerName: "some-server-name", + CAData: []byte("some-ca-data"), + }, + proxyURL: "https://some-proxy-url:12345", + }, + }, + { + name: "v1beta1 nil config", + data: marshal(t, clientauthenticationv1beta1.SchemeGroupVersion, &clientauthenticationv1beta1.ExecCredential{ + Spec: clientauthenticationv1beta1.ExecCredentialSpec{ + Cluster: &clientauthenticationv1beta1.Cluster{ + Server: "https://some-server/some/path", + TLSServerName: "some-server-name", + InsecureSkipTLSVerify: true, + CertificateAuthorityData: []byte("some-ca-data"), + ProxyURL: "https://some-proxy-url:12345", + }, + }, + }), + wantExecCredential: &clientauthenticationv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(), + }, + Spec: clientauthenticationv1beta1.ExecCredentialSpec{ + Cluster: &clientauthenticationv1beta1.Cluster{ + Server: "https://some-server/some/path", + TLSServerName: "some-server-name", + InsecureSkipTLSVerify: true, + CertificateAuthorityData: []byte("some-ca-data"), + ProxyURL: "https://some-proxy-url:12345", + }, + }, + }, + wantRESTInfo: restInfo{ + host: "https://some-server/some/path", + tlsClientConfig: rest.TLSClientConfig{ + Insecure: true, + ServerName: "some-server-name", + CAData: []byte("some-ca-data"), + }, + proxyURL: "https://some-proxy-url:12345", + }, + }, + { + name: "v1beta1 invalid cluster", + data: marshal(t, clientauthenticationv1beta1.SchemeGroupVersion, &clientauthenticationv1beta1.ExecCredential{ + Spec: clientauthenticationv1beta1.ExecCredentialSpec{ + Cluster: &clientauthenticationv1beta1.Cluster{ + ProxyURL: "invalid- url\n", + }, + }, + }), + wantErrorPrefix: "cannot create rest.Config", + }, + { + name: "v1beta1 nil cluster", + data: marshal(t, clientauthenticationv1beta1.SchemeGroupVersion, &clientauthenticationv1beta1.ExecCredential{}), + wantErrorPrefix: "ExecCredential does not contain cluster information", + }, + { + name: "v1alpha1", + data: marshal(t, clientauthenticationv1alpha1.SchemeGroupVersion, &clientauthenticationv1alpha1.ExecCredential{}), + wantErrorPrefix: "ExecCredential does not contain cluster information", + }, + { + name: "invalid object kind", + data: marshal(t, metav1.SchemeGroupVersion, &metav1.Status{}), + wantErrorPrefix: "invalid group/kind: wanted ExecCredential.client.authentication.k8s.io, got Status", + }, + { + name: "bad data", + data: []byte("bad data"), + wantErrorPrefix: "decode: ", + }, + } + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + execCredential, restConfig, err := LoadExecCredential(test.data) + if test.wantErrorPrefix != "" { + if err == nil { + t.Error("wanted error, got success") + } else if !strings.HasPrefix(err.Error(), test.wantErrorPrefix) { + t.Errorf("wanted '%s', got '%s'", test.wantErrorPrefix, err.Error()) + } + } else if err != nil { + t.Error(err) + } else { + if diff := cmp.Diff(test.wantExecCredential, execCredential); diff != "" { + t.Error(diff) + } + + if diff := cmp.Diff(test.wantRESTInfo.host, restConfig.Host); diff != "" { + t.Error(diff) + } + if diff := cmp.Diff(test.wantRESTInfo.tlsClientConfig, restConfig.TLSClientConfig); diff != "" { + t.Error(diff) + } + + proxyURL, err := restConfig.Proxy(nil) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(test.wantRESTInfo.proxyURL, proxyURL.String()); diff != "" { + t.Error(diff) + } + } + }) + } +} + +func marshal(t *testing.T, gv schema.GroupVersion, obj runtime.Object) []byte { + t.Helper() + + data, err := runtime.Encode(codecs.LegacyCodec(gv), obj) + if err != nil { + t.Fatal(err) + } + + return data +} diff --git a/staging/src/k8s.io/client-go/tools/auth/exec/types_test.go b/staging/src/k8s.io/client-go/tools/auth/exec/types_test.go new file mode 100644 index 00000000000..e4b95a34643 --- /dev/null +++ b/staging/src/k8s.io/client-go/tools/auth/exec/types_test.go @@ -0,0 +1,151 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package exec + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + clientauthenticationv1alpha1 "k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1" + clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1" +) + +// TestV1beta1ClusterTypesAreSynced ensures that clientauthenticationv1beta1.Cluster stays in sync +// with clientcmdv1.Cluster. +// +// We want clientauthenticationv1beta1.Cluster to offer the same knobs as clientcmdv1.Cluster to +// allow someone to connect to the kubernetes API. This test should fail if a new field is added to +// one of the structs without updating the other. +func TestV1beta1ClusterTypesAreSynced(t *testing.T) { + t.Parallel() + + execType := reflect.TypeOf(clientauthenticationv1beta1.Cluster{}) + clientcmdType := reflect.TypeOf(clientcmdv1.Cluster{}) + + t.Run("exec cluster fields match clientcmd cluster fields", func(t *testing.T) { + t.Parallel() + + // These are fields that are specific to Cluster and shouldn't be in clientcmdv1.Cluster. + execSkippedFieldNames := sets.NewString( + // Cluster uses Config to provide its cluster-specific configuration object. + "Config", + ) + + for i := 0; i < execType.NumField(); i++ { + execField := execType.Field(i) + if execSkippedFieldNames.Has(execField.Name) { + continue + } + + t.Run(execField.Name, func(t *testing.T) { + t.Parallel() + clientcmdField, ok := clientcmdType.FieldByName(execField.Name) + if !ok { + t.Errorf("unknown field (please add field to clientcmdv1.Cluster): '%s'", execField.Name) + } else if execField.Type != clientcmdField.Type { + t.Errorf( + "type mismatch (please update Cluster.%s field type to match clientcmdv1.Cluster.%s field type): %q != %q", + execField.Name, + clientcmdField.Name, + execField.Type, + clientcmdField.Type, + ) + } else if execField.Tag != clientcmdField.Tag { + t.Errorf( + "tag mismatch (please update Cluster.%s tag to match clientcmdv1.Cluster.%s tag): %q != %q", + execField.Name, + clientcmdField.Name, + execField.Tag, + clientcmdField.Tag, + ) + } + }) + } + }) + + t.Run("clientcmd cluster fields match exec cluster fields", func(t *testing.T) { + t.Parallel() + + // These are the fields that we don't want to shadow from clientcmdv1.Cluster. + clientcmdSkippedFieldNames := sets.NewString( + // CA data will be passed via CertificateAuthorityData, so we don't need this field. + "CertificateAuthority", + // Cluster uses Config to provide its cluster-specific configuration object. + "Extensions", + ) + + for i := 0; i < clientcmdType.NumField(); i++ { + clientcmdField := clientcmdType.Field(i) + if clientcmdSkippedFieldNames.Has(clientcmdField.Name) { + continue + } + + t.Run(clientcmdField.Name, func(t *testing.T) { + t.Parallel() + execField, ok := execType.FieldByName(clientcmdField.Name) + if !ok { + t.Errorf("unknown field (please add field to Cluster): '%s'", clientcmdField.Name) + } else if clientcmdField.Type != execField.Type { + t.Errorf( + "type mismatch (please update clientcmdv1.Cluster.%s field type to match Cluster.%s field type): %q != %q", + clientcmdField.Name, + execField.Name, + clientcmdField.Type, + execField.Type, + ) + } else if clientcmdField.Tag != execField.Tag { + t.Errorf( + "tag mismatch (please update clientcmdv1.Cluster.%s tag to match Cluster.%s tag): %q != %q", + clientcmdField.Name, + execField.Name, + clientcmdField.Tag, + execField.Tag, + ) + } + }) + } + }) +} + +// TestAllClusterTypesAreSynced is a TODO so that we remember to write a test similar to +// TestV1beta1ClusterTypesAreSynced for any future ExecCredential version. It should start failing +// when someone adds support for any other ExecCredential type to this package. +func TestAllClusterTypesAreSynced(t *testing.T) { + versionsThatDontNeedTests := sets.NewString( + // The internal Cluster type should only be used...internally...and therefore doesn't + // necessarily need to be synced with clientcmdv1. + runtime.APIVersionInternal, + // V1alpha1 does not contain a Cluster type. + clientauthenticationv1alpha1.SchemeGroupVersion.Version, + // We have a test for v1beta1 above. + clientauthenticationv1beta1.SchemeGroupVersion.Version, + ) + for gvk := range scheme.AllKnownTypes() { + if gvk.Group == clientauthenticationv1beta1.SchemeGroupVersion.Group && + gvk.Kind == "ExecCredential" { + if !versionsThatDontNeedTests.Has(gvk.Version) { + t.Errorf( + "TODO: add test similar to TestV1beta1ClusterTypesAreSynced for client.authentication.k8s.io/%s", + gvk.Version, + ) + } + } + } +} From 409f22832a9f949e7fa534379574ce4fb9802394 Mon Sep 17 00:00:00 2001 From: Andrew Keesler Date: Thu, 29 Oct 2020 17:02:55 -0400 Subject: [PATCH 6/6] exec credential provider: exec -> client.authentication.k8s.io/exec Hopefully we've fixed all of these references now... Signed-off-by: Andrew Keesler --- .../k8s.io/client-go/pkg/apis/clientauthentication/types.go | 3 ++- .../client-go/pkg/apis/clientauthentication/v1beta1/types.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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 d99ca818792..bdae85b1e47 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 @@ -115,7 +115,8 @@ type Cluster struct { // 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: + // This data is sourced from the clientcmd Cluster object's + // extensions[client.authentication.k8s.io/exec] field: // // clusters: // - name: my-cluster 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 a3119f154ad..4c3cbc8b2bc 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 @@ -96,7 +96,8 @@ type Cluster struct { // 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: + // This data is sourced from the clientcmd Cluster object's + // extensions[client.authentication.k8s.io/exec] field: // // clusters: // - name: my-cluster