exec credential provider: wire in cluster info

Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
Monis Khan 2020-05-06 01:01:09 -04:00 committed by Andrew Keesler
parent 5f2ebe4bbc
commit f97422c8bd
No known key found for this signature in database
GPG Key ID: 27CE0444346F9413
10 changed files with 431 additions and 60 deletions

View File

@ -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")

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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...)

View File

@ -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)
}

View File

@ -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,

View File

@ -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,
)

View File

@ -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
}

View File

@ -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

View File

@ -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) {