Implement server-snippet and configuration-snippet annotations

Co-authored-by: Kevin Pollet <pollet.kevin@gmail.com>
This commit is contained in:
Julien Salleyron 2026-03-04 10:24:05 +01:00 committed by GitHub
parent 6163601db0
commit d680fef7f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 4307 additions and 87 deletions

View File

@ -393,6 +393,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
| <a id="opt-providers-kubernetesingress-token" href="#opt-providers-kubernetesingress-token" title="#opt-providers-kubernetesingress-token">providers.kubernetesingress.token</a> | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
| <a id="opt-providers-kubernetesingressnginx" href="#opt-providers-kubernetesingressnginx" title="#opt-providers-kubernetesingressnginx">providers.kubernetesingressnginx</a> | Enables Kubernetes Ingress NGINX provider. | false |
| <a id="opt-providers-kubernetesingressnginx-allowcrossnamespaceresources" href="#opt-providers-kubernetesingressnginx-allowcrossnamespaceresources" title="#opt-providers-kubernetesingressnginx-allowcrossnamespaceresources">providers.kubernetesingressnginx.allowcrossnamespaceresources</a> | Allow Ingress to reference resources (e.g. ConfigMaps, Secrets) in different namespaces. | false |
| <a id="opt-providers-kubernetesingressnginx-allowsnippetannotations" href="#opt-providers-kubernetesingressnginx-allowsnippetannotations" title="#opt-providers-kubernetesingressnginx-allowsnippetannotations">providers.kubernetesingressnginx.allowsnippetannotations</a> | Enables to parse and add -snippet annotations/directives. | false |
| <a id="opt-providers-kubernetesingressnginx-certauthfilepath" href="#opt-providers-kubernetesingressnginx-certauthfilepath" title="#opt-providers-kubernetesingressnginx-certauthfilepath">providers.kubernetesingressnginx.certauthfilepath</a> | Kubernetes certificate authority file path (not needed for in-cluster client). | |
| <a id="opt-providers-kubernetesingressnginx-clientbodybuffersize" href="#opt-providers-kubernetesingressnginx-clientbodybuffersize" title="#opt-providers-kubernetesingressnginx-clientbodybuffersize">providers.kubernetesingressnginx.clientbodybuffersize</a> | Default buffer size for reading client request body. | 16384 |
| <a id="opt-providers-kubernetesingressnginx-controllerclass" href="#opt-providers-kubernetesingressnginx-controllerclass" title="#opt-providers-kubernetesingressnginx-controllerclass">providers.kubernetesingressnginx.controllerclass</a> | Ingress Class Controller value this controller satisfies. | k8s.io/ingress-nginx |

View File

