Add HTTPUrlRewrite Filter in Gateway API

This commit is contained in:
Manuel Zapf 2024-06-13 17:06:04 +02:00 committed by GitHub
parent 3ca667a3d4
commit a696f7c654
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 754 additions and 110 deletions

View file

@ -219,8 +219,6 @@ func (s *K8sConformanceSuite) TestK8sGatewayAPIConformance() {
SkipTests: []string{
tests.HTTPRouteMethodMatching.ShortName,
tests.HTTPRouteQueryParamMatching.ShortName,
tests.HTTPRouteRewriteHost.ShortName,
tests.HTTPRouteRewritePath.ShortName,
},
}

View file

@ -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"`
}

View file

@ -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) {
{

View file

@ -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) {

View file

@ -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 {

View file

@ -18,8 +18,9 @@ const (
)
type redirect struct {
name string
next http.Handler
name string
next http.Handler
scheme *string
hostname *string
port *string

View file

@ -2,7 +2,6 @@ package redirect
import (
"context"
"crypto/tls"
"net/http"
"net/http/httptest"
"testing"
@ -15,15 +14,12 @@ import (
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
desc string
config dynamic.RequestRedirect
url string
wantURL string
wantStatus int
wantErr bool
}{
{
desc: "wrong status code",
@ -31,44 +27,44 @@ func TestRequestRedirectHandler(t *testing.T) {
Path: ptr.To("/baz"),
StatusCode: http.StatusOK,
},
url: "http://foo.com:80/foo/bar",
errorExpected: true,
url: "http://foo.com:80/foo/bar",
wantErr: true,
},
{
desc: "replace path",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz",
expectedStatus: http.StatusFound,
url: "http://foo.com:80/foo/bar",
wantURL: "http://foo.com:80/baz",
wantStatus: http.StatusFound,
},
{
desc: "replace path without trailing slash",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz"),
},
url: "http://foo.com:80/foo/bar/",
expectedURL: "http://foo.com:80/baz",
expectedStatus: http.StatusFound,
url: "http://foo.com:80/foo/bar/",
wantURL: "http://foo.com:80/baz",
wantStatus: http.StatusFound,
},
{
desc: "replace path with trailing slash",
config: dynamic.RequestRedirect{
Path: ptr.To("/baz/"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz/",
expectedStatus: http.StatusFound,
url: "http://foo.com:80/foo/bar",
wantURL: "http://foo.com:80/baz/",
wantStatus: http.StatusFound,
},
{
desc: "only hostname",
config: dynamic.RequestRedirect{
Hostname: ptr.To("bar.com"),
},
url: "http://foo.com:8080/foo/",
expectedURL: "http://bar.com:8080/foo/",
expectedStatus: http.StatusFound,
url: "http://foo.com:8080/foo/",
wantURL: "http://bar.com:8080/foo/",
wantStatus: http.StatusFound,
},
{
desc: "replace prefix path",
@ -76,9 +72,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Path: ptr.To("/baz"),
PathPrefix: ptr.To("/foo"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz/bar",
expectedStatus: http.StatusFound,
url: "http://foo.com:80/foo/bar",
wantURL: "http://foo.com:80/baz/bar",
wantStatus: http.StatusFound,
},
{
desc: "replace prefix path with trailing slash",
@ -86,9 +82,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Path: ptr.To("/baz"),
PathPrefix: ptr.To("/foo"),
},
url: "http://foo.com:80/foo/bar/",
expectedURL: "http://foo.com:80/baz/bar/",
expectedStatus: http.StatusFound,
url: "http://foo.com:80/foo/bar/",
wantURL: "http://foo.com:80/baz/bar/",
wantStatus: http.StatusFound,
},
{
desc: "replace prefix path without slash prefix",
@ -96,9 +92,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Path: ptr.To("baz"),
PathPrefix: ptr.To("/foo"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz/bar",
expectedStatus: http.StatusFound,
url: "http://foo.com:80/foo/bar",
wantURL: "http://foo.com:80/baz/bar",
wantStatus: http.StatusFound,
},
{
desc: "replace prefix path without slash prefix",
@ -106,9 +102,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Path: ptr.To("/baz"),
PathPrefix: ptr.To("/foo/"),
},
url: "http://foo.com:80/foo/bar",
expectedURL: "http://foo.com:80/baz/bar",
expectedStatus: http.StatusFound,
url: "http://foo.com:80/foo/bar",
wantURL: "http://foo.com:80/baz/bar",
wantStatus: http.StatusFound,
},
{
desc: "simple redirection",
@ -117,9 +113,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Hostname: ptr.To("foobar.com"),
Port: ptr.To("443"),
},
url: "http://foo.com:80",
expectedURL: "https://foobar.com:443",
expectedStatus: http.StatusFound,
url: "http://foo.com:80",
wantURL: "https://foobar.com:443",
wantStatus: http.StatusFound,
},
{
desc: "HTTP to HTTPS permanent",
@ -127,9 +123,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Scheme: ptr.To("https"),
StatusCode: http.StatusMovedPermanently,
},
url: "http://foo",
expectedURL: "https://foo",
expectedStatus: http.StatusMovedPermanently,
url: "http://foo",
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,
url: "https://foo",
wantURL: "http://foo",
wantStatus: http.StatusMovedPermanently,
},
{
desc: "HTTP to HTTPS",
@ -148,9 +143,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Scheme: ptr.To("https"),
Port: ptr.To("443"),
},
url: "http://foo:80",
expectedURL: "https://foo:443",
expectedStatus: http.StatusFound,
url: "http://foo:80",
wantURL: "https://foo:443",
wantStatus: http.StatusFound,
},
{
desc: "HTTP to HTTPS, with X-Forwarded-Proto",
@ -158,12 +153,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Scheme: ptr.To("https"),
Port: ptr.To("443"),
},
url: "http://foo:80",
headers: map[string]string{
"X-Forwarded-Proto": "https",
},
expectedURL: "https://foo:443",
expectedStatus: http.StatusFound,
url: "http://foo:80",
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,
url: "https://foo:443",
wantURL: "http://foo:80",
wantStatus: http.StatusFound,
},
{
desc: "HTTP to HTTP",
@ -182,9 +173,9 @@ func TestRequestRedirectHandler(t *testing.T) {
Scheme: ptr.To("http"),
Port: ptr.To("88"),
},
url: "http://foo:80",
expectedURL: "http://foo:88",
expectedStatus: http.StatusFound,
url: "http://foo:80",
wantURL: "http://foo:88",
wantStatus: http.StatusFound,
},
}
@ -193,45 +184,32 @@ 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)
handler.ServeHTTP(recorder, req)
assert.Equal(t, test.wantStatus, recorder.Code)
switch test.wantStatus {
case http.StatusMovedPermanently, http.StatusFound:
location, err := recorder.Result().Location()
require.NoError(t, err)
require.NotNil(t, handler)
recorder := httptest.NewRecorder()
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 {
case http.StatusMovedPermanently, http.StatusFound:
location, err := recorder.Result().Location()
require.NoError(t, err)
assert.Equal(t, test.expectedURL, location.String())
default:
location, err := recorder.Result().Location()
require.Errorf(t, err, "Location %v", location)
}
assert.Equal(t, test.wantURL, location.String())
default:
location, err := recorder.Result().Location()
require.Errorf(t, err, "Location %v", location)
}
})
}

View 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)
}

View 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)
})
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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") {

View file

@ -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 {

View file

@ -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)
}