Add HTTPUrlRewrite Filter in Gateway API
This commit is contained in:
parent
3ca667a3d4
commit
a696f7c654
15 changed files with 754 additions and 110 deletions
|
@ -219,8 +219,6 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
|
|||
SkipTests: []string{
|
||||
tests.HTTPRouteMethodMatching.ShortName,
|
||||
tests.HTTPRouteQueryParamMatching.ShortName,
|
||||
tests.HTTPRouteRewriteHost.ShortName,
|
||||
tests.HTTPRouteRewritePath.ShortName,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ type Middleware struct {
|
|||
// Gateway API HTTPRoute filters middlewares.
|
||||
RequestHeaderModifier *RequestHeaderModifier `json:"requestHeaderModifier,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||
RequestRedirect *RequestRedirect `json:"requestRedirect,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||
URLRewrite *URLRewrite `json:"URLRewrite,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
@ -703,3 +704,12 @@ type RequestRedirect struct {
|
|||
PathPrefix *string `json:"pathPrefix,omitempty"`
|
||||
StatusCode int `json:"statusCode,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// URLRewrite holds the URL rewrite middleware configuration.
|
||||
type URLRewrite struct {
|
||||
Hostname *string `json:"hostname,omitempty"`
|
||||
Path *string `json:"path,omitempty"`
|
||||
PathPrefix *string `json:"pathPrefix,omitempty"`
|
||||
}
|
||||
|
|
|
@ -869,6 +869,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
|
|||
*out = new(RequestRedirect)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.URLRewrite != nil {
|
||||
in, out := &in.URLRewrite, &out.URLRewrite
|
||||
*out = new(URLRewrite)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -2205,6 +2210,37 @@ func (in *UDPWeightedRoundRobin) DeepCopy() *UDPWeightedRoundRobin {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *URLRewrite) DeepCopyInto(out *URLRewrite) {
|
||||
*out = *in
|
||||
if in.Hostname != nil {
|
||||
in, out := &in.Hostname, &out.Hostname
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.Path != nil {
|
||||
in, out := &in.Path, &out.Path
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
if in.PathPrefix != nil {
|
||||
in, out := &in.PathPrefix, &out.PathPrefix
|
||||
*out = new(string)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new URLRewrite.
|
||||
func (in *URLRewrite) DeepCopy() *URLRewrite {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(URLRewrite)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in Users) DeepCopyInto(out *Users) {
|
||||
{
|
||||
|
|
|
@ -22,7 +22,7 @@ type requestHeaderModifier struct {
|
|||
}
|
||||
|
||||
// NewRequestHeaderModifier creates a new request header modifier middleware.
|
||||
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) (http.Handler, error) {
|
||||
func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dynamic.RequestHeaderModifier, name string) http.Handler {
|
||||
logger := middlewares.GetLogger(ctx, name, typeName)
|
||||
logger.Debug().Msg("Creating middleware")
|
||||
|
||||
|
@ -32,7 +32,7 @@ func NewRequestHeaderModifier(ctx context.Context, next http.Handler, config dyn
|
|||
set: config.Set,
|
||||
add: config.Add,
|
||||
remove: config.Remove,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *requestHeaderModifier) GetTracingInformation() (string, string, trace.SpanKind) {
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/testhelpers"
|
||||
)
|
||||
|
@ -104,8 +103,7 @@ func TestRequestHeaderModifier(t *testing.T) {
|
|||
gotHeaders = r.Header
|
||||
})
|
||||
|
||||
handler, err := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier")
|
||||
require.NoError(t, err)
|
||||
handler := NewRequestHeaderModifier(context.Background(), next, test.config, "foo-request-header-modifier")
|
||||
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
|
||||
if test.requestHeaders != nil {
|
||||
|
|
|
@ -20,6 +20,7 @@ const (
|
|||
type redirect struct {
|
||||
name string
|
||||
next http.Handler
|
||||
|
||||
scheme *string
|
||||
hostname *string
|
||||
port *string
|
||||
|
|
|
@ -2,7 +2,6 @@ package redirect
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
@ -17,13 +16,10 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
testCases := []struct {
|
||||
desc string
|
||||
config dynamic.RequestRedirect
|
||||
method string
|
||||
url string
|
||||
headers map[string]string
|
||||
secured bool
|
||||
expectedURL string
|
||||
expectedStatus int
|
||||
errorExpected bool
|
||||
wantURL string
|
||||
wantStatus int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "wrong status code",
|
||||
|
@ -32,7 +28,7 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
StatusCode: http.StatusOK,
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
errorExpected: true,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "replace path",
|
||||
|
@ -40,8 +36,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Path: ptr.To("/baz"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo.com:80/baz",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace path without trailing slash",
|
||||
|
@ -49,8 +45,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Path: ptr.To("/baz"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar/",
|
||||
expectedURL: "http://foo.com:80/baz",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo.com:80/baz",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace path with trailing slash",
|
||||
|
@ -58,8 +54,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Path: ptr.To("/baz/"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz/",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo.com:80/baz/",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "only hostname",
|
||||
|
@ -67,8 +63,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Hostname: ptr.To("bar.com"),
|
||||
},
|
||||
url: "http://foo.com:8080/foo/",
|
||||
expectedURL: "http://bar.com:8080/foo/",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://bar.com:8080/foo/",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path",
|
||||
|
@ -77,8 +73,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz/bar",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo.com:80/baz/bar",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path with trailing slash",
|
||||
|
@ -87,8 +83,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar/",
|
||||
expectedURL: "http://foo.com:80/baz/bar/",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo.com:80/baz/bar/",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path without slash prefix",
|
||||
|
@ -97,8 +93,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz/bar",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo.com:80/baz/bar",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path without slash prefix",
|
||||
|
@ -107,8 +103,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
PathPrefix: ptr.To("/foo/"),
|
||||
},
|
||||
url: "http://foo.com:80/foo/bar",
|
||||
expectedURL: "http://foo.com:80/baz/bar",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo.com:80/baz/bar",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "simple redirection",
|
||||
|
@ -118,8 +114,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Port: ptr.To("443"),
|
||||
},
|
||||
url: "http://foo.com:80",
|
||||
expectedURL: "https://foobar.com:443",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "https://foobar.com:443",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "HTTP to HTTPS permanent",
|
||||
|
@ -128,8 +124,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
StatusCode: http.StatusMovedPermanently,
|
||||
},
|
||||
url: "http://foo",
|
||||
expectedURL: "https://foo",
|
||||
expectedStatus: http.StatusMovedPermanently,
|
||||
wantURL: "https://foo",
|
||||
wantStatus: http.StatusMovedPermanently,
|
||||
},
|
||||
{
|
||||
desc: "HTTPS to HTTP permanent",
|
||||
|
@ -137,10 +133,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Scheme: ptr.To("http"),
|
||||
StatusCode: http.StatusMovedPermanently,
|
||||
},
|
||||
secured: true,
|
||||
url: "https://foo",
|
||||
expectedURL: "http://foo",
|
||||
expectedStatus: http.StatusMovedPermanently,
|
||||
wantURL: "http://foo",
|
||||
wantStatus: http.StatusMovedPermanently,
|
||||
},
|
||||
{
|
||||
desc: "HTTP to HTTPS",
|
||||
|
@ -149,8 +144,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Port: ptr.To("443"),
|
||||
},
|
||||
url: "http://foo:80",
|
||||
expectedURL: "https://foo:443",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "https://foo:443",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "HTTP to HTTPS, with X-Forwarded-Proto",
|
||||
|
@ -159,11 +154,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Port: ptr.To("443"),
|
||||
},
|
||||
url: "http://foo:80",
|
||||
headers: map[string]string{
|
||||
"X-Forwarded-Proto": "https",
|
||||
},
|
||||
expectedURL: "https://foo:443",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "https://foo:443",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "HTTPS to HTTP",
|
||||
|
@ -171,10 +163,9 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Scheme: ptr.To("http"),
|
||||
Port: ptr.To("80"),
|
||||
},
|
||||
secured: true,
|
||||
url: "https://foo:443",
|
||||
expectedURL: "http://foo:80",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo:80",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
{
|
||||
desc: "HTTP to HTTP",
|
||||
|
@ -183,8 +174,8 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
Port: ptr.To("88"),
|
||||
},
|
||||
url: "http://foo:80",
|
||||
expectedURL: "http://foo:88",
|
||||
expectedStatus: http.StatusFound,
|
||||
wantURL: "http://foo:88",
|
||||
wantStatus: http.StatusFound,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -193,46 +184,33 @@ func TestRequestRedirectHandler(t *testing.T) {
|
|||
t.Parallel()
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
handler, err := NewRequestRedirect(context.Background(), next, test.config, "traefikTest")
|
||||
|
||||
if test.errorExpected {
|
||||
handler, err := NewRequestRedirect(context.Background(), next, test.config, "traefikTest")
|
||||
if test.wantErr {
|
||||
require.Error(t, err)
|
||||
require.Nil(t, handler)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, test.url, nil)
|
||||
|
||||
method := http.MethodGet
|
||||
if test.method != "" {
|
||||
method = test.method
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(method, test.url, nil)
|
||||
if test.secured {
|
||||
req.TLS = &tls.ConnectionState{}
|
||||
}
|
||||
|
||||
for k, v := range test.headers {
|
||||
req.Header.Set(k, v)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Foo", "bar")
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.expectedStatus, recorder.Code)
|
||||
switch test.expectedStatus {
|
||||
assert.Equal(t, test.wantStatus, recorder.Code)
|
||||
switch test.wantStatus {
|
||||
case http.StatusMovedPermanently, http.StatusFound:
|
||||
location, err := recorder.Result().Location()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expectedURL, location.String())
|
||||
assert.Equal(t, test.wantURL, location.String())
|
||||
default:
|
||||
location, err := recorder.Result().Location()
|
||||
require.Errorf(t, err, "Location %v", location)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
68
pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go
Normal file
68
pkg/middlewares/gatewayapi/urlrewrite/url_rewrite.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package urlrewrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const (
|
||||
typeName = "URLRewrite"
|
||||
)
|
||||
|
||||
type urlRewrite struct {
|
||||
name string
|
||||
next http.Handler
|
||||
|
||||
hostname *string
|
||||
path *string
|
||||
pathPrefix *string
|
||||
}
|
||||
|
||||
// NewURLRewrite creates a URL rewrite middleware.
|
||||
func NewURLRewrite(ctx context.Context, next http.Handler, conf dynamic.URLRewrite, name string) http.Handler {
|
||||
logger := middlewares.GetLogger(ctx, name, typeName)
|
||||
logger.Debug().Msg("Creating middleware")
|
||||
|
||||
return urlRewrite{
|
||||
name: name,
|
||||
next: next,
|
||||
hostname: conf.Hostname,
|
||||
path: conf.Path,
|
||||
pathPrefix: conf.PathPrefix,
|
||||
}
|
||||
}
|
||||
|
||||
func (u urlRewrite) GetTracingInformation() (string, string, trace.SpanKind) {
|
||||
return u.name, typeName, trace.SpanKindInternal
|
||||
}
|
||||
|
||||
func (u urlRewrite) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
newPath := req.URL.Path
|
||||
if u.path != nil && u.pathPrefix == nil {
|
||||
newPath = *u.path
|
||||
}
|
||||
if u.path != nil && u.pathPrefix != nil {
|
||||
newPath = path.Join(*u.path, strings.TrimPrefix(req.URL.Path, *u.pathPrefix))
|
||||
|
||||
// add the trailing slash if needed, as path.Join removes trailing slashes.
|
||||
if strings.HasSuffix(req.URL.Path, "/") && !strings.HasSuffix(newPath, "/") {
|
||||
newPath += "/"
|
||||
}
|
||||
}
|
||||
|
||||
req.URL.Path = newPath
|
||||
req.URL.RawPath = req.URL.EscapedPath()
|
||||
req.RequestURI = req.URL.RequestURI()
|
||||
|
||||
if u.hostname != nil {
|
||||
req.Host = *u.hostname
|
||||
}
|
||||
|
||||
u.next.ServeHTTP(rw, req)
|
||||
}
|
126
pkg/middlewares/gatewayapi/urlrewrite/url_rewrite_test.go
Normal file
126
pkg/middlewares/gatewayapi/urlrewrite/url_rewrite_test.go
Normal file
|
@ -0,0 +1,126 @@
|
|||
package urlrewrite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestURLRewriteHandler(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
config dynamic.URLRewrite
|
||||
url string
|
||||
wantURL string
|
||||
wantHost string
|
||||
}{
|
||||
{
|
||||
desc: "replace path",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace path without trailing slash",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar/",
|
||||
wantURL: "http://foo.com/baz",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace path with trailing slash",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz/"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz/",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "only host",
|
||||
config: dynamic.URLRewrite{
|
||||
Hostname: ptr.To("bar.com"),
|
||||
},
|
||||
url: "http://foo.com/foo/",
|
||||
wantURL: "http://foo.com/foo/",
|
||||
wantHost: "bar.com",
|
||||
},
|
||||
{
|
||||
desc: "host and path",
|
||||
config: dynamic.URLRewrite{
|
||||
Hostname: ptr.To("bar.com"),
|
||||
Path: ptr.To("/baz/"),
|
||||
},
|
||||
url: "http://foo.com/foo/",
|
||||
wantURL: "http://foo.com/baz/",
|
||||
wantHost: "bar.com",
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz/bar",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path with trailing slash",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar/",
|
||||
wantURL: "http://foo.com/baz/bar/",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path without slash prefix",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("baz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz/bar",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
{
|
||||
desc: "replace prefix path without slash prefix",
|
||||
config: dynamic.URLRewrite{
|
||||
Path: ptr.To("/baz"),
|
||||
PathPrefix: ptr.To("/foo/"),
|
||||
},
|
||||
url: "http://foo.com/foo/bar",
|
||||
wantURL: "http://foo.com/baz/bar",
|
||||
wantHost: "foo.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
handler := NewURLRewrite(context.Background(), next, test.config, "traefikTest")
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, test.url, nil)
|
||||
handler.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, test.wantURL, req.URL.String())
|
||||
assert.Equal(t, test.wantHost, req.Host)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: http
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: HTTPRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: HTTPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: http-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- "example.com"
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /foo
|
||||
backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
kind: Service
|
||||
group: ""
|
||||
filters:
|
||||
- type: URLRewrite
|
||||
urlRewrite:
|
||||
hostname: www.foo.bar
|
||||
path:
|
||||
type: ReplacePrefixMatch
|
||||
replacePrefixMatch: /xyz
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: http
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: HTTPRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: HTTPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: http-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- "example.com"
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /foo
|
||||
backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
kind: Service
|
||||
group: ""
|
||||
filters:
|
||||
- type: URLRewrite
|
||||
urlRewrite:
|
||||
path:
|
||||
type: ReplaceFullPath
|
||||
replaceFullPath: /bar
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: http
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: HTTPRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: HTTPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: http-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- "example.com"
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /foo
|
||||
backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
kind: Service
|
||||
group: ""
|
||||
filters:
|
||||
- type: URLRewrite
|
||||
urlRewrite:
|
||||
hostname: www.foo.bar
|
|
@ -301,15 +301,19 @@ func (p *Provider) loadHTTPBackendRef(namespace string, backendRef gatev1.HTTPBa
|
|||
}
|
||||
|
||||
func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, routerName string, filters []gatev1.HTTPRouteFilter, pathMatch *gatev1.HTTPPathMatch) ([]string, error) {
|
||||
pm := ptr.Deref(pathMatch, gatev1.HTTPPathMatch{
|
||||
Type: ptr.To(gatev1.PathMatchPathPrefix),
|
||||
Value: ptr.To("/"),
|
||||
})
|
||||
|
||||
middlewares := make(map[string]*dynamic.Middleware)
|
||||
for i, filter := range filters {
|
||||
name := fmt.Sprintf("%s-%s-%d", routerName, strings.ToLower(string(filter.Type)), i)
|
||||
switch filter.Type {
|
||||
case gatev1.HTTPRouteFilterRequestRedirect:
|
||||
name := fmt.Sprintf("%s-%s-%d", routerName, strings.ToLower(string(filter.Type)), i)
|
||||
middlewares[name] = createRequestRedirect(filter.RequestRedirect, pathMatch)
|
||||
middlewares[name] = createRequestRedirect(filter.RequestRedirect, pm)
|
||||
|
||||
case gatev1.HTTPRouteFilterRequestHeaderModifier:
|
||||
name := fmt.Sprintf("%s-%s-%d", routerName, strings.ToLower(string(filter.Type)), i)
|
||||
middlewares[name] = createRequestHeaderModifier(filter.RequestHeaderModifier)
|
||||
|
||||
case gatev1.HTTPRouteFilterExtensionRef:
|
||||
|
@ -320,6 +324,14 @@ func (p *Provider) loadMiddlewares(conf *dynamic.Configuration, namespace, route
|
|||
|
||||
middlewares[name] = middleware
|
||||
|
||||
case gatev1.HTTPRouteFilterURLRewrite:
|
||||
var err error
|
||||
middleware, err := createURLRewrite(filter.URLRewrite, pm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid filter %s: %w", filter.Type, err)
|
||||
}
|
||||
middlewares[name] = middleware
|
||||
|
||||
default:
|
||||
// As per the spec: https://gateway-api.sigs.k8s.io/api-types/httproute/#filters-optional
|
||||
// In all cases where incompatible or unsupported filters are
|
||||
|
@ -560,7 +572,7 @@ func createRequestHeaderModifier(filter *gatev1.HTTPHeaderFilter) *dynamic.Middl
|
|||
}
|
||||
}
|
||||
|
||||
func createRequestRedirect(filter *gatev1.HTTPRequestRedirectFilter, pathMatch *gatev1.HTTPPathMatch) *dynamic.Middleware {
|
||||
func createRequestRedirect(filter *gatev1.HTTPRequestRedirectFilter, pathMatch gatev1.HTTPPathMatch) *dynamic.Middleware {
|
||||
var hostname *string
|
||||
if filter.Hostname != nil {
|
||||
hostname = ptr.To(string(*filter.Hostname))
|
||||
|
@ -599,6 +611,37 @@ func createRequestRedirect(filter *gatev1.HTTPRequestRedirectFilter, pathMatch *
|
|||
}
|
||||
}
|
||||
|
||||
func createURLRewrite(filter *gatev1.HTTPURLRewriteFilter, pathMatch gatev1.HTTPPathMatch) (*dynamic.Middleware, error) {
|
||||
if filter.Path == nil && filter.Hostname == nil {
|
||||
return nil, errors.New("empty configuration")
|
||||
}
|
||||
|
||||
var host *string
|
||||
if filter.Hostname != nil {
|
||||
host = ptr.To(string(*filter.Hostname))
|
||||
}
|
||||
|
||||
var path *string
|
||||
var pathPrefix *string
|
||||
if filter.Path != nil {
|
||||
switch filter.Path.Type {
|
||||
case gatev1.FullPathHTTPPathModifier:
|
||||
path = filter.Path.ReplaceFullPath
|
||||
case gatev1.PrefixMatchHTTPPathModifier:
|
||||
path = filter.Path.ReplacePrefixMatch
|
||||
pathPrefix = pathMatch.Value
|
||||
}
|
||||
}
|
||||
|
||||
return &dynamic.Middleware{
|
||||
URLRewrite: &dynamic.URLRewrite{
|
||||
Hostname: host,
|
||||
Path: path,
|
||||
PathPrefix: pathPrefix,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getProtocol(portSpec corev1.ServicePort) string {
|
||||
protocol := "http"
|
||||
if portSpec.Port == 443 || strings.HasPrefix(portSpec.Name, "https") {
|
||||
|
|
|
@ -1734,6 +1734,212 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
|||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple HTTPRoute URL rewrite FullPath",
|
||||
paths: []string{"services.yml", "httproute/filter_url_rewrite_fullpath.yml"},
|
||||
entryPoints: map[string]Entrypoint{"web": {
|
||||
Address: ":80",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "default-http-app-1-my-gateway-web-0-wrr",
|
||||
Rule: "Host(`example.com`) && (Path(`/foo`) || PathPrefix(`/foo/`))",
|
||||
RuleSyntax: "v3",
|
||||
Priority: 10412,
|
||||
Middlewares: []string{"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0"},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0": {
|
||||
URLRewrite: &dynamic.URLRewrite{
|
||||
Path: ptr.To("/bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-http-app-1-my-gateway-web-0-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
Name: "default-whoami-80",
|
||||
Weight: func(i int) *int { return &i }(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple HTTPRoute URL rewrite Hostname",
|
||||
paths: []string{"services.yml", "httproute/filter_url_rewrite_hostname.yml"},
|
||||
entryPoints: map[string]Entrypoint{"web": {
|
||||
Address: ":80",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "default-http-app-1-my-gateway-web-0-wrr",
|
||||
Rule: "Host(`example.com`) && (Path(`/foo`) || PathPrefix(`/foo/`))",
|
||||
RuleSyntax: "v3",
|
||||
Priority: 10412,
|
||||
Middlewares: []string{"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0"},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0": {
|
||||
URLRewrite: &dynamic.URLRewrite{
|
||||
Hostname: ptr.To("www.foo.bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-http-app-1-my-gateway-web-0-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
Name: "default-whoami-80",
|
||||
Weight: func(i int) *int { return &i }(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple HTTPRoute URL rewrite Combined",
|
||||
paths: []string{"services.yml", "httproute/filter_url_rewrite_combined.yml"},
|
||||
entryPoints: map[string]Entrypoint{"web": {
|
||||
Address: ":80",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "default-http-app-1-my-gateway-web-0-wrr",
|
||||
Rule: "Host(`example.com`) && (Path(`/foo`) || PathPrefix(`/foo/`))",
|
||||
RuleSyntax: "v3",
|
||||
Priority: 10412,
|
||||
Middlewares: []string{"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0"},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-http-app-1-my-gateway-web-0-7f90cf546b15efadf2f8-urlrewrite-0": {
|
||||
URLRewrite: &dynamic.URLRewrite{
|
||||
Hostname: ptr.To("www.foo.bar"),
|
||||
Path: ptr.To("/xyz"),
|
||||
PathPrefix: ptr.To("/foo"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-http-app-1-my-gateway-web-0-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
Name: "default-whoami-80",
|
||||
Weight: func(i int) *int { return &i }(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "http://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "http://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"github.com/traefik/traefik/v3/pkg/middlewares/customerrors"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/headermodifier"
|
||||
gapiredirect "github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/redirect"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/gatewayapi/urlrewrite"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/grpcweb"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/headers"
|
||||
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
|
||||
|
@ -392,7 +393,7 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
|
|||
return nil, badConf
|
||||
}
|
||||
middleware = func(next http.Handler) (http.Handler, error) {
|
||||
return headermodifier.NewRequestHeaderModifier(ctx, next, *config.RequestHeaderModifier, middlewareName)
|
||||
return headermodifier.NewRequestHeaderModifier(ctx, next, *config.RequestHeaderModifier, middlewareName), nil
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,6 +406,15 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
|
|||
}
|
||||
}
|
||||
|
||||
if config.URLRewrite != nil {
|
||||
if middleware != nil {
|
||||
return nil, badConf
|
||||
}
|
||||
middleware = func(next http.Handler) (http.Handler, error) {
|
||||
return urlrewrite.NewURLRewrite(ctx, next, *config.URLRewrite, middlewareName), nil
|
||||
}
|
||||
}
|
||||
|
||||
if middleware == nil {
|
||||
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue