diff --git a/contrib/for-tests/porter/pod.json b/contrib/for-tests/porter/pod.json index 61cf0970060..1b21be5f8ec 100644 --- a/contrib/for-tests/porter/pod.json +++ b/contrib/for-tests/porter/pod.json @@ -8,7 +8,7 @@ "containers": [ { "name": "porter", - "image": "gcr.io/google_containers/porter:91d46193649807d1340b46797774d8b2", + "image": "gcr.io/google_containers/porter:59ad46ed2c56ba50fa7f1dc176c07c37", "env": [ { "name": "SERVE_PORT_80", diff --git a/contrib/for-tests/porter/porter b/contrib/for-tests/porter/porter deleted file mode 100755 index 44d19c3e6b6..00000000000 Binary files a/contrib/for-tests/porter/porter and /dev/null differ diff --git a/contrib/for-tests/porter/porter.go b/contrib/for-tests/porter/porter.go index 1e5a3f35e35..f80704383ca 100644 --- a/contrib/for-tests/porter/porter.go +++ b/contrib/for-tests/porter/porter.go @@ -33,7 +33,10 @@ const prefix = "SERVE_PORT_" func main() { for _, vk := range os.Environ() { - parts := strings.Split(vk, "=") + // Put everything before the first = sign in parts[0], and + // everything else in parts[1] (even if there are multiple = + // characters). + parts := strings.SplitN(vk, "=", 2) key := parts[0] value := parts[1] if strings.HasPrefix(key, prefix) { diff --git a/docs/kubectl_proxy.md b/docs/kubectl_proxy.md index a54d1ff52c1..06a1397114b 100644 --- a/docs/kubectl_proxy.md +++ b/docs/kubectl_proxy.md @@ -41,7 +41,7 @@ $ kubectl proxy --api-prefix=/k8s-api ``` --accept-hosts="^localhost$,^127\\.0\\.0\\.1$,^\\[::1\\]$": Regular expression for hosts that the proxy should accept. - --accept-paths="^/api/.*": Regular expression for paths that the proxy should accept. + --accept-paths="^/.*": Regular expression for paths that the proxy should accept. --api-prefix="/api/": Prefix to serve the proxied API under. --disable-filter=false: If true, disable request filtering in the proxy. This is dangerous, and can leave you vulnerable to XSRF attacks. Use with caution. -h, --help=false: help for proxy @@ -84,6 +84,6 @@ $ kubectl proxy --api-prefix=/k8s-api ### SEE ALSO * [kubectl](kubectl.md) - kubectl controls the Kubernetes cluster manager -###### Auto generated by spf13/cobra at 2015-06-11 03:49:29.837564354 +0000 UTC +###### Auto generated by spf13/cobra at 2015-06-23 19:00:28.69764897 +0000 UTC [![Analytics](https://kubernetes-site.appspot.com/UA-36037335-10/GitHub/docs/kubectl_proxy.md?pixel)]() diff --git a/docs/man/man1/kubectl-proxy.1 b/docs/man/man1/kubectl-proxy.1 index 6057ae73fbe..81a8bae67d4 100644 --- a/docs/man/man1/kubectl-proxy.1 +++ b/docs/man/man1/kubectl-proxy.1 @@ -43,7 +43,7 @@ The above lets you 'curl localhost:8001/custom/api/v1/pods' Regular expression for hosts that the proxy should accept. .PP -\fB\-\-accept\-paths\fP="^/api/.*" +\fB\-\-accept\-paths\fP="^/.*" Regular expression for paths that the proxy should accept. .PP diff --git a/hack/test-cmd.sh b/hack/test-cmd.sh index 23fe05cfe0e..4500627d730 100755 --- a/hack/test-cmd.sh +++ b/hack/test-cmd.sh @@ -25,16 +25,54 @@ KUBE_ROOT=$(dirname "${BASH_SOURCE}")/.. source "${KUBE_ROOT}/hack/lib/init.sh" source "${KUBE_ROOT}/hack/lib/test.sh" +# Stops the running kubectl proxy, if there is one. +function stop-proxy() +{ + [[ -n "${PROXY_PID-}" ]] && kill "${PROXY_PID}" 1>&2 2>/dev/null + PROXY_PID= +} + +# Starts "kubect proxy" to test the client proxy. You may pass options, e.g. +# --api-prefix. +function start-proxy() +{ + stop-proxy + + kube::log::status "Starting kubectl proxy" + # the --www and --www-prefix are just to make something definitely show up for + # wait_for_url to see. + kubectl proxy -p ${PROXY_PORT} --www=. --www-prefix=/healthz "$@" 1>&2 & + PROXY_PID=$! + kube::util::wait_for_url "http://127.0.0.1:${PROXY_PORT}/healthz" "kubectl proxy $@" +} + function cleanup() { - [[ -n ${APISERVER_PID-} ]] && kill ${APISERVER_PID} 1>&2 2>/dev/null - [[ -n ${CTLRMGR_PID-} ]] && kill ${CTLRMGR_PID} 1>&2 2>/dev/null - [[ -n ${KUBELET_PID-} ]] && kill ${KUBELET_PID} 1>&2 2>/dev/null + [[ -n "${APISERVER_PID-}" ]] && kill "${APISERVER_PID}" 1>&2 2>/dev/null + [[ -n "${CTLRMGR_PID-}" ]] && kill "${CTLRMGR_PID}" 1>&2 2>/dev/null + [[ -n "${KUBELET_PID-}" ]] && kill "${KUBELET_PID}" 1>&2 2>/dev/null + stop-proxy - kube::etcd::cleanup - rm -rf "${KUBE_TEMP}" + kube::etcd::cleanup + rm -rf "${KUBE_TEMP}" - kube::log::status "Clean up complete" + kube::log::status "Clean up complete" +} + +# Executes curl against the proxy. $1 is the path to use, $2 is the desired +# return code. Prints a helpful message on failure. +function check-curl-proxy-code() +{ + local status + local -r address=$1 + local -r desired=$2 + local -r full_address="${PROXY_HOST}:${PROXY_PORT}${address}" + status=$(curl -w "%{http_code}" --silent --output /dev/null "${full_address}") + if [ "${status}" == "${desired}" ]; then + return 0 + fi + echo "For address ${full_address}, got ${status} but wanted ${desired}" + return 1 } trap cleanup EXIT SIGINT @@ -49,6 +87,8 @@ API_HOST=${API_HOST:-127.0.0.1} KUBELET_PORT=${KUBELET_PORT:-10250} KUBELET_HEALTHZ_PORT=${KUBELET_HEALTHZ_PORT:-10248} CTLRMGR_PORT=${CTLRMGR_PORT:-10252} +PROXY_PORT=${PROXY_PORT:-8001} +PROXY_HOST=127.0.0.1 # kubectl only serves on localhost. # Check kubectl kube::log::status "Running kubectl with no options" @@ -146,6 +186,38 @@ for version in "${kube_api_versions[@]}"; do # Passing no arguments to create is an error ! kubectl create + ####################### + # kubectl local proxy # + ####################### + + # Make sure the UI can be proxied + start-proxy --api-prefix=/ + check-curl-proxy-code /ui 301 + check-curl-proxy-code /metrics 200 + if [[ -n "${version}" ]]; then + check-curl-proxy-code /api/${version}/namespaces 200 + fi + stop-proxy + + # Default proxy locks you into the /api path (legacy behavior) + start-proxy + check-curl-proxy-code /ui 404 + check-curl-proxy-code /metrics 404 + check-curl-proxy-code /api/ui 404 + if [[ -n "${version}" ]]; then + check-curl-proxy-code /api/${version}/namespaces 200 + fi + stop-proxy + + # Custom paths let you see everything. + start-proxy --api-prefix=/custom + check-curl-proxy-code /custom/ui 301 + check-curl-proxy-code /custom/metrics 200 + if [[ -n "${version}" ]]; then + check-curl-proxy-code /custom/api/${version}/namespaces 200 + fi + stop-proxy + ########################### # POD creation / deletion # ########################### diff --git a/pkg/apiserver/proxy.go b/pkg/apiserver/proxy.go index e04b2a6478a..a7a538c4abf 100644 --- a/pkg/apiserver/proxy.go +++ b/pkg/apiserver/proxy.go @@ -20,6 +20,7 @@ import ( "crypto/tls" "fmt" "io" + "math/rand" "net" "net/http" "net/http/httputil" @@ -55,6 +56,8 @@ type ProxyHandler struct { } func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + proxyHandlerTraceID := rand.Int63() + var verb string var apiResource string var httpCode int @@ -108,7 +111,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - location, transport, err := redirector.ResourceLocation(ctx, id) + location, roundTripper, err := redirector.ResourceLocation(ctx, id) if err != nil { httplog.LogOf(req, w).Addf("Error getting ResourceLocation: %v", err) status := errToAPIStatus(err) @@ -123,8 +126,11 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } // If we have a custom dialer, and no pre-existing transport, initialize it to use the dialer. - if transport == nil && r.dial != nil { - transport = &http.Transport{Dial: r.dial} + if roundTripper == nil && r.dial != nil { + glog.V(5).Infof("[%x: %v] making a dial-only transport...", proxyHandlerTraceID, req.URL) + roundTripper = &http.Transport{Dial: r.dial} + } else if roundTripper != nil { + glog.V(5).Infof("[%x: %v] using transport %T...", proxyHandlerTraceID, req.URL, roundTripper) } // Default to http @@ -158,7 +164,7 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { // TODO convert this entire proxy to an UpgradeAwareProxy similar to // https://github.com/openshift/origin/blob/master/pkg/util/httpproxy/upgradeawareproxy.go. // That proxy needs to be modified to support multiple backends, not just 1. - if r.tryUpgrade(w, req, newReq, location, transport) { + if r.tryUpgrade(w, req, newReq, location, roundTripper) { return } @@ -175,19 +181,33 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } + start := time.Now() + glog.V(4).Infof("[%x] Beginning proxy %s...", proxyHandlerTraceID, req.URL) + defer func() { + glog.V(4).Infof("[%x] Proxy %v finished %v.", proxyHandlerTraceID, req.URL, time.Now().Sub(start)) + }() + proxy := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: location.Scheme, Host: location.Host}) - if transport == nil { + alreadyRewriting := false + if roundTripper != nil { + _, alreadyRewriting = roundTripper.(*proxyutil.Transport) + glog.V(5).Infof("[%x] Not making a reriting transport for proxy %s...", proxyHandlerTraceID, req.URL) + } + if !alreadyRewriting { + glog.V(5).Infof("[%x] making a transport for proxy %s...", proxyHandlerTraceID, req.URL) prepend := path.Join(r.prefix, resource, id) if len(namespace) > 0 { prepend = path.Join(r.prefix, "namespaces", namespace, resource, id) } - transport = &proxyutil.Transport{ - Scheme: req.URL.Scheme, - Host: req.URL.Host, - PathPrepend: prepend, + pTransport := &proxyutil.Transport{ + Scheme: req.URL.Scheme, + Host: req.URL.Host, + PathPrepend: prepend, + RoundTripper: roundTripper, } + roundTripper = pTransport } - proxy.Transport = transport + proxy.Transport = roundTripper proxy.FlushInterval = 200 * time.Millisecond proxy.ServeHTTP(w, newReq) } diff --git a/pkg/client/request.go b/pkg/client/request.go index ab577167f9c..00a8d9fb79e 100644 --- a/pkg/client/request.go +++ b/pkg/client/request.go @@ -803,9 +803,9 @@ func (r *Request) transformResponse(resp *http.Response, req *http.Request) Resu } return Result{ - body: body, - created: resp.StatusCode == http.StatusCreated, - codec: r.codec, + body: body, + statusCode: resp.StatusCode, + codec: r.codec, } } @@ -879,9 +879,9 @@ func retryAfterSeconds(resp *http.Response) (int, bool) { // Result contains the result of calling Request.Do(). type Result struct { - body []byte - created bool - err error + body []byte + err error + statusCode int codec runtime.Codec } @@ -899,6 +899,13 @@ func (r Result) Get() (runtime.Object, error) { return r.codec.Decode(r.body) } +// StatusCode returns the HTTP status code of the request. (Only valid if no +// error was returned.) +func (r Result) StatusCode(statusCode *int) Result { + *statusCode = r.statusCode + return r +} + // Into stores the result into obj, if possible. func (r Result) Into(obj runtime.Object) error { if r.err != nil { @@ -910,7 +917,7 @@ func (r Result) Into(obj runtime.Object) error { // WasCreated updates the provided bool pointer to whether the server returned // 201 created or a different response. func (r Result) WasCreated(wasCreated *bool) Result { - *wasCreated = r.created + *wasCreated = r.statusCode == http.StatusCreated return r } diff --git a/pkg/client/request_test.go b/pkg/client/request_test.go index 70e038d72ae..53dcc0d7e45 100644 --- a/pkg/client/request_test.go +++ b/pkg/client/request_test.go @@ -274,7 +274,7 @@ func TestTransformResponse(t *testing.T) { test.Response.Body = ioutil.NopCloser(bytes.NewReader([]byte{})) } result := r.transformResponse(test.Response, &http.Request{}) - response, created, err := result.body, result.created, result.err + response, created, err := result.body, result.statusCode == http.StatusCreated, result.err hasErr := err != nil if hasErr != test.Error { t.Errorf("%d: unexpected error: %t %v", i, test.Error, err) diff --git a/pkg/kubectl/proxy_server.go b/pkg/kubectl/proxy_server.go index 6755fdd8281..70620f201af 100644 --- a/pkg/kubectl/proxy_server.go +++ b/pkg/kubectl/proxy_server.go @@ -31,7 +31,7 @@ import ( const ( DefaultHostAcceptRE = "^localhost$,^127\\.0\\.0\\.1$,^\\[::1\\]$" - DefaultPathAcceptRE = "^/api/.*" + DefaultPathAcceptRE = "^/.*" DefaultPathRejectRE = "^/api/.*/exec,^/api/.*/run" DefaultMethodRejectRE = "POST,PUT,PATCH" ) @@ -75,6 +75,7 @@ func MakeRegexpArrayOrDie(str string) []*regexp.Regexp { func matchesRegexp(str string, regexps []*regexp.Regexp) bool { for _, re := range regexps { if re.MatchString(str) { + glog.V(6).Infof("%v matched %s", str, re) return true } } @@ -83,17 +84,28 @@ func matchesRegexp(str string, regexps []*regexp.Regexp) bool { func (f *FilterServer) accept(method, path, host string) bool { if matchesRegexp(path, f.RejectPaths) { + glog.V(3).Infof("Filter rejecting %v %v %v", method, path, host) return false } if matchesRegexp(method, f.RejectMethods) { + glog.V(3).Infof("Filter rejecting %v %v %v", method, path, host) return false } if matchesRegexp(path, f.AcceptPaths) && matchesRegexp(host, f.AcceptHosts) { + glog.V(3).Infof("Filter accepting %v %v %v", method, path, host) return true } + glog.V(3).Infof("Filter rejecting %v %v %v", method, path, host) return false } +// Make a copy of f which passes requests along to the new delegate. +func (f *FilterServer) HandlerFor(delegate http.Handler) *FilterServer { + f2 := *f + f2.delegate = delegate + return &f2 +} + func (f *FilterServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { host, _, _ := net.SplitHostPort(req.Host) if f.accept(req.Method, req.URL.Path, host) { @@ -106,12 +118,12 @@ func (f *FilterServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { // ProxyServer is a http.Handler which proxies Kubernetes APIs to remote API server. type ProxyServer struct { - mux *http.ServeMux - httputil.ReverseProxy + handler http.Handler } // NewProxyServer creates and installs a new ProxyServer. // It automatically registers the created ProxyServer to http.DefaultServeMux. +// 'filter', if non-nil, protects requests to the api only. func NewProxyServer(filebase string, apiProxyPrefix string, staticPrefix string, filter *FilterServer, cfg *client.Config) (*ProxyServer, error) { host := cfg.Host if !strings.HasSuffix(host, "/") { @@ -121,46 +133,45 @@ func NewProxyServer(filebase string, apiProxyPrefix string, staticPrefix string, if err != nil { return nil, err } - proxy := newProxyServer(target) + proxy := newProxy(target) if proxy.Transport, err = client.TransportFor(cfg); err != nil { return nil, err } - - var server http.Handler - if strings.HasPrefix(apiProxyPrefix, "/api") { - server = proxy - } else { - server = http.StripPrefix(apiProxyPrefix, proxy) - } + proxyServer := http.Handler(proxy) if filter != nil { - filter.delegate = server - server = filter + proxyServer = filter.HandlerFor(proxyServer) } - proxy.mux.Handle(apiProxyPrefix, server) - proxy.mux.Handle(staticPrefix, newFileHandler(staticPrefix, filebase)) - return proxy, nil + if !strings.HasPrefix(apiProxyPrefix, "/api") { + proxyServer = stripLeaveSlash(apiProxyPrefix, proxyServer) + } + + mux := http.NewServeMux() + mux.Handle(apiProxyPrefix, proxyServer) + if filebase != "" { + // Require user to explicitly request this behavior rather than + // serving their working directory by default. + mux.Handle(staticPrefix, newFileHandler(staticPrefix, filebase)) + } + return &ProxyServer{handler: mux}, nil } // Serve starts the server (http.DefaultServeMux) on given port, loops forever. func (s *ProxyServer) Serve(port int) error { server := http.Server{ Addr: fmt.Sprintf(":%d", port), - Handler: s.mux, + Handler: s.handler, } return server.ListenAndServe() } -func newProxyServer(target *url.URL) *ProxyServer { +func newProxy(target *url.URL) *httputil.ReverseProxy { director := func(req *http.Request) { req.URL.Scheme = target.Scheme req.URL.Host = target.Host req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path) } - return &ProxyServer{ - ReverseProxy: httputil.ReverseProxy{Director: director}, - mux: http.NewServeMux(), - } + return &httputil.ReverseProxy{Director: director} } func newFileHandler(prefix, base string) http.Handler { @@ -178,3 +189,20 @@ func singleJoiningSlash(a, b string) string { } return a + b } + +// like http.StripPrefix, but always leaves an initial slash. (so that our +// regexps will work.) +func stripLeaveSlash(prefix string, h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + p := strings.TrimPrefix(req.URL.Path, prefix) + if len(p) >= len(req.URL.Path) { + http.NotFound(w, req) + return + } + if len(p) > 0 && p[:1] != "/" { + p = "/" + p + } + req.URL.Path = p + h.ServeHTTP(w, req) + }) +} diff --git a/pkg/kubectl/proxy_server_test.go b/pkg/kubectl/proxy_server_test.go index 8ad4fe65f61..10eba1133bb 100644 --- a/pkg/kubectl/proxy_server_test.go +++ b/pkg/kubectl/proxy_server_test.go @@ -98,10 +98,10 @@ func TestAccept(t *testing.T) { acceptPaths: DefaultPathAcceptRE, rejectPaths: DefaultPathRejectRE, acceptHosts: DefaultHostAcceptRE, - path: "/foo/v1/pods", + path: "/ui", host: "localhost", method: "GET", - expectAccept: false, + expectAccept: true, }, { acceptPaths: DefaultPathAcceptRE, @@ -230,7 +230,7 @@ func TestAPIRequests(t *testing.T) { // httptest.NewServer should always generate a valid URL. target, _ := url.Parse(ts.URL) - proxy := newProxyServer(target) + proxy := newProxy(target) tests := []struct{ method, body string }{ {"GET", ""}, @@ -291,7 +291,7 @@ func TestPathHandling(t *testing.T) { if err != nil { t.Fatalf("%#v: %v", item, err) } - pts := httptest.NewServer(p.mux) + pts := httptest.NewServer(p.handler) defer pts.Close() r, err := http.Get(pts.URL + item.reqPath) diff --git a/pkg/util/proxy/transport.go b/pkg/util/proxy/transport.go index c0544dfa8e6..aa9d3cff682 100644 --- a/pkg/util/proxy/transport.go +++ b/pkg/util/proxy/transport.go @@ -73,6 +73,8 @@ type Transport struct { Scheme string Host string PathPrepend string + + http.RoundTripper } // RoundTrip implements the http.RoundTripper interface @@ -86,7 +88,11 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("X-Forwarded-Host", t.Host) req.Header.Set("X-Forwarded-Proto", t.Scheme) - resp, err := http.DefaultTransport.RoundTrip(req) + rt := t.RoundTripper + if rt == nil { + rt = http.DefaultTransport + } + resp, err := rt.RoundTrip(req) if err != nil { message := fmt.Sprintf("Error: '%s'\nTrying to reach: '%v'", err.Error(), req.URL.String()) diff --git a/test/e2e/proxy.go b/test/e2e/proxy.go index 8ec1d962dd5..4a04ffec056 100644 --- a/test/e2e/proxy.go +++ b/test/e2e/proxy.go @@ -18,7 +18,9 @@ package e2e import ( "fmt" + "net/http" "strings" + "sync" "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -37,52 +39,23 @@ var _ = Describe("Proxy", func() { } }) +const ( + // Try all the proxy tests this many times (to catch even rare flakes). + proxyAttempts = 20 + // Only print this many characters of the response (to keep the logs + // legible). + maxDisplayBodyLen = 100 +) + func proxyContext(version string) { f := NewFramework("proxy") prefix := "/api/" + version - It("should proxy logs on node with explicit kubelet port", func() { - node, err := pickNode(f.Client) - Expect(err).NotTo(HaveOccurred()) - // AbsPath preserves the trailing '/'. - body, err := f.Client.Get().AbsPath(prefix + "/proxy/nodes/" + node + ":10250/logs/").Do().Raw() - if len(body) > 0 { - if len(body) > 100 { - body = body[:100] - body = append(body, '.', '.', '.') - } - Logf("Got: %s", body) - } - Expect(err).NotTo(HaveOccurred()) - }) + It("should proxy logs on node with explicit kubelet port", func() { nodeProxyTest(f, version, ":10250/logs/") }) - It("should proxy logs on node", func() { - node, err := pickNode(f.Client) - Expect(err).NotTo(HaveOccurred()) - body, err := f.Client.Get().AbsPath(prefix + "/proxy/nodes/" + node + "/logs/").Do().Raw() - if len(body) > 0 { - if len(body) > 100 { - body = body[:100] - body = append(body, '.', '.', '.') - } - Logf("Got: %s", body) - } - Expect(err).NotTo(HaveOccurred()) - }) + It("should proxy logs on node", func() { nodeProxyTest(f, version, "/logs/") }) - It("should proxy to cadvisor", func() { - node, err := pickNode(f.Client) - Expect(err).NotTo(HaveOccurred()) - body, err := f.Client.Get().AbsPath(prefix + "/proxy/nodes/" + node + ":4194/containers/").Do().Raw() - if len(body) > 0 { - if len(body) > 100 { - body = body[:100] - body = append(body, '.', '.', '.') - } - Logf("Got: %s", body) - } - Expect(err).NotTo(HaveOccurred()) - }) + It("should proxy to cadvisor", func() { nodeProxyTest(f, version, ":4194/containers/") }) It("should proxy through a service and a pod", func() { labels := map[string]string{"proxy-service-target": "true"} @@ -118,13 +91,13 @@ func proxyContext(version string) { pods := []*api.Pod{} cfg := RCConfig{ Client: f.Client, - Image: "gcr.io/google_containers/porter:91d46193649807d1340b46797774d8b2", + Image: "gcr.io/google_containers/porter:59ad46ed2c56ba50fa7f1dc176c07c37", Name: service.Name, Namespace: f.Namespace.Name, Replicas: 1, PollInterval: time.Second, Env: map[string]string{ - "SERVE_PORT_80": "not accessible via service", + "SERVE_PORT_80": `test`, "SERVE_PORT_160": "foo", "SERVE_PORT_162": "bar", }, @@ -144,11 +117,11 @@ func proxyContext(version string) { svcPrefix := prefix + "/proxy/namespaces/" + f.Namespace.Name + "/services/" + service.Name podPrefix := prefix + "/proxy/namespaces/" + f.Namespace.Name + "/pods/" + pods[0].Name expectations := map[string]string{ - svcPrefix + ":portname1": "foo", - svcPrefix + ":portname2": "bar", - podPrefix + ":80": "not accessible via service", - podPrefix + ":160": "foo", - podPrefix + ":162": "bar", + svcPrefix + ":portname1/": "foo", + svcPrefix + ":portname2/": "bar", + podPrefix + ":80/": `test`, + podPrefix + ":160/": "foo", + podPrefix + ":162/": "bar", // TODO: below entries don't work, but I believe we should make them work. // svcPrefix + ":80": "foo", // svcPrefix + ":81": "bar", @@ -156,17 +129,38 @@ func proxyContext(version string) { // podPrefix + ":dest2": "bar", } + wg := sync.WaitGroup{} errors := []string{} - for path, val := range expectations { - body, err := f.Client.Get().AbsPath(path).Do().Raw() - if err != nil { - errors = append(errors, fmt.Sprintf("path %v gave error %v", path, err)) - continue - } - if e, a := val, string(body); e != a { - errors = append(errors, fmt.Sprintf("path %v: wanted %v, got %v", path, e, a)) + errLock := sync.Mutex{} + recordError := func(s string) { + errLock.Lock() + defer errLock.Unlock() + errors = append(errors, s) + } + for i := 0; i < proxyAttempts; i++ { + for path, val := range expectations { + wg.Add(1) + go func(i int, path, val string) { + defer wg.Done() + body, status, d, err := doProxy(f, path) + if err != nil { + recordError(fmt.Sprintf("%v: path %v gave error: %v", i, path, err)) + return + } + if status != http.StatusOK { + recordError(fmt.Sprintf("%v: path %v gave status: %v", i, path, status)) + } + if e, a := val, string(body); e != a { + recordError(fmt.Sprintf("%v: path %v: wanted %v, got %v", i, path, e, a)) + } + if d > 15*time.Second { + recordError(fmt.Sprintf("%v: path %v took %v > 15s", i, path, d)) + } + }(i, path, val) + time.Sleep(150 * time.Millisecond) } } + wg.Wait() if len(errors) != 0 { Fail(strings.Join(errors, "\n")) @@ -174,6 +168,31 @@ func proxyContext(version string) { }) } +func doProxy(f *Framework, path string) (body []byte, statusCode int, d time.Duration, err error) { + // About all of the proxy accesses in this file: + // * AbsPath is used because it preserves the trailing '/'. + // * Do().Raw() is used (instead of DoRaw()) because it will turn an + // error from apiserver proxy into an actual error, and there is no + // chance of the things we are talking to being confused for an error + // that apiserver would have emitted. + start := time.Now() + body, err = f.Client.Get().AbsPath(path).Do().StatusCode(&statusCode).Raw() + d = time.Since(start) + if len(body) > 0 { + Logf("%v: %s (%v; %v)", path, truncate(body, maxDisplayBodyLen), statusCode, d) + } + return +} + +func truncate(b []byte, maxLen int) []byte { + if len(b) <= maxLen-3 { + return b + } + b2 := append([]byte(nil), b[:maxLen-3]...) + b2 = append(b2, '.', '.', '.') + return b2 +} + func pickNode(c *client.Client) (string, error) { nodes, err := c.Nodes().List(labels.Everything(), fields.Everything()) if err != nil { @@ -184,3 +203,15 @@ func pickNode(c *client.Client) (string, error) { } return nodes.Items[0].Name, nil } + +func nodeProxyTest(f *Framework, version, nodeDest string) { + prefix := "/api/" + version + node, err := pickNode(f.Client) + Expect(err).NotTo(HaveOccurred()) + for i := 0; i < proxyAttempts; i++ { + _, status, d, err := doProxy(f, prefix+"/proxy/nodes/"+node+nodeDest) + Expect(err).NotTo(HaveOccurred()) + Expect(status).To(Equal(http.StatusOK)) + Expect(d).To(BeNumerically("<", 15*time.Second)) + } +}