Skip to content

Commit f3f666d

Browse files
committed
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
1 parent dac4f81 commit f3f666d

File tree

15 files changed

+292
-27
lines changed

15 files changed

+292
-27
lines changed

staging/src/k8s.io/client-go/rest/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ go_test(
3838
"//staging/src/k8s.io/client-go/transport:go_default_library",
3939
"//staging/src/k8s.io/client-go/util/flowcontrol:go_default_library",
4040
"//staging/src/k8s.io/client-go/util/testing:go_default_library",
41+
"//vendor/github.com/google/go-cmp/cmp:go_default_library",
4142
"//vendor/github.com/google/gofuzz:go_default_library",
4243
"//vendor/github.com/stretchr/testify/assert:go_default_library",
4344
"//vendor/k8s.io/klog:go_default_library",

staging/src/k8s.io/client-go/rest/client_test.go

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ package rest
1818

1919
import (
2020
"context"
21+
"fmt"
2122
"net/http"
2223
"net/http/httptest"
24+
"net/http/httputil"
2325
"net/url"
2426
"os"
2527
"reflect"
2628
"testing"
2729
"time"
2830

29-
"fmt"
30-
3131
v1 "k8s.io/api/core/v1"
3232
v1beta1 "k8s.io/api/extensions/v1beta1"
3333
"k8s.io/apimachinery/pkg/api/errors"
@@ -37,6 +37,8 @@ import (
3737
"k8s.io/apimachinery/pkg/util/diff"
3838
"k8s.io/client-go/kubernetes/scheme"
3939
utiltesting "k8s.io/client-go/util/testing"
40+
41+
"github.com/google/go-cmp/cmp"
4042
)
4143

4244
type TestParam struct {
@@ -252,7 +254,7 @@ func validate(testParam TestParam, t *testing.T, body []byte, fakeHandler *utilt
252254

253255
}
254256

255-
func TestHttpMethods(t *testing.T) {
257+
func TestHTTPMethods(t *testing.T) {
256258
testServer, _, _ := testServerEnv(t, 200)
257259
defer testServer.Close()
258260
c, _ := restClient(testServer)
@@ -283,6 +285,57 @@ func TestHttpMethods(t *testing.T) {
283285
}
284286
}
285287

288+
func TestHTTPProxy(t *testing.T) {
289+
ctx := context.Background()
290+
testServer, fh, _ := testServerEnv(t, 200)
291+
fh.ResponseBody = "backend data"
292+
defer testServer.Close()
293+
294+
testProxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
295+
to, err := url.Parse(req.RequestURI)
296+
if err != nil {
297+
t.Fatalf("err: %v", err)
298+
}
299+
w.Write([]byte("proxied: "))
300+
httputil.NewSingleHostReverseProxy(to).ServeHTTP(w, req)
301+
}))
302+
defer testProxyServer.Close()
303+
304+
t.Logf(testProxyServer.URL)
305+
306+
u, err := url.Parse(testProxyServer.URL)
307+
if err != nil {
308+
t.Fatalf("Failed to parse test proxy server url: %v", err)
309+
}
310+
311+
c, err := RESTClientFor(&Config{
312+
Host: testServer.URL,
313+
ContentConfig: ContentConfig{
314+
GroupVersion: &v1.SchemeGroupVersion,
315+
NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
316+
},
317+
Proxy: http.ProxyURL(u),
318+
Username: "user",
319+
Password: "pass",
320+
})
321+
if err != nil {
322+
t.Fatalf("Failed to create client: %v", err)
323+
}
324+
325+
request := c.Get()
326+
if request == nil {
327+
t.Fatalf("Get: Object returned should not be nil")
328+
}
329+
330+
b, err := request.DoRaw(ctx)
331+
if err != nil {
332+
t.Fatalf("unexpected err: %v", err)
333+
}
334+
if got, want := string(b), "proxied: backend data"; !cmp.Equal(got, want) {
335+
t.Errorf("unexpected body: %v", cmp.Diff(want, got))
336+
}
337+
}
338+
286339
func TestCreateBackoffManager(t *testing.T) {
287340

288341
theUrl, _ := url.Parse("http://localhost")

staging/src/k8s.io/client-go/rest/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io/ioutil"
2424
"net"
2525
"net/http"
26+
"net/url"
2627
"os"
2728
"path/filepath"
2829
gruntime "runtime"
@@ -128,6 +129,13 @@ type Config struct {
128129
// Dial specifies the dial function for creating unencrypted TCP connections.
129130
Dial func(ctx context.Context, network, address string) (net.Conn, error)
130131

132+
// Proxy is the the proxy func to be used for all requests made by this
133+
// transport. If Proxy is nil, http.ProxyFromEnvironment is used. If Proxy
134+
// returns a nil *URL, no proxy is used.
135+
//
136+
// socks5 proxying does not currently support spdy streaming endpoints.
137+
Proxy func(*http.Request) (*url.URL, error)
138+
131139
// Version forces a specific version to be used (if registered)
132140
// Do we need this?
133141
// Version string
@@ -560,6 +568,7 @@ func AnonymousClientConfig(config *Config) *Config {
560568
Burst: config.Burst,
561569
Timeout: config.Timeout,
562570
Dial: config.Dial,
571+
Proxy: config.Proxy,
563572
}
564573
}
565574

@@ -601,5 +610,6 @@ func CopyConfig(config *Config) *Config {
601610
RateLimiter: config.RateLimiter,
602611
Timeout: config.Timeout,
603612
Dial: config.Dial,
613+
Proxy: config.Proxy,
604614
}
605615
}

staging/src/k8s.io/client-go/rest/config_test.go

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"io"
2424
"net"
2525
"net/http"
26+
"net/url"
2627
"path/filepath"
2728
"reflect"
2829
"strings"
@@ -32,12 +33,12 @@ import (
3233
v1 "k8s.io/api/core/v1"
3334
"k8s.io/apimachinery/pkg/runtime"
3435
"k8s.io/apimachinery/pkg/runtime/schema"
35-
"k8s.io/apimachinery/pkg/util/diff"
3636
"k8s.io/client-go/kubernetes/scheme"
3737
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
3838
"k8s.io/client-go/transport"
3939
"k8s.io/client-go/util/flowcontrol"
4040

41+
"github.com/google/go-cmp/cmp"
4142
fuzz "github.com/google/gofuzz"
4243
"github.com/stretchr/testify/assert"
4344
)
@@ -274,8 +275,13 @@ func (n *fakeNegotiatedSerializer) DecoderToVersion(serializer runtime.Decoder,
274275
var fakeDialFunc = func(ctx context.Context, network, addr string) (net.Conn, error) {
275276
return nil, fakeDialerError
276277
}
278+
277279
var fakeDialerError = errors.New("fakedialer")
278280

281+
func fakeProxyFunc(*http.Request) (*url.URL, error) {
282+
return nil, errors.New("fakeproxy")
283+
}
284+
279285
type fakeAuthProviderConfigPersister struct{}
280286

281287
func (fakeAuthProviderConfigPersister) Persist(map[string]string) error {
@@ -318,8 +324,12 @@ func TestAnonymousConfig(t *testing.T) {
318324
func(r *clientcmdapi.AuthProviderConfig, f fuzz.Continue) {
319325
r.Config = map[string]string{}
320326
},
321-
// Dial does not require fuzzer
322-
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {},
327+
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {
328+
*r = fakeDialFunc
329+
},
330+
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
331+
*r = fakeProxyFunc
332+
},
323333
)
324334
for i := 0; i < 20; i++ {
325335
original := &Config{}
@@ -350,13 +360,22 @@ func TestAnonymousConfig(t *testing.T) {
350360
if !reflect.DeepEqual(expectedError, actualError) {
351361
t.Fatalf("AnonymousClientConfig dropped the Dial field")
352362
}
353-
} else {
354-
actual.Dial = nil
355-
expected.Dial = nil
356363
}
364+
actual.Dial = nil
365+
expected.Dial = nil
357366

358-
if !reflect.DeepEqual(*actual, expected) {
359-
t.Fatalf("AnonymousClientConfig dropped unexpected fields, identify whether they are security related or not: %s", diff.ObjectGoPrintDiff(expected, actual))
367+
if actual.Proxy != nil {
368+
_, actualError := actual.Proxy(nil)
369+
_, expectedError := expected.Proxy(nil)
370+
if !reflect.DeepEqual(expectedError, actualError) {
371+
t.Fatalf("AnonymousClientConfig dropped the Proxy field")
372+
}
373+
}
374+
actual.Proxy = nil
375+
expected.Proxy = nil
376+
377+
if diff := cmp.Diff(*actual, expected); diff != "" {
378+
t.Fatalf("AnonymousClientConfig dropped unexpected fields, identify whether they are security related or not (-got, +want): %s", diff)
360379
}
361380
}
362381
}
@@ -396,6 +415,9 @@ func TestCopyConfig(t *testing.T) {
396415
func(r *func(ctx context.Context, network, addr string) (net.Conn, error), f fuzz.Continue) {
397416
*r = fakeDialFunc
398417
},
418+
func(r *func(*http.Request) (*url.URL, error), f fuzz.Continue) {
419+
*r = fakeProxyFunc
420+
},
399421
)
400422
for i := 0; i < 20; i++ {
401423
original := &Config{}
@@ -410,10 +432,10 @@ func TestCopyConfig(t *testing.T) {
410432
// function return the expected object.
411433
if actual.WrapTransport == nil || !reflect.DeepEqual(expected.WrapTransport(nil), &fakeRoundTripper{}) {
412434
t.Fatalf("CopyConfig dropped the WrapTransport field")
413-
} else {
414-
actual.WrapTransport = nil
415-
expected.WrapTransport = nil
416435
}
436+
actual.WrapTransport = nil
437+
expected.WrapTransport = nil
438+
417439
if actual.Dial != nil {
418440
_, actualError := actual.Dial(context.Background(), "", "")
419441
_, expectedError := expected.Dial(context.Background(), "", "")
@@ -423,6 +445,7 @@ func TestCopyConfig(t *testing.T) {
423445
}
424446
actual.Dial = nil
425447
expected.Dial = nil
448+
426449
if actual.AuthConfigPersister != nil {
427450
actualError := actual.AuthConfigPersister.Persist(nil)
428451
expectedError := expected.AuthConfigPersister.Persist(nil)
@@ -433,8 +456,18 @@ func TestCopyConfig(t *testing.T) {
433456
actual.AuthConfigPersister = nil
434457
expected.AuthConfigPersister = nil
435458

436-
if !reflect.DeepEqual(*actual, expected) {
437-
t.Fatalf("CopyConfig dropped unexpected fields, identify whether they are security related or not: %s", diff.ObjectReflectDiff(expected, *actual))
459+
if actual.Proxy != nil {
460+
_, actualError := actual.Proxy(nil)
461+
_, expectedError := expected.Proxy(nil)
462+
if !reflect.DeepEqual(expectedError, actualError) {
463+
t.Fatalf("CopyConfig dropped the Proxy field")
464+
}
465+
}
466+
actual.Proxy = nil
467+
expected.Proxy = nil
468+
469+
if diff := cmp.Diff(*actual, expected); diff != "" {
470+
t.Fatalf("CopyConfig dropped unexpected fields, identify whether they are security related or not (-got, +want): %s", diff)
438471
}
439472
}
440473
}
@@ -564,10 +597,11 @@ func TestConfigSprint(t *testing.T) {
564597
RateLimiter: &fakeLimiter{},
565598
Timeout: 3 * time.Second,
566599
Dial: fakeDialFunc,
600+
Proxy: fakeProxyFunc,
567601
}
568602
want := fmt.Sprintf(
569-
`&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)}`,
570-
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc,
603+
`&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)}`,
604+
c.Transport, fakeWrapperFunc, c.RateLimiter, fakeDialFunc, fakeProxyFunc,
571605
)
572606

573607
for _, f := range []string{"%s", "%v", "%+v", "%#v"} {

staging/src/k8s.io/client-go/rest/transport.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ func (c *Config) TransportConfig() (*transport.Config, error) {
8585
Groups: c.Impersonate.Groups,
8686
Extra: c.Impersonate.Extra,
8787
},
88-
Dial: c.Dial,
88+
Dial: c.Dial,
89+
Proxy: c.Proxy,
8990
}
9091

9192
if c.ExecProvider != nil && c.AuthProvider != nil {

staging/src/k8s.io/client-go/tools/clientcmd/api/types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ type Cluster struct {
8282
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
8383
// +optional
8484
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
85+
// ProxyURL is the URL to the proxy to be used for all requests made by this
86+
// client. URLs with "http", "https", and "socks5" schemes are supported. If
87+
// this configuration is not provided or the empty string, the client
88+
// attempts to construct a proxy configuration from http_proxy and
89+
// https_proxy environment variables. If these environment variables are not
90+
// set, the client does not attempt to proxy requests.
91+
//
92+
// socks5 proxying does not currently support spdy streaming endpoints (exec,
93+
// attach, port forward).
94+
// +optional
95+
ProxyURL string `json:"proxy-url,omitempty"`
8596
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
8697
// +optional
8798
Extensions map[string]runtime.Object `json:"extensions,omitempty"`

staging/src/k8s.io/client-go/tools/clientcmd/api/v1/types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,17 @@ type Cluster struct {
7575
// CertificateAuthorityData contains PEM-encoded certificate authority certificates. Overrides CertificateAuthority
7676
// +optional
7777
CertificateAuthorityData []byte `json:"certificate-authority-data,omitempty"`
78+
// ProxyURL is the URL to the proxy to be used for all requests made by this
79+
// client. URLs with "http", "https", and "socks5" schemes are supported. If
80+
// this configuration is not provided or the empty string, the client
81+
// attempts to construct a proxy configuration from http_proxy and
82+
// https_proxy environment variables. If these environment variables are not
83+
// set, the client does not attempt to proxy requests.
84+
//
85+
// socks5 proxying does not currently support spdy streaming endpoints (exec,
86+
// attach, port forward).
87+
// +optional
88+
ProxyURL string `json:"proxy-url,omitempty"`
7889
// Extensions holds additional information. This is useful for extenders so that reads and writes don't clobber unknown fields
7990
// +optional
8091
Extensions []NamedExtension `json:"extensions,omitempty"`

staging/src/k8s.io/client-go/tools/clientcmd/api/v1/zz_generated.conversion.go

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

staging/src/k8s.io/client-go/tools/clientcmd/client_config.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@ import (
2020
"fmt"
2121
"io"
2222
"io/ioutil"
23+
"net/http"
2324
"net/url"
2425
"os"
2526
"strings"
2627

27-
"github.com/imdario/mergo"
28-
"k8s.io/klog"
29-
3028
restclient "k8s.io/client-go/rest"
3129
clientauth "k8s.io/client-go/tools/auth"
3230
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
31+
"k8s.io/klog"
32+
33+
"github.com/imdario/mergo"
3334
)
3435

3536
var (
@@ -150,6 +151,13 @@ func (config *DirectClientConfig) ClientConfig() (*restclient.Config, error) {
150151

151152
clientConfig := &restclient.Config{}
152153
clientConfig.Host = configClusterInfo.Server
154+
if configClusterInfo.ProxyURL != "" {
155+
u, err := parseProxyURL(configClusterInfo.ProxyURL)
156+
if err != nil {
157+
return nil, err
158+
}
159+
clientConfig.Proxy = http.ProxyURL(u)
160+
}
153161

154162
if len(config.overrides.Timeout) > 0 {
155163
timeout, err := ParseTimeout(config.overrides.Timeout)

0 commit comments

Comments
 (0)