From dc6d54532d57315e111093600c5a75ec2a594526 Mon Sep 17 00:00:00 2001 From: LBF38 Date: Tue, 13 Jan 2026 11:18:04 +0100 Subject: [PATCH] Add rewrite-target nginx annotations support Co-authored-by: Kevin Pollet --- .../kubernetes/ingress-nginx.md | 2 +- .../kubernetes/ingress-nginx/annotations.go | 3 +- .../11-ingress-with-rewrite-target.yml | 23 ++++++++ .../kubernetes/ingress-nginx/kubernetes.go | 30 ++++++++--- .../ingress-nginx/kubernetes_test.go | 53 +++++++++++++++++++ 5 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-rewrite-target.yml diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md index 412cdb5e5..31a11d6e6 100644 --- a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md +++ b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md @@ -316,6 +316,7 @@ The following annotations are organized by category for easier navigation. | Annotation | Limitations / Notes | |-------------------------------------------------------|--------------------------------------------------------------------------------------------| | `nginx.ingress.kubernetes.io/use-regex` | | +| `nginx.ingress.kubernetes.io/rewrite-target` | | ### IP Whitelist @@ -411,7 +412,6 @@ The following annotations are organized by category for easier navigation. | `nginx.ingress.kubernetes.io/proxy-ssl-verify-depth` | | | `nginx.ingress.kubernetes.io/proxy-ssl-protocols` | | | `nginx.ingress.kubernetes.io/enable-rewrite-log` | | -| `nginx.ingress.kubernetes.io/rewrite-target` | | | `nginx.ingress.kubernetes.io/satisfy` | | | `nginx.ingress.kubernetes.io/server-alias` | | | `nginx.ingress.kubernetes.io/server-snippet` | | diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go index 06edd65d9..60401399f 100644 --- a/pkg/provider/kubernetes/ingress-nginx/annotations.go +++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go @@ -23,7 +23,8 @@ type ingressConfig struct { SSLPassthrough *bool `annotation:"nginx.ingress.kubernetes.io/ssl-passthrough"` - UseRegex *bool `annotation:"nginx.ingress.kubernetes.io/use-regex"` + UseRegex *bool `annotation:"nginx.ingress.kubernetes.io/use-regex"` + RewriteTarget *string `annotation:"nginx.ingress.kubernetes.io/rewrite-target"` Affinity *string `annotation:"nginx.ingress.kubernetes.io/affinity"` SessionCookieName *string `annotation:"nginx.ingress.kubernetes.io/session-cookie-name"` diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-rewrite-target.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-rewrite-target.yml new file mode 100644 index 000000000..fd94661aa --- /dev/null +++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/11-ingress-with-rewrite-target.yml @@ -0,0 +1,23 @@ +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-with-rewrite-target + namespace: default + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 + +spec: + ingressClassName: nginx + rules: + - host: rewrite-target.localhost + http: + paths: + - path: /something(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: whoami + port: + number: 80 diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go index f9360fbbd..294206796 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go @@ -317,7 +317,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration Service: defaultBackendName, } - if err := p.applyMiddlewares(ingress.Namespace, defaultBackendName, ingressConfig, hasTLS, rt, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, defaultBackendName, "", ingressConfig, hasTLS, rt, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } @@ -332,7 +332,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration TLS: &dynamic.RouterTLSConfig{}, } - if err := p.applyMiddlewares(ingress.Namespace, defaultBackendTLSName, ingressConfig, false, rtTLS, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, defaultBackendTLSName, "", ingressConfig, false, rtTLS, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } @@ -409,7 +409,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration Service: key, } - if err := p.applyMiddlewares(ingress.Namespace, key, ingressConfig, hasTLS, rt, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, key, "", ingressConfig, hasTLS, rt, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } @@ -423,7 +423,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration TLS: &dynamic.RouterTLSConfig{}, } - if err := p.applyMiddlewares(ingress.Namespace, key+"-tls", ingressConfig, false, rtTLS, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, key+"-tls", "", ingressConfig, false, rtTLS, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } @@ -488,7 +488,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration conf.HTTP.ServersTransports[namedServersTransport.Name] = namedServersTransport.ServersTransport } - if err := p.applyMiddlewares(ingress.Namespace, routerKey, ingressConfig, hasTLS, rt, conf); err != nil { + if err := p.applyMiddlewares(ingress.Namespace, routerKey, pa.Path, ingressConfig, hasTLS, rt, conf); err != nil { logger.Error().Err(err).Msg("Error applying middlewares") } } @@ -788,7 +788,7 @@ func (p *Provider) loadCertificates(ctx context.Context, ingress *netv1.Ingress, return nil } -func (p *Provider) applyMiddlewares(namespace, routerKey string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) error { +func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) error { if err := p.applyBasicAuthConfiguration(namespace, routerKey, ingressConfig, rt, conf); err != nil { return fmt.Errorf("applying basic auth configuration: %w", err) } @@ -801,6 +801,8 @@ func (p *Provider) applyMiddlewares(namespace, routerKey string, ingressConfig i applyCORSConfiguration(routerKey, ingressConfig, rt, conf) + applyRewriteTargetConfiguration(rulePath, routerKey, ingressConfig, rt, conf) + // Apply SSL redirect is mandatory to be applied after all other middlewares. // TODO: check how to remove this, and create the HTTP router elsewhere. p.applySSLRedirectConfiguration(routerKey, ingressConfig, hasTLS, rt, conf) @@ -844,6 +846,22 @@ func (p *Provider) applyCustomHeaders(routerName string, ingressConfig ingressCo return nil } +func applyRewriteTargetConfiguration(rulePath, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) { + if ingressConfig.RewriteTarget == nil || !ptr.Deref(ingressConfig.UseRegex, false) { + return + } + + rewriteTargetMiddlewareName := routerName + "-rewrite-target" + conf.HTTP.Middlewares[rewriteTargetMiddlewareName] = &dynamic.Middleware{ + ReplacePathRegex: &dynamic.ReplacePathRegex{ + Regex: rulePath, + Replacement: *ingressConfig.RewriteTarget, + }, + } + + rt.Middlewares = append(rt.Middlewares, rewriteTargetMiddlewareName) +} + func (p *Provider) applyBasicAuthConfiguration(namespace, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error { if ingressConfig.AuthType == nil { return nil diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go index 88383f464..115693ca9 100644 --- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go +++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go @@ -754,6 +754,59 @@ func TestLoadIngresses(t *testing.T) { TLS: &dynamic.TLSConfiguration{}, }, }, + { + desc: "Rewrite Target", + paths: []string{ + "services.yml", + "ingressclasses.yml", + "ingresses/11-ingress-with-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-rewrite-target-rule-0-path-0": { + Rule: "Host(`rewrite-target.localhost`) && PathRegexp(`^/something(/|$)(.*)`)", + RuleSyntax: "default", + Service: "default-ingress-with-rewrite-target-whoami-80", + Middlewares: []string{"default-ingress-with-rewrite-target-rule-0-path-0-rewrite-target"}, + }, + }, + Middlewares: map[string]*dynamic.Middleware{ + "default-ingress-with-rewrite-target-rule-0-path-0-rewrite-target": { + ReplacePathRegex: &dynamic.ReplacePathRegex{ + Regex: "/something(/|$)(.*)", + Replacement: "/$2", + }, + }, + }, + Services: map[string]*dynamic.Service{ + "default-ingress-with-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, + }, + }, + }, + }, + ServersTransports: map[string]*dynamic.ServersTransport{}, + }, + TLS: &dynamic.TLSConfiguration{}, + }, + }, { desc: "Default Backend", defaultBackendServiceName: "whoami",