Nginx x-forwarded-prefix annotation

This commit is contained in:
Nándor Kollár 2026-03-06 17:16:04 +01:00 committed by GitHub
parent efcc60fbdb
commit ee07a31ae3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 672 additions and 17 deletions

View File

@ -339,6 +339,7 @@ The following annotations are organized by category for easier navigation.
| <a id="opt-nginx-ingress-kubernetes-iocanary-by-cookie" href="#opt-nginx-ingress-kubernetes-iocanary-by-cookie" title="#opt-nginx-ingress-kubernetes-iocanary-by-cookie">`nginx.ingress.kubernetes.io/canary-by-cookie`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocanary-weight" href="#opt-nginx-ingress-kubernetes-iocanary-weight" title="#opt-nginx-ingress-kubernetes-iocanary-weight">`nginx.ingress.kubernetes.io/canary-weight`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocanary-weight-total" href="#opt-nginx-ingress-kubernetes-iocanary-weight-total" title="#opt-nginx-ingress-kubernetes-iocanary-weight-total">`nginx.ingress.kubernetes.io/canary-weight-total`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iox-forwarded-prefix" href="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix" title="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix">`nginx.ingress.kubernetes.io/x-forwarded-prefix`</a> | |
### CORS
@ -488,7 +489,6 @@ In practice, Traefik is slightly more lenient under bursty load, as it smooths o
| <a id="opt-nginx-ingress-kubernetes-iomirror-request-body" href="#opt-nginx-ingress-kubernetes-iomirror-request-body" title="#opt-nginx-ingress-kubernetes-iomirror-request-body">`nginx.ingress.kubernetes.io/mirror-request-body`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iomirror-target" href="#opt-nginx-ingress-kubernetes-iomirror-target" title="#opt-nginx-ingress-kubernetes-iomirror-target">`nginx.ingress.kubernetes.io/mirror-target`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iomirror-host" href="#opt-nginx-ingress-kubernetes-iomirror-host" title="#opt-nginx-ingress-kubernetes-iomirror-host">`nginx.ingress.kubernetes.io/mirror-host`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iox-forwarded-prefix" href="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix" title="#opt-nginx-ingress-kubernetes-iox-forwarded-prefix">`nginx.ingress.kubernetes.io/x-forwarded-prefix`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iodenylist-source-range" href="#opt-nginx-ingress-kubernetes-iodenylist-source-range" title="#opt-nginx-ingress-kubernetes-iodenylist-source-range">`nginx.ingress.kubernetes.io/denylist-source-range`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iostream-snippet" href="#opt-nginx-ingress-kubernetes-iostream-snippet" title="#opt-nginx-ingress-kubernetes-iostream-snippet">`nginx.ingress.kubernetes.io/stream-snippet`</a> | |

View File

