mirror of https://github.com/k3s-io/k3s
Add TLS support to exec authenticator plugin
https://github.com/kubernetes/community/blob/master/contributors/design-proposals/auth/kubectl-exec-plugins.md#tls-client-certificate-support Allows exec plugin to return raw TLS key/cert data. This data populates transport.Config.TLS fields. transport.Config.TLS propagates custom credentials using tls.Config.GetClientCertificate callback. On key/cert rotation, all connections using old credentials are closedpull/8/head
parent
f3d54f3f95
commit
cd89f9473f
|
@ -2010,6 +2010,10 @@
|
||||||
"ImportPath": "k8s.io/client-go/util/cert",
|
"ImportPath": "k8s.io/client-go/util/cert",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "k8s.io/client-go/util/connrotation",
|
||||||
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
|
@ -1738,6 +1738,10 @@
|
||||||
"ImportPath": "k8s.io/client-go/util/cert",
|
"ImportPath": "k8s.io/client-go/util/cert",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "k8s.io/client-go/util/connrotation",
|
||||||
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
|
@ -57,7 +57,14 @@ type ExecCredentialStatus struct {
|
||||||
// +optional
|
// +optional
|
||||||
ExpirationTimestamp *metav1.Time
|
ExpirationTimestamp *metav1.Time
|
||||||
// Token is a bearer token used by the client for request authentication.
|
// Token is a bearer token used by the client for request authentication.
|
||||||
|
// +optional
|
||||||
Token string
|
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
|
// Response defines metadata about a failed request, including HTTP status code and
|
||||||
|
|
|
@ -52,12 +52,20 @@ type ExecCredentialSpec struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecCredentialStatus holds credentials for the transport to use.
|
// 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 {
|
type ExecCredentialStatus struct {
|
||||||
// ExpirationTimestamp indicates a time when the provided credentials expire.
|
// ExpirationTimestamp indicates a time when the provided credentials expire.
|
||||||
// +optional
|
// +optional
|
||||||
ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"`
|
ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"`
|
||||||
// Token is a bearer token used by the client for request authentication.
|
// Token is a bearer token used by the client for request authentication.
|
||||||
Token string `json:"token,omitempty"`
|
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
|
// Response defines metadata about a failed request, including HTTP status code and
|
||||||
|
|
|
@ -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 {
|
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.ExpirationTimestamp = (*v1.Time)(unsafe.Pointer(in.ExpirationTimestamp))
|
||||||
out.Token = in.Token
|
out.Token = in.Token
|
||||||
|
out.ClientCertificateData = in.ClientCertificateData
|
||||||
|
out.ClientKeyData = in.ClientKeyData
|
||||||
return nil
|
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 {
|
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.ExpirationTimestamp = (*v1.Time)(unsafe.Pointer(in.ExpirationTimestamp))
|
||||||
out.Token = in.Token
|
out.Token = in.Token
|
||||||
|
out.ClientCertificateData = in.ClientCertificateData
|
||||||
|
out.ClientKeyData = in.ClientKeyData
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1: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/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/**"]),
|
data = glob(["testdata/**"]),
|
||||||
embed = [":go_default_library"],
|
embed = [":go_default_library"],
|
||||||
deps = [
|
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/pkg/apis/clientauthentication:go_default_library",
|
||||||
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
"//vendor/k8s.io/client-go/tools/clientcmd/api:go_default_library",
|
||||||
|
"//vendor/k8s.io/client-go/transport:go_default_library",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,15 @@ package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -35,6 +39,8 @@ import (
|
||||||
"k8s.io/client-go/pkg/apis/clientauthentication"
|
"k8s.io/client-go/pkg/apis/clientauthentication"
|
||||||
"k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
|
"k8s.io/client-go/pkg/apis/clientauthentication/v1alpha1"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
"k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
"k8s.io/client-go/transport"
|
||||||
|
"k8s.io/client-go/util/connrotation"
|
||||||
)
|
)
|
||||||
|
|
||||||
const execInfoEnv = "KUBERNETES_EXEC_INFO"
|
const execInfoEnv = "KUBERNETES_EXEC_INFO"
|
||||||
|
@ -147,14 +153,55 @@ type Authenticator struct {
|
||||||
// The mutex also guards calling the plugin. Since the plugin could be
|
// The mutex also guards calling the plugin. Since the plugin could be
|
||||||
// interactive we want to make sure it's only called once.
|
// interactive we want to make sure it's only called once.
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
cachedToken string
|
cachedCreds *credentials
|
||||||
exp time.Time
|
exp time.Time
|
||||||
|
|
||||||
|
onRotate func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapTransport instruments an existing http.RoundTripper with credentials returned
|
type credentials struct {
|
||||||
// by the plugin.
|
token string
|
||||||
func (a *Authenticator) WrapTransport(rt http.RoundTripper) http.RoundTripper {
|
cert *tls.Certificate
|
||||||
return &roundTripper{a, rt}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
type roundTripper struct {
|
||||||
|
@ -169,11 +216,13 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
return r.base.RoundTrip(req)
|
return r.base.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := r.a.token()
|
creds, err := r.a.getCreds()
|
||||||
if err != nil {
|
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)
|
res, err := r.base.RoundTrip(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -184,47 +233,60 @@ func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
Header: res.Header,
|
Header: res.Header,
|
||||||
Code: int32(res.StatusCode),
|
Code: int32(res.StatusCode),
|
||||||
}
|
}
|
||||||
if err := r.a.refresh(token, resp); err != nil {
|
if err := r.a.maybeRefreshCreds(creds, resp); err != nil {
|
||||||
glog.Errorf("refreshing token: %v", err)
|
glog.Errorf("refreshing credentials: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) tokenExpired() bool {
|
func (a *Authenticator) credsExpired() bool {
|
||||||
if a.exp.IsZero() {
|
if a.exp.IsZero() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return a.now().After(a.exp)
|
return a.now().After(a.exp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Authenticator) token() (string, error) {
|
func (a *Authenticator) cert() (*tls.Certificate, error) {
|
||||||
a.mu.Lock()
|
creds, err := a.getCreds()
|
||||||
defer a.mu.Unlock()
|
if err != nil {
|
||||||
if a.cachedToken != "" && !a.tokenExpired() {
|
return nil, err
|
||||||
return a.cachedToken, nil
|
|
||||||
}
|
}
|
||||||
|
return creds.cert, nil
|
||||||
return a.getToken(nil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// refresh executes the plugin to force a rotation of the token.
|
func (a *Authenticator) getCreds() (*credentials, error) {
|
||||||
func (a *Authenticator) refresh(token string, r *clientauthentication.Response) 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()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
if token != a.cachedToken {
|
// Since we're not making a new pointer to a.cachedCreds in getCreds, no
|
||||||
// Token already rotated.
|
// need to do deep comparison.
|
||||||
|
if creds != a.cachedCreds {
|
||||||
|
// Credentials already rotated.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := a.getToken(r)
|
return a.refreshCredsLocked(r)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getToken executes the plugin and reads the credentials from stdout. It must be
|
// refreshCredsLocked executes the plugin and reads the credentials from
|
||||||
// called while holding the Authenticator's mutex.
|
// stdout. It must be called while holding the Authenticator's mutex.
|
||||||
func (a *Authenticator) getToken(r *clientauthentication.Response) (string, error) {
|
func (a *Authenticator) refreshCredsLocked(r *clientauthentication.Response) error {
|
||||||
cred := &clientauthentication.ExecCredential{
|
cred := &clientauthentication.ExecCredential{
|
||||||
Spec: clientauthentication.ExecCredentialSpec{
|
Spec: clientauthentication.ExecCredentialSpec{
|
||||||
Response: r,
|
Response: r,
|
||||||
|
@ -234,7 +296,7 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro
|
||||||
|
|
||||||
data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
|
data, err := runtime.Encode(codecs.LegacyCodec(a.group), cred)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("encode ExecCredentials: %v", err)
|
return fmt.Errorf("encode ExecCredentials: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
env := append(a.environ(), a.env...)
|
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 {
|
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)
|
_, gvk, err := codecs.UniversalDecoder(a.group).Decode(stdout.Bytes(), nil, cred)
|
||||||
if err != nil {
|
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 {
|
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})
|
a.group, schema.GroupVersion{Group: gvk.Group, Version: gvk.Version})
|
||||||
}
|
}
|
||||||
|
|
||||||
if cred.Status == nil {
|
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 == "" {
|
if cred.Status.Token == "" && cred.Status.ClientCertificateData == "" && cred.Status.ClientKeyData == "" {
|
||||||
return "", fmt.Errorf("exec plugin didn't return a token")
|
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 {
|
if cred.Status.ExpirationTimestamp != nil {
|
||||||
|
@ -274,7 +339,24 @@ func (a *Authenticator) getToken(r *clientauthentication.Response) (string, erro
|
||||||
} else {
|
} else {
|
||||||
a.exp = time.Time{}
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,19 +18,88 @@ package exec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/elliptic"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/pkg/apis/clientauthentication"
|
||||||
"k8s.io/client-go/tools/clientcmd/api"
|
"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) {
|
func TestCacheKey(t *testing.T) {
|
||||||
c1 := &api.ExecConfig{
|
c1 := &api.ExecConfig{
|
||||||
Command: "foo-bar",
|
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 {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
config api.ExecConfig
|
config api.ExecConfig
|
||||||
|
@ -101,7 +170,7 @@ func TestGetToken(t *testing.T) {
|
||||||
interactive bool
|
interactive bool
|
||||||
response *clientauthentication.Response
|
response *clientauthentication.Response
|
||||||
wantInput string
|
wantInput string
|
||||||
wantToken string
|
wantCreds credentials
|
||||||
wantExpiry time.Time
|
wantExpiry time.Time
|
||||||
wantErr bool
|
wantErr bool
|
||||||
}{
|
}{
|
||||||
|
@ -122,7 +191,7 @@ func TestGetToken(t *testing.T) {
|
||||||
"token": "foo-bar"
|
"token": "foo-bar"
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
wantToken: "foo-bar",
|
wantCreds: credentials{token: "foo-bar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "interactive",
|
name: "interactive",
|
||||||
|
@ -144,7 +213,7 @@ func TestGetToken(t *testing.T) {
|
||||||
"token": "foo-bar"
|
"token": "foo-bar"
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
wantToken: "foo-bar",
|
wantCreds: credentials{token: "foo-bar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "response",
|
name: "response",
|
||||||
|
@ -178,7 +247,7 @@ func TestGetToken(t *testing.T) {
|
||||||
"token": "foo-bar"
|
"token": "foo-bar"
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
wantToken: "foo-bar",
|
wantCreds: credentials{token: "foo-bar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "expiry",
|
name: "expiry",
|
||||||
|
@ -199,7 +268,7 @@ func TestGetToken(t *testing.T) {
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC),
|
wantExpiry: time.Date(2006, 01, 02, 15, 04, 05, 0, time.UTC),
|
||||||
wantToken: "foo-bar",
|
wantCreds: credentials{token: "foo-bar"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no-group-version",
|
name: "no-group-version",
|
||||||
|
@ -236,7 +305,7 @@ func TestGetToken(t *testing.T) {
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no-token",
|
name: "no-creds",
|
||||||
config: api.ExecConfig{
|
config: api.ExecConfig{
|
||||||
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
APIVersion: "client.authentication.k8s.io/v1alpha1",
|
||||||
},
|
},
|
||||||
|
@ -252,6 +321,65 @@ func TestGetToken(t *testing.T) {
|
||||||
}`,
|
}`,
|
||||||
wantErr: true,
|
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 {
|
for _, test := range tests {
|
||||||
|
@ -274,8 +402,7 @@ func TestGetToken(t *testing.T) {
|
||||||
a.interactive = test.interactive
|
a.interactive = test.interactive
|
||||||
a.environ = func() []string { return nil }
|
a.environ = func() []string { return nil }
|
||||||
|
|
||||||
token, err := a.getToken(test.response)
|
if err := a.refreshCredsLocked(test.response); err != nil {
|
||||||
if err != nil {
|
|
||||||
if !test.wantErr {
|
if !test.wantErr {
|
||||||
t.Errorf("get token %v", err)
|
t.Errorf("get token %v", err)
|
||||||
}
|
}
|
||||||
|
@ -285,8 +412,8 @@ func TestGetToken(t *testing.T) {
|
||||||
t.Fatal("expected error getting token")
|
t.Fatal("expected error getting token")
|
||||||
}
|
}
|
||||||
|
|
||||||
if token != test.wantToken {
|
if !reflect.DeepEqual(a.cachedCreds, &test.wantCreds) {
|
||||||
t.Errorf("expected token %q got %q", test.wantToken, token)
|
t.Errorf("expected credentials %+v got %+v", &test.wantCreds, a.cachedCreds)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !a.exp.Equal(test.wantExpiry) {
|
if !a.exp.Equal(test.wantExpiry) {
|
||||||
|
@ -342,8 +469,12 @@ func TestRoundTripper(t *testing.T) {
|
||||||
a.now = now
|
a.now = now
|
||||||
a.stderr = ioutil.Discard
|
a.stderr = ioutil.Discard
|
||||||
|
|
||||||
|
tc := &transport.Config{}
|
||||||
|
if err := a.UpdateTransportConfig(tc); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Transport: a.WrapTransport(http.DefaultTransport),
|
Transport: tc.WrapTransport(http.DefaultTransport),
|
||||||
}
|
}
|
||||||
|
|
||||||
get := func(t *testing.T, statusCode int) {
|
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.
|
// Old token is expired, should refresh automatically without hitting a 401.
|
||||||
get(t, http.StatusOK)
|
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})
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh -e
|
#!/bin/bash -e
|
||||||
|
|
||||||
# Copyright 2018 The Kubernetes Authors.
|
# Copyright 2018 The Kubernetes Authors.
|
||||||
#
|
#
|
||||||
|
|
|
@ -59,39 +59,10 @@ func HTTPWrappersForConfig(config *Config, rt http.RoundTripper) (http.RoundTrip
|
||||||
|
|
||||||
// TransportConfig converts a client config to an appropriate transport config.
|
// TransportConfig converts a client config to an appropriate transport config.
|
||||||
func (c *Config) TransportConfig() (*transport.Config, error) {
|
func (c *Config) TransportConfig() (*transport.Config, error) {
|
||||||
wt := c.WrapTransport
|
conf := &transport.Config{
|
||||||
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{
|
|
||||||
UserAgent: c.UserAgent,
|
UserAgent: c.UserAgent,
|
||||||
Transport: c.Transport,
|
Transport: c.Transport,
|
||||||
WrapTransport: wt,
|
WrapTransport: c.WrapTransport,
|
||||||
TLS: transport.TLSConfig{
|
TLS: transport.TLSConfig{
|
||||||
Insecure: c.Insecure,
|
Insecure: c.Insecure,
|
||||||
ServerName: c.ServerName,
|
ServerName: c.ServerName,
|
||||||
|
@ -111,5 +82,29 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
|
||||||
Extra: c.Impersonate.Extra,
|
Extra: c.Impersonate.Extra,
|
||||||
},
|
},
|
||||||
Dial: c.Dial,
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ type tlsCacheKey struct {
|
||||||
caData string
|
caData string
|
||||||
certData string
|
certData string
|
||||||
keyData string
|
keyData string
|
||||||
|
getCert string
|
||||||
serverName string
|
serverName string
|
||||||
dial string
|
dial string
|
||||||
}
|
}
|
||||||
|
@ -52,7 +53,7 @@ func (t tlsCacheKey) String() string {
|
||||||
if len(t.keyData) > 0 {
|
if len(t.keyData) > 0 {
|
||||||
keyText = "<redacted>"
|
keyText = "<redacted>"
|
||||||
}
|
}
|
||||||
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) {
|
func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
|
||||||
|
@ -109,6 +110,7 @@ func tlsConfigKey(c *Config) (tlsCacheKey, error) {
|
||||||
caData: string(c.TLS.CAData),
|
caData: string(c.TLS.CAData),
|
||||||
certData: string(c.TLS.CertData),
|
certData: string(c.TLS.CertData),
|
||||||
keyData: string(c.TLS.KeyData),
|
keyData: string(c.TLS.KeyData),
|
||||||
|
getCert: fmt.Sprintf("%p", c.TLS.GetCert),
|
||||||
serverName: c.TLS.ServerName,
|
serverName: c.TLS.ServerName,
|
||||||
dial: fmt.Sprintf("%p", c.Dial),
|
dial: fmt.Sprintf("%p", c.Dial),
|
||||||
}, nil
|
}, nil
|
||||||
|
|
|
@ -18,6 +18,7 @@ package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -54,6 +55,7 @@ func TestTLSConfigKey(t *testing.T) {
|
||||||
|
|
||||||
// Make sure config fields that affect the tls config affect the cache key
|
// Make sure config fields that affect the tls config affect the cache key
|
||||||
dialer := net.Dialer{}
|
dialer := net.Dialer{}
|
||||||
|
getCert := func() (*tls.Certificate, error) { return nil, nil }
|
||||||
uniqueConfigurations := map[string]*Config{
|
uniqueConfigurations := map[string]*Config{
|
||||||
"no tls": {},
|
"no tls": {},
|
||||||
"dialer": {Dial: dialer.DialContext},
|
"dialer": {Dial: dialer.DialContext},
|
||||||
|
@ -106,6 +108,24 @@ func TestTLSConfigKey(t *testing.T) {
|
||||||
KeyData: []byte{1},
|
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 nameA, valueA := range uniqueConfigurations {
|
||||||
for nameB, valueB := range uniqueConfigurations {
|
for nameB, valueB := range uniqueConfigurations {
|
||||||
|
|
|
@ -18,6 +18,7 @@ package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
@ -84,7 +85,12 @@ func (c *Config) HasTokenAuth() bool {
|
||||||
|
|
||||||
// HasCertAuth returns whether the configuration has certificate authentication or not.
|
// HasCertAuth returns whether the configuration has certificate authentication or not.
|
||||||
func (c *Config) HasCertAuth() bool {
|
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.
|
// 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.
|
CAData []byte // Bytes of the PEM-encoded server trusted root certificates. Supercedes CAFile.
|
||||||
CertData []byte // Bytes of the PEM-encoded client certificate. Supercedes CertFile.
|
CertData []byte // Bytes of the PEM-encoded client certificate. Supercedes CertFile.
|
||||||
KeyData []byte // Bytes of the PEM-encoded client key. Supercedes KeyFile.
|
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.
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import (
|
||||||
// or transport level security defined by the provided Config.
|
// or transport level security defined by the provided Config.
|
||||||
func New(config *Config) (http.RoundTripper, error) {
|
func New(config *Config) (http.RoundTripper, error) {
|
||||||
// Set transport level security
|
// 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")
|
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
|
// 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.
|
// by the provided Config. Will return nil if no transport level security is requested.
|
||||||
func TLSConfigFor(c *Config) (*tls.Config, error) {
|
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
|
return nil, nil
|
||||||
}
|
}
|
||||||
if c.HasCA() && c.TLS.Insecure {
|
if c.HasCA() && c.TLS.Insecure {
|
||||||
|
@ -75,12 +75,40 @@ func TLSConfigFor(c *Config) (*tls.Config, error) {
|
||||||
tlsConfig.RootCAs = rootCertPool(c.TLS.CAData)
|
tlsConfig.RootCAs = rootCertPool(c.TLS.CAData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var staticCert *tls.Certificate
|
||||||
if c.HasCertAuth() {
|
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)
|
cert, err := tls.X509KeyPair(c.TLS.CertData, c.TLS.KeyData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return tlsConfig, nil
|
||||||
|
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
||||||
package transport
|
package transport
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -94,6 +96,8 @@ func TestNew(t *testing.T) {
|
||||||
Config *Config
|
Config *Config
|
||||||
Err bool
|
Err bool
|
||||||
TLS bool
|
TLS bool
|
||||||
|
TLSCert bool
|
||||||
|
TLSErr bool
|
||||||
Default bool
|
Default bool
|
||||||
}{
|
}{
|
||||||
"default transport": {
|
"default transport": {
|
||||||
|
@ -135,7 +139,8 @@ func TestNew(t *testing.T) {
|
||||||
},
|
},
|
||||||
|
|
||||||
"cert transport": {
|
"cert transport": {
|
||||||
TLS: true,
|
TLS: true,
|
||||||
|
TLSCert: true,
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
TLS: TLSConfig{
|
TLS: TLSConfig{
|
||||||
CAData: []byte(rootCACert),
|
CAData: []byte(rootCACert),
|
||||||
|
@ -165,7 +170,8 @@ func TestNew(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"key data overriding bad file cert transport": {
|
"key data overriding bad file cert transport": {
|
||||||
TLS: true,
|
TLS: true,
|
||||||
|
TLSCert: true,
|
||||||
Config: &Config{
|
Config: &Config{
|
||||||
TLS: TLSConfig{
|
TLS: TLSConfig{
|
||||||
CAData: []byte(rootCACert),
|
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 {
|
for k, testCase := range testCases {
|
||||||
transport, err := New(testCase.Config)
|
t.Run(k, func(t *testing.T) {
|
||||||
switch {
|
rt, err := New(testCase.Config)
|
||||||
case testCase.Err && err == nil:
|
switch {
|
||||||
t.Errorf("%s: unexpected non-error", k)
|
case testCase.Err && err == nil:
|
||||||
continue
|
t.Fatal("unexpected non-error")
|
||||||
case !testCase.Err && err != nil:
|
case !testCase.Err && err != nil:
|
||||||
t.Errorf("%s: unexpected error: %v", k, err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
continue
|
}
|
||||||
}
|
if testCase.Err {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case testCase.Default && transport != http.DefaultTransport:
|
case testCase.Default && rt != http.DefaultTransport:
|
||||||
t.Errorf("%s: expected the default transport, got %#v", k, transport)
|
t.Fatalf("got %#v, expected the default transport", rt)
|
||||||
continue
|
case !testCase.Default && rt == http.DefaultTransport:
|
||||||
case !testCase.Default && transport == http.DefaultTransport:
|
t.Fatalf("got %#v, expected non-default transport", rt)
|
||||||
t.Errorf("%s: expected non-default transport, got %#v", k, transport)
|
}
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only know how to check TLSConfig on http.Transports
|
// We only know how to check TLSConfig on http.Transports
|
||||||
if transport, ok := transport.(*http.Transport); ok {
|
transport := rt.(*http.Transport)
|
||||||
switch {
|
switch {
|
||||||
case testCase.TLS && transport.TLSClientConfig == nil:
|
case testCase.TLS && transport.TLSClientConfig == nil:
|
||||||
t.Errorf("%s: expected TLSClientConfig, got %#v", k, transport)
|
t.Fatalf("got %#v, expected TLSClientConfig", transport)
|
||||||
continue
|
|
||||||
case !testCase.TLS && transport.TLSClientConfig != nil:
|
case !testCase.TLS && transport.TLSClientConfig != nil:
|
||||||
t.Errorf("%s: expected no TLSClientConfig, got %#v", k, transport)
|
t.Fatalf("got %#v, expected no TLSClientConfig", transport)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
}
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1654,6 +1654,10 @@
|
||||||
"ImportPath": "k8s.io/client-go/util/cert",
|
"ImportPath": "k8s.io/client-go/util/cert",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "k8s.io/client-go/util/connrotation",
|
||||||
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
|
@ -458,6 +458,10 @@
|
||||||
"ImportPath": "k8s.io/client-go/util/cert",
|
"ImportPath": "k8s.io/client-go/util/cert",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "k8s.io/client-go/util/connrotation",
|
||||||
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
|
@ -1622,6 +1622,10 @@
|
||||||
"ImportPath": "k8s.io/client-go/util/cert",
|
"ImportPath": "k8s.io/client-go/util/cert",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "k8s.io/client-go/util/connrotation",
|
||||||
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
|
@ -1046,6 +1046,10 @@
|
||||||
"ImportPath": "k8s.io/client-go/util/cert",
|
"ImportPath": "k8s.io/client-go/util/cert",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ImportPath": "k8s.io/client-go/util/connrotation",
|
||||||
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
"ImportPath": "k8s.io/client-go/util/flowcontrol",
|
||||||
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
"Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||||
|
|
Loading…
Reference in New Issue