mirror of
https://github.com/kubernetes/client-go.git
synced 2025-06-21 12:48:30 +00:00
rest.Config: support configuring an explict proxy URL
With support of http, https, and socks5 proxy support. We already support configuring this via environmnet variables, but this approach becomes inconvenient dealing with multiple clusters on different networks, that require different proxies to connect to. Most solutions require wrapping clients (like kubectl) in bash scripts. Part of: https://github.com/kubernetes/client-go/issues/351 Kubernetes-commit: f3f666d5f1f6f74a8c948a5c64af993696178244
This commit is contained in:
parent
06f6a9f888
commit
0caa50056a
@ -18,16 +18,16 @@ package rest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/http/httputil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
v1beta1 "k8s.io/api/extensions/v1beta1"
|
v1beta1 "k8s.io/api/extensions/v1beta1"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
@ -37,6 +37,8 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/diff"
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
utiltesting "k8s.io/client-go/util/testing"
|
utiltesting "k8s.io/client-go/util/testing"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TestParam struct {
|
type TestParam struct {
|
||||||
@ -252,7 +254,7 @@ func validate(testParam TestParam, t *testing.T, body []byte, fakeHandler *utilt
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHttpMethods(t *testing.T) {
|
func TestHTTPMethods(t *testing.T) {
|
||||||
testServer, _, _ := testServerEnv(t, 200)
|
testServer, _, _ := testServerEnv(t, 200)
|
||||||
defer testServer.Close()
|
defer testServer.Close()
|
||||||
c, _ := restClient(testServer)
|
c, _ := restClient(testServer)
|
||||||
@ -283,6 +285,57 @@ func TestHttpMethods(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHTTPProxy(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
testServer, fh, _ := testServerEnv(t, 200)
|
||||||
|
fh.ResponseBody = "backend data"
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
testProxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
to, err := url.Parse(req.RequestURI)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
w.Write([]byte("proxied: "))
|
||||||
|
httputil.NewSingleHostReverseProxy(to).ServeHTTP(w, req)
|
||||||
|
}))
|
||||||
|
defer testProxyServer.Close()
|
||||||
|
|
||||||
|
t.Logf(testProxyServer.URL)
|
||||||
|
|
||||||
|
u, err := url.Parse(testProxyServer.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to parse test proxy server url: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := RESTClientFor(&Config{
|
||||||
|
Host: testServer.URL,
|
||||||
|
ContentConfig: ContentConfig{
|
||||||
|
GroupVersion: &v1.SchemeGroupVersion,
|
||||||
|
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
|
||||||
|
},
|
||||||
|
Proxy: http.ProxyURL(u),
|
||||||
|
Username: "user",
|
||||||
|
Password: "pass",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
request := c.Get()
|
||||||
|
if request == nil {
|
||||||
|
t.Fatalf("Get: Object returned should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := request.DoRaw(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected err: %v", err)
|
||||||
|
}
|
||||||
|
if got, want := string(b), "proxied: backend data"; !cmp.Equal(got, want) {
|
||||||
|
t.Errorf("unexpected body: %v", cmp.Diff(want, got))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateBackoffManager(t *testing.T) {
|
func TestCreateBackoffManager(t *testing.T) {
|
||||||
|
|
||||||
theUrl, _ := url.Parse("http://localhost")
|
theUrl, _ := url.Parse("http://localhost")
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
gruntime "runtime"
|
gruntime "runtime"
|
||||||
@ -128,6 +129,13 @@ type Config struct {
|
|||||||
// Dial specifies the dial function for creating unencrypted TCP connections.
|
// Dial specifies the dial function for creating unencrypted TCP connections.
|
||||||
Dial func(ctx context.Context, network, address string) (net.Conn, error)
|
Dial func(ctx context.Context, network, address string) (net.Conn, error)
|
||||||
|
|
||||||
|
// Proxy is the the proxy func to be used for all requests made by this
|
||||||
|
// transport. If Proxy is nil, http.ProxyFromEnvironment is used. If Proxy
|
||||||
|
// returns a nil *URL, no proxy is used.
|
||||||
|
//
|
||||||
|
// socks5 proxying does not currently support spdy streaming endpoints.
|
||||||
|
Proxy func(*http.Request) (*url.URL, error)
|
||||||
|
|
||||||
// Version forces a specific version to be used (if registered)
|
// Version forces a specific version to be used (if registered)
|
||||||
// Do we need this?
|
// Do we need this?
|
||||||
// Version string
|
// Version string
|
||||||
@ -560,6 +568,7 @@ func AnonymousClientConfig(config *Config) *Config {
|
|||||||
Burst: config.Burst,
|
Burst: config.Burst,
|
||||||
Timeout: config.Timeout,
|
Timeout: config.Timeout,
|
||||||
Dial: config.Dial,
|
Dial: config.Dial,
|
||||||
|
Proxy: config.Proxy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -601,5 +610,6 @@ func CopyConfig(config *Config) *Config {
|
|||||||
RateLimiter: config.RateLimiter,
|
RateLimiter: config.RateLimiter,
|
||||||
Timeout: config.Timeout,
|
Timeout: config.Timeout,
|
||||||
Dial: config.Dial,
|
Dial: config.Dial,
|
||||||
|
Proxy: config.Proxy,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
@ -32,12 +33,12 @@ import (
|
|||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apimachinery/pkg/util/diff"
|
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
"k8s.io/client-go/kubernetes/scheme"
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
"k8s.io/client-go/transport"
|
"k8s.io/client-go/transport"
|
||||||
"k8s.io/client-go/util/flowcontrol"
|
"k8s.io/client-go/util/flowcontrol"
|
||||||
|
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
fuzz "github.com/google/gofuzz"
|
fuzz "github.com/google/gofuzz"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -274,8 +275,13 @@ func (n *fakeNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder,
|
|||||||
var fakeDialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
var fakeDialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return nil, fakeDialerError
|
return nil, fakeDialerError
|
||||||
}
|
}
|
||||||
|
|
||||||
var fakeDialerError = errors.New("fakedialer")
|
var fakeDialerError = errors.New("fakedialer")
|
||||||
|
|
||||||
|
func fakeProxyFunc(*http.Request) (*url.URL, error) {
|
||||||
|
return nil, errors.New("fakeproxy")
|
||||||
|
}
|
||||||
|
|
||||||
type fakeAuthProviderConfigPersister struct{}
|
type fakeAuthProviderConfigPersister struct{}
|
||||||
|
|
||||||
func (fakeAuthProviderConfigPersister) Persist(map[string]string) error {
|
func (fakeAuthProviderConfigPersister) Persist(map[string]string) error {
|
||||||
@ -318,8 +324,12 @@ func TestAnonymousConfig(t *testing.T) {
|
|||||||
func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) {
|
func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) {
|
||||||
r.Config = map[string]string{}
|
r.Config = map[string]string{}
|
||||||
},
|
},
|
||||||
// Dial does not require fuzzer
|
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {
|
||||||
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {},
|
*r = fakeDialFunc
|
||||||
|
},
|
||||||
|
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
|
||||||
|
*r = fakeProxyFunc
|
||||||
|
},
|
||||||
)
|
)
|
||||||
for i := 0; i < 20; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
original := &Config{}
|
original := &Config{}
|
||||||
@ -350,13 +360,22 @@ func TestAnonymousConfig(t *testing.T) {
|
|||||||
if !reflect.DeepEqual(expectedError, actualError) {
|
if !reflect.DeepEqual(expectedError, actualError) {
|
||||||
t.Fatalf("AnonymousClientConfig dropped the Dial field")
|
t.Fatalf("AnonymousClientConfig dropped the Dial field")
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
actual.Dial = nil
|
actual.Dial = nil
|
||||||
expected.Dial = nil
|
expected.Dial = nil
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(*actual, expected) {
|
if actual.Proxy != nil {
|
||||||
t.Fatalf("AnonymousClientConfig dropped unexpected fields, identify whether they are security related or not: %s", diff.ObjectGoPrintDiff(expected, actual))
|
_, actualError := actual.Proxy(nil)
|
||||||
|
_, expectedError := expected.Proxy(nil)
|
||||||
|
if !reflect.DeepEqual(expectedError, actualError) {
|
||||||
|
t.Fatalf("AnonymousClientConfig dropped the Proxy field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actual.Proxy = nil
|
||||||
|
expected.Proxy = nil
|
||||||
|
|
||||||
|
if diff := cmp.Diff(*actual, expected); diff != "" {
|
||||||
|
t.Fatalf("AnonymousClientConfig dropped unexpected fields, identify whether they are security related or not (-got, +want): %s", diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -396,6 +415,9 @@ func TestCopyConfig(t *testing.T) {
|
|||||||
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {
|
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {
|
||||||
*r = fakeDialFunc
|
*r = fakeDialFunc
|
||||||
},
|
},
|
||||||
|
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
|
||||||
|
*r = fakeProxyFunc
|
||||||
|
},
|
||||||
)
|
)
|
||||||
for i := 0; i < 20; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
original := &Config{}
|
original := &Config{}
|
||||||
@ -410,10 +432,10 @@ func TestCopyConfig(t *testing.T) {
|
|||||||
// function return the expected object.
|
// function return the expected object.
|
||||||
if actual.WrapTransport == nil || !reflect.DeepEqual(expected.WrapTransport(nil), &fakeRoundTripper{}) {
|
if actual.WrapTransport == nil || !reflect.DeepEqual(expected.WrapTransport(nil), &fakeRoundTripper{}) {
|
||||||
t.Fatalf("CopyConfig dropped the WrapTransport field")
|
t.Fatalf("CopyConfig dropped the WrapTransport field")
|
||||||
} else {
|
}
|
||||||
actual.WrapTransport = nil
|
actual.WrapTransport = nil
|
||||||
expected.WrapTransport = nil
|
expected.WrapTransport = nil
|
||||||
}
|
|
||||||
if actual.Dial != nil {
|
if actual.Dial != nil {
|
||||||
_, actualError := actual.Dial(context.Background(), "", "")
|
_, actualError := actual.Dial(context.Background(), "", "")
|
||||||
_, expectedError := expected.Dial(context.Background(), "", "")
|
_, expectedError := expected.Dial(context.Background(), "", "")
|
||||||
@ -423,6 +445,7 @@ func TestCopyConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual.Dial = nil
|
actual.Dial = nil
|
||||||
expected.Dial = nil
|
expected.Dial = nil
|
||||||
|
|
||||||
if actual.AuthConfigPersister != nil {
|
if actual.AuthConfigPersister != nil {
|
||||||
actualError := actual.AuthConfigPersister.Persist(nil)
|
actualError := actual.AuthConfigPersister.Persist(nil)
|
||||||
expectedError := expected.AuthConfigPersister.Persist(nil)
|
expectedError := expected.AuthConfigPersister.Persist(nil)
|
||||||
@ -433,8 +456,18 @@ func TestCopyConfig(t *testing.T) {
|
|||||||
actual.AuthConfigPersister = nil
|
actual.AuthConfigPersister = nil
|
||||||
expected.AuthConfigPersister = nil
|
expected.AuthConfigPersister = nil
|
||||||
|
|
||||||
if !reflect.DeepEqual(*actual, expected) {
|
if actual.Proxy != nil {
|
||||||
t.Fatalf("CopyConfig dropped unexpected fields, identify whether they are security related or not: %s", diff.ObjectReflectDiff(expected, *actual))
|
_, actualError := actual.Proxy(nil)
|
||||||
|
_, expectedError := expected.Proxy(nil)
|
||||||
|
if !reflect.DeepEqual(expectedError, actualError) {
|
||||||
|
t.Fatalf("CopyConfig dropped the Proxy field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
actual.Proxy = nil
|
||||||
|
expected.Proxy = nil
|
||||||
|
|
||||||
|
if diff := cmp.Diff(*actual, expected); diff != "" {
|
||||||
|
t.Fatalf("CopyConfig dropped unexpected fields, identify whether they are security related or not (-got, +want): %s", diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -564,10 +597,11 @@ func TestConfigSprint(t *testing.T) {
|
|||||||
RateLimiter: &fakeLimiter{},
|
RateLimiter: &fakeLimiter{},
|
||||||
Timeout: 3 * time.Second,
|
Timeout: 3 * time.Second,
|
||||||
Dial: fakeDialFunc,
|
Dial: fakeDialFunc,
|
||||||
|
Proxy: fakeProxyFunc,
|
||||||
}
|
}
|
||||||
want := fmt.Sprintf(
|
want := fmt.Sprintf(
|
||||||
`&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p)}`,
|
`&rest.Config{Host:"localhost:8080", APIPath:"v1", ContentConfig:rest.ContentConfig{AcceptContentTypes:"application/json", ContentType:"application/json", GroupVersion:(*schema.GroupVersion)(nil), NegotiatedSerializer:runtime.NegotiatedSerializer(nil)}, Username:"gopher", Password:"--- REDACTED ---", BearerToken:"--- REDACTED ---", BearerTokenFile:"", Impersonate:rest.ImpersonationConfig{UserName:"gopher2", Groups:[]string(nil), Extra:map[string][]string(nil)}, AuthProvider:api.AuthProviderConfig{Name: "gopher", Config: map[string]string{--- REDACTED ---}}, AuthConfigPersister:rest.AuthProviderConfigPersister(--- REDACTED ---), ExecProvider:api.AuthProviderConfig{Command: "sudo", Args: []string{"--- REDACTED ---"}, Env: []ExecEnvVar{--- REDACTED ---}, APIVersion: ""}, TLSClientConfig:rest.sanitizedTLSClientConfig{Insecure:false, ServerName:"", CertFile:"a.crt", KeyFile:"a.key", CAFile:"", CertData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x54, 0x52, 0x55, 0x4e, 0x43, 0x41, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, KeyData:[]uint8{0x2d, 0x2d, 0x2d, 0x20, 0x52, 0x45, 0x44, 0x41, 0x43, 0x54, 0x45, 0x44, 0x20, 0x2d, 0x2d, 0x2d}, CAData:[]uint8(nil), NextProtos:[]string{"h2", "http/1.1"}}, UserAgent:"gobot", DisableCompression:false, Transport:(*rest.fakeRoundTripper)(%p), WrapTransport:(transport.WrapperFunc)(%p), QPS:1, Burst:2, RateLimiter:(*rest.fakeLimiter)(%p), Timeout:3000000000, Dial:(func(context.Context, string, string) (net.Conn, error))(%p), Proxy:(func(*http.Request) (*url.URL, error))(%p)}`,
|
||||||
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc,
|
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc,
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, f := range []string{"%s", "%v", "%+v", "%#v"} {
|
for _, f := range []string{"%s", "%v", "%+v", "%#v"} {
|
||||||
|
@ -86,6 +86,7 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
|
|||||||
Extra: c.Impersonate.Extra,
|
Extra: c.Impersonate.Extra,
|
||||||
},
|
},
|
||||||
Dial: c.Dial,
|
Dial: c.Dial,
|
||||||
|
Proxy: c.Proxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.ExecProvider != nil && c.AuthProvider != nil {
|
if c.ExecProvider != nil && c.AuthProvider != nil {
|
||||||
|
@ -82,6 +82,17 @@ type Cluster struct {
|
|||||||
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
|
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
|
||||||
// +optional
|
// +optional
|
||||||
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
|
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
|
||||||
|
// ProxyURL is the URL to the proxy to be used for all requests made by this
|
||||||
|
// client. URLs with "http", "https", and "socks5" schemes are supported. If
|
||||||
|
// this configuration is not provided or the empty string, the client
|
||||||
|
// attempts to construct a proxy configuration from http_proxy and
|
||||||
|
// https_proxy environment variables. If these environment variables are not
|
||||||
|
// set, the client does not attempt to proxy requests.
|
||||||
|
//
|
||||||
|
// socks5 proxying does not currently support spdy streaming endpoints (exec,
|
||||||
|
// attach, port forward).
|
||||||
|
// +optional
|
||||||
|
ProxyURL string `json:"proxy-url,omitempty"`
|
||||||
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
||||||
// +optional
|
// +optional
|
||||||
Extensions map[string]runtime.Object `json:"extensions,omitempty"`
|
Extensions map[string]runtime.Object `json:"extensions,omitempty"`
|
||||||
|
@ -75,6 +75,17 @@ type Cluster struct {
|
|||||||
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
|
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
|
||||||
// +optional
|
// +optional
|
||||||
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
|
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
|
||||||
|
// ProxyURL is the URL to the proxy to be used for all requests made by this
|
||||||
|
// client. URLs with "http", "https", and "socks5" schemes are supported. If
|
||||||
|
// this configuration is not provided or the empty string, the client
|
||||||
|
// attempts to construct a proxy configuration from http_proxy and
|
||||||
|
// https_proxy environment variables. If these environment variables are not
|
||||||
|
// set, the client does not attempt to proxy requests.
|
||||||
|
//
|
||||||
|
// socks5 proxying does not currently support spdy streaming endpoints (exec,
|
||||||
|
// attach, port forward).
|
||||||
|
// +optional
|
||||||
|
ProxyURL string `json:"proxy-url,omitempty"`
|
||||||
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
|
||||||
// +optional
|
// +optional
|
||||||
Extensions []NamedExtension `json:"extensions,omitempty"`
|
Extensions []NamedExtension `json:"extensions,omitempty"`
|
||||||
|
@ -237,6 +237,7 @@ func autoConvert_v1_Cluster_To_api_Cluster(in *Cluster, out *api.Cluster, s conv
|
|||||||
out.InsecureSkipTLSVerify = in.InsecureSkipTLSVerify
|
out.InsecureSkipTLSVerify = in.InsecureSkipTLSVerify
|
||||||
out.CertificateAuthority = in.CertificateAuthority
|
out.CertificateAuthority = in.CertificateAuthority
|
||||||
out.CertificateAuthorityData = *(*[]byte)(unsafe.Pointer(&in.CertificateAuthorityData))
|
out.CertificateAuthorityData = *(*[]byte)(unsafe.Pointer(&in.CertificateAuthorityData))
|
||||||
|
out.ProxyURL = in.ProxyURL
|
||||||
if err := Convert_Slice_v1_NamedExtension_To_Map_string_To_runtime_Object(&in.Extensions, &out.Extensions, s); err != nil {
|
if err := Convert_Slice_v1_NamedExtension_To_Map_string_To_runtime_Object(&in.Extensions, &out.Extensions, s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -255,6 +256,7 @@ func autoConvert_api_Cluster_To_v1_Cluster(in *api.Cluster, out *Cluster, s conv
|
|||||||
out.InsecureSkipTLSVerify = in.InsecureSkipTLSVerify
|
out.InsecureSkipTLSVerify = in.InsecureSkipTLSVerify
|
||||||
out.CertificateAuthority = in.CertificateAuthority
|
out.CertificateAuthority = in.CertificateAuthority
|
||||||
out.CertificateAuthorityData = *(*[]byte)(unsafe.Pointer(&in.CertificateAuthorityData))
|
out.CertificateAuthorityData = *(*[]byte)(unsafe.Pointer(&in.CertificateAuthorityData))
|
||||||
|
out.ProxyURL = in.ProxyURL
|
||||||
if err := Convert_Map_string_To_runtime_Object_To_Slice_v1_NamedExtension(&in.Extensions, &out.Extensions, s); err != nil {
|
if err := Convert_Map_string_To_runtime_Object_To_Slice_v1_NamedExtension(&in.Extensions, &out.Extensions, s); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -20,16 +20,17 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/imdario/mergo"
|
|
||||||
"k8s.io/klog"
|
|
||||||
|
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
clientauth "k8s.io/client-go/tools/auth"
|
clientauth "k8s.io/client-go/tools/auth"
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
"k8s.io/klog"
|
||||||
|
|
||||||
|
"github.com/imdario/mergo"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -150,6 +151,13 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
|
|||||||
|
|
||||||
clientConfig := &restclient.Config{}
|
clientConfig := &restclient.Config{}
|
||||||
clientConfig.Host = configClusterInfo.Server
|
clientConfig.Host = configClusterInfo.Server
|
||||||
|
if configClusterInfo.ProxyURL != "" {
|
||||||
|
u, err := parseProxyURL(configClusterInfo.ProxyURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientConfig.Proxy = http.ProxyURL(u)
|
||||||
|
}
|
||||||
|
|
||||||
if len(config.overrides.Timeout) > 0 {
|
if len(config.overrides.Timeout) > 0 {
|
||||||
timeout, err := ParseTimeout(config.overrides.Timeout)
|
timeout, err := ParseTimeout(config.overrides.Timeout)
|
||||||
|
@ -23,10 +23,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/imdario/mergo"
|
|
||||||
|
|
||||||
restclient "k8s.io/client-go/rest"
|
restclient "k8s.io/client-go/rest"
|
||||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
|
||||||
|
"github.com/imdario/mergo"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMergoSemantics(t *testing.T) {
|
func TestMergoSemantics(t *testing.T) {
|
||||||
@ -330,6 +330,84 @@ func TestCertificateData(t *testing.T) {
|
|||||||
matchByteArg(keyData, clientConfig.TLSClientConfig.KeyData, t)
|
matchByteArg(keyData, clientConfig.TLSClientConfig.KeyData, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProxyURL(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
proxyURL string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "no proxy-url",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "socks5 proxy-url",
|
||||||
|
proxyURL: "socks5://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "https proxy-url",
|
||||||
|
proxyURL: "https://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "http proxy-url",
|
||||||
|
proxyURL: "http://example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "bad scheme proxy-url",
|
||||||
|
proxyURL: "socks6://example.com",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "no scheme proxy-url",
|
||||||
|
proxyURL: "example.com",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "not a url proxy-url",
|
||||||
|
proxyURL: "chewbacca@example.com",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.proxyURL, func(t *testing.T) {
|
||||||
|
|
||||||
|
config := clientcmdapi.NewConfig()
|
||||||
|
config.Clusters["clean"] = &clientcmdapi.Cluster{
|
||||||
|
Server: "https://localhost:8443",
|
||||||
|
ProxyURL: test.proxyURL,
|
||||||
|
}
|
||||||
|
config.AuthInfos["clean"] = &clientcmdapi.AuthInfo{}
|
||||||
|
config.Contexts["clean"] = &clientcmdapi.Context{
|
||||||
|
Cluster: "clean",
|
||||||
|
AuthInfo: "clean",
|
||||||
|
}
|
||||||
|
config.CurrentContext = "clean"
|
||||||
|
|
||||||
|
clientBuilder := NewNonInteractiveClientConfig(*config, "clean", &ConfigOverrides{}, nil)
|
||||||
|
|
||||||
|
clientConfig, err := clientBuilder.ClientConfig()
|
||||||
|
if test.expectErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Expected error constructing config")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error constructing config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if test.proxyURL == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gotURL, err := clientConfig.Proxy(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error from proxier: %v", err)
|
||||||
|
}
|
||||||
|
matchStringArg(test.proxyURL, gotURL.String(), t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBasicAuthData(t *testing.T) {
|
func TestBasicAuthData(t *testing.T) {
|
||||||
username := "myuser"
|
username := "myuser"
|
||||||
password := "mypass" // Fake value for testing.
|
password := "mypass" // Fake value for testing.
|
||||||
|
@ -18,6 +18,7 @@ package clientcmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -33,3 +34,17 @@ func ParseTimeout(duration string) (time.Duration, error) {
|
|||||||
}
|
}
|
||||||
return 0, fmt.Errorf("Invalid timeout value. Timeout must be a single integer in seconds, or an integer followed by a corresponding time unit (e.g. 1s | 2m | 3h)")
|
return 0, fmt.Errorf("Invalid timeout value. Timeout must be a single integer in seconds, or an integer followed by a corresponding time unit (e.g. 1s | 2m | 3h)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseProxyURL(proxyURL string) (*url.URL, error) {
|
||||||
|
u, err := url.Parse(proxyURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not parse: %v", proxyURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "http", "https", "socks5":
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported scheme %q, must be http, https, or socks5", u.Scheme)
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
@ -227,6 +227,11 @@ func validateClusterInfo(clusterName string, clusterInfo clientcmdapi.Cluster) [
|
|||||||
validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName))
|
validationErrors = append(validationErrors, fmt.Errorf("no server found for cluster %q", clusterName))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if proxyURL := clusterInfo.ProxyURL; proxyURL != "" {
|
||||||
|
if _, err := parseProxyURL(proxyURL); err != nil {
|
||||||
|
validationErrors = append(validationErrors, fmt.Errorf("invalid 'proxy-url' %q for cluster %q: %v", proxyURL, clusterName, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
// Make sure CA data and CA file aren't both specified
|
// Make sure CA data and CA file aren't both specified
|
||||||
if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
|
if len(clusterInfo.CertificateAuthority) != 0 && len(clusterInfo.CertificateAuthorityData) != 0 {
|
||||||
validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName))
|
validationErrors = append(validationErrors, fmt.Errorf("certificate-authority-data and certificate-authority are both specified for %v. certificate-authority-data will override.", clusterName))
|
||||||
|
@ -52,6 +52,7 @@ type tlsCacheKey struct {
|
|||||||
nextProtos string
|
nextProtos string
|
||||||
dial string
|
dial string
|
||||||
disableCompression bool
|
disableCompression bool
|
||||||
|
proxy string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t tlsCacheKey) String() string {
|
func (t tlsCacheKey) String() string {
|
||||||
@ -59,7 +60,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, getCert: %s, serverName:%s, dial:%s disableCompression:%t", t.insecure, t.caData, t.certData, keyText, t.getCert, t.serverName, t.dial, t.disableCompression)
|
return fmt.Sprintf("insecure:%v, caData:%#v, certData:%#v, keyData:%s, getCert: %s, serverName:%s, dial:%s disableCompression:%t, proxy: %s", t.insecure, t.caData, t.certData, keyText, t.getCert, t.serverName, t.dial, t.disableCompression, t.proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
|
func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
|
||||||
@ -83,7 +84,7 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// The options didn't require a custom TLS config
|
// The options didn't require a custom TLS config
|
||||||
if tlsConfig == nil && config.Dial == nil {
|
if tlsConfig == nil && config.Dial == nil && config.Proxy == nil {
|
||||||
return http.DefaultTransport, nil
|
return http.DefaultTransport, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,9 +105,14 @@ func (c *tlsTransportCache) get(config *Config) (http.RoundTripper, error) {
|
|||||||
go dynamicCertDialer.Run(wait.NeverStop)
|
go dynamicCertDialer.Run(wait.NeverStop)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxy := http.ProxyFromEnvironment
|
||||||
|
if config.Proxy != nil {
|
||||||
|
proxy = config.Proxy
|
||||||
|
}
|
||||||
|
|
||||||
// Cache a single transport for these options
|
// Cache a single transport for these options
|
||||||
c.transports[key] = utilnet.SetTransportDefaults(&http.Transport{
|
c.transports[key] = utilnet.SetTransportDefaults(&http.Transport{
|
||||||
Proxy: http.ProxyFromEnvironment,
|
Proxy: proxy,
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
TLSHandshakeTimeout: 10 * time.Second,
|
||||||
TLSClientConfig: tlsConfig,
|
TLSClientConfig: tlsConfig,
|
||||||
MaxIdleConnsPerHost: idleConnsPerHost,
|
MaxIdleConnsPerHost: idleConnsPerHost,
|
||||||
@ -130,6 +136,7 @@ func tlsConfigKey(c *Config) (tlsCacheKey, error) {
|
|||||||
nextProtos: strings.Join(c.TLS.NextProtos, ","),
|
nextProtos: strings.Join(c.TLS.NextProtos, ","),
|
||||||
dial: fmt.Sprintf("%p", c.Dial),
|
dial: fmt.Sprintf("%p", c.Dial),
|
||||||
disableCompression: c.DisableCompression,
|
disableCompression: c.DisableCompression,
|
||||||
|
proxy: fmt.Sprintf("%p", c.Proxy),
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.TLS.ReloadTLSFiles {
|
if c.TLS.ReloadTLSFiles {
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -157,3 +158,23 @@ func TestTLSConfigKey(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTLSConfigKeyFuncPtr(t *testing.T) {
|
||||||
|
keys := make(map[tlsCacheKey]struct{})
|
||||||
|
makeKey := func(p func(*http.Request) (*url.URL, error)) tlsCacheKey {
|
||||||
|
key, err := tlsConfigKey(&Config{Proxy: p})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error creating cache key: %v", err)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
keys[makeKey(http.ProxyFromEnvironment)] = struct{}{}
|
||||||
|
keys[makeKey(http.ProxyFromEnvironment)] = struct{}{}
|
||||||
|
keys[makeKey(http.ProxyURL(nil))] = struct{}{}
|
||||||
|
keys[makeKey(nil)] = struct{}{}
|
||||||
|
|
||||||
|
if got, want := len(keys), 3; got != want {
|
||||||
|
t.Fatalf("Unexpected number of keys: got=%d want=%d", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds various options for establishing a transport.
|
// Config holds various options for establishing a transport.
|
||||||
@ -68,6 +69,13 @@ type Config struct {
|
|||||||
|
|
||||||
// Dial specifies the dial function for creating unencrypted TCP connections.
|
// Dial specifies the dial function for creating unencrypted TCP connections.
|
||||||
Dial func(ctx context.Context, network, address string) (net.Conn, error)
|
Dial func(ctx context.Context, network, address string) (net.Conn, error)
|
||||||
|
|
||||||
|
// Proxy is the the proxy func to be used for all requests made by this
|
||||||
|
// transport. If Proxy is nil, http.ProxyFromEnvironment is used. If Proxy
|
||||||
|
// returns a nil *URL, no proxy is used.
|
||||||
|
//
|
||||||
|
// socks5 proxying does not currently support spdy streaming endpoints.
|
||||||
|
Proxy func(*http.Request) (*url.URL, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImpersonationConfig has all the available impersonation options
|
// ImpersonationConfig has all the available impersonation options
|
||||||
|
Loading…
Reference in New Issue
Block a user