@ -59,6 +59,7 @@ type Middleware struct {
// ingress-nginx middlewares.
AuthTLSPassCertificateToUpstream *AuthTLSPassCertificateToUpstream `json:"authTLSPassCertificateToUpstream,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
Snippet *Snippet `json:"snippet,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
RewriteTarget *RewriteTarget `json:"rewriteTarget,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
}
// +k8s:deepcopy-gen=true
@ -904,3 +905,16 @@ type Snippet struct {
ServerSnippet string `json:"serverSnippet,omitempty"`
ConfigurationSnippet string `json:"configurationSnippet,omitempty"`
}
// +k8s:deepcopy-gen=true
// RewriteTarget holds the rewrite target middleware configuration used by ingress-nginx provider.
// This middleware replaces the path of a URL.
type RewriteTarget struct {
// Regex defines the regular expression used to match and capture the path from the request URL.
Regex string `json:"regex,omitempty"`
// Replacement defines the replacement path format, which can include captured variables.
Replacement string `json:"replacement,omitempty"`
// XForwardedPrefix defines the value of the X-Forwarded-Prefix header.
XForwardedPrefix string `json:"xForwardedPrefix,omitempty"`
}

View File

@ -1122,6 +1122,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
*out = new(Snippet)
**out = **in
}
if in.RewriteTarget != nil {
in, out := &in.RewriteTarget, &out.RewriteTarget
*out = new(RewriteTarget)
**out = **in
}
return
}
@ -1491,6 +1496,22 @@ func (in *Retry) DeepCopy() *Retry {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *RewriteTarget) DeepCopyInto(out *RewriteTarget) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RewriteTarget.
func (in *RewriteTarget) DeepCopy() *RewriteTarget {
if in == nil {
return nil
}
out := new(RewriteTarget)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Router) DeepCopyInto(out *Router) {
*out = *in

View File

@ -0,0 +1,100 @@
package rewritetarget
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
)
const (
typeName = "RewriteTarget"
xForwardedPrefixHeader = "X-Forwarded-Prefix"
)
// RewriteTarget is a middleware used to replace the path of a URL request.
type rewriteTarget struct {
next http.Handler
regexp *regexp.Regexp
replacement string
xForwardedPrefix string
name string
}
// New creates a new rewrite target middleware.
func New(ctx context.Context, next http.Handler, config dynamic.RewriteTarget, name string) (http.Handler, error) {
middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware")
if config.Replacement == "" {
return nil, errors.New("replacement cannot be empty")
}
mw := &rewriteTarget{
next: next,
replacement: strings.TrimSpace(config.Replacement),
xForwardedPrefix: config.XForwardedPrefix,
name: name,
}
if config.Regex != "" {
exp, err := regexp.Compile(strings.TrimSpace(config.Regex))
if err != nil {
return nil, fmt.Errorf("compiling regular expression %s: %w", config.Regex, err)
}
mw.regexp = exp
}
return mw, nil
}
func (rt *rewriteTarget) GetTracingInformation() (string, string) {
return rt.name, typeName
}
func (rt *rewriteTarget) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
currentPath := req.URL.RawPath
if currentPath == "" {
currentPath = req.URL.EscapedPath()
}
if rt.regexp != nil {
if !rt.regexp.MatchString(currentPath) {
rt.next.ServeHTTP(rw, req)
return
}
req.URL.RawPath = rt.regexp.ReplaceAllString(currentPath, rt.replacement)
} else {
req.URL.RawPath = rt.replacement
}
if rt.xForwardedPrefix != "" {
prefix := rt.xForwardedPrefix
if rt.regexp != nil {
prefix = rt.regexp.ReplaceAllString(currentPath, rt.xForwardedPrefix)
}
req.Header.Set(xForwardedPrefixHeader, prefix)
}
// as replacement can introduce escaped characters
// Path must remain an unescaped version of RawPath
// Doesn't handle multiple times encoded replacement (`/` => `%2F` => `%252F` => ...)
var err error
req.URL.Path, err = url.PathUnescape(req.URL.RawPath)
if err != nil {
middlewares.GetLogger(context.Background(), rt.name, typeName).Error().Msgf("Unable to unescape url raw path %q: %v", req.URL.RawPath, err)
observability.SetStatusErrorf(req.Context(), "Unable to unescape url raw path %q: %v", req.URL.RawPath, err)
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
req.RequestURI = req.URL.RequestURI()
rt.next.ServeHTTP(rw, req)
}

View File

@ -0,0 +1,180 @@
package rewritetarget
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
)
func TestRewriteTarget(t *testing.T) {
testCases := []struct {
desc string
path string
config dynamic.RewriteTarget
expectedPath string
expectedRawPath string
expectedXForwardedPrefix string
expectsError bool
}{
{
desc: "empty replacement",
config: dynamic.RewriteTarget{
Replacement: "",
},
expectsError: true,
},
{
desc: "plain replacement",
path: "/foo/bar",
config: dynamic.RewriteTarget{
Replacement: "/replacement",
},
expectedPath: "/replacement",
expectedRawPath: "/replacement",
},
{
desc: "plain replacement with escaped char in replacement",
path: "/foo",
config: dynamic.RewriteTarget{
Replacement: "/foo%2Fbar",
},
expectedPath: "/foo/bar",
expectedRawPath: "/foo%2Fbar",
},
{
desc: "plain replacement with x-forwarded-prefix",
path: "/foo/bar",
config: dynamic.RewriteTarget{
Replacement: "/replacement",
XForwardedPrefix: "/foo",
},
expectedPath: "/replacement",
expectedRawPath: "/replacement",
expectedXForwardedPrefix: "/foo",
},
{
desc: "regex with capture group",
path: "/foo/bar",
config: dynamic.RewriteTarget{
Regex: `^/foo/(.*)`,
Replacement: "/new/$1",
},
expectedPath: "/new/bar",
expectedRawPath: "/new/bar",
},
{
desc: "regex with multiple capture groups",
path: "/downloads/src/source.go",
config: dynamic.RewriteTarget{
Regex: `^(?i)/downloads/([^/]+)/([^/]+)$`,
Replacement: "/downloads/$1-$2",
},
expectedPath: "/downloads/src-source.go",
expectedRawPath: "/downloads/src-source.go",
},
{
desc: "regex with escaped char in replacement",
path: "/aaa/bbb",
config: dynamic.RewriteTarget{
Regex: `/aaa/bbb`,
Replacement: "/foo%2Fbar",
},
expectedPath: "/foo/bar",
expectedRawPath: "/foo%2Fbar",
},
{
desc: "regex - no match passthrough",
path: "/foo/bar",
config: dynamic.RewriteTarget{
Regex: `^/baz/(.*)`,
Replacement: "/new/$1",
},
expectedPath: "/foo/bar",
expectedRawPath: "",
},
{
desc: "invalid regex",
config: dynamic.RewriteTarget{
Regex: `^(?err)/invalid/regexp/([^/]+)$`,
Replacement: "/valid/$1",
},
expectsError: true,
},
{
desc: "regex with x-forwarded-prefix capture group",
path: "/foo/bar",
config: dynamic.RewriteTarget{
Regex: `^(/foo)/(.*)`,
Replacement: "/$2",
XForwardedPrefix: "$1",
},
expectedPath: "/bar",
expectedRawPath: "/bar",
expectedXForwardedPrefix: "/foo",
},
{
desc: "regex with x-forwarded-prefix using third capture group",
path: "/prefix/sub/endpoint",
config: dynamic.RewriteTarget{
Regex: `^/(prefix)/(sub)/(.*)`,
Replacement: "/$3",
XForwardedPrefix: "/$1/$2",
},
expectedPath: "/endpoint",
expectedRawPath: "/endpoint",
expectedXForwardedPrefix: "/prefix/sub",
},
{
desc: "x-forwarded-prefix not set when regex does not match",
path: "/foo/bar",
config: dynamic.RewriteTarget{
Regex: `^/baz/(.*)`,
Replacement: "/$1",
XForwardedPrefix: "/baz",
},
expectedPath: "/foo/bar",
expectedRawPath: "",
expectedXForwardedPrefix: "",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
var actualPath, actualRawPath, actualXForwardedPrefix, requestURI string
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
actualPath = r.URL.Path
actualRawPath = r.URL.RawPath
actualXForwardedPrefix = r.Header.Get(xForwardedPrefixHeader)
requestURI = r.RequestURI
})
handler, err := New(t.Context(), next, test.config, "test-rewrite-target")
if test.expectsError {
require.Error(t, err)
return
}
require.NoError(t, err)
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL + test.path)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, test.expectedPath, actualPath, "Unexpected path.")
assert.Equal(t, test.expectedRawPath, actualRawPath, "Unexpected raw path.")
assert.Equal(t, test.expectedXForwardedPrefix, actualXForwardedPrefix, "Unexpected %s header.", xForwardedPrefixHeader)
if actualRawPath == "" {
assert.Equal(t, actualPath, requestURI, "Unexpected request URI.")
} else {
assert.Equal(t, actualRawPath, requestURI, "Unexpected request URI.")
}
})
}
}

View File

@ -83,8 +83,9 @@ type ingressConfig struct {
LimitRPM *int `annotation:"nginx.ingress.kubernetes.io/limit-rpm"`
LimitRPS *int `annotation:"nginx.ingress.kubernetes.io/limit-rps"`
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`
CustomHeaders *string `annotation:"nginx.ingress.kubernetes.io/custom-headers"`
UpstreamVhost *string `annotation:"nginx.ingress.kubernetes.io/upstream-vhost"`
XForwardedPrefix *string `annotation:"nginx.ingress.kubernetes.io/x-forwarded-prefix"`
CustomHTTPErrors *[]string `annotation:"nginx.ingress.kubernetes.io/custom-http-errors"`
DefaultBackend *string `annotation:"nginx.ingress.kubernetes.io/default-backend"`

View File

@ -38,6 +38,8 @@ func Test_parseIngressConfig(t *testing.T) {
"nginx.ingress.kubernetes.io/proxy-buffers-number": "8",
"nginx.ingress.kubernetes.io/proxy-max-temp-file-size": "100m",
"nginx.ingress.kubernetes.io/limit-rpm": "120",
"nginx.ingress.kubernetes.io/x-forwarded-prefix": "/test",
"nginx.ingress.kubernetes.io/upstream-vhost": "upstream-vhost",
},
expected: ingressConfig{
SSLPassthrough: ptr.To(true),
@ -61,6 +63,8 @@ func Test_parseIngressConfig(t *testing.T) {
ProxyBuffersNumber: ptr.To(8),
ProxyMaxTempFileSize: ptr.To("100m"),
LimitRPM: ptr.To(120),
XForwardedPrefix: ptr.To("/test"),
UpstreamVhost: ptr.To("upstream-vhost"),
},
},
{

View File

@ -0,0 +1,22 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-x-forwarded-prefix-no-rewrite-target
namespace: default
annotations:
nginx.ingress.kubernetes.io/x-forwarded-prefix: "x-forwarded-prefix-header-value"
spec:
ingressClassName: nginx
rules:
- host: x-forwarded-prefix.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80

View File

@ -0,0 +1,71 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-x-forwarded-prefix
namespace: default
annotations:
nginx.ingress.kubernetes.io/x-forwarded-prefix: "x-forwarded-prefix-header-value"
nginx.ingress.kubernetes.io/rewrite-target: "/path"
spec:
ingressClassName: nginx
rules:
- host: x-forwarded-prefix.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-x-forwarded-prefix-regex
namespace: default
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: "$2"
nginx.ingress.kubernetes.io/x-forwarded-prefix: "$1"
spec:
ingressClassName: nginx
rules:
- host: x-forwarded-prefix-regex.localhost
http:
paths:
- path: (/something)(/.+)
pathType: ImplementationSpecific
backend:
service:
name: whoami
port:
number: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ingress-with-x-forwarded-prefix-three-groups
namespace: default
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: "/$3"
nginx.ingress.kubernetes.io/x-forwarded-prefix: "/$1/$2"
spec:
ingressClassName: nginx
rules:
- host: x-forwarded-prefix-three-groups.localhost
http:
paths:
- path: /(prefix)/(sub)/(.*)
pathType: ImplementationSpecific
backend:
service:
name: whoami
port:
number: 80

View File

@ -1433,28 +1433,41 @@ func (p *Provider) applyCustomHeaders(routerName string, ingressConfig ingressCo
return nil
}
// Validation identical to ingress-nginx.
var regexPathWithCapture = regexp.MustCompile(`^/?[-._~a-zA-Z0-9/$:]*$`)
func applyRewriteTargetConfiguration(rulePath, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) {
if ingressConfig.RewriteTarget == nil {
return
}
// Skip rewrite if the path is equal to the target.
if *ingressConfig.RewriteTarget == rulePath {
return
}
rewriteTargetMiddlewareName := routerName + "-rewrite-target"
rewriteTarget := &dynamic.RewriteTarget{
Replacement: *ingressConfig.RewriteTarget,
}
if ptr.Deref(ingressConfig.UseRegex, false) {
conf.HTTP.Middlewares[rewriteTargetMiddlewareName] = &dynamic.Middleware{
ReplacePathRegex: &dynamic.ReplacePathRegex{
Regex: rulePath,
Replacement: *ingressConfig.RewriteTarget,
},
}
} else {
conf.HTTP.Middlewares[rewriteTargetMiddlewareName] = &dynamic.Middleware{
ReplacePath: &dynamic.ReplacePath{
Path: *ingressConfig.RewriteTarget,
},
rewriteTarget.Regex = rulePath
}
if ingressConfig.XForwardedPrefix != nil {
if !regexPathWithCapture.MatchString(*ingressConfig.XForwardedPrefix) {
log.Error().Msgf("Invalid x-forwarded-prefix value %q for router %q, skipping x-forwarded-prefix configuration", *ingressConfig.XForwardedPrefix, routerName)
} else {
rewriteTarget.XForwardedPrefix = *ingressConfig.XForwardedPrefix
}
}
conf.HTTP.Middlewares[rewriteTargetMiddlewareName] = &dynamic.Middleware{
RewriteTarget: rewriteTarget,
}
rt.Middlewares = append(rt.Middlewares, rewriteTargetMiddlewareName)
}

View File

@ -1157,6 +1157,224 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "X-forwarded-prefix with missing rewrite-target",
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-x-forwarded-prefix-no-rewrite-target.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-x-forwarded-prefix-no-rewrite-target-rule-0-path-0": {
Rule: "Host(`x-forwarded-prefix.localhost`) && Path(`/`)",
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-x-forwarded-prefix-no-rewrite-target-rule-0-path-0-retry"},
Service: "default-ingress-with-x-forwarded-prefix-no-rewrite-target-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-x-forwarded-prefix-no-rewrite-target-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-x-forwarded-prefix-no-rewrite-target-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
ServersTransport: "default-ingress-with-x-forwarded-prefix-no-rewrite-target",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-x-forwarded-prefix-no-rewrite-target": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "X-forwarded-prefix",
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-x-forwarded-prefix.yml",
},
expected: &dynamic.Configuration{
TCP: &dynamic.TCPConfiguration{
Routers: map[string]*dynamic.TCPRouter{},
Services: map[string]*dynamic.TCPService{},
},
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-x-forwarded-prefix-rule-0-path-0": {
Rule: "Host(`x-forwarded-prefix.localhost`) && Path(`/`)",
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-x-forwarded-prefix-rule-0-path-0-rewrite-target", "default-ingress-with-x-forwarded-prefix-rule-0-path-0-retry"},
Service: "default-ingress-with-x-forwarded-prefix-whoami-80",
},
"default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0": {
Rule: "Host(`x-forwarded-prefix-regex.localhost`) && PathRegexp(`^(/something)(/.+)`)",
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0-rewrite-target", "default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0-retry"},
Service: "default-ingress-with-x-forwarded-prefix-regex-whoami-80",
},
"default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0": {
Rule: "Host(`x-forwarded-prefix-three-groups.localhost`) && PathRegexp(`^/(prefix)/(sub)/(.*)`)",
RuleSyntax: "default",
Middlewares: []string{"default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0-rewrite-target", "default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0-retry"},
Service: "default-ingress-with-x-forwarded-prefix-three-groups-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0-rewrite-target": {
RewriteTarget: &dynamic.RewriteTarget{
Regex: "/(prefix)/(sub)/(.*)",
Replacement: "/$3",
XForwardedPrefix: "/$1/$2",
},
},
"default-ingress-with-x-forwarded-prefix-three-groups-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
"default-ingress-with-x-forwarded-prefix-rule-0-path-0-rewrite-target": {
RewriteTarget: &dynamic.RewriteTarget{
Replacement: "/path",
XForwardedPrefix: "x-forwarded-prefix-header-value",
},
},
"default-ingress-with-x-forwarded-prefix-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
"default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0-rewrite-target": {
RewriteTarget: &dynamic.RewriteTarget{
Regex: "(/something)(/.+)",
Replacement: "$2",
XForwardedPrefix: "$1",
},
},
"default-ingress-with-x-forwarded-prefix-regex-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-x-forwarded-prefix-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
ServersTransport: "default-ingress-with-x-forwarded-prefix",
},
},
"default-ingress-with-x-forwarded-prefix-three-groups-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
ServersTransport: "default-ingress-with-x-forwarded-prefix-three-groups",
},
},
"default-ingress-with-x-forwarded-prefix-regex-whoami-80": {
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{
{
URL: "http://10.10.0.1:80",
},
{
URL: "http://10.10.0.2:80",
},
},
Strategy: "wrr",
PassHostHeader: ptr.To(true),
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
ServersTransport: "default-ingress-with-x-forwarded-prefix-regex",
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-x-forwarded-prefix": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
"default-ingress-with-x-forwarded-prefix-three-groups": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
"default-ingress-with-x-forwarded-prefix-regex": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Use Regex",
paths: []string{
@ -1242,7 +1460,7 @@ func TestLoadIngresses(t *testing.T) {
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-rewrite-target-rule-0-path-0-rewrite-target": {
ReplacePathRegex: &dynamic.ReplacePathRegex{
RewriteTarget: &dynamic.RewriteTarget{
Regex: "/something(/|$)(.*)",
Replacement: "/$2",
},
@ -1313,8 +1531,8 @@ func TestLoadIngresses(t *testing.T) {
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-rewrite-target-no-regex-rule-0-path-0-rewrite-target": {
ReplacePath: &dynamic.ReplacePath{
Path: "/rewritten",
RewriteTarget: &dynamic.RewriteTarget{
Replacement: "/rewritten",
},
},
"default-ingress-with-rewrite-target-no-regex-rule-0-path-0-retry": {

View File

@ -26,6 +26,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/headers"
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
"github.com/traefik/traefik/v3/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream"
"github.com/traefik/traefik/v3/pkg/middlewares/ingressnginx/rewritetarget"
"github.com/traefik/traefik/v3/pkg/middlewares/ingressnginx/snippet"
"github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist"
"github.com/traefik/traefik/v3/pkg/middlewares/ipwhitelist"
@ -340,6 +341,16 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
}
}
// RewriteTarget
if config.RewriteTarget != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return rewritetarget.New(ctx, next, *config.RewriteTarget, middlewareName)
}
}
// Retry
if config.Retry != nil {
if middleware != nil {