diff --git a/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md b/docs/content/reference/routing-configuration/kubernetes/ingress-nginx.md
index e1593c1d5..d354047de 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/app-root` | |
| `nginx.ingress.kubernetes.io/use-regex` | |
| `nginx.ingress.kubernetes.io/rewrite-target` | |
| `nginx.ingress.kubernetes.io/permanent-redirect` | Defaults to a 301 Moved Permanently status code. |
@@ -356,7 +357,6 @@ The following annotations are organized by category for easier navigation.
| Annotation | Notes |
|-----------------------------------------------------------------------------|------------------------------------------------------|
-| `nginx.ingress.kubernetes.io/app-root` | |
| `nginx.ingress.kubernetes.io/affinity-canary-behavior` | |
| `nginx.ingress.kubernetes.io/auth-signin` | |
| `nginx.ingress.kubernetes.io/auth-tls-secret` | |
diff --git a/pkg/provider/kubernetes/ingress-nginx/annotations.go b/pkg/provider/kubernetes/ingress-nginx/annotations.go
index c44c632dc..fbe3285ce 100644
--- a/pkg/provider/kubernetes/ingress-nginx/annotations.go
+++ b/pkg/provider/kubernetes/ingress-nginx/annotations.go
@@ -25,6 +25,7 @@ type ingressConfig struct {
UseRegex *bool `annotation:"nginx.ingress.kubernetes.io/use-regex"`
RewriteTarget *string `annotation:"nginx.ingress.kubernetes.io/rewrite-target"`
+ AppRoot *string `annotation:"nginx.ingress.kubernetes.io/app-root"`
PermanentRedirect *string `annotation:"nginx.ingress.kubernetes.io/permanent-redirect"`
PermanentRedirectCode *int `annotation:"nginx.ingress.kubernetes.io/permanent-redirect-code"`
diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/18-ingress-with-app-root-wrong.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/18-ingress-with-app-root-wrong.yml
new file mode 100644
index 000000000..72a6f927f
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/18-ingress-with-app-root-wrong.yml
@@ -0,0 +1,22 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-with-app-root
+ namespace: default
+ annotations:
+ nginx.ingress.kubernetes.io/app-root: foo
+
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: app-root.localhost
+ http:
+ paths:
+ - path: /bar
+ pathType: Prefix
+ backend:
+ service:
+ name: whoami
+ port:
+ number: 80
diff --git a/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/18-ingress-with-app-root.yml b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/18-ingress-with-app-root.yml
new file mode 100644
index 000000000..f72758013
--- /dev/null
+++ b/pkg/provider/kubernetes/ingress-nginx/fixtures/ingresses/18-ingress-with-app-root.yml
@@ -0,0 +1,22 @@
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ingress-with-app-root
+ namespace: default
+ annotations:
+ nginx.ingress.kubernetes.io/app-root: /foo
+
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: app-root.localhost
+ http:
+ paths:
+ - path: /bar
+ pathType: Prefix
+ 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 3c8834069..a95623ec6 100644
--- a/pkg/provider/kubernetes/ingress-nginx/kubernetes.go
+++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes.go
@@ -793,6 +793,12 @@ func (p *Provider) loadCertificates(ctx context.Context, ingress *netv1.Ingress,
}
func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath string, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) error {
+ applyAppRootConfiguration(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)
+
if err := p.applyBasicAuthConfiguration(namespace, routerKey, ingressConfig, rt, conf); err != nil {
return fmt.Errorf("applying basic auth configuration: %w", err)
}
@@ -807,10 +813,6 @@ func (p *Provider) applyMiddlewares(namespace, routerKey, rulePath string, ingre
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)
-
applyRedirect(routerKey, ingressConfig, rt, conf)
applyUpstreamVhost(routerKey, ingressConfig, rt, conf)
@@ -910,6 +912,22 @@ func applyRewriteTargetConfiguration(rulePath, routerName string, ingressConfig
rt.Middlewares = append(rt.Middlewares, rewriteTargetMiddlewareName)
}
+func applyAppRootConfiguration(routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) {
+ if ingressConfig.AppRoot == nil || !strings.HasPrefix(*ingressConfig.AppRoot, "/") {
+ return
+ }
+
+ appRootMiddlewareName := routerName + "-app-root"
+ conf.HTTP.Middlewares[appRootMiddlewareName] = &dynamic.Middleware{
+ RedirectRegex: &dynamic.RedirectRegex{
+ Regex: `^(https?://[^/]+)/$`,
+ Replacement: "$1" + *ingressConfig.AppRoot,
+ },
+ }
+
+ rt.Middlewares = append(rt.Middlewares, appRootMiddlewareName)
+}
+
func (p *Provider) applyBasicAuthConfiguration(namespace, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
if ingressConfig.AuthType == nil {
return nil
@@ -1125,7 +1143,7 @@ func (p *Provider) applySSLRedirectConfiguration(routerName string, ingressConfi
ForcePermanentRedirect: true,
},
}
- rt.Middlewares = append([]string{redirectMiddlewareName}, rt.Middlewares...)
+ rt.Middlewares = append(rt.Middlewares, redirectMiddlewareName)
}
// An Ingress that is not forcing sslRedirect and has no TLS configuration does not redirect,
diff --git a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
index b419b8db6..5170c38f8 100644
--- a/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
+++ b/pkg/provider/kubernetes/ingress-nginx/kubernetes_test.go
@@ -809,6 +809,104 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
+ {
+ desc: "App Root",
+ paths: []string{
+ "services.yml",
+ "ingressclasses.yml",
+ "ingresses/18-ingress-with-app-root.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-app-root-rule-0-path-0": {
+ Rule: "Host(`app-root.localhost`) && (Path(`/bar`) || PathPrefix(`/bar/`))",
+ RuleSyntax: "default",
+ Service: "default-ingress-with-app-root-whoami-80",
+ Middlewares: []string{"default-ingress-with-app-root-rule-0-path-0-app-root"},
+ },
+ },
+ Middlewares: map[string]*dynamic.Middleware{
+ "default-ingress-with-app-root-rule-0-path-0-app-root": {
+ RedirectRegex: &dynamic.RedirectRegex{
+ Regex: `^(https?://[^/]+)/$`,
+ Replacement: "$1/foo",
+ },
+ },
+ },
+ Services: map[string]*dynamic.Service{
+ "default-ingress-with-app-root-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: "App Root - no prefix slash",
+ paths: []string{
+ "services.yml",
+ "ingressclasses.yml",
+ "ingresses/18-ingress-with-app-root-wrong.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-app-root-rule-0-path-0": {
+ Rule: "Host(`app-root.localhost`) && (Path(`/bar`) || PathPrefix(`/bar/`))",
+ RuleSyntax: "default",
+ Service: "default-ingress-with-app-root-whoami-80",
+ },
+ },
+ Middlewares: map[string]*dynamic.Middleware{},
+ Services: map[string]*dynamic.Service{
+ "default-ingress-with-app-root-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",