Skip to content

Commit e0f7533

Browse files
author
Kubernetes Submit Queue
authored
Merge pull request #52933 from liggitt/proxy-subpath-slash
Automatic merge from submit-queue. If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.. Preserve leading and trailing slashes on proxy subpaths subresource parsing was not populating path parameters correctly (leading and trailing slashes were being stripped) this caused bad locations to be sent to the proxy, causing #52022. the first attempt to fix that (#52065) unconditionally prefixed '/', which broke the redirect case (#52813 #52729) fixes #52813, fixes #52729 needs to be picked to 1.7 and 1.8 ```release-note Restores redirect behavior for proxy subresources ```
2 parents 7008b90 + 04eede9 commit e0f7533

File tree

9 files changed

+97
-13
lines changed

9 files changed

+97
-13
lines changed

pkg/registry/core/node/rest/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ go_library(
1414
"//pkg/kubelet/client:go_default_library",
1515
"//pkg/registry/core/node:go_default_library",
1616
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
17+
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
1718
"//vendor/k8s.io/apimachinery/pkg/util/proxy:go_default_library",
1819
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
1920
"//vendor/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library",

pkg/registry/core/node/rest/proxy.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
"fmt"
2121
"net/http"
2222
"net/url"
23-
"path"
2423

2524
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/util/net"
2626
"k8s.io/apimachinery/pkg/util/proxy"
2727
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
2828
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
@@ -70,7 +70,7 @@ func (r *ProxyREST) Connect(ctx genericapirequest.Context, id string, opts runti
7070
if err != nil {
7171
return nil, err
7272
}
73-
location.Path = path.Join("/", location.Path, proxyOpts.Path)
73+
location.Path = net.JoinPreservingTrailingSlash(location.Path, proxyOpts.Path)
7474
// Return a proxy handler that uses the desired transport, wrapped with additional proxy handling (to get URL rewriting, X-Forwarded-* headers, etc)
7575
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, responder), nil
7676
}

pkg/registry/core/pod/rest/BUILD

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ go_library(
2020
"//pkg/registry/core/pod:go_default_library",
2121
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
2222
"//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library",
23+
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
2324
"//vendor/k8s.io/apimachinery/pkg/util/proxy:go_default_library",
2425
"//vendor/k8s.io/apiserver/pkg/endpoints/request:go_default_library",
2526
"//vendor/k8s.io/apiserver/pkg/features:go_default_library",

pkg/registry/core/pod/rest/subresources.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
"fmt"
2121
"net/http"
2222
"net/url"
23-
"path"
2423

2524
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/util/net"
2626
"k8s.io/apimachinery/pkg/util/proxy"
2727
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
2828
genericfeatures "k8s.io/apiserver/pkg/features"
@@ -71,7 +71,7 @@ func (r *ProxyREST) Connect(ctx genericapirequest.Context, id string, opts runti
7171
if err != nil {
7272
return nil, err
7373
}
74-
location.Path = path.Join("/", location.Path, proxyOpts.Path)
74+
location.Path = net.JoinPreservingTrailingSlash(location.Path, proxyOpts.Path)
7575
// Return a proxy handler that uses the desired transport, wrapped with additional proxy handling (to get URL rewriting, X-Forwarded-* headers, etc)
7676
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, false, responder), nil
7777
}

