diff --git a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json index fba71ace59..9fd2162d97 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json @@ -2010,6 +2010,10 @@ "ImportPath": "k8s.io/client-go/util/cert", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/client-go/util/connrotation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/client-go/util/flowcontrol", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiserver/Godeps/Godeps.json index 0bdf000102..8e3d9436a4 100644 --- a/staging/src/k8s.io/apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiserver/Godeps/Godeps.json @@ -1738,6 +1738,10 @@ "ImportPath": "k8s.io/client-go/util/cert", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/client-go/util/connrotation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/client-go/util/flowcontrol", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go index 5c05825f67..6fb53cecf9 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/types.go @@ -57,7 +57,14 @@ type ExecCredentialStatus struct { // +optional ExpirationTimestamp *metav1.Time // Token is a bearer token used by the client for request authentication. + // +optional Token string + // PEM-encoded client TLS certificate. + // +optional + ClientCertificateData string + // PEM-encoded client TLS private key. + // +optional + ClientKeyData string } // Response defines metadata about a failed request, including HTTP status code and diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/types.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/types.go index 8920d31876..921f3a2b94 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/types.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/types.go @@ -52,12 +52,20 @@ type ExecCredentialSpec struct { } // ExecCredentialStatus holds credentials for the transport to use. +// +// Token and ClientKeyData are sensitive fields. This data should only be +// transmitted in-memory between client and exec plugin process. Exec plugin +// itself should at least be protected via file permissions. type ExecCredentialStatus struct { // ExpirationTimestamp indicates a time when the provided credentials expire. // +optional ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"` // Token is a bearer token used by the client for request authentication. Token string `json:"token,omitempty"` + // PEM-encoded client TLS certificates (including intermediates, if any). + ClientCertificateData string `json:"clientCertificateData,omitempty"` + // PEM-encoded private key for the above certificate. + ClientKeyData string `json:"clientKeyData,omitempty"` } // Response defines metadata about a failed request, including HTTP status code and diff --git a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/zz_generated.conversion.go b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/zz_generated.conversion.go index 032eaf38f2..9921c7ee58 100644 --- a/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/zz_generated.conversion.go +++ b/staging/src/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1/zz_generated.conversion.go @@ -99,6 +99,8 @@ func Convert_clientauthentication_ExecCredentialSpec_To_v1alpha1_ExecCredentialS 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 + out.ClientCertificateData = in.ClientCertificateData + out.ClientKeyData = in.ClientKeyData return nil } @@ -110,6 +112,8 @@ func Convert_v1alpha1_ExecCredentialStatus_To_clientauthentication_ExecCredentia func autoConvert_clientauthentication_ExecCredentialStatus_To_v1alpha1_ExecCredentialStatus(in *clientauthentication.ExecCredentialStatus, out *ExecCredentialStatus, s conversion.Scope) error { out.ExpirationTimestamp = (*v1.Time)(unsafe.Pointer(in.ExpirationTimestamp)) out.Token = in.Token + out.ClientCertificateData = in.ClientCertificateData + out.ClientKeyData = in.ClientKeyData return nil } diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/BUILD b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/BUILD index 38cf9bdc29..a111f67d79 100644 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/BUILD +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/BUILD @@ -15,6 +15,8 @@ go_library( "//vendor/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library", "//vendor/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library", + "//vendor/k8s.io/client-go/transport:go_default_library", + "//vendor/k8s.io/client-go/util/connrotation:go_default_library", ], ) @@ -24,8 +26,11 @@ go_test( data = glob(["testdata/**"]), embed = [":go_default_library"], deps = [ + "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/client-go/pkg/apis/clientauthentication:go_default_library", "//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library", + "//vendor/k8s.io/client-go/transport:go_default_library", ], ) diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go index dfd434d0c2..30f6156240 100644 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec.go @@ -18,11 +18,15 @@ package exec import ( "bytes" + "context" + "crypto/tls" "fmt" "io" + "net" "net/http" "os" "os/exec" + "reflect" "sync" "time" @@ -35,6 +39,8 @@ import ( "k8s.io/client-go/pkg/apis/clientauthentication" "k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1" "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/transport" + "k8s.io/client-go/util/connrotation" ) const execInfoEnv = "KUBERNETES_EXEC_INFO" @@ -147,14 +153,55 @@ type Authenticator struct { // The mutex also guards calling the plugin. Since the plugin could be // interactive we want to make sure it's only called once. mu sync.Mutex - cachedToken string + cachedCreds *credentials exp time.Time + + onRotate func() } -// WrapTransport instruments an existing http.RoundTripper with credentials returned -// by the plugin. -func (a *Authenticator) WrapTransport(rt http.RoundTripper) http.RoundTripper { - return &roundTripper{a, rt} +type credentials struct { + token string + cert *tls.Certificate +} + +// UpdateTransportConfig updates the transport.Config to use credentials +// returned by the plugin. +func (a *Authenticator) UpdateTransportConfig(c *transport.Config) error { + wt := c.WrapTransport + c.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + if wt != nil { + rt = wt(rt) + } + return &roundTripper{a, rt} + } + + getCert := c.TLS.GetCert + c.TLS.GetCert = func() (*tls.Certificate, error) { + // If previous GetCert is present and returns a valid non-nil + // certificate, use that. Otherwise use cert from exec plugin. + if getCert != nil { + cert, err := getCert() + if err != nil { + return nil, err + } + if cert != nil { + return cert, nil + } + } + return a.cert() + } + + var dial func(ctx context.Context, network, addr string) (net.Conn, error) + if c.Dial != nil { + dial = c.Dial + } else { + dial = (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext + } + d := connrotation.NewDialer(dial) + a.onRotate = d.CloseAll + c.Dial = d.DialContext + + return nil } type roundTripper struct { @@ -169,11 +216,13 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return r.base.RoundTrip(req) } - token, err := r.a.token() + creds, err := r.a.getCreds() if err != nil { - return nil, fmt.Errorf("getting token: %v", err) + return nil, fmt.Errorf("getting credentials: %v", err) + } + if creds.token != "" { + req.Header.Set("Authorization", "Bearer "+creds.token) } - req.Header.Set("Authorization", "Bearer "+token) res, err := r.base.RoundTrip(req) if err != nil { @@ -184,47 +233,60 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { Header: res.Header, Code: int32(res.StatusCode), } - if err := r.a.refresh(token, resp); err != nil { - glog.Errorf("refreshing token: %v", err) + if err := r.a.maybeRefreshCreds(creds, resp); err != nil { + glog.Errorf("refreshing credentials: %v", err) } } return res, nil } -func (a *Authenticator) tokenExpired() bool { +func (a *Authenticator) credsExpired() bool { if a.exp.IsZero() { return false } return a.now().After(a.exp) } -func (a *Authenticator) token() (string, error) { - a.mu.Lock() - defer a.mu.Unlock() - if a.cachedToken != "" && !a.tokenExpired() { - return a.cachedToken, nil +func (a *Authenticator) cert() (*tls.Certificate, error) { + creds, err := a.getCreds() + if err != nil { + return nil, err } - - return a.getToken(nil) + return creds.cert, nil } -// refresh executes the plugin to force a rotation of the token. -func (a *Authenticator) refresh(token string, r *clientauthentication.Response) error { +func (a *Authenticator) getCreds() (*credentials, error) { + a.mu.Lock() + defer a.mu.Unlock() + if a.cachedCreds != nil && !a.credsExpired() { + return a.cachedCreds, nil + } + + if err := a.refreshCredsLocked(nil); err != nil { + return nil, err + } + return a.cachedCreds, nil +} + +// maybeRefreshCreds executes the plugin to force a rotation of the +// credentials, unless they were rotated already. +func (a *Authenticator) maybeRefreshCreds(creds *credentials, r *clientauthentication.Response) error { a.mu.Lock() defer a.mu.Unlock() - if token != a.cachedToken { - // Token already rotated. + // Since we're not making a new pointer to a.cachedCreds in getCreds, no + // need to do deep comparison. + if creds != a.cachedCreds { + // Credentials already rotated. return nil } - _, err := a.getToken(r) - return err + return a.refreshCredsLocked(r) } -// getToken executes the plugin and reads the credentials from stdout. It must be -// called while holding the Authenticator's mutex. -func (a *Authenticator) getToken(r *clientauthentication.Response) (string, error) { +// refreshCredsLocked executes the plugin and reads the credentials from +// stdout. It must be called while holding the Authenticator's mutex. +func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) error { cred := &clientauthentication.ExecCredential{ Spec: clientauthentication.ExecCredentialSpec{ Response: r, @@ -234,7 +296,7 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred) if err != nil { - return "", fmt.Errorf("encode ExecCredentials: %v", err) + return fmt.Errorf("encode ExecCredentials: %v", err) } env := append(a.environ(), a.env...) @@ -250,23 +312,26 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro } if err := cmd.Run(); err != nil { - return "", fmt.Errorf("exec: %v", err) + return fmt.Errorf("exec: %v", err) } _, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred) if err != nil { - return "", fmt.Errorf("decode stdout: %v", err) + return fmt.Errorf("decoding stdout: %v", err) } if gvk.Group != a.group.Group || gvk.Version != a.group.Version { - return "", fmt.Errorf("exec plugin is configured to use API version %s, plugin returned version %s", + return fmt.Errorf("exec plugin is configured to use API version %s, plugin returned version %s", a.group, schema.GroupVersion{Group: gvk.Group, Version: gvk.Version}) } if cred.Status == nil { - return "", fmt.Errorf("exec plugin didn't return a status field") + return fmt.Errorf("exec plugin didn't return a status field") } - if cred.Status.Token == "" { - return "", fmt.Errorf("exec plugin didn't return a token") + if cred.Status.Token == "" && cred.Status.ClientCertificateData == "" && cred.Status.ClientKeyData == "" { + return fmt.Errorf("exec plugin didn't return a token or cert/key pair") + } + if (cred.Status.ClientCertificateData == "") != (cred.Status.ClientKeyData == "") { + return fmt.Errorf("exec plugin returned only certificate or key, not both") } if cred.Status.ExpirationTimestamp != nil { @@ -274,7 +339,24 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro } else { a.exp = time.Time{} } - a.cachedToken = cred.Status.Token - return a.cachedToken, nil + newCreds := &credentials{ + token: cred.Status.Token, + } + if cred.Status.ClientKeyData != "" && cred.Status.ClientCertificateData != "" { + cert, err := tls.X509KeyPair([]byte(cred.Status.ClientCertificateData), []byte(cred.Status.ClientKeyData)) + if err != nil { + return fmt.Errorf("failed parsing client key/certificate: %v", err) + } + newCreds.cert = &cert + } + + oldCreds := a.cachedCreds + a.cachedCreds = newCreds + // Only close all connections when TLS cert rotates. Token rotation doesn't + // need the extra noise. + if a.onRotate != nil && oldCreds != nil && !reflect.DeepEqual(oldCreds.cert, a.cachedCreds.cert) { + a.onRotate() + } + return nil } diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go index 28137eb847..c37ee4c67a 100644 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/exec_test.go @@ -18,19 +18,88 @@ package exec import ( "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "encoding/json" + "encoding/pem" "fmt" "io/ioutil" + "math/big" "net/http" "net/http/httptest" + "reflect" "strings" "testing" "time" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/pkg/apis/clientauthentication" "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/transport" ) +var ( + certData = []byte(`-----BEGIN CERTIFICATE----- +MIIC6jCCAdSgAwIBAgIBCzALBgkqhkiG9w0BAQswIzEhMB8GA1UEAwwYMTAuMTMu +MTI5LjEwNkAxNDIxMzU5MDU4MB4XDTE1MDExNTIyMDEzMVoXDTE2MDExNTIyMDEz +MlowGzEZMBcGA1UEAxMQb3BlbnNoaWZ0LWNsaWVudDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAKtdhz0+uCLXw5cSYns9rU/XifFSpb/x24WDdrm72S/v +b9BPYsAStiP148buylr1SOuNi8sTAZmlVDDIpIVwMLff+o2rKYDicn9fjbrTxTOj +lI4pHJBH+JU3AJ0tbajupioh70jwFS0oYpwtneg2zcnE2Z4l6mhrj2okrc5Q1/X2 +I2HChtIU4JYTisObtin10QKJX01CLfYXJLa8upWzKZ4/GOcHG+eAV3jXWoXidtjb +1Usw70amoTZ6mIVCkiu1QwCoa8+ycojGfZhvqMsAp1536ZcCul+Na+AbCv4zKS7F +kQQaImVrXdUiFansIoofGlw/JNuoKK6ssVpS5Ic3pgcCAwEAAaM1MDMwDgYDVR0P +AQH/BAQDAgCgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJ +KoZIhvcNAQELA4IBAQCKLREH7bXtXtZ+8vI6cjD7W3QikiArGqbl36bAhhWsJLp/ +p/ndKz39iFNaiZ3GlwIURWOOKx3y3GA0x9m8FR+Llthf0EQ8sUjnwaknWs0Y6DQ3 +jjPFZOpV3KPCFrdMJ3++E3MgwFC/Ih/N2ebFX9EcV9Vcc6oVWMdwT0fsrhu683rq +6GSR/3iVX1G/pmOiuaR0fNUaCyCfYrnI4zHBDgSfnlm3vIvN2lrsR/DQBakNL8DJ +HBgKxMGeUPoneBv+c8DMXIL0EhaFXRlBv9QW45/GiAIOuyFJ0i6hCtGZpJjq4OpQ +BRjCI+izPzFTjsxD4aORE+WOkyWFCGPWKfNejfw0 +-----END CERTIFICATE-----`) + keyData = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAq12HPT64ItfDlxJiez2tT9eJ8VKlv/HbhYN2ubvZL+9v0E9i +wBK2I/Xjxu7KWvVI642LyxMBmaVUMMikhXAwt9/6jaspgOJyf1+NutPFM6OUjikc +kEf4lTcAnS1tqO6mKiHvSPAVLShinC2d6DbNycTZniXqaGuPaiStzlDX9fYjYcKG +0hTglhOKw5u2KfXRAolfTUIt9hcktry6lbMpnj8Y5wcb54BXeNdaheJ22NvVSzDv +RqahNnqYhUKSK7VDAKhrz7JyiMZ9mG+oywCnXnfplwK6X41r4BsK/jMpLsWRBBoi +ZWtd1SIVqewiih8aXD8k26gorqyxWlLkhzemBwIDAQABAoIBAD2XYRs3JrGHQUpU +FkdbVKZkvrSY0vAZOqBTLuH0zUv4UATb8487anGkWBjRDLQCgxH+jucPTrztekQK +aW94clo0S3aNtV4YhbSYIHWs1a0It0UdK6ID7CmdWkAj6s0T8W8lQT7C46mWYVLm +5mFnCTHi6aB42jZrqmEpC7sivWwuU0xqj3Ml8kkxQCGmyc9JjmCB4OrFFC8NNt6M +ObvQkUI6Z3nO4phTbpxkE1/9dT0MmPIF7GhHVzJMS+EyyRYUDllZ0wvVSOM3qZT0 +JMUaBerkNwm9foKJ1+dv2nMKZZbJajv7suUDCfU44mVeaEO+4kmTKSGCGjjTBGkr +7L1ySDECgYEA5ElIMhpdBzIivCuBIH8LlUeuzd93pqssO1G2Xg0jHtfM4tz7fyeI +cr90dc8gpli24dkSxzLeg3Tn3wIj/Bu64m2TpZPZEIlukYvgdgArmRIPQVxerYey +OkrfTNkxU1HXsYjLCdGcGXs5lmb+K/kuTcFxaMOs7jZi7La+jEONwf8CgYEAwCs/ +rUOOA0klDsWWisbivOiNPII79c9McZCNBqncCBfMUoiGe8uWDEO4TFHN60vFuVk9 +8PkwpCfvaBUX+ajvbafIfHxsnfk1M04WLGCeqQ/ym5Q4sQoQOcC1b1y9qc/xEWfg +nIUuia0ukYRpl7qQa3tNg+BNFyjypW8zukUAC/kCgYB1/Kojuxx5q5/oQVPrx73k +2bevD+B3c+DYh9MJqSCNwFtUpYIWpggPxoQan4LwdsmO0PKzocb/ilyNFj4i/vII +NToqSc/WjDFpaDIKyuu9oWfhECye45NqLWhb/6VOuu4QA/Nsj7luMhIBehnEAHW+ +GkzTKM8oD1PxpEG3nPKXYQKBgQC6AuMPRt3XBl1NkCrpSBy/uObFlFaP2Enpf39S +3OZ0Gv0XQrnSaL1kP8TMcz68rMrGX8DaWYsgytstR4W+jyy7WvZwsUu+GjTJ5aMG +77uEcEBpIi9CBzivfn7hPccE8ZgqPf+n4i6q66yxBJflW5xhvafJqDtW2LcPNbW/ +bvzdmQKBgExALRUXpq+5dbmkdXBHtvXdRDZ6rVmrnjy4nI5bPw+1GqQqk6uAR6B/ +F6NmLCQOO4PDG/cuatNHIr2FrwTmGdEL6ObLUGWn9Oer9gJhHVqqsY5I4sEPo4XX +stR0Yiw0buV6DL/moUO0HIM9Bjh96HJp+LxiIS6UCdIhMPp5HoQa +-----END RSA PRIVATE KEY-----`) + validCert *tls.Certificate +) + +func init() { + cert, err := tls.X509KeyPair(certData, keyData) + if err != nil { + panic(err) + } + validCert = &cert +} + func TestCacheKey(t *testing.T) { c1 := &api.ExecConfig{ Command: "foo-bar", @@ -93,7 +162,7 @@ func compJSON(t *testing.T, got, want []byte) { } } -func TestGetToken(t *testing.T) { +func TestRefreshCreds(t *testing.T) { tests := []struct { name string config api.ExecConfig @@ -101,7 +170,7 @@ func TestGetToken(t *testing.T) { interactive bool response *clientauthentication.Response wantInput string - wantToken string + wantCreds credentials wantExpiry time.Time wantErr bool }{ @@ -122,7 +191,7 @@ func TestGetToken(t *testing.T) { "token": "foo-bar" } }`, - wantToken: "foo-bar", + wantCreds: credentials{token: "foo-bar"}, }, { name: "interactive", @@ -144,7 +213,7 @@ func TestGetToken(t *testing.T) { "token": "foo-bar" } }`, - wantToken: "foo-bar", + wantCreds: credentials{token: "foo-bar"}, }, { name: "response", @@ -178,7 +247,7 @@ func TestGetToken(t *testing.T) { "token": "foo-bar" } }`, - wantToken: "foo-bar", + wantCreds: credentials{token: "foo-bar"}, }, { name: "expiry", @@ -199,7 +268,7 @@ func TestGetToken(t *testing.T) { } }`, wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC), - wantToken: "foo-bar", + wantCreds: credentials{token: "foo-bar"}, }, { name: "no-group-version", @@ -236,7 +305,7 @@ func TestGetToken(t *testing.T) { wantErr: true, }, { - name: "no-token", + name: "no-creds", config: api.ExecConfig{ APIVersion: "client.authentication.k8s.io/v1alpha1", }, @@ -252,6 +321,65 @@ func TestGetToken(t *testing.T) { }`, wantErr: true, }, + { + name: "TLS credentials", + config: api.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1alpha1", + }, + wantInput: `{ + "kind":"ExecCredential", + "apiVersion":"client.authentication.k8s.io/v1alpha1", + "spec": {} + }`, + output: fmt.Sprintf(`{ + "kind": "ExecCredential", + "apiVersion": "client.authentication.k8s.io/v1alpha1", + "status": { + "clientKeyData": %q, + "clientCertificateData": %q + } + }`, keyData, certData), + wantCreds: credentials{cert: validCert}, + }, + { + name: "bad TLS credentials", + config: api.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1alpha1", + }, + wantInput: `{ + "kind":"ExecCredential", + "apiVersion":"client.authentication.k8s.io/v1alpha1", + "spec": {} + }`, + output: `{ + "kind": "ExecCredential", + "apiVersion": "client.authentication.k8s.io/v1alpha1", + "status": { + "clientKeyData": "foo", + "clientCertificateData": "bar" + } + }`, + wantErr: true, + }, + { + name: "cert but no key", + config: api.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1alpha1", + }, + wantInput: `{ + "kind":"ExecCredential", + "apiVersion":"client.authentication.k8s.io/v1alpha1", + "spec": {} + }`, + output: fmt.Sprintf(`{ + "kind": "ExecCredential", + "apiVersion": "client.authentication.k8s.io/v1alpha1", + "status": { + "clientCertificateData": %q + } + }`, certData), + wantErr: true, + }, } for _, test := range tests { @@ -274,8 +402,7 @@ func TestGetToken(t *testing.T) { a.interactive = test.interactive a.environ = func() []string { return nil } - token, err := a.getToken(test.response) - if err != nil { + if err := a.refreshCredsLocked(test.response); err != nil { if !test.wantErr { t.Errorf("get token %v", err) } @@ -285,8 +412,8 @@ func TestGetToken(t *testing.T) { t.Fatal("expected error getting token") } - if token != test.wantToken { - t.Errorf("expected token %q got %q", test.wantToken, token) + if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) { + t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds) } if !a.exp.Equal(test.wantExpiry) { @@ -342,8 +469,12 @@ func TestRoundTripper(t *testing.T) { a.now = now a.stderr = ioutil.Discard + tc := &transport.Config{} + if err := a.UpdateTransportConfig(tc); err != nil { + t.Fatal(err) + } client := http.Client{ - Transport: a.WrapTransport(http.DefaultTransport), + Transport: tc.WrapTransport(http.DefaultTransport), } get := func(t *testing.T, statusCode int) { @@ -411,3 +542,134 @@ func TestRoundTripper(t *testing.T) { // Old token is expired, should refresh automatically without hitting a 401. get(t, http.StatusOK) } + +func TestTLSCredentials(t *testing.T) { + now := time.Now() + + certPool := x509.NewCertPool() + cert, key := genClientCert(t) + if !certPool.AppendCertsFromPEM(cert) { + t.Fatal("failed to add client cert to CertPool") + } + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "ok") + })) + server.TLS = &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: certPool, + } + server.StartTLS() + defer server.Close() + + a, err := newAuthenticator(newCache(), &api.ExecConfig{ + Command: "./testdata/test-plugin.sh", + APIVersion: "client.authentication.k8s.io/v1alpha1", + }) + if err != nil { + t.Fatal(err) + } + var output *clientauthentication.ExecCredential + a.environ = func() []string { + data, err := runtime.Encode(codecs.LegacyCodec(a.group), output) + if err != nil { + t.Fatal(err) + } + return []string{"TEST_OUTPUT=" + string(data)} + } + a.now = func() time.Time { return now } + a.stderr = ioutil.Discard + + // We're not interested in server's cert, this test is about client cert. + tc := &transport.Config{TLS: transport.TLSConfig{Insecure: true}} + if err := a.UpdateTransportConfig(tc); err != nil { + t.Fatal(err) + } + + get := func(t *testing.T, desc string, wantErr bool) { + t.Run(desc, func(t *testing.T) { + tlsCfg, err := transport.TLSConfigFor(tc) + if err != nil { + t.Fatal("TLSConfigFor:", err) + } + client := http.Client{ + Transport: &http.Transport{TLSClientConfig: tlsCfg}, + } + resp, err := client.Get(server.URL) + switch { + case err != nil && !wantErr: + t.Errorf("got client.Get error: %q, want nil", err) + case err == nil && wantErr: + t.Error("got nil client.Get error, want non-nil") + } + if err == nil { + resp.Body.Close() + } + }) + } + + output = &clientauthentication.ExecCredential{ + Status: &clientauthentication.ExecCredentialStatus{ + ClientCertificateData: string(cert), + ClientKeyData: string(key), + ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, + }, + } + get(t, "valid TLS cert", false) + + // Advance time to force re-exec. + nCert, nKey := genClientCert(t) + now = now.Add(time.Hour * 2) + output = &clientauthentication.ExecCredential{ + Status: &clientauthentication.ExecCredentialStatus{ + ClientCertificateData: string(nCert), + ClientKeyData: string(nKey), + ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, + }, + } + get(t, "untrusted TLS cert", true) + + now = now.Add(time.Hour * 2) + output = &clientauthentication.ExecCredential{ + Status: &clientauthentication.ExecCredentialStatus{ + ClientCertificateData: string(cert), + ClientKeyData: string(key), + ExpirationTimestamp: &v1.Time{now.Add(time.Hour)}, + }, + } + get(t, "valid TLS cert again", false) +} + +// genClientCert generates an x509 certificate for testing. Certificate and key +// are returned in PEM encoding. +func genClientCert(t *testing.T) ([]byte, []byte) { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + keyRaw, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatal(err) + } + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + t.Fatal(err) + } + cert := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{Organization: []string{"Acme Co"}}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + BasicConstraintsValid: true, + } + certRaw, err := x509.CreateCertificate(rand.Reader, cert, cert, key.Public(), key) + if err != nil { + t.Fatal(err) + } + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certRaw}), + pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyRaw}) +} diff --git a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/testdata/test-plugin.sh b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/testdata/test-plugin.sh index 6b9bb100c7..aa7daad5fd 100755 --- a/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/testdata/test-plugin.sh +++ b/staging/src/k8s.io/client-go/plugin/pkg/client/auth/exec/testdata/test-plugin.sh @@ -1,4 +1,4 @@ -#!/bin/sh -e +#!/bin/bash -e # Copyright 2018 The Kubernetes Authors. # diff --git a/staging/src/k8s.io/client-go/rest/transport.go b/staging/src/k8s.io/client-go/rest/transport.go index b6a0676326..7f01823d16 100644 --- a/staging/src/k8s.io/client-go/rest/transport.go +++ b/staging/src/k8s.io/client-go/rest/transport.go @@ -59,39 +59,10 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip // TransportConfig converts a client config to an appropriate transport config. func (c *Config) TransportConfig() (*transport.Config, error) { - wt := c.WrapTransport - if c.ExecProvider != nil { - provider, err := exec.GetAuthenticator(c.ExecProvider) - if err != nil { - return nil, err - } - if wt != nil { - previousWT := wt - wt = func(rt http.RoundTripper) http.RoundTripper { - return provider.WrapTransport(previousWT(rt)) - } - } else { - wt = provider.WrapTransport - } - } - if c.AuthProvider != nil { - provider, err := GetAuthProvider(c.Host, c.AuthProvider, c.AuthConfigPersister) - if err != nil { - return nil, err - } - if wt != nil { - previousWT := wt - wt = func(rt http.RoundTripper) http.RoundTripper { - return provider.WrapTransport(previousWT(rt)) - } - } else { - wt = provider.WrapTransport - } - } - return &transport.Config{ + conf := &transport.Config{ UserAgent: c.UserAgent, Transport: c.Transport, - WrapTransport: wt, + WrapTransport: c.WrapTransport, TLS: transport.TLSConfig{ Insecure: c.Insecure, ServerName: c.ServerName, @@ -111,5 +82,29 @@ func (c *Config) TransportConfig() (*transport.Config, error) { Extra: c.Impersonate.Extra, }, Dial: c.Dial, - }, nil + } + if c.ExecProvider != nil { + provider, err := exec.GetAuthenticator(c.ExecProvider) + if err != nil { + return nil, err + } + if err := provider.UpdateTransportConfig(conf); err != nil { + return nil, err + } + } + if c.AuthProvider != nil { + provider, err := GetAuthProvider(c.Host, c.AuthProvider, c.AuthConfigPersister) + if err != nil { + return nil, err + } + wt := conf.WrapTransport + if wt != nil { + conf.WrapTransport = func(rt http.RoundTripper) http.RoundTripper { + return provider.WrapTransport(wt(rt)) + } + } else { + conf.WrapTransport = provider.WrapTransport + } + } + return conf, nil } diff --git a/staging/src/k8s.io/client-go/transport/cache.go b/staging/src/k8s.io/client-go/transport/cache.go index 540af84946..7cffe2a5fa 100644 --- a/staging/src/k8s.io/client-go/transport/cache.go +++ b/staging/src/k8s.io/client-go/transport/cache.go @@ -43,6 +43,7 @@ type tlsCacheKey struct { caData string certData string keyData string + getCert string serverName string dial string } @@ -52,7 +53,7 @@ func (t tlsCacheKey) String() string { if len(t.keyData) > 0 { keyText = "" } - return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, serverName:%s, dial:%s", t.insecure, t.caData, t.certData, keyText, t.serverName, t.dial) + return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, getCert: %s, serverName:%s, dial:%s", t.insecure, t.caData, t.certData, keyText, t.getCert, t.serverName, t.dial) } func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) { @@ -109,6 +110,7 @@ func tlsConfigKey(c *Config) (tlsCacheKey, error) { caData: string(c.TLS.CAData), certData: string(c.TLS.CertData), keyData: string(c.TLS.KeyData), + getCert: fmt.Sprintf("%p", c.TLS.GetCert), serverName: c.TLS.ServerName, dial: fmt.Sprintf("%p", c.Dial), }, nil diff --git a/staging/src/k8s.io/client-go/transport/cache_test.go b/staging/src/k8s.io/client-go/transport/cache_test.go index 61f3affc62..9b740cdeca 100644 --- a/staging/src/k8s.io/client-go/transport/cache_test.go +++ b/staging/src/k8s.io/client-go/transport/cache_test.go @@ -18,6 +18,7 @@ package transport import ( "context" + "crypto/tls" "net" "net/http" "testing" @@ -54,6 +55,7 @@ func TestTLSConfigKey(t *testing.T) { // Make sure config fields that affect the tls config affect the cache key dialer := net.Dialer{} + getCert := func() (*tls.Certificate, error) { return nil, nil } uniqueConfigurations := map[string]*Config{ "no tls": {}, "dialer": {Dial: dialer.DialContext}, @@ -106,6 +108,24 @@ func TestTLSConfigKey(t *testing.T) { KeyData: []byte{1}, }, }, + "getCert1": { + TLS: TLSConfig{ + KeyData: []byte{1}, + GetCert: getCert, + }, + }, + "getCert2": { + TLS: TLSConfig{ + KeyData: []byte{1}, + GetCert: func() (*tls.Certificate, error) { return nil, nil }, + }, + }, + "getCert1, key 2": { + TLS: TLSConfig{ + KeyData: []byte{2}, + GetCert: getCert, + }, + }, } for nameA, valueA := range uniqueConfigurations { for nameB, valueB := range uniqueConfigurations { diff --git a/staging/src/k8s.io/client-go/transport/config.go b/staging/src/k8s.io/client-go/transport/config.go index 90f705d25f..4081c23e7f 100644 --- a/staging/src/k8s.io/client-go/transport/config.go +++ b/staging/src/k8s.io/client-go/transport/config.go @@ -18,6 +18,7 @@ package transport import ( "context" + "crypto/tls" "net" "net/http" ) @@ -84,7 +85,12 @@ func (c *Config) HasTokenAuth() bool { // HasCertAuth returns whether the configuration has certificate authentication or not. func (c *Config) HasCertAuth() bool { - return len(c.TLS.CertData) != 0 || len(c.TLS.CertFile) != 0 + return (len(c.TLS.CertData) != 0 || len(c.TLS.CertFile) != 0) && (len(c.TLS.KeyData) != 0 || len(c.TLS.KeyFile) != 0) +} + +// HasCertCallbacks returns whether the configuration has certificate callback or not. +func (c *Config) HasCertCallback() bool { + return c.TLS.GetCert != nil } // TLSConfig holds the information needed to set up a TLS transport. @@ -99,4 +105,6 @@ type TLSConfig struct { CAData []byte // Bytes of the PEM-encoded server trusted root certificates. Supercedes CAFile. CertData []byte // Bytes of the PEM-encoded client certificate. Supercedes CertFile. KeyData []byte // Bytes of the PEM-encoded client key. Supercedes KeyFile. + + GetCert func() (*tls.Certificate, error) // Callback that returns a TLS client certificate. CertData, CertFile, KeyData and KeyFile supercede this field. } diff --git a/staging/src/k8s.io/client-go/transport/transport.go b/staging/src/k8s.io/client-go/transport/transport.go index c2bb7ae5e4..c19739fdfe 100644 --- a/staging/src/k8s.io/client-go/transport/transport.go +++ b/staging/src/k8s.io/client-go/transport/transport.go @@ -28,7 +28,7 @@ import ( // or transport level security defined by the provided Config. func New(config *Config) (http.RoundTripper, error) { // Set transport level security - if config.Transport != nil && (config.HasCA() || config.HasCertAuth() || config.TLS.Insecure) { + if config.Transport != nil && (config.HasCA() || config.HasCertAuth() || config.HasCertCallback() || config.TLS.Insecure) { return nil, fmt.Errorf("using a custom transport with TLS certificate options or the insecure flag is not allowed") } @@ -52,7 +52,7 @@ func New(config *Config) (http.RoundTripper, error) { // TLSConfigFor returns a tls.Config that will provide the transport level security defined // by the provided Config. Will return nil if no transport level security is requested. func TLSConfigFor(c *Config) (*tls.Config, error) { - if !(c.HasCA() || c.HasCertAuth() || c.TLS.Insecure || len(c.TLS.ServerName) > 0) { + if !(c.HasCA() || c.HasCertAuth() || c.HasCertCallback() || c.TLS.Insecure || len(c.TLS.ServerName) > 0) { return nil, nil } if c.HasCA() && c.TLS.Insecure { @@ -75,12 +75,40 @@ func TLSConfigFor(c *Config) (*tls.Config, error) { tlsConfig.RootCAs = rootCertPool(c.TLS.CAData) } + var staticCert *tls.Certificate if c.HasCertAuth() { + // If key/cert were provided, verify them before setting up + // tlsConfig.GetClientCertificate. cert, err := tls.X509KeyPair(c.TLS.CertData, c.TLS.KeyData) if err != nil { return nil, err } - tlsConfig.Certificates = []tls.Certificate{cert} + staticCert = &cert + } + + if c.HasCertAuth() || c.HasCertCallback() { + tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { + // Note: static key/cert data always take precedence over cert + // callback. + if staticCert != nil { + return staticCert, nil + } + if c.HasCertCallback() { + cert, err := c.TLS.GetCert() + if err != nil { + return nil, err + } + // GetCert may return empty value, meaning no cert. + if cert != nil { + return cert, nil + } + } + + // Both c.TLS.CertData/KeyData were unset and GetCert didn't return + // anything. Return an empty tls.Certificate, no client cert will + // be sent to the server. + return &tls.Certificate{}, nil + } } return tlsConfig, nil diff --git a/staging/src/k8s.io/client-go/transport/transport_test.go b/staging/src/k8s.io/client-go/transport/transport_test.go index 8de751562d..2e9896a08b 100644 --- a/staging/src/k8s.io/client-go/transport/transport_test.go +++ b/staging/src/k8s.io/client-go/transport/transport_test.go @@ -17,6 +17,8 @@ limitations under the License. package transport import ( + "crypto/tls" + "errors" "net/http" "testing" ) @@ -94,6 +96,8 @@ func TestNew(t *testing.T) { Config *Config Err bool TLS bool + TLSCert bool + TLSErr bool Default bool }{ "default transport": { @@ -135,7 +139,8 @@ func TestNew(t *testing.T) { }, "cert transport": { - TLS: true, + TLS: true, + TLSCert: true, Config: &Config{ TLS: TLSConfig{ CAData: []byte(rootCACert), @@ -165,7 +170,8 @@ func TestNew(t *testing.T) { }, }, "key data overriding bad file cert transport": { - TLS: true, + TLS: true, + TLSCert: true, Config: &Config{ TLS: TLSConfig{ CAData: []byte(rootCACert), @@ -175,37 +181,108 @@ func TestNew(t *testing.T) { }, }, }, + "callback cert and key": { + TLS: true, + TLSCert: true, + Config: &Config{ + TLS: TLSConfig{ + CAData: []byte(rootCACert), + GetCert: func() (*tls.Certificate, error) { + crt, err := tls.X509KeyPair([]byte(certData), []byte(keyData)) + return &crt, err + }, + }, + }, + }, + "cert callback error": { + TLS: true, + TLSCert: true, + TLSErr: true, + Config: &Config{ + TLS: TLSConfig{ + CAData: []byte(rootCACert), + GetCert: func() (*tls.Certificate, error) { + return nil, errors.New("GetCert failure") + }, + }, + }, + }, + "cert data overrides empty callback result": { + TLS: true, + TLSCert: true, + Config: &Config{ + TLS: TLSConfig{ + CAData: []byte(rootCACert), + GetCert: func() (*tls.Certificate, error) { + return nil, nil + }, + CertData: []byte(certData), + KeyData: []byte(keyData), + }, + }, + }, + "callback returns nothing": { + TLS: true, + TLSCert: true, + Config: &Config{ + TLS: TLSConfig{ + CAData: []byte(rootCACert), + GetCert: func() (*tls.Certificate, error) { + return nil, nil + }, + }, + }, + }, } for k, testCase := range testCases { - transport, err := New(testCase.Config) - switch { - case testCase.Err && err == nil: - t.Errorf("%s: unexpected non-error", k) - continue - case !testCase.Err && err != nil: - t.Errorf("%s: unexpected error: %v", k, err) - continue - } + t.Run(k, func(t *testing.T) { + rt, err := New(testCase.Config) + switch { + case testCase.Err && err == nil: + t.Fatal("unexpected non-error") + case !testCase.Err && err != nil: + t.Fatalf("unexpected error: %v", err) + } + if testCase.Err { + return + } - switch { - case testCase.Default && transport != http.DefaultTransport: - t.Errorf("%s: expected the default transport, got %#v", k, transport) - continue - case !testCase.Default && transport == http.DefaultTransport: - t.Errorf("%s: expected non-default transport, got %#v", k, transport) - continue - } + switch { + case testCase.Default && rt != http.DefaultTransport: + t.Fatalf("got %#v, expected the default transport", rt) + case !testCase.Default && rt == http.DefaultTransport: + t.Fatalf("got %#v, expected non-default transport", rt) + } - // We only know how to check TLSConfig on http.Transports - if transport, ok := transport.(*http.Transport); ok { + // We only know how to check TLSConfig on http.Transports + transport := rt.(*http.Transport) switch { case testCase.TLS && transport.TLSClientConfig == nil: - t.Errorf("%s: expected TLSClientConfig, got %#v", k, transport) - continue + t.Fatalf("got %#v, expected TLSClientConfig", transport) case !testCase.TLS && transport.TLSClientConfig != nil: - t.Errorf("%s: expected no TLSClientConfig, got %#v", k, transport) - continue + t.Fatalf("got %#v, expected no TLSClientConfig", transport) } - } + if !testCase.TLS { + return + } + + switch { + case testCase.TLSCert && transport.TLSClientConfig.GetClientCertificate == nil: + t.Fatalf("got %#v, expected TLSClientConfig.GetClientCertificate", transport.TLSClientConfig) + case !testCase.TLSCert && transport.TLSClientConfig.GetClientCertificate != nil: + t.Fatalf("got %#v, expected no TLSClientConfig.GetClientCertificate", transport.TLSClientConfig) + } + if !testCase.TLSCert { + return + } + + _, err = transport.TLSClientConfig.GetClientCertificate(nil) + switch { + case testCase.TLSErr && err == nil: + t.Error("got nil error from GetClientCertificate, expected non-nil") + case !testCase.TLSErr && err != nil: + t.Errorf("got error from GetClientCertificate: %q, expected nil", err) + } + }) } } diff --git a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json index 9a0970403a..4e684a1612 100644 --- a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json @@ -1654,6 +1654,10 @@ "ImportPath": "k8s.io/client-go/util/cert", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/client-go/util/connrotation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/client-go/util/flowcontrol", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/metrics/Godeps/Godeps.json b/staging/src/k8s.io/metrics/Godeps/Godeps.json index 9415da5b54..656f0776ac 100644 --- a/staging/src/k8s.io/metrics/Godeps/Godeps.json +++ b/staging/src/k8s.io/metrics/Godeps/Godeps.json @@ -458,6 +458,10 @@ "ImportPath": "k8s.io/client-go/util/cert", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/client-go/util/connrotation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/client-go/util/flowcontrol", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json index f063522078..008ec6a99d 100644 --- a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json @@ -1622,6 +1622,10 @@ "ImportPath": "k8s.io/client-go/util/cert", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/client-go/util/connrotation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/client-go/util/flowcontrol", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/sample-controller/Godeps/Godeps.json b/staging/src/k8s.io/sample-controller/Godeps/Godeps.json index 9d2e0acd71..7fbae5e9bd 100644 --- a/staging/src/k8s.io/sample-controller/Godeps/Godeps.json +++ b/staging/src/k8s.io/sample-controller/Godeps/Godeps.json @@ -1046,6 +1046,10 @@ "ImportPath": "k8s.io/client-go/util/cert", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/client-go/util/connrotation", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/client-go/util/flowcontrol", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"