@ -278,16 +278,16 @@ The following annotations are organized by category for easier navigation.
### Authentication
| Annotation | Limitations / Notes |
|-------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <a id="opt-nginx-ingress-kubernetes-ioauth-type" href="#opt-nginx-ingress-kubernetes-ioauth-type" title="#opt-nginx-ingress-kubernetes-ioauth-type">`nginx.ingress.kubernetes.io/auth-type`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-secret" href="#opt-nginx-ingress-kubernetes-ioauth-secret" title="#opt-nginx-ingress-kubernetes-ioauth-secret">`nginx.ingress.kubernetes.io/auth-secret`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-secret-type" href="#opt-nginx-ingress-kubernetes-ioauth-secret-type" title="#opt-nginx-ingress-kubernetes-ioauth-secret-type">`nginx.ingress.kubernetes.io/auth-secret-type`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-realm" href="#opt-nginx-ingress-kubernetes-ioauth-realm" title="#opt-nginx-ingress-kubernetes-ioauth-realm">`nginx.ingress.kubernetes.io/auth-realm`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-url" href="#opt-nginx-ingress-kubernetes-ioauth-url" title="#opt-nginx-ingress-kubernetes-ioauth-url">`nginx.ingress.kubernetes.io/auth-url`</a> | Only URL and response headers copy supported. Forward auth behaves differently than NGINX. It supports minimal variable interpolation by using the following NGINX variables: `$scheme`, `$host`, `$http_*`, `$best_http_host`, `$hostname`, `$request_uri`, `$escaped_request_uri`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`. |
| <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> | Redirects to signin URL on 401 response. It supports minimal variable interpolation by using the following NGINX variables: `$scheme`, `$host`, `$http_*`, `$best_http_host`, `$hostname`, `$request_uri`, `$escaped_request_uri`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`. |
| <a id="opt-nginx-ingress-kubernetes-ioauth-method" href="#opt-nginx-ingress-kubernetes-ioauth-method" title="#opt-nginx-ingress-kubernetes-ioauth-method">`nginx.ingress.kubernetes.io/auth-method`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-response-headers" href="#opt-nginx-ingress-kubernetes-ioauth-response-headers" title="#opt-nginx-ingress-kubernetes-ioauth-response-headers">`nginx.ingress.kubernetes.io/auth-response-headers`</a> | |
| Annotation | Limitations / Notes |
|-------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <a id="opt-nginx-ingress-kubernetes-ioauth-type" href="#opt-nginx-ingress-kubernetes-ioauth-type" title="#opt-nginx-ingress-kubernetes-ioauth-type">`nginx.ingress.kubernetes.io/auth-type`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-secret" href="#opt-nginx-ingress-kubernetes-ioauth-secret" title="#opt-nginx-ingress-kubernetes-ioauth-secret">`nginx.ingress.kubernetes.io/auth-secret`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-secret-type" href="#opt-nginx-ingress-kubernetes-ioauth-secret-type" title="#opt-nginx-ingress-kubernetes-ioauth-secret-type">`nginx.ingress.kubernetes.io/auth-secret-type`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-realm" href="#opt-nginx-ingress-kubernetes-ioauth-realm" title="#opt-nginx-ingress-kubernetes-ioauth-realm">`nginx.ingress.kubernetes.io/auth-realm`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-url" href="#opt-nginx-ingress-kubernetes-ioauth-url" title="#opt-nginx-ingress-kubernetes-ioauth-url">`nginx.ingress.kubernetes.io/auth-url`</a> | Only URL and response headers copy supported. Forward auth behaves differently than NGINX. It supports minimal variable interpolation by using the following NGINX variables: `$scheme`, `$host`, `$http_*`, `$hostname`, `$request_uri`, `$request_method`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`, `$uri`, `$document_uri`, `$server_name`, `$server_port`, `$content_type`, `$content_length`, `$cookie_*`, `$is_args`, `$best_http_host`, `$escaped_request_uri`, `$proxy_add_x_forwarded_for`. |
| <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> | Redirects to signin URL on 401 response. It supports minimal variable interpolation by using the following NGINX variables: `$scheme`, `$host`, `$http_*`, `$hostname`, `$request_uri`, `$request_method`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`, `$uri`, `$document_uri`, `$server_name`, `$server_port`, `$content_type`, `$content_length`, `$cookie_*`, `$is_args`, `$best_http_host`, `$escaped_request_uri`, `$proxy_add_x_forwarded_for`. |
| <a id="opt-nginx-ingress-kubernetes-ioauth-method" href="#opt-nginx-ingress-kubernetes-ioauth-method" title="#opt-nginx-ingress-kubernetes-ioauth-method">`nginx.ingress.kubernetes.io/auth-method`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioauth-response-headers" href="#opt-nginx-ingress-kubernetes-ioauth-response-headers" title="#opt-nginx-ingress-kubernetes-ioauth-response-headers">`nginx.ingress.kubernetes.io/auth-response-headers`</a> | |
### SSL/TLS
@ -345,18 +345,20 @@ The following annotations are organized by category for easier navigation.
### Routing
| 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-iofrom-to-www-redirect" href="#opt-nginx-ingress-kubernetes-iofrom-to-www-redirect" title="#opt-nginx-ingress-kubernetes-iofrom-to-www-redirect">`nginx.ingress.kubernetes.io/from-to-www-redirect`</a> | Doesn't support wildcard hosts. |
| <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-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-code" href="#opt-nginx-ingress-kubernetes-iopermanent-redirect-code" title="#opt-nginx-ingress-kubernetes-iopermanent-redirect-code">`nginx.ingress.kubernetes.io/permanent-redirect-code`</a> | Only valid 3XX HTTP Status Codes are accepted. |
| <a id="opt-nginx-ingress-kubernetes-iotemporal-redirect" href="#opt-nginx-ingress-kubernetes-iotemporal-redirect" title="#opt-nginx-ingress-kubernetes-iotemporal-redirect">`nginx.ingress.kubernetes.io/temporal-redirect`</a> | Takes precedence over the `permanent-redirect` annotation. Defaults to a 302 Found status code. |
| <a id="opt-nginx-ingress-kubernetes-iotemporal-redirect-code" href="#opt-nginx-ingress-kubernetes-iotemporal-redirect-code" title="#opt-nginx-ingress-kubernetes-iotemporal-redirect-code">`nginx.ingress.kubernetes.io/temporal-redirect-code`</a> | Only valid 3XX HTTP Status Codes are accepted. |
| 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-iofrom-to-www-redirect" href="#opt-nginx-ingress-kubernetes-iofrom-to-www-redirect" title="#opt-nginx-ingress-kubernetes-iofrom-to-www-redirect">`nginx.ingress.kubernetes.io/from-to-www-redirect`</a> | Doesn't support wildcard hosts. |
| <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-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-code" href="#opt-nginx-ingress-kubernetes-iopermanent-redirect-code" title="#opt-nginx-ingress-kubernetes-iopermanent-redirect-code">`nginx.ingress.kubernetes.io/permanent-redirect-code`</a> | Only valid 3XX HTTP Status Codes are accepted. |
| <a id="opt-nginx-ingress-kubernetes-iotemporal-redirect" href="#opt-nginx-ingress-kubernetes-iotemporal-redirect" title="#opt-nginx-ingress-kubernetes-iotemporal-redirect">`nginx.ingress.kubernetes.io/temporal-redirect`</a> | Takes precedence over the `permanent-redirect` annotation. Defaults to a 302 Found status code. |
| <a id="opt-nginx-ingress-kubernetes-iotemporal-redirect-code" href="#opt-nginx-ingress-kubernetes-iotemporal-redirect-code" title="#opt-nginx-ingress-kubernetes-iotemporal-redirect-code">`nginx.ingress.kubernetes.io/temporal-redirect-code`</a> | Only valid 3XX HTTP Status Codes are accepted. |
| <a id="opt-nginx-ingress-kubernetes-iocustom-http-errors" href="#opt-nginx-ingress-kubernetes-iocustom-http-errors" title="#opt-nginx-ingress-kubernetes-iocustom-http-errors">`nginx.ingress.kubernetes.io/custom-http-errors`</a> | Specifies a comma-separated list of HTTP status codes that should be intercepted and served by an error page backend. When any of these status codes occur, the request is forwarded to the global default backend, or to the backend defined by the [default-backend](#opt-nginx-ingress-kubernetes-iodefault-backend) annotation if specified. |
| <a id="opt-nginx-ingress-kubernetes-ioserver-alias" href="#opt-nginx-ingress-kubernetes-ioserver-alias" title="#opt-nginx-ingress-kubernetes-ioserver-alias">`nginx.ingress.kubernetes.io/server-alias`</a> | Ignored if the alias conflicts with an existing Ingress Host rule. Ingress Host rules always take precedence. |
| <a id="opt-nginx-ingress-kubernetes-ioserver-snippet" href="#opt-nginx-ingress-kubernetes-ioserver-snippet" title="#opt-nginx-ingress-kubernetes-ioserver-snippet">`nginx.ingress.kubernetes.io/server-snippet`</a> | Supported directives: `add_header`, `more_set_headers`, `proxy_set_header`, `more_set_input_headers`, `set`, `if`, `return code [text]`. It supports minimal variable interpolation by using the following NGINX variables: `$scheme`, `$host`, `$http_*`, `$hostname`, `$request_uri`, `$request_method`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`, `$uri`, `$document_uri`, `$server_name`, `$server_port`, `$content_type`, `$content_length`, `$cookie_*`, `$is_args`, `$best_http_host`, `$escaped_request_uri`, `$proxy_add_x_forwarded_for`. |
| <a id="opt-nginx-ingress-kubernetes-ioconfiguration-snippet" href="#opt-nginx-ingress-kubernetes-ioconfiguration-snippet" title="#opt-nginx-ingress-kubernetes-ioconfiguration-snippet">`nginx.ingress.kubernetes.io/configuration-snippet`</a> | Supported directives: `add_header`, `more_set_headers`, `proxy_set_header`, `more_set_input_headers`, `set`, `if`, `return code [text]`. It supports minimal variable interpolation by using the following NGINX variables: `$scheme`, `$host`, `$http_*`, `$hostname`, `$request_uri`, `$request_method`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`, `$uri`, `$document_uri`, `$server_name`, `$server_port`, `$content_type`, `$content_length`, `$cookie_*`, `$is_args`, `$best_http_host`, `$escaped_request_uri`, `$proxy_add_x_forwarded_for`. |
### IP Whitelist
@ -438,7 +440,6 @@ The following annotations are organized by category for easier navigation.
| <a id="opt-nginx-ingress-kubernetes-iocanary-by-cookie" href="#opt-nginx-ingress-kubernetes-iocanary-by-cookie" title="#opt-nginx-ingress-kubernetes-iocanary-by-cookie">`nginx.ingress.kubernetes.io/canary-by-cookie`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocanary-weight" href="#opt-nginx-ingress-kubernetes-iocanary-weight" title="#opt-nginx-ingress-kubernetes-iocanary-weight">`nginx.ingress.kubernetes.io/canary-weight`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iocanary-weight-total" href="#opt-nginx-ingress-kubernetes-iocanary-weight-total" title="#opt-nginx-ingress-kubernetes-iocanary-weight-total">`nginx.ingress.kubernetes.io/canary-weight-total`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioconfiguration-snippet" href="#opt-nginx-ingress-kubernetes-ioconfiguration-snippet" title="#opt-nginx-ingress-kubernetes-ioconfiguration-snippet">`nginx.ingress.kubernetes.io/configuration-snippet`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors" href="#opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors" title="#opt-nginx-ingress-kubernetes-iodisable-proxy-intercept-errors">`nginx.ingress.kubernetes.io/disable-proxy-intercept-errors`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate-after" href="#opt-nginx-ingress-kubernetes-iolimit-rate-after" title="#opt-nginx-ingress-kubernetes-iolimit-rate-after">`nginx.ingress.kubernetes.io/limit-rate-after`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iolimit-rate" href="#opt-nginx-ingress-kubernetes-iolimit-rate" title="#opt-nginx-ingress-kubernetes-iolimit-rate">`nginx.ingress.kubernetes.io/limit-rate`</a> | |
@ -461,7 +462,6 @@ The following annotations are organized by category for easier navigation.
| <a id="opt-nginx-ingress-kubernetes-ioproxy-ssl-protocols" href="#opt-nginx-ingress-kubernetes-ioproxy-ssl-protocols" title="#opt-nginx-ingress-kubernetes-ioproxy-ssl-protocols">`nginx.ingress.kubernetes.io/proxy-ssl-protocols`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioenable-rewrite-log" href="#opt-nginx-ingress-kubernetes-ioenable-rewrite-log" title="#opt-nginx-ingress-kubernetes-ioenable-rewrite-log">`nginx.ingress.kubernetes.io/enable-rewrite-log`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iosatisfy" href="#opt-nginx-ingress-kubernetes-iosatisfy" title="#opt-nginx-ingress-kubernetes-iosatisfy">`nginx.ingress.kubernetes.io/satisfy`</a> | |
| <a id="opt-nginx-ingress-kubernetes-ioserver-snippet" href="#opt-nginx-ingress-kubernetes-ioserver-snippet" title="#opt-nginx-ingress-kubernetes-ioserver-snippet">`nginx.ingress.kubernetes.io/server-snippet`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iosession-cookie-conditional-samesite-none" href="#opt-nginx-ingress-kubernetes-iosession-cookie-conditional-samesite-none" title="#opt-nginx-ingress-kubernetes-iosession-cookie-conditional-samesite-none">`nginx.ingress.kubernetes.io/session-cookie-conditional-samesite-none`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iosession-cookie-change-on-failure" href="#opt-nginx-ingress-kubernetes-iosession-cookie-change-on-failure" title="#opt-nginx-ingress-kubernetes-iosession-cookie-change-on-failure">`nginx.ingress.kubernetes.io/session-cookie-change-on-failure`</a> | |
| <a id="opt-nginx-ingress-kubernetes-iossl-ciphers" href="#opt-nginx-ingress-kubernetes-iossl-ciphers" title="#opt-nginx-ingress-kubernetes-iossl-ciphers">`nginx.ingress.kubernetes.io/ssl-ciphers`</a> | |

1
go.mod
View File

@ -74,6 +74,7 @@ require (
github.com/traefik/paerser v0.2.2
github.com/traefik/traefik/dynamic/ext v0.0.0-00010101000000-000000000000
github.com/traefik/yaegi v0.16.1
github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701 // latest tag is too old.
github.com/unrolled/render v1.0.2
github.com/unrolled/secure v1.0.9
github.com/valyala/fasthttp v1.58.0

8
go.sum
View File

@ -742,6 +742,8 @@ github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhK
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef h1:RC993DdTIHNItsyLj79fgZNLzrf9tBN0GR6W5ZPms6s=
github.com/imega/luaformatter v0.0.0-20211025140405-86b0a68d6bef/go.mod h1:i2XCfvmO94HrEOQWllihhtPrkvNfuB2R2p/o6+OVnRU=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/influxdata/influxdb-client-go/v2 v2.7.0 h1:QgP5mlBE9sGnzplpnf96pr+p7uqlIlL4W2GAP3n+XZg=
github.com/influxdata/influxdb-client-go/v2 v2.7.0/go.mod h1:Y/0W1+TZir7ypoQZYd2IrnVOKB3Tq6oegAQeSVN/+EU=
@ -1274,6 +1276,10 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/timtadh/data-structures v0.5.3 h1:F2tEjoG9qWIyUjbvXVgJqEOGJPMIiYn7U5W5mE+i/vQ=
github.com/timtadh/data-structures v0.5.3/go.mod h1:9R4XODhJ8JdWFEI8P/HJKqxuJctfBQw6fDibMQny2oU=
github.com/timtadh/lexmachine v0.2.2 h1:g55RnjdYazm5wnKv59pwFcBJHOyvTPfDEoz21s4PHmY=
github.com/timtadh/lexmachine v0.2.2/go.mod h1:GBJvD5OAfRn/gnp92zb9KTgHLB7akKyxmVivoYCcjQI=
github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
@ -1294,6 +1300,8 @@ github.com/traefik/yaegi v0.16.1 h1:f1De3DVJqIDKmnasUF6MwmWv1dSEEat0wcpXhD2On3E=
github.com/traefik/yaegi v0.16.1/go.mod h1:4eVhbPb3LnD2VigQjhYbEJ69vDRFdT2HQNrXx8eEwUY=
github.com/transip/gotransip/v6 v6.26.1 h1:MeqIjkTBBsZwWAK6giZyMkqLmKMclVHEuTNmoBdx4MA=
github.com/transip/gotransip/v6 v6.26.1/go.mod h1:x0/RWGRK/zob817O3tfO2xhFoP1vu8YOHORx6Jpk80s=
github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701 h1:JgeHIJzRSEdcuLXufZrni5+a4yDnBhQG+DdKhqCFhq0=
github.com/tufanbarisyildirim/gonginx v0.0.0-20250620092546-c3e307e36701/go.mod h1:ALbEe81QPWOZjDKCKNWodG2iqCMtregG8+ebQgjx2+4=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=

View File

@ -58,6 +58,7 @@ type Middleware struct {
// ingress-nginx middlewares.
AuthTLSPassCertificateToUpstream *AuthTLSPassCertificateToUpstream `json:"authTLSPassCertificateToUpstream,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
Snippet *Snippet `json:"snippet,omitempty" toml:"-" yaml:"-" label:"-" file:"-" kv:"-" export:"true"`
}
// +k8s:deepcopy-gen=true
@ -895,3 +896,11 @@ type URLRewrite struct {
Path *string `json:"path,omitempty"`
PathPrefix *string `json:"pathPrefix,omitempty"`
}
// +k8s:deepcopy-gen=true
// Snippet holds the NGINX snippet configuration.
type Snippet struct {
ServerSnippet string `json:"serverSnippet,omitempty"`
ConfigurationSnippet string `json:"configurationSnippet,omitempty"`
}