pkg/registry/core/service/proxy.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ import (
2020
"fmt"
2121
"net/http"
2222
"net/url"
23-
"path"
2423

2524
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/util/net"
2626
"k8s.io/apimachinery/pkg/util/proxy"
2727
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
2828
"k8s.io/apiserver/pkg/registry/rest"
@@ -66,7 +66,7 @@ func (r *ProxyREST) Connect(ctx genericapirequest.Context, id string, opts runti
6666
if err != nil {
6767
return nil, err
6868
}
69-
location.Path = path.Join("/", location.Path, proxyOpts.Path)
69+
location.Path = net.JoinPreservingTrailingSlash(location.Path, proxyOpts.Path)
7070
// Return a proxy handler that uses the desired transport, wrapped with additional proxy handling (to get URL rewriting, X-Forwarded-* headers, etc)
7171
return newThrottledUpgradeAwareProxyHandler(location, transport, true, false, responder), nil
7272
}

staging/src/k8s.io/apimachinery/pkg/util/net/http.go

+21
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,34 @@ import (
2626
"net/http"
2727
"net/url"
2828
"os"
29+
"path"
2930
"strconv"
3031
"strings"
3132

3233
"github.com/golang/glog"
3334
"golang.org/x/net/http2"
3435
)
3536

37+
// JoinPreservingTrailingSlash does a path.Join of the specified elements,
38+
// preserving any trailing slash on the last non-empty segment
39+
func JoinPreservingTrailingSlash(elem ...string) string {
40+
// do the basic path join
41+
result := path.Join(elem...)
42+
43+
// find the last non-empty segment
44+
for i := len(elem) - 1; i >= 0; i-- {
45+
if len(elem[i]) > 0 {
46+
// if the last segment ended in a slash, ensure our result does as well
47+
if strings.HasSuffix(elem[i], "/") && !strings.HasSuffix(result, "/") {
48+
result += "/"
49+
}
50+
break
51+
}
52+
}
53+
54+
return result
55+
}
56+
3657
// IsProbableEOF returns true if the given error resembles a connection termination
3758
// scenario that would justify assuming that the watch is empty.
3859
// These errors are what the Go http stack returns back to us which are general

staging/src/k8s.io/apimachinery/pkg/util/net/http_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package net
2020

2121
import (
2222
"crypto/tls"
23+
"fmt"
2324
"net"
2425
"net/http"
2526
"net/url"
@@ -218,3 +219,40 @@ func TestTLSClientConfigHolder(t *testing.T) {
218219
t.Errorf("didn't find tls config")
219220
}
220221
}
222+
223+
func TestJoinPreservingTrailingSlash(t *testing.T) {
224+
tests := []struct {
225+
a string
226+
b string
227+
want string
228+
}{
229+
// All empty
230+
{"", "", ""},
231+
232+
// Empty a
233+
{"", "/", "/"},
234+
{"", "foo", "foo"},
235+
{"", "/foo", "/foo"},
236+
{"", "/foo/", "/foo/"},
237+
238+
// Empty b
239+
{"/", "", "/"},
240+
{"foo", "", "foo"},
241+
{"/foo", "", "/foo"},
242+
{"/foo/", "", "/foo/"},
243+
244+
// Both populated
245+
{"/", "/", "/"},
246+
{"foo", "foo", "foo/foo"},
247+
{"/foo", "/foo", "/foo/foo"},
248+
{"/foo/", "/foo/", "/foo/foo/"},
249+
}
250+
for _, tt := range tests {
251+
name := fmt.Sprintf("%q+%q=%q", tt.a, tt.b, tt.want)
252+
t.Run(name, func(t *testing.T) {
253+
if got := JoinPreservingTrailingSlash(tt.a, tt.b); got != tt.want {
254+
t.Errorf("JoinPreservingTrailingSlash() = %v, want %v", got, tt.want)
255+
}
256+
})
257+
}
258+
}

staging/src/k8s.io/apiserver/pkg/endpoints/apiserver_test.go

+16-6
Original file line numberDiff line numberDiff line change
@@ -2135,15 +2135,25 @@ func TestGetWithOptions(t *testing.T) {
21352135
requestURL: "/namespaces/default/simple/id?param1=test1&param2=test2",
21362136
expectedPath: "",
21372137
},
2138+
{
2139+
name: "with root slash",
2140+
requestURL: "/namespaces/default/simple/id/?param1=test1&param2=test2",
2141+
expectedPath: "/",
2142+
},
21382143
{
21392144
name: "with path",
21402145
requestURL: "/namespaces/default/simple/id/a/different/path?param1=test1&param2=test2",
2141-
expectedPath: "a/different/path",
2146+
expectedPath: "/a/different/path",
2147+
},
2148+
{
2149+
name: "with path with trailing slash",
2150+
requestURL: "/namespaces/default/simple/id/a/different/path/?param1=test1&param2=test2",
2151+
expectedPath: "/a/different/path/",
21422152
},
21432153
{
21442154
name: "as subresource",
21452155
requestURL: "/namespaces/default/simple/id/subresource/another/different/path?param1=test1&param2=test2",
2146-
expectedPath: "another/different/path",
2156+
expectedPath: "/another/different/path",
21472157
},
21482158
{
21492159
name: "cluster-scoped basic",
@@ -2155,13 +2165,13 @@ func TestGetWithOptions(t *testing.T) {
21552165
name: "cluster-scoped basic with path",
21562166
rootScoped: true,
21572167
requestURL: "/simple/id/a/cluster/path?param1=test1&param2=test2",
2158-
expectedPath: "a/cluster/path",
2168+
expectedPath: "/a/cluster/path",
21592169
},
21602170
{
21612171
name: "cluster-scoped basic as subresource",
21622172
rootScoped: true,
21632173
requestURL: "/simple/id/subresource/another/cluster/path?param1=test1&param2=test2",
2164-
expectedPath: "another/cluster/path",
2174+
expectedPath: "/another/cluster/path",
21652175
},
21662176
}
21672177

@@ -2556,7 +2566,7 @@ func TestConnectWithOptions(t *testing.T) {
25562566
func TestConnectWithOptionsAndPath(t *testing.T) {
25572567
responseText := "Hello World"
25582568
itemID := "theID"
2559-
testPath := "a/b/c/def"
2569+
testPath := "/a/b/c/def"
25602570
connectStorage := &ConnecterRESTStorage{
25612571
connectHandler: &OutputConnect{
25622572
response: responseText,
@@ -2572,7 +2582,7 @@ func TestConnectWithOptionsAndPath(t *testing.T) {
25722582
server := httptest.NewServer(handler)
25732583
defer server.Close()
25742584

2575-
resp, err := http.Get(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + itemID + "/connect/" + testPath + "?param1=value1&param2=value2")
2585+
resp, err := http.Get(server.URL + "/" + prefix + "/" + testGroupVersion.Group + "/" + testGroupVersion.Version + "/namespaces/default/simple/" + itemID + "/connect" + testPath + "?param1=value1&param2=value2")
25762586

25772587
if err != nil {
25782588
t.Errorf("unexpected error: %v", err)

staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,20 @@ func getRequestOptions(req *http.Request, scope RequestScope, into runtime.Objec
212212
if isSubresource {
213213
startingIndex = 3
214214
}
215-
newQuery[subpathKey] = []string{strings.Join(requestInfo.Parts[startingIndex:], "/")}
215+
216+
p := strings.Join(requestInfo.Parts[startingIndex:], "/")
217+
218+
// ensure non-empty subpaths correctly reflect a leading slash
219+
if len(p) > 0 && !strings.HasPrefix(p, "/") {
220+
p = "/" + p
221+
}
222+
223+
// ensure subpaths correctly reflect the presence of a trailing slash on the original request
224+
if strings.HasSuffix(requestInfo.Path, "/") && !strings.HasSuffix(p, "/") {
225+
p += "/"
226+
}
227+
228+
newQuery[subpathKey] = []string{p}
216229
query = newQuery
217230
}
218231
return scope.ParameterCodec.DecodeParameters(query, scope.Kind.GroupVersion(), into)

0 commit comments

Comments
 (0)