Add support for app-root nginx annotation
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Build and Publish Documentation / Doc Process (push) Waiting to run
Build experimental image on branch / build-webui (push) Waiting to run
Build experimental image on branch / Build experimental image on branch (push) Waiting to run

This commit is contained in:
LBF38 2026-01-26 17:44:04 +01:00 committed by GitHub
parent 27912e3849
commit a9c5a3828b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 167 additions and 6 deletions

View file

@ -316,6 +316,7 @@ The following annotations are organized by category for easier navigation.
| Annotation | Limitations / Notes | | Annotation | Limitations / Notes |
|-------------------------------------------------------|--------------------------------------------------------------------------------------------| |-------------------------------------------------------|--------------------------------------------------------------------------------------------|
| <a id="opt-nginx-ingress-kubernetes-ioapp-root" href="#opt-nginx-ingress-kubernetes-ioapp-root" title="#opt-nginx-ingress-kubernetes-ioapp-root">`nginx.ingress.kubernetes.io/app-root`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iouse-regex" href="#opt-nginx-ingress-kubernetes-iouse-regex" title="#opt-nginx-ingress-kubernetes-iouse-regex">`nginx.ingress.kubernetes.io/use-regex`</a> | | | <a id="opt-nginx-ingress-kubernetes-iouse-regex" href="#opt-nginx-ingress-kubernetes-iouse-regex" title="#opt-nginx-ingress-kubernetes-iouse-regex">`nginx.ingress.kubernetes.io/use-regex`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iorewrite-target" href="#opt-nginx-ingress-kubernetes-iorewrite-target" title="#opt-nginx-ingress-kubernetes-iorewrite-target">`nginx.ingress.kubernetes.io/rewrite-target`</a> | | | <a id="opt-nginx-ingress-kubernetes-iorewrite-target" href="#opt-nginx-ingress-kubernetes-iorewrite-target" title="#opt-nginx-ingress-kubernetes-iorewrite-target">`nginx.ingress.kubernetes.io/rewrite-target`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iopermanent-redirect" href="#opt-nginx-ingress-kubernetes-iopermanent-redirect" title="#opt-nginx-ingress-kubernetes-iopermanent-redirect">`nginx.ingress.kubernetes.io/permanent-redirect`</a> | Defaults to a 301 Moved Permanently status code. | | <a id="opt-nginx-ingress-kubernetes-iopermanent-redirect" href="#opt-nginx-ingress-kubernetes-iopermanent-redirect" title="#opt-nginx-ingress-kubernetes-iopermanent-redirect">`nginx.ingress.kubernetes.io/permanent-redirect`</a> | Defaults to a 301 Moved Permanently status code. |
@ -356,7 +357,6 @@ The following annotations are organized by category for easier navigation.
| Annotation | Notes | | Annotation | Notes |
|-----------------------------------------------------------------------------|------------------------------------------------------| |-----------------------------------------------------------------------------|------------------------------------------------------|
| <a id="opt-nginx-ingress-kubernetes-ioapp-root" href="#opt-nginx-ingress-kubernetes-ioapp-root" title="#opt-nginx-ingress-kubernetes-ioapp-root">`nginx.ingress.kubernetes.io/app-root`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior" href="#opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior" title="#opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior">`nginx.ingress.kubernetes.io/affinity-canary-behavior`</a> | | | <a id="opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior" href="#opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior" title="#opt-nginx-ingress-kubernetes-ioaffinity-canary-behavior">`nginx.ingress.kubernetes.io/affinity-canary-behavior`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-signin" href="#opt-nginx-ingress-kubernetes-ioauth-signin" title="#opt-nginx-ingress-kubernetes-ioauth-signin">`nginx.ingress.kubernetes.io/auth-signin`</a> | | | <a id="opt-nginx-ingress-kubernetes-ioauth-signin" href="#opt-nginx-ingress-kubernetes-ioauth-signin" title="#opt-nginx-ingress-kubernetes-ioauth-signin">`nginx.ingress.kubernetes.io/auth-signin`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-tls-secret" href="#opt-nginx-ingress-kubernetes-ioauth-tls-secret" title="#opt-nginx-ingress-kubernetes-ioauth-tls-secret">`nginx.ingress.kubernetes.io/auth-tls-secret`</a> | | | <a id="opt-nginx-ingress-kubernetes-ioauth-tls-secret" href="#opt-nginx-ingress-kubernetes-ioauth-tls-secret" title="#opt-nginx-ingress-kubernetes-ioauth-tls-secret">`nginx.ingress.kubernetes.io/auth-tls-secret`</a> | |

View file

@ -25,6 +25,7 @@ type ingressConfig struct {
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"` 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"` PermanentRedirect *string `annotation:"nginx.ingress.kubernetes.io/permanent-redirect"`
PermanentRedirectCode *int `annotation:"nginx.ingress.kubernetes.io/permanent-redirect-code"` PermanentRedirectCode *int `annotation:"nginx.ingress.kubernetes.io/permanent-redirect-code"`

View file

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

View file

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

View file

@ -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 { 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 { if err := p.applyBasicAuthConfiguration(namespace, routerKey, ingressConfig, rt, conf); err != nil {
return fmt.Errorf("applying basic auth configuration: %w", err) 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) 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) applyRedirect(routerKey, ingressConfig, rt, conf)
applyUpstreamVhost(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) 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 { func (p *Provider) applyBasicAuthConfiguration(namespace, routerName string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
if ingressConfig.AuthType == nil { if ingressConfig.AuthType == nil {
return nil return nil
@ -1125,7 +1143,7 @@ func (p *Provider) applySSLRedirectConfiguration(routerName string, ingressConfi
ForcePermanentRedirect: true, 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, // An Ingress that is not forcing sslRedirect and has no TLS configuration does not redirect,

View file

@ -809,6 +809,104 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{}, 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", desc: "Default Backend",
defaultBackendServiceName: "whoami", defaultBackendServiceName: "whoami",