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",