View File

@ -1112,6 +1112,11 @@ func (in *Middleware) DeepCopyInto(out *Middleware) {
*out = new(AuthTLSPassCertificateToUpstream)
(*in).DeepCopyInto(*out)
}
if in.Snippet != nil {
in, out := &in.Snippet, &out.Snippet
*out = new(Snippet)
**out = **in
}
return
}
@ -1810,6 +1815,22 @@ func (in *Service) DeepCopy() *Service {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Snippet) DeepCopyInto(out *Snippet) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Snippet.
func (in *Snippet) DeepCopy() *Snippet {
if in == nil {
return nil
}
out := new(Snippet)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SourceCriterion) DeepCopyInto(out *SourceCriterion) {
*out = *in

View File

@ -157,7 +157,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
address := fa.address
if fa.interpolate {
address = ingressnginx.ReplaceVariables(address, req)
address = ingressnginx.ReplaceVariables(address, req, nil)
}
forwardReqMethod := http.MethodGet
@ -268,7 +268,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// If the signin URL doesn't contain "rd=" parameter,
// add it with the original request URL to match the NGINX behavior.
if !strings.Contains(signinURL, "rd=") {
suffix := "rd=$scheme://$host$escaped_request_uri"
suffix := "rd=$scheme://$best_http_host$escaped_request_uri"
if !strings.Contains(signinURL, "?") {
signinURL += "?" + suffix
} else {
@ -276,7 +276,7 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}
signinURL = ingressnginx.ReplaceVariables(signinURL, req)
signinURL = ingressnginx.ReplaceVariables(signinURL, req, nil)
}
tracer.CaptureResponse(forwardSpan, forwardResponse.Header, http.StatusFound, trace.SpanKindClient)

View File

@ -2,8 +2,10 @@ package ingressnginx
import (
"fmt"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
@ -18,23 +20,39 @@ const (
httpHeaders = "$http_"
hostname = "$hostname"
requestURI = "$request_uri"
requestMethod = "$request_method"
queryString = "$query_string"
args = "$args"
arg = "$arg_"
remoteAddress = "$remote_addr"
uri = "$uri"
documentURI = "$document_uri"
serverName = "$server_name"
serverPort = "$server_port"
contentType = "$content_type"
contentLength = "$content_length"
cookie = "$cookie_"
isArgs = "$is_args"
// Variables set by ingress-nginx template.
bestHTTPHost = "$best_http_host"
escapedRequestURI = "$escaped_request_uri"
bestHTTPHost = "$best_http_host"
escapedRequestURI = "$escaped_request_uri"
proxyAddXForwardedFor = "$proxy_add_x_forwarded_for"
)
// varRegexp is a regular expression to match NGINX variables in the form of $variable or $variable_name.
var varRegexp = regexp.MustCompile(`\$[a-zA-Z_][a-zA-Z0-9_]*`)
// varRegexp is a regular expression to match NGINX variables in the form of $variable, $variable_name,
// or capture group references $1-$9.
var varRegexp = regexp.MustCompile(`\$[a-zA-Z_][a-zA-Z0-9_]*|\$[1-9]`)
// ReplaceVariables replaces NGINX variables in the given string with their corresponding values from the HTTP request.
func ReplaceVariables(str string, req *http.Request) string {
// Today this supports the `$scheme`, `$host`, `$http_*`, `$best_http_host`, `$hostname`, `$request_uri`,
// `$escaped_request_uri`, `$query_string`, `$args`, `$arg_*`, `$remote_addr`, `$request_method`,
// `$uri`, `$document_uri`, `$server_name`, `$server_port`, `$content_type`, `$content_length`,
// `$cookie_*`, `$is_args`, and `$proxy_add_x_forwarded_for` variables.
// Custom variables can be passed through the vars param.
func ReplaceVariables(str string, req *http.Request, vars map[string]string) string {
return varRegexp.ReplaceAllStringFunc(str, func(variable string) string {
val, err := variableValue(variable, req)
val, err := variableValue(variable, req, vars)
if err != nil {
log.Ctx(req.Context()).Debug().Err(err).Msgf("Error replacing variable: %s", variable)
return variable
@ -43,8 +61,8 @@ func ReplaceVariables(str string, req *http.Request) string {
})
}
// variableValue returns the value of the given NGINX variable based on the HTTP request.
func variableValue(variable string, req *http.Request) (string, error) {
// variableValue returns the value of the given NGINX variable based on the HTTP request and the custom vars map.
func variableValue(variable string, req *http.Request, vars map[string]string) (string, error) {
// $http_name variables are used to access HTTP headers in the request.
if header, ok := strings.CutPrefix(variable, httpHeaders); ok {
return strings.Join(req.Header.Values(strings.ReplaceAll(header, "_", "-")), ","), nil
@ -55,10 +73,34 @@ func variableValue(variable string, req *http.Request) (string, error) {
return req.URL.Query().Get(arg), nil
}
// $cookie_name variables are used to access cookie values in the request.
if name, ok := strings.CutPrefix(variable, cookie); ok {
c, _ := req.Cookie(name)
if c == nil {
return "", nil
}
return c.Value, nil
}
switch variable {
case host, hostname, bestHTTPHost:
case host:
// NGINX's $host returns the hostname without port, lowercased.
if hostOnly, _, err := net.SplitHostPort(req.Host); err == nil {
return strings.ToLower(hostOnly), nil
}
return strings.ToLower(req.Host), nil
case bestHTTPHost:
// ingress-nginx's $best_http_host preserves the port.
return req.Host, nil
case hostname:
h, err := os.Hostname()
if err != nil {
return "", fmt.Errorf("getting hostname: %w", err)
}
return h, nil
case requestURI:
return req.URL.RequestURI(), nil
@ -72,12 +114,66 @@ func variableValue(variable string, req *http.Request) (string, error) {
return "http", nil
case args, queryString:
return req.URL.Query().Encode(), nil
return req.URL.RawQuery, nil
case remoteAddress:
return req.RemoteAddr, nil
return stripPort(req.RemoteAddr), nil
case proxyAddXForwardedFor:
clientIP := stripPort(req.RemoteAddr)
if prior := req.Header.Get("X-Forwarded-For"); prior != "" {
return prior + ", " + clientIP, nil
}
return clientIP, nil
case requestMethod:
return req.Method, nil
case uri, documentURI:
return req.URL.Path, nil
case serverName:
if hostOnly, _, err := net.SplitHostPort(req.Host); err == nil {
return hostOnly, nil
}
return req.Host, nil
case serverPort:
if _, port, err := net.SplitHostPort(req.Host); err == nil {
return port, nil
}
if req.TLS != nil {
return "443", nil
}
return "80", nil
case contentType:
return req.Header.Get("Content-Type"), nil
case contentLength:
return req.Header.Get("Content-Length"), nil
case isArgs:
if req.URL.RawQuery != "" {
return "?", nil
}
return "", nil
default:
if value, ok := vars[variable]; ok {
return value, nil
}
return "", fmt.Errorf("unsupported variable: %s", variable)
}
}
// stripPort removes the port from a host:port address.
// It handles both IPv4 (192.168.1.1:8080) and IPv6 ([::1]:8080) formats.
func stripPort(addr string) string {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
return host
}

View File

@ -1,8 +1,10 @@
package ingressnginx
import (
"crypto/tls"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/require"
@ -13,6 +15,7 @@ func Test_ReplaceVariables(t *testing.T) {
desc string
src string
req *http.Request
vars map[string]string
expected string
}{
{
@ -28,10 +31,11 @@ func Test_ReplaceVariables(t *testing.T) {
expected: `val=baz.com`,
},
{
desc: "$hostname",
src: "val=$hostname",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo/bar?key=value&other=test", http.NoBody),
expected: `val=baz.com`,
desc: "$hostname",
src: "val=$hostname",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo/bar?key=value&other=test", http.NoBody),
// $hostname returns the machine hostname (os.Hostname), not the HTTP Host header.
expected: "val=" + mustHostname(t),
},
{
desc: "$http_*",
@ -59,13 +63,13 @@ func Test_ReplaceVariables(t *testing.T) {
desc: "$args",
src: "val=$args",
req: httptest.NewRequest(http.MethodGet, "http://baz.com?q=test&test=1&test=2&token=token_1234&val=foo,bar,baz", http.NoBody),
expected: `val=q=test&test=1&test=2&token=token_1234&val=foo%2Cbar%2Cbaz`,
expected: `val=q=test&test=1&test=2&token=token_1234&val=foo,bar,baz`,
},
{
desc: "$query_string",
src: "val=$query_string",
req: httptest.NewRequest(http.MethodGet, "http://baz.com?q=test&test=1&test=2&token=token_1234&val=foo,bar,baz", http.NoBody),
expected: `val=q=test&test=1&test=2&token=token_1234&val=foo%2Cbar%2Cbaz`,
expected: `val=q=test&test=1&test=2&token=token_1234&val=foo,bar,baz`,
},
{
desc: "$host && $escaped_request_uri",
@ -85,13 +89,178 @@ func Test_ReplaceVariables(t *testing.T) {
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo/bar?key=value&other=test", http.NoBody),
expected: "val=${invalid}",
},
{
desc: "$scheme http",
src: "val=$scheme",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo/bar", http.NoBody),
expected: "val=http",
},
{
desc: "$scheme https",
src: "val=$scheme",
req: mustNewRequestWithTLS(t, http.MethodGet, "https://baz.com/foo/bar"),
expected: "val=https",
},
{
desc: "$request_uri",
src: "val=$request_uri",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo/bar?key=value&other=test", http.NoBody),
expected: "val=/foo/bar?key=value&other=test",
},
{
desc: "$remote_addr",
src: "val=$remote_addr",
req: mustNewRequestWithRemoteAddr(t, http.MethodGet, "http://baz.com/foo/bar", "192.168.1.1:12345"),
expected: "val=192.168.1.1",
},
{
desc: "$remote_addr without port",
src: "val=$remote_addr",
req: mustNewRequestWithRemoteAddr(t, http.MethodGet, "http://baz.com/foo/bar", "192.168.1.1"),
expected: "val=192.168.1.1",
},
{
desc: "custom vars",
src: "val=$custom_var",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo/bar", http.NoBody),
vars: map[string]string{"$custom_var": "custom_value"},
expected: "val=custom_value",
},
{
desc: "$uri",
src: "val=$uri",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo/bar?key=value", http.NoBody),
expected: "val=/foo/bar",
},
{
desc: "$document_uri",
src: "val=$document_uri",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo/bar?key=value", http.NoBody),
expected: "val=/foo/bar",
},
{
desc: "$server_name with port",
src: "val=$server_name",
req: mustNewRequestWithHost(t, http.MethodGet, "http://baz.com:8080/foo", "baz.com:8080"),
expected: "val=baz.com",
},
{
desc: "$server_name without port",
src: "val=$server_name",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo", http.NoBody),
expected: "val=baz.com",
},
{
desc: "$server_port with explicit port",
src: "val=$server_port",
req: mustNewRequestWithHost(t, http.MethodGet, "http://baz.com:8080/foo", "baz.com:8080"),
expected: "val=8080",
},
{
desc: "$server_port without port http",
src: "val=$server_port",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo", http.NoBody),
expected: "val=80",
},
{
desc: "$server_port without port https",
src: "val=$server_port",
req: mustNewRequestWithTLS(t, http.MethodGet, "https://baz.com/foo"),
expected: "val=443",
},
{
desc: "$content_type",
src: "val=$content_type",
req: mustNewRequestWithHeaders(t, http.MethodGet, "http://baz.com/foo", map[string][]string{
"Content-Type": {"application/json"},
}),
expected: "val=application/json",
},
{
desc: "$content_length",
src: "val=$content_length",
req: mustNewRequestWithHeaders(t, http.MethodGet, "http://baz.com/foo", map[string][]string{
"Content-Length": {"42"},
}),
expected: "val=42",
},
{
desc: "$cookie_session",
src: "val=$cookie_session",
req: mustNewRequestWithCookie(t, http.MethodGet, "http://baz.com/foo", "session", "abc123"),
expected: "val=abc123",
},
{
desc: "$cookie_* not found",
src: "val=$cookie_missing",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo", http.NoBody),
expected: "val=",
},
{
desc: "$is_args with query string",
src: "val=$is_args",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo?key=value", http.NoBody),
expected: "val=?",
},
{
desc: "$is_args without query string",
src: "val=$is_args",
req: httptest.NewRequest(http.MethodGet, "http://baz.com/foo", http.NoBody),
expected: "val=",
},
{
desc: "$host strips port",
src: "val=$host",
req: mustNewRequestWithHost(t, http.MethodGet, "http://baz.com:8080/foo", "baz.com:8080"),
expected: "val=baz.com",
},
{
desc: "$host lowercased",
src: "val=$host",
req: mustNewRequestWithHost(t, http.MethodGet, "http://BAZ.COM/foo", "BAZ.COM"),
expected: "val=baz.com",
},
{
desc: "$best_http_host preserves port",
src: "val=$best_http_host",
req: mustNewRequestWithHost(t, http.MethodGet, "http://baz.com:8080/foo", "baz.com:8080"),
expected: "val=baz.com:8080",
},
{
desc: "$server_name with IPv6 and port",
src: "val=$server_name",
req: mustNewRequestWithHost(t, http.MethodGet, "http://[::1]:8080/foo", "[::1]:8080"),
expected: "val=::1",
},
{
desc: "$server_port with IPv6 and port",
src: "val=$server_port",
req: mustNewRequestWithHost(t, http.MethodGet, "http://[::1]:8080/foo", "[::1]:8080"),
expected: "val=8080",
},
{
desc: "$proxy_add_x_forwarded_for without existing header",
src: "val=$proxy_add_x_forwarded_for",
req: mustNewRequestWithRemoteAddr(t, http.MethodGet, "http://baz.com/foo", "192.168.1.1:12345"),
expected: "val=192.168.1.1",
},
{
desc: "$proxy_add_x_forwarded_for with existing header",
src: "val=$proxy_add_x_forwarded_for",
req: func() *http.Request {
r := mustNewRequestWithRemoteAddr(t, http.MethodGet, "http://baz.com/foo", "10.0.0.1:9999")
r.Header.Set("X-Forwarded-For", "203.0.113.50, 70.41.3.18")
return r
}(),
expected: "val=203.0.113.50, 70.41.3.18, 10.0.0.1",
},
}
for _, testCase := range testCases {
t.Run(testCase.desc, func(t *testing.T) {
t.Parallel()
got := ReplaceVariables(testCase.src, testCase.req)
got := ReplaceVariables(testCase.src, testCase.req, testCase.vars)
require.Equal(t, testCase.expected, got)
})
}
@ -105,3 +274,48 @@ func mustNewRequestWithHeaders(t *testing.T, method, target string, headers map[
return req
}
func mustNewRequestWithTLS(t *testing.T, method, target string) *http.Request {
t.Helper()
req := httptest.NewRequest(method, target, http.NoBody)
req.TLS = &tls.ConnectionState{}
return req
}
func mustNewRequestWithRemoteAddr(t *testing.T, method, target, remoteAddr string) *http.Request {
t.Helper()
req := httptest.NewRequest(method, target, http.NoBody)
req.RemoteAddr = remoteAddr
return req
}
func mustNewRequestWithHost(t *testing.T, method, target, host string) *http.Request {
t.Helper()
req := httptest.NewRequest(method, target, http.NoBody)
req.Host = host
return req
}
func mustNewRequestWithCookie(t *testing.T, method, target, cookieName, cookieValue string) *http.Request {
t.Helper()
req := httptest.NewRequest(method, target, http.NoBody)
req.AddCookie(&http.Cookie{Name: cookieName, Value: cookieValue})
return req
}
func mustHostname(t *testing.T) string {
t.Helper()
h, err := os.Hostname()
require.NoError(t, err)
return h
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,53 @@
package snippet
import (
"fmt"
"slices"
"github.com/tufanbarisyildirim/gonginx/config"
)
const (
contextServer = "server"
contextLocation = "location"
contextIf = "if"
contextIfInLocation = "if_in_location"
)
var directiveContexts = map[string][]string{
"add_header": {contextServer, contextLocation, contextIfInLocation},
"more_set_headers": {contextServer, contextLocation, contextIf},
"more_clear_headers": {contextServer, contextLocation, contextIf},
"proxy_set_header": {contextServer, contextLocation},
"more_set_input_headers": {contextServer, contextLocation, contextIf},
"more_clear_input_headers": {contextServer, contextLocation, contextIf},
"if": {contextServer, contextLocation},
"set": {contextServer, contextLocation, contextIf},
"return": {contextServer, contextLocation, contextIf},
"rewrite": {contextServer, contextLocation, contextIf},
"location": {contextServer},
"allow": {contextServer, contextLocation, contextIf},
"deny": {contextServer, contextLocation, contextIf},
"proxy_hide_header": {contextServer, contextLocation},
"expires": {contextServer, contextLocation, contextIfInLocation},
}
// isAllowedInContext checks if the directive is allowed in the context of its parent directive.
func isAllowedInContext(directive config.IDirective) error {
ctx := directive.GetParent().GetName()
allowedCtxs := directiveContexts[directive.GetName()]
if slices.Contains(allowedCtxs, ctx) {
return nil
}
if slices.Contains(allowedCtxs, contextIfInLocation) && ctx == contextIf {
// Here we are checking if the parent of the "if" directive is a "location" directive,
// which means that the "if" directive is inside a "location" block.
if directive.GetParent().GetParent().GetName() == contextLocation {
return nil
}
}
return fmt.Errorf("context %s is not valid for this directive %s: %+v ", ctx, directive.GetName(), allowedCtxs)
}

View File

@ -0,0 +1,232 @@
package snippet
import (
"bufio"
"context"
"errors"
"fmt"
"net"
"net/http"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/tufanbarisyildirim/gonginx/parser"
)
const typeName = "Snippet"
// Snippet is a middleware allowing to parse and interpret NGINX snippets containing directives.
type Snippet struct {
next http.Handler
name string
serverActions *actions
configurationActions *actions
}
// New creates a new Snippet middleware instance.
// It parses the provided snippets and builds the corresponding actions.
func New(ctx context.Context, next http.Handler, config *dynamic.Snippet, name string) (h http.Handler, err error) {
// Here we are adding a recover block as the snippet parsing can panic.
defer func() {
if recErr := recover(); recErr != nil {
err = fmt.Errorf("snippet parsing recover: %v", recErr)
}
}()
logger := middlewares.GetLogger(ctx, name, typeName)
logger.Debug().Msg("Creating middleware")
if config.ServerSnippet == "" && config.ConfigurationSnippet == "" {
return nil, errors.New("at least one of serverSnippet or configurationSnippet option must be provided")
}
parserOptions := []parser.Option{
parser.WithSkipComments(),
parser.WithCustomDirectives("more_set_headers", "more_set_input_headers", "more_clear_headers", "more_clear_input_headers", "proxy_hide_header"),
}
var serverActions *actions
if config.ServerSnippet != "" {
// Parse the snippet, note that we are wrapping the server snippet in a server block to ensure that it is parsed in the correct context.
p := parser.NewStringParser(fmt.Sprintf("server{%s}", config.ServerSnippet), parserOptions...)
conf, parseErr := p.Parse()
if parseErr != nil {
return nil, fmt.Errorf("parsing server-snippet: %w", parseErr)
}
serverActions, err = buildActions(conf.GetDirectives()[0].GetBlock())
if err != nil {
return nil, fmt.Errorf("building actions from server-snippet: %w", err)
}
}
var (
buildErr error
configurationActions *actions
)
if config.ConfigurationSnippet != "" {
// Parse the snippet, note that we are wrapping the configuration snippet in a location block to ensure that it is parsed in the correct context.
p := parser.NewStringParser(fmt.Sprintf("location / {%s}", config.ConfigurationSnippet), parserOptions...)
conf, parseErr := p.Parse()
if parseErr != nil {
return nil, fmt.Errorf("parsing configuration-snippet: %w", parseErr)
}
configurationActions, buildErr = buildActions(conf.GetDirectives()[0].GetBlock())
if buildErr != nil {
return nil, fmt.Errorf("building actions from configuration-snippet: %w", buildErr)
}
}
return &Snippet{
next: next,
name: name,
serverActions: serverActions,
configurationActions: configurationActions,
}, nil
}
func (s *Snippet) GetTracingInformation() (string, string) {
return s.name, typeName
}
func (s *Snippet) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
wrappedRW := &snippetResponseWriter{ResponseWriter: rw}
ctx := &actionContext{
vars: make(map[string]string),
nonMergeablePostActions: make(map[string][]action),
}
stop, err := s.serverActions.Execute(wrappedRW, req, ctx)
if err != nil {
http.Error(wrappedRW, err.Error(), http.StatusInternalServerError)
return
}
if stop {
if err = executePostActions(wrappedRW, req, ctx); err != nil {
http.Error(wrappedRW, err.Error(), http.StatusInternalServerError)
return
}
writeResponse(wrappedRW, req, ctx)
return
}
// In NGINX, proxy_set_header directives in the server snippet are ignored
// because the generated location block always contains proxy_set_header directives that override them.
ctx.nonMergeablePostActions["proxy_set_header"] = nil
// rewrite...break in the server snippet stops all directive processing,
// but post-actions (headers, etc.) and upstream forwarding still proceed.
if !ctx.stopAllDirectives {
ctx.stopCurrentBlock = false
stop, err = s.configurationActions.Execute(wrappedRW, req, ctx)
if err != nil {
http.Error(wrappedRW, err.Error(), http.StatusInternalServerError)
return
}
}
if err = executePostActions(wrappedRW, req, ctx); err != nil {
http.Error(wrappedRW, err.Error(), http.StatusInternalServerError)
return
}
if stop {
writeResponse(wrappedRW, req, ctx)
return
}
s.next.ServeHTTP(wrappedRW, req)
}
// snippetResponseWriter wraps http.ResponseWriter to intercept WriteHeader calls.
// This allows deferred response header operations (e.g., proxy_hide_header, conditional
// header setting with -s/-t flags) to be applied based on the actual response status
// code and content type set by the upstream.
type snippetResponseWriter struct {
http.ResponseWriter
headerWritten bool
onWriteHeader []func(code int, h http.Header)
}
func (w *snippetResponseWriter) WriteHeader(code int) {
if w.headerWritten {
return
}
w.headerWritten = true
for _, fn := range w.onWriteHeader {
fn(code, w.Header())
}
w.ResponseWriter.WriteHeader(code)
}
func (w *snippetResponseWriter) Write(b []byte) (int, error) {
if !w.headerWritten {
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(b)
}
// Unwrap returns the underlying ResponseWriter, enabling http.ResponseController
// to discover the underlying writer's capabilities (Flusher, Hijacker, etc.).
func (w *snippetResponseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
// Flush implements http.Flusher.
func (w *snippetResponseWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
// Hijack implements http.Hijacker.
func (w *snippetResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if h, ok := w.ResponseWriter.(http.Hijacker); ok {
return h.Hijack()
}
return nil, nil, fmt.Errorf("not a hijacker: %T", w.ResponseWriter)
}
// writeResponse writes the final response based on the action context.
// For redirect status codes (301, 302, 303, 307, 308) with a URL, it performs an HTTP redirect.
// For other status codes, it writes the status code and optional body text.
func writeResponse(rw http.ResponseWriter, req *http.Request, ctx *actionContext) {
if ctx.statusCode == 0 {
return
}
if ctx.redirectURL != "" {
http.Redirect(rw, req, ctx.redirectURL, ctx.statusCode)
return
}
rw.WriteHeader(ctx.statusCode)
if ctx.body != "" {
_, _ = rw.Write([]byte(ctx.body))
}
}
func executePostActions(rw http.ResponseWriter, req *http.Request, ctx *actionContext) error {
for _, postActions := range ctx.nonMergeablePostActions {
for _, postAction := range postActions {
if _, err := postAction(rw, req, ctx); err != nil {
return fmt.Errorf("executing non-mergeable configuration action: %w", err)
}
}
}
for _, postActions := range ctx.mergeablePostActions {
if _, err := postActions(rw, req, ctx); err != nil {
return fmt.Errorf("executing mergeable configuration action: %w", err)
}
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -97,6 +97,9 @@ type ingressConfig struct {
ProxyBuffersNumber *int `annotation:"nginx.ingress.kubernetes.io/proxy-buffers-number"`
// ProxyMaxTempFileSize sets the maximum size of a temporary file used to buffer responses.
ProxyMaxTempFileSize *string `annotation:"nginx.ingress.kubernetes.io/proxy-max-temp-file-size"`
ConfigurationSnippet *string `annotation:"nginx.ingress.kubernetes.io/configuration-snippet"`
ServerSnippet *string `annotation:"nginx.ingress.kubernetes.io/server-snippet"`
}
// parseIngressConfig parses the annotations from an Ingress object into an ingressConfig struct.

View File

@ -0,0 +1,25 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-with-both-snippets
namespace: default
annotations:
nginx.ingress.kubernetes.io/server-snippet: |
add_header X-Server-Snippet "server-value";
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Configuration-Snippet "configuration-value";
spec:
ingressClassName: nginx
rules:
- host: snippet.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80

View File

@ -0,0 +1,23 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-with-configuration-snippet
namespace: default
annotations:
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header X-Configuration-Snippet "configuration-value";
spec:
ingressClassName: nginx
rules:
- host: snippet.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80

View File

@ -0,0 +1,23 @@
---
kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
name: ingress-with-server-snippet
namespace: default
annotations:
nginx.ingress.kubernetes.io/server-snippet: |
add_header X-Server-Snippet "server-value";
spec:
ingressClassName: nginx
rules:
- host: snippet.localhost
http:
paths:
- path: /
pathType: Exact
backend:
service:
name: whoami
port:
number: 80

View File

@ -88,6 +88,12 @@ type certBlocks struct {
Certificate *tls.Certificate
}
type ingress struct {
*netv1.Ingress
IngressConfig ingressConfig
}
// Provider holds configurations of the provider.
type Provider struct {
Endpoint string `description:"Kubernetes server endpoint (required for external cluster client)." json:"endpoint,omitempty" toml:"endpoint,omitempty" yaml:"endpoint,omitempty"`
@ -129,6 +135,8 @@ type Provider struct {
AllowCrossNamespaceResources bool `description:"Allow Ingress to reference resources (e.g. ConfigMaps, Secrets) in different namespaces." json:"allowCrossNamespaceResources,omitempty" toml:"allowCrossNamespaceResources,omitempty" yaml:"allowCrossNamespaceResources,omitempty" export:"true"`
GlobalAllowedResponseHeaders []string `description:"List of allowed response headers inside the custom headers annotations." json:"globalAllowedResponseHeaders,omitempty" toml:"globalAllowedResponseHeaders,omitempty" yaml:"globalAllowedResponseHeaders,omitempty" export:"true"`
AllowSnippetAnnotations bool `description:"Enables to parse and add -snippet annotations/directives." json:"allowSnippetAnnotations,omitempty" toml:"allowSnippetAnnotations,omitempty" yaml:"allowSnippetAnnotations,omitempty" export:"true"`
// NonTLSEntryPoints contains the names of entrypoints that are configured without TLS.
NonTLSEntryPoints []string `json:"-" toml:"-" yaml:"-" label:"-" file:"-"`
@ -338,17 +346,31 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
}
ingressClasses = filterIngressClass(ics, p.IngressClassByName, p.IngressClass, p.ControllerClass)
ingresses := p.k8sClient.ListIngresses()
var ingresses []ingress
hosts := make(map[string]bool)
for _, ing := range ingresses {
serverSnippets := make(map[string]string)
for _, ing := range p.k8sClient.ListIngresses() {
if !p.shouldProcessIngress(ing, ingressClasses) {
continue
}
logger := log.Ctx(ctx).With().Str("ingress", ing.Name).Str("namespace", ing.Namespace).Logger()
ingressConfig := parseIngressConfig(ing)
for _, rule := range ing.Spec.Rules {
hosts[strings.ToLower(rule.Host)] = true
hosts[rule.Host] = true
if srvSnippet := ptr.Deref(ingressConfig.ServerSnippet, ""); srvSnippet != "" {
if serverSnippets[rule.Host] != "" {
logger.Debug().Msgf("Ignoring Server snippet because it is already defined for Host: %s", rule.Host)
} else {
serverSnippets[rule.Host] = srvSnippet
}
}
}
ingresses = append(ingresses, ingress{Ingress: ing, IngressConfig: ingressConfig})
}
uniqCerts := make(map[string]*tls.CertAndStores)
@ -357,31 +379,25 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger()
ctxIngress := logger.WithContext(ctx)
if !p.shouldProcessIngress(ingress, ingressClasses) {
continue
}
if err := p.updateIngressStatus(ingress); err != nil {
if err := p.updateIngressStatus(ingress.Ingress); err != nil {
logger.Error().Err(err).Msg("Error while updating ingress status")
}
var hasTLS bool
if len(ingress.Spec.TLS) > 0 {
hasTLS = true
if err := p.loadCertificates(ctxIngress, ingress, uniqCerts); err != nil {
if err := p.loadCertificates(ctxIngress, ingress.Ingress, uniqCerts); err != nil {
logger.Error().Err(err).Msg("Error configuring TLS")
continue
}
}
ingressConfig := parseIngressConfig(ingress)
var clientAuthTLSOptionName string
if ingressConfig.AuthTLSSecret != nil {
tlsOptName := provider.Normalize(ingress.Namespace + "-" + ingress.Name + "-" + *ingressConfig.AuthTLSSecret)
if ingress.IngressConfig.AuthTLSSecret != nil {
tlsOptName := provider.Normalize(ingress.Namespace + "-" + ingress.Name + "-" + *ingress.IngressConfig.AuthTLSSecret)
if _, exists := tlsOptions[tlsOptName]; !exists {
tlsOpt, err := p.buildClientAuthTLSOption(ingress.Namespace, ingressConfig)
tlsOpt, err := p.buildClientAuthTLSOption(ingress.Namespace, ingress.IngressConfig)
if err != nil {
logger.Error().Err(err).Msg("Error configuring client auth TLS")
continue
@ -393,7 +409,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
clientAuthTLSOptionName = tlsOptName
}
namedServersTransport, err := p.buildServersTransport(ctxIngress, ingress.Namespace, ingress.Name, ingressConfig)
namedServersTransport, err := p.buildServersTransport(ctxIngress, ingress.Namespace, ingress.Name, ingress.IngressConfig)
if err != nil {
logger.Error().Err(err).Msg("Ignoring Ingress cannot create proxy SSL configuration")
continue
@ -402,7 +418,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
var defaultBackendService *dynamic.Service
if ingress.Spec.DefaultBackend != nil && ingress.Spec.DefaultBackend.Service != nil {
var err error
defaultBackendService, err = p.buildService(ingress.Namespace, *ingress.Spec.DefaultBackend, ingressConfig)
defaultBackendService, err = p.buildService(ingress.Namespace, *ingress.Spec.DefaultBackend, ingress.IngressConfig)
if err != nil {
logger.Error().
Str("serviceName", ingress.Spec.DefaultBackend.Service.Name).
@ -421,7 +437,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
Service: defaultBackendName,
}
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, defaultBackendName, "", "", ingress.Spec.DefaultBackend, hosts, ingressConfig, hasTLS, rt, conf); err != nil {
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, defaultBackendName, "", "", ingress.Spec.DefaultBackend, hosts, ingress.IngressConfig, hasTLS, rt, conf, ""); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
@ -439,7 +455,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
rtTLS.TLS.Options = clientAuthTLSOptionName
}
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, defaultBackendTLSName, "", "", ingress.Spec.DefaultBackend, hosts, ingressConfig, false, rtTLS, conf); err != nil {
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, defaultBackendTLSName, "", "", ingress.Spec.DefaultBackend, hosts, ingress.IngressConfig, false, rtTLS, conf, ""); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
@ -453,7 +469,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
}
for ri, rule := range ingress.Spec.Rules {
if ptr.Deref(ingressConfig.SSLPassthrough, false) {
if ptr.Deref(ingress.IngressConfig.SSLPassthrough, false) {
if rule.Host == "" {
logger.Error().Err(err).Msg("Cannot process ssl-passthrough for rule without host")
continue
@ -477,7 +493,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
continue
}
service, err := p.buildPassthroughService(ingress.Namespace, *backend, ingressConfig)
service, err := p.buildPassthroughService(ingress.Namespace, *backend, ingress.IngressConfig)
if err != nil {
logger.Error().Err(err).Msgf("Cannot create passthrough service for %s", backend.Service.Name)
continue
@ -516,7 +532,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
Service: key,
}
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, key, "", "", ingress.Spec.DefaultBackend, hosts, ingressConfig, hasTLS, rt, conf); err != nil {
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, key, "", "", ingress.Spec.DefaultBackend, hosts, ingress.IngressConfig, hasTLS, rt, conf, serverSnippets[rule.Host]); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
@ -533,7 +549,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
rtTLS.TLS.Options = clientAuthTLSOptionName
}
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, key+"-tls", "", "", ingress.Spec.DefaultBackend, hosts, ingressConfig, false, rtTLS, conf); err != nil {
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, key+"-tls", "", "", ingress.Spec.DefaultBackend, hosts, ingress.IngressConfig, false, rtTLS, conf, serverSnippets[rule.Host]); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
@ -568,7 +584,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
// TODO: if no service, do not add middlewares and 503.
serviceName := provider.Normalize(ingress.Namespace + "-" + ingress.Name + "-" + pa.Backend.Service.Name + "-" + portString)
service, err := p.buildService(ingress.Namespace, pa.Backend, ingressConfig)
service, err := p.buildService(ingress.Namespace, pa.Backend, ingress.IngressConfig)
if err != nil {
logger.Error().
Str("serviceName", pa.Backend.Service.Name).
@ -579,7 +595,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
}
rt := &dynamic.Router{
Rule: buildRule(ctxIngress, rule.Host, pa, ingressConfig, hosts),
Rule: buildRule(ctxIngress, rule.Host, pa, ingress.IngressConfig, hosts),
// "default" stands for the default rule syntax in Traefik v3, i.e. the v3 syntax.
RuleSyntax: "default",
Service: serviceName,
@ -602,7 +618,7 @@ func (p *Provider) loadConfiguration(ctx context.Context) *dynamic.Configuration
conf.HTTP.ServersTransports[namedServersTransport.Name] = namedServersTransport.ServersTransport
}
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, routerKey, pa.Path, rule.Host, &pa.Backend, hosts, ingressConfig, hasTLS, rt, conf); err != nil {
if err := p.applyMiddlewares(ingress.Namespace, ingress.Name, routerKey, pa.Path, rule.Host, &pa.Backend, hosts, ingress.IngressConfig, hasTLS, rt, conf, serverSnippets[rule.Host]); err != nil {
logger.Error().Err(err).Msg("Error applying middlewares")
}
}
@ -968,12 +984,11 @@ func (p *Provider) loadCertificates(ctx context.Context, ingress *netv1.Ingress,
return nil
}
func (p *Provider) applyMiddlewares(namespace, ingressName, routerKey, rulePath, ruleHost string, backend *netv1.IngressBackend, hosts map[string]bool, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration) error {
func (p *Provider) applyMiddlewares(namespace, ingressName, routerKey, rulePath, ruleHost string, backend *netv1.IngressBackend, hosts map[string]bool, ingressConfig ingressConfig, hasTLS bool, rt *dynamic.Router, conf *dynamic.Configuration, serverSnippet string) error {
err := p.applyCustomHTTPErrors(namespace, ingressName, routerKey, backend, ingressConfig, rt, conf)
if err != nil {
return err
}
applyAppRootConfiguration(routerKey, ingressConfig, rt, conf)
applyFromToWwwRedirect(hosts, ruleHost, routerKey, ingressConfig, rt, conf)
applyRedirect(routerKey, ingressConfig, rt, conf)
@ -1010,11 +1025,38 @@ func (p *Provider) applyMiddlewares(namespace, ingressName, routerKey, rulePath,
return fmt.Errorf("applying custom headers: %w", err)
}
if err := p.applySnippets(routerKey, serverSnippet, ingressConfig, rt, conf); err != nil {
return fmt.Errorf("applying snippets: %w", err)
}
p.applyRetry(routerKey, ingressConfig, rt, conf)
return nil
}
func (p *Provider) applySnippets(routerName, serverSnippet string, ingressConfig ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
configurationSnippet := ptr.Deref(ingressConfig.ConfigurationSnippet, "")
if serverSnippet == "" && configurationSnippet == "" {
return nil
}
if !p.AllowSnippetAnnotations {
return errors.New("snippet annotations are not allowed")
}
snippetMiddlewareName := routerName + "-snippet"
conf.HTTP.Middlewares[snippetMiddlewareName] = &dynamic.Middleware{
Snippet: &dynamic.Snippet{
ServerSnippet: serverSnippet,
ConfigurationSnippet: configurationSnippet,
},
}
rt.Middlewares = append(rt.Middlewares, snippetMiddlewareName)
return nil
}
func (p *Provider) applyCustomHTTPErrors(namespace, ingressName, routerName string, targetedService *netv1.IngressBackend, config ingressConfig, rt *dynamic.Router, conf *dynamic.Configuration) error {
customHTTPErrors := ptr.Deref(config.CustomHTTPErrors, p.CustomHTTPErrors)
if len(customHTTPErrors) == 0 {
@ -1210,7 +1252,6 @@ func applyFromToWwwRedirect(hosts map[string]bool, ruleHost, routerName string,
return
}
ruleHost = strings.ToLower(ruleHost)
wwwType := strings.HasPrefix(ruleHost, "www.")
wildcardType := strings.HasPrefix(ruleHost, "*.")
bypass := wwwType && hosts[strings.TrimPrefix(ruleHost, "www.")] || !wwwType && hosts["www."+ruleHost] || wildcardType

View File

@ -30,6 +30,7 @@ func TestLoadIngresses(t *testing.T) {
defaultBackendServiceNamespace string
allowCrossNamespaceResources bool
globalAllowedResponseHeaders []string
allowSnippetAnnotations bool
paths []string
expected *dynamic.Configuration
}{
@ -1301,10 +1302,13 @@ func TestLoadIngresses(t *testing.T) {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-rewrite-target-no-regex-rule-0-path-0": {
Rule: "Host(`rewrite-target-no-regex.localhost`) && Path(`/original`)",
RuleSyntax: "default",
Service: "default-ingress-with-rewrite-target-no-regex-whoami-80",
Middlewares: []string{"default-ingress-with-rewrite-target-no-regex-rule-0-path-0-rewrite-target", "default-ingress-with-rewrite-target-no-regex-rule-0-path-0-retry"},
Rule: "Host(`rewrite-target-no-regex.localhost`) && Path(`/original`)",
RuleSyntax: "default",
Service: "default-ingress-with-rewrite-target-no-regex-whoami-80",
Middlewares: []string{
"default-ingress-with-rewrite-target-no-regex-rule-0-path-0-rewrite-target",
"default-ingress-with-rewrite-target-no-regex-rule-0-path-0-retry",
},
},
},
Middlewares: map[string]*dynamic.Middleware{
@ -1314,7 +1318,9 @@ func TestLoadIngresses(t *testing.T) {
},
},
"default-ingress-with-rewrite-target-no-regex-rule-0-path-0-retry": {
Retry: &dynamic.Retry{Attempts: 3},
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
@ -3205,10 +3211,13 @@ func TestLoadIngresses(t *testing.T) {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-custom-http-errors-and-default-backend-rule-0-path-0": {
Rule: "Host(`whoami.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-custom-http-errors-and-default-backend-whoami-80",
Middlewares: []string{"default-ingress-with-custom-http-errors-and-default-backend-rule-0-path-0-custom-http-errors", "default-ingress-with-custom-http-errors-and-default-backend-rule-0-path-0-retry"},
Rule: "Host(`whoami.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-custom-http-errors-and-default-backend-whoami-80",
Middlewares: []string{
"default-ingress-with-custom-http-errors-and-default-backend-rule-0-path-0-custom-http-errors",
"default-ingress-with-custom-http-errors-and-default-backend-rule-0-path-0-retry",
},
},
},
Middlewares: map[string]*dynamic.Middleware{
@ -3225,7 +3234,9 @@ func TestLoadIngresses(t *testing.T) {
},
},
"default-ingress-with-custom-http-errors-and-default-backend-rule-0-path-0-retry": {
Retry: &dynamic.Retry{Attempts: 3},
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
@ -3296,10 +3307,13 @@ func TestLoadIngresses(t *testing.T) {
HTTP: &dynamic.HTTPConfiguration{
Routers: map[string]*dynamic.Router{
"default-ingress-with-custom-http-errors-rule-0-path-0": {
Rule: "Host(`whoami.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-custom-http-errors-whoami-80",
Middlewares: []string{"default-ingress-with-custom-http-errors-rule-0-path-0-custom-http-errors", "default-ingress-with-custom-http-errors-rule-0-path-0-retry"},
Rule: "Host(`whoami.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-custom-http-errors-whoami-80",
Middlewares: []string{
"default-ingress-with-custom-http-errors-rule-0-path-0-custom-http-errors",
"default-ingress-with-custom-http-errors-rule-0-path-0-retry",
},
},
"default-backend": {
Rule: "PathPrefix(`/`)",
@ -3329,7 +3343,9 @@ func TestLoadIngresses(t *testing.T) {
},
},
"default-ingress-with-custom-http-errors-rule-0-path-0-retry": {
Retry: &dynamic.Retry{Attempts: 3},
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
@ -3466,7 +3482,9 @@ func TestLoadIngresses(t *testing.T) {
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-default-backend-annotation-rule-0-path-0-retry": {
Retry: &dynamic.Retry{Attempts: 3},
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
@ -3997,6 +4015,713 @@ func TestLoadIngresses(t *testing.T) {
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Server snippet with allowSnippetAnnotations enabled",
allowSnippetAnnotations: true,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-server-snippet.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-server-snippet-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-server-snippet-whoami-80",
Middlewares: []string{
"default-ingress-with-server-snippet-rule-0-path-0-snippet",
"default-ingress-with-server-snippet-rule-0-path-0-retry",
},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-server-snippet-rule-0-path-0-snippet": {
Snippet: &dynamic.Snippet{
ServerSnippet: "add_header X-Server-Snippet \"server-value\";\n",
},
},
"default-ingress-with-server-snippet-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-server-snippet-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),
ServersTransport: "default-ingress-with-server-snippet",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-server-snippet": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Configuration snippet with allowSnippetAnnotations enabled",
allowSnippetAnnotations: true,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-configuration-snippet.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-configuration-snippet-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-configuration-snippet-whoami-80",
Middlewares: []string{
"default-ingress-with-configuration-snippet-rule-0-path-0-snippet",
"default-ingress-with-configuration-snippet-rule-0-path-0-retry",
},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-configuration-snippet-rule-0-path-0-snippet": {
Snippet: &dynamic.Snippet{
ConfigurationSnippet: "add_header X-Configuration-Snippet \"configuration-value\";\n",
},
},
"default-ingress-with-configuration-snippet-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-configuration-snippet-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),
ServersTransport: "default-ingress-with-configuration-snippet",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-configuration-snippet": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Both snippets with allowSnippetAnnotations enabled",
allowSnippetAnnotations: true,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-both-snippets.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-both-snippets-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-both-snippets-whoami-80",
Middlewares: []string{
"default-ingress-with-both-snippets-rule-0-path-0-snippet",
"default-ingress-with-both-snippets-rule-0-path-0-retry",
},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-both-snippets-rule-0-path-0-snippet": {
Snippet: &dynamic.Snippet{
ServerSnippet: "add_header X-Server-Snippet \"server-value\";\n",
ConfigurationSnippet: "add_header X-Configuration-Snippet \"configuration-value\";\n",
},
},
"default-ingress-with-both-snippets-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-both-snippets-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),
ServersTransport: "default-ingress-with-both-snippets",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-both-snippets": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Server snippet with allowSnippetAnnotations disabled",
allowSnippetAnnotations: false,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-server-snippet.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-server-snippet-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-server-snippet-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-ingress-with-server-snippet-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),
ServersTransport: "default-ingress-with-server-snippet",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-server-snippet": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Configuration snippet with allowSnippetAnnotations disabled",
allowSnippetAnnotations: false,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-configuration-snippet.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-configuration-snippet-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-configuration-snippet-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-ingress-with-configuration-snippet-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),
ServersTransport: "default-ingress-with-configuration-snippet",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-configuration-snippet": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Both snippets with allowSnippetAnnotations disabled",
allowSnippetAnnotations: false,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-both-snippets.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-both-snippets-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-both-snippets-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-ingress-with-both-snippets-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),
ServersTransport: "default-ingress-with-both-snippets",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-both-snippets": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Server snippet with allowSnippetAnnotations enabled",
allowSnippetAnnotations: true,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-server-snippet.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-server-snippet-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-server-snippet-whoami-80",
Middlewares: []string{"default-ingress-with-server-snippet-rule-0-path-0-snippet", "default-ingress-with-server-snippet-rule-0-path-0-retry"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-server-snippet-rule-0-path-0-snippet": {
Snippet: &dynamic.Snippet{
ServerSnippet: "add_header X-Server-Snippet \"server-value\";\n",
},
},
"default-ingress-with-server-snippet-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-server-snippet-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),
ServersTransport: "default-ingress-with-server-snippet",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-server-snippet": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Configuration snippet with allowSnippetAnnotations enabled",
allowSnippetAnnotations: true,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-configuration-snippet.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-configuration-snippet-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-configuration-snippet-whoami-80",
Middlewares: []string{"default-ingress-with-configuration-snippet-rule-0-path-0-snippet", "default-ingress-with-configuration-snippet-rule-0-path-0-retry"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-configuration-snippet-rule-0-path-0-snippet": {
Snippet: &dynamic.Snippet{
ConfigurationSnippet: "add_header X-Configuration-Snippet \"configuration-value\";\n",
},
},
"default-ingress-with-configuration-snippet-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-configuration-snippet-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),
ServersTransport: "default-ingress-with-configuration-snippet",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-configuration-snippet": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Both snippets with allowSnippetAnnotations enabled",
allowSnippetAnnotations: true,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-both-snippets.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-both-snippets-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-both-snippets-whoami-80",
Middlewares: []string{"default-ingress-with-both-snippets-rule-0-path-0-snippet", "default-ingress-with-both-snippets-rule-0-path-0-retry"},
},
},
Middlewares: map[string]*dynamic.Middleware{
"default-ingress-with-both-snippets-rule-0-path-0-snippet": {
Snippet: &dynamic.Snippet{
ServerSnippet: "add_header X-Server-Snippet \"server-value\";\n",
ConfigurationSnippet: "add_header X-Configuration-Snippet \"configuration-value\";\n",
},
},
"default-ingress-with-both-snippets-rule-0-path-0-retry": {
Retry: &dynamic.Retry{
Attempts: 3,
},
},
},
Services: map[string]*dynamic.Service{
"default-ingress-with-both-snippets-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),
ServersTransport: "default-ingress-with-both-snippets",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-both-snippets": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Server snippet with allowSnippetAnnotations disabled",
allowSnippetAnnotations: false,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-server-snippet.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-server-snippet-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-server-snippet-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-ingress-with-server-snippet-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),
ServersTransport: "default-ingress-with-server-snippet",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-server-snippet": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Configuration snippet with allowSnippetAnnotations disabled",
allowSnippetAnnotations: false,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-configuration-snippet.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-configuration-snippet-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-configuration-snippet-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-ingress-with-configuration-snippet-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),
ServersTransport: "default-ingress-with-configuration-snippet",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-configuration-snippet": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Both snippets with allowSnippetAnnotations disabled",
allowSnippetAnnotations: false,
paths: []string{
"services.yml",
"ingressclasses.yml",
"ingresses/ingress-with-both-snippets.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-both-snippets-rule-0-path-0": {
Rule: "Host(`snippet.localhost`) && Path(`/`)",
RuleSyntax: "default",
Service: "default-ingress-with-both-snippets-whoami-80",
},
},
Middlewares: map[string]*dynamic.Middleware{},
Services: map[string]*dynamic.Service{
"default-ingress-with-both-snippets-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),
ServersTransport: "default-ingress-with-both-snippets",
ResponseForwarding: &dynamic.ResponseForwarding{
FlushInterval: dynamic.DefaultFlushInterval,
},
},
},
},
ServersTransports: map[string]*dynamic.ServersTransport{
"default-ingress-with-both-snippets": {
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
DialTimeout: ptypes.Duration(60 * time.Second),
ReadTimeout: ptypes.Duration(60 * time.Second),
WriteTimeout: ptypes.Duration(60 * time.Second),
IdleConnTimeout: ptypes.Duration(60 * time.Second),
},
},
},
},
TLS: &dynamic.TLSConfiguration{},
},
},
{
desc: "Auth TLS pass certificate to upstream",
paths: []string{
@ -4645,6 +5370,7 @@ func TestLoadIngresses(t *testing.T) {
k8sClient: client,
defaultBackendServiceName: test.defaultBackendServiceName,
defaultBackendServiceNamespace: test.defaultBackendServiceNamespace,
AllowSnippetAnnotations: test.allowSnippetAnnotations,
NonTLSEntryPoints: []string{"web"},
allowedHeaders: test.globalAllowedResponseHeaders,
AllowCrossNamespaceResources: test.allowCrossNamespaceResources,

View File

@ -26,6 +26,7 @@ import (
"github.com/traefik/traefik/v3/pkg/middlewares/headers"
"github.com/traefik/traefik/v3/pkg/middlewares/inflightreq"
"github.com/traefik/traefik/v3/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream"
"github.com/traefik/traefik/v3/pkg/middlewares/ingressnginx/snippet"
"github.com/traefik/traefik/v3/pkg/middlewares/ipallowlist"
"github.com/traefik/traefik/v3/pkg/middlewares/ipwhitelist"
"github.com/traefik/traefik/v3/pkg/middlewares/observability"
@ -428,6 +429,16 @@ func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (
}
}
// ingress-nginx middlewares.
if config.Snippet != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return snippet.New(ctx, next, config.Snippet, middlewareName)
}
}
if middleware == nil {
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
}