Add support for app-root nginx annotation

This commit is contained in:
LBF38
2026-01-26 17:44:04 +01:00
committed by GitHub
parent 27912e3849
commit a9c5a3828b
6 changed files with 167 additions and 6 deletions
@@ -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> | |
@@ -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"`
@@ -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
@@ -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
@@ -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,
@@ -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",