Merge pull request #95489 from ankeesler/ankeesler/enj/f/exec_plugin_cluster

exec credential provider: wire in cluster info (superset of #91192)
This commit is contained in:
Kubernetes Prow Robot 2020-10-29 18:36:05 -07:00 committed by GitHub
commit 53913a7c67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1749 additions and 74 deletions

View File

@ -468,6 +468,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

View File

@ -18,11 +18,12 @@ 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
// 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
@ -37,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,
@ -49,6 +50,13 @@ 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. 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.
@ -75,3 +83,56 @@ 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.
//
// 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
// 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
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
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.
//
// This data is sourced from the clientcmd Cluster object's
// extensions[client.authentication.k8s.io/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.
// +optional
Config runtime.Object
}

View File

@ -8,6 +8,7 @@ load(
go_library(
name = "go_default_library",
srcs = [
"conversion.go",
"doc.go",
"register.go",
"types.go",

View File

@ -0,0 +1,27 @@
/*
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 {
// This conversion intentionally omits the Cluster field which is only supported in newer versions.
return autoConvert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialSpec(in, out, s)
}

View File

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

View File

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

View File

@ -17,10 +17,12 @@ 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
// 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)
}

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,16 @@ 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. 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.
//
@ -57,3 +64,56 @@ 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.
//
// 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"`
// 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
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
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.
//
// This data is sourced from the clientcmd Cluster object's
// extensions[client.authentication.k8s.io/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.
// +optional
Config runtime.RawExtension `json:"config,omitempty"`
}

View File

@ -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,40 @@ 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.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
}
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.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
}
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 +140,15 @@ 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 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
}
@ -107,6 +160,15 @@ 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 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
}

View File

@ -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.CertificateAuthorityData != nil {
in, out := &in.CertificateAuthorityData, &out.CertificateAuthorityData
*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,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
if in.Cluster != nil {
in, out := &in.Cluster, &out.Cluster
*out = new(Cluster)
(*in).DeepCopyInto(*out)
}
return
}

View File

@ -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.CertificateAuthorityData != nil {
in, out := &in.CertificateAuthorityData, &out.CertificateAuthorityData
*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,11 @@ func (in *ExecCredentialSpec) DeepCopyInto(out *ExecCredentialSpec) {
*out = new(Response)
(*in).DeepCopyInto(*out)
}
if in.Cluster != nil {
in, out := &in.Cluster, &out.Cluster
*out = new(Cluster)
(*in).DeepCopyInto(*out)
}
return
}

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,11 @@ 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,
provideClusterInfo: config.ProvideClusterInfo,
installHint: config.InstallHint,
sometimes: &sometimes{
@ -200,10 +209,12 @@ 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
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
@ -367,19 +378,16 @@ func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) err
Interactive: a.interactive,
},
}
if a.provideClusterInfo {
cred.Spec.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

@ -115,8 +115,24 @@ 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",
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",
},
}
c2 := &api.ExecConfig{
Command: "foo-bar",
Args: []string{"1", "2"},
@ -125,8 +141,24 @@ 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",
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",
},
}
c3 := &api.ExecConfig{
Command: "foo-bar",
Args: []string{"1", "2"},
@ -136,9 +168,88 @@ 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",
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",
},
}
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",
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",
},
}
// 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")
}
@ -148,6 +259,15 @@ func TestCacheKey(t *testing.T) {
if key2 == key3 {
t.Error("key2 and key3 matched")
}
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) {
@ -173,6 +293,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 +514,11 @@ func TestRefreshCreds(t *testing.T) {
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
},
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
@ -407,6 +533,11 @@ func TestRefreshCreds(t *testing.T) {
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
},
wantInput: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
"spec": {}
}`,
output: `{
"kind": "ExecCredential",
"apiVersion": "client.authentication.k8s.io/v1beta1",
@ -473,6 +604,146 @@ 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",
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",
},
},
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-and-provide-cluster-info-is-serialized",
config: api.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1beta1",
ProvideClusterInfo: true,
},
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": {
"cluster": {
"server": "foo",
"tls-server-name": "bar",
"certificate-authority-data": "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"},
},
{
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 {
@ -491,7 +762,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 +840,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, nil)
if err != nil {
t.Fatal(err)
}
@ -655,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)
}
@ -694,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",
})
}, nil)
if err != nil {
t.Fatal(err)
}
@ -784,7 +1055,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, nil)
if err != nil {
t.Fatal(err)
}
@ -851,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)
}

View File

@ -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",
@ -71,6 +75,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",

View File

@ -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,7 +192,9 @@ func (c *Config) String() string {
if cc.AuthConfigPersister != nil {
cc.AuthConfigPersister = sanitizedAuthConfigPersister{cc.AuthConfigPersister}
}
if cc.ExecProvider != nil && cc.ExecProvider.Config != nil {
cc.ExecProvider.Config = sanitizedObject{Object: cc.ExecProvider.Config}
}
return fmt.Sprintf("%#v", cc)
}
@ -588,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,
@ -627,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
}

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{}
@ -428,6 +433,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{}
@ -525,8 +535,9 @@ func TestConfigStringer(t *testing.T) {
Config: map[string]string{"secret": "s3cr3t"},
},
ExecProvider: &clientcmdapi.ExecConfig{
Args: []string{"secret"},
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
Args: []string{"secret"},
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
Config: &runtime.Unknown{Raw: []byte("here is some config data")},
},
},
expectContent: []string{
@ -545,6 +556,8 @@ func TestConfigStringer(t *testing.T) {
formatBytes([]byte("fake key")),
"secret",
"s3cr3t",
"here is some config data",
formatBytes([]byte("super secret password")),
},
},
}
@ -588,9 +601,11 @@ func TestConfigSprint(t *testing.T) {
},
AuthConfigPersister: fakeAuthProviderConfigPersister{},
ExecProvider: &clientcmdapi.ExecConfig{
Command: "sudo",
Args: []string{"secret"},
Env: []clientcmdapi.ExecEnvVar{{Name: "secret", Value: "s3cr3t"}},
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",
@ -611,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 ---), 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 ---), 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,
)

View File

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

View File

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

View File

@ -0,0 +1 @@
a CA bundle lives here

View File

@ -21,6 +21,7 @@ import (
"errors"
"net/http"
"k8s.io/client-go/pkg/apis/clientauthentication"
"k8s.io/client-go/plugin/pkg/client/auth/exec"
"k8s.io/client-go/transport"
)
@ -94,7 +95,15 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
}
if c.ExecProvider != nil {
provider, err := exec.GetAuthenticator(c.ExecProvider)
var cluster *clientauthentication.Cluster
if c.ExecProvider.ProvideClusterInfo {
var err error
cluster, err = ConfigToExecCluster(c)
if err != nil {
return nil, err
}
}
provider, err := exec.GetAuthenticator(c.ExecProvider, cluster)
if err != nil {
return nil, err
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
@ -189,7 +194,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 +237,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
@ -271,6 +276,7 @@ func (config *DirectClientConfig) getUserIdentificationPartialConfig(configAuthI
if configAuthInfo.Exec != nil {
mergedConfig.ExecProvider = configAuthInfo.Exec
mergedConfig.ExecProvider.InstallHint = cleanANSIEscapeCodes(mergedConfig.ExecProvider.InstallHint)
mergedConfig.ExecProvider.Config = configClusterInfo.Extensions[clusterExtensionKey]
}
// if there still isn't enough information to authenticate the user, try prompting

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: client.authentication.k8s.io/exec
extension:
audience: foo
other: bar
name: foo-cluster
contexts:
- context:
@ -852,6 +858,7 @@ users:
- arg-1
- arg-2
command: foo-command
provideClusterInfo: true
`
tmpfile, err := ioutil.TempFile("", "kubeconfig")
if err != nil {
@ -868,7 +875,16 @@ users:
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.ExecProvider.Config, want) {
t.Errorf("Got config %v when it should be %v\n", config.ExecProvider.Config, want)
}
}
func TestCleanANSIEscapeCodes(t *testing.